ref: 7b71c726d69844921bd959ab0cc9b561a3d03e52
parent: 15f1fbeaf9ee6513f32d6cae633fb860137d6cbc
parent: 50819e2b5e6275ec6b2633522acfe33aeadf9ce4
author: penny64 <penny64@noreply.codeberg.org>
date: Fri Oct 3 22:59:59 EDT 2025
Merge pull request 'Refactor prompt, support media attachments' (#2) from file_attachments into main Reviewed-on: https://codeberg.org/penny64/hellclient/pulls/2
--- a/commands.go
+++ b/commands.go
@@ -4,7 +4,7 @@
"strings"
)
-var commands = []string{"examine", "reply", "like", "thread", "open", "prev", "download", "dm", "rt", "parent", "children", "rm", "mark", "unmark", "account", "import", "pause", "resume", "url", "fpost", "ufpost", "edit", "notice", "stats", "next", "view", "bookmarks", "follow", "unfollow", "likes", "help", "reload"}+var commands = []string{"examine", "reply", "like", "thread", "open", "prev", "download", "dm", "rt", "parent", "children", "rm", "mark", "unmark", "account", "import", "pause", "resume", "url", "fpost", "ufpost", "edit", "notice", "stats", "next", "view", "bookmarks", "follow", "unfollow", "likes", "help", "reload", "attach"} func processInput(input string) (command string, arguments string, found bool) {--- a/dispatch.go
+++ b/dispatch.go
@@ -129,8 +129,18 @@
}
}
-func (hc *Hellclient) dispatchStatus(status string, visibility string) {- hc.dispatch <- postStatus(status, visibility)
+func (hc *Hellclient) dispatchStatus(poststring string, visibility string) {+ status := postStatus(poststring, visibility)
+ if hc.attacher.enqueuedAttachments() == 0 {+ hc.dispatch <- status
+ return
+ }
+ for item := range hc.attacher.attachments {+ status.MediaIDs = append(status.MediaIDs, hc.attacher.attachments[item].ID)
+ }
+ hc.attacher.attachments = nil
+ hc.prompt.UpdatePrompt()
+ hc.dispatch <- status
}
func (hc *Hellclient) dispatchReply(posttext string, replyto mastodon.ID, postItem *mastodon.Status) {--- a/edit.go
+++ b/edit.go
@@ -1,9 +1,9 @@
package main
import (
+ "bytes"
"errors"
"strings"
- "bytes"
mastodon "codeberg.org/penny64/hellclient-go-mastodon"
"golang.org/x/net/html"
@@ -35,7 +35,7 @@
}
}
}
-
+
//Rip off the HTML body the parser made for us
for node := range doc.Descendants() { if node.Data == "body" {--- a/filehandler.go
+++ b/filehandler.go
@@ -9,11 +9,14 @@
"os/exec"
"path"
"path/filepath"
+ "context"
+ "bytes"
mastodon "codeberg.org/penny64/hellclient-go-mastodon"
"github.com/google/shlex"
)
+
func openItemInOS(command string, url string) {cmdstring := fmt.Sprintf(command, url)
cmdargv, err := shlex.Split(cmdstring)
@@ -136,4 +139,43 @@
}
return file, nil
+}
+
+type StatusAttachmentHolder struct {+ *Hellclient
+ attachments []*mastodon.Attachment
+}
+
+// Return a byte array and error given a filepath
+// Expands ~ and environmental variables
+func loadFile(path string) (*[]byte, error) {+ path = expandDir(path)
+ data, err := os.ReadFile(path)
+ if err != nil {+ return nil, fmt.Errorf("read error: %s: %s", path, err)+ }
+ return &data, nil
+}
+
+// Upload media by path to the mastodon server
+func (sam *StatusAttachmentHolder) uploadAttachment(path string) error {+ file, err := loadFile(path)
+ if err != nil {+ return err
+ }
+ config := sam.rl.GetConfig()
+ config.Prompt = "Image description: "
+ description, _ := sam.rl.ReadLineWithConfig(config)
+ reader := bytes.NewReader(*file)
+ media := &mastodon.Media{File: reader, Description: description}+ attachment, err := sam.client.UploadMediaFromMedia(context.Background(), media)
+ sam.attachments = append(sam.attachments, attachment)
+ if err != nil {+ return err
+ }
+ return nil
+}
+
+func (sam *StatusAttachmentHolder) enqueuedAttachments() int {+ return len(sam.attachments)
}
--- a/hellclient.go
+++ b/hellclient.go
@@ -2,10 +2,8 @@
import (
"context"
- "fmt"
"io"
"os"
- "strings"
"sync"
"time"
@@ -28,6 +26,8 @@
//if you're gonna touch or read anything here lock the mutex with hc.lock()
isPaused bool
rl *readline.Instance
+ prompt *PromptBar
+ attacher *StatusAttachmentHolder
client *mastodon.Client
currentuser *mastodon.Account
dispatch chan *mastodon.Toot
@@ -77,7 +77,7 @@
FuncFilterInputRune: func(r rune) (rune, bool) { if r == readline.CharCtrlJ {hc.multiLineMode = !hc.multiLineMode
- hc.updatePrompt()
+ hc.prompt.UpdatePrompt()
return r, false
}
if r == readline.CharInterrupt {@@ -127,7 +127,7 @@
defer func() {//Got some stuff to do when we're done
- hc.updatePrompt()
+ hc.prompt.UpdatePrompt()
//start up post dispatcher
go hc.clientDispatch()
@@ -167,6 +167,17 @@
postmap: homeMap,
}
+ prompt := &PromptBar{prompt: "Hell> ", rl: rl}+
+ attacher := &StatusAttachmentHolder{Hellclient: &hc}+
+ prompt.items = []PromptItem{+ &NotificationCounter{Hellclient: &hc},+ &MultiLineIndicator{Hellclient: &hc},+ &PauseIndicator{Hellclient: &hc},+ attacher,
+ }
+
debugMap := make(map[string]any)
urlMap := make(map[string][]string)
prefs := &account.Preferences
@@ -174,6 +185,7 @@
homeMap: homeMap,
ctxref: ctxref,
homeref: homeref,
+ prompt: prompt,
debugMap: debugMap,
isPaused: false,
urlMap: urlMap,
@@ -184,37 +196,20 @@
preferences: prefs,
stats: &Stats{},configPath: configpath,
+ attacher: attacher,
}
return &hc, nil
}
-func (hc *Hellclient) updatePrompt() {- var sb strings.Builder
-
- unread, err := hc.client.GetUnreadNotifications(context.Background(), nil, nil, 0)
- if err == nil {- sb.WriteString(fmt.Sprintf("ur:%v ", unread.Count))- }
- if hc.multiLineMode {- sb.WriteString("MULTI-LINE ")- }
- if hc.isPaused {- sb.WriteString("STREAMING PAUSED ")- }
-
- sb.WriteString("Hell> ")- hc.rl.SetPrompt(sb.String())
-}
-
func (hc *Hellclient) pause(on bool) {hc.isPaused = on
- hc.updatePrompt()
+ hc.prompt.UpdatePrompt()
hc.printPauseBuffer()
}
func (hc *Hellclient) togglepause() {hc.isPaused = !hc.isPaused
- hc.updatePrompt()
+ hc.prompt.UpdatePrompt()
hc.printPauseBuffer()
}
--- a/main.go
+++ b/main.go
@@ -98,6 +98,13 @@
//Contextual commands that need to handle their own requirements
switch command {+ case "attach":
+ err := hc.attacher.uploadAttachment(arguments)
+ if err != nil {+ fmt.Printf("Upload error: %s\n", err)+ }
+ hc.prompt.UpdatePrompt()
+ return
case "help":
fmt.Println(hyphenate(helpString(hc.configPath)))
return
@@ -151,7 +158,7 @@
fmt.Print(hc.page.String())
return
case "notice":
- defer hc.updatePrompt()
+ defer hc.prompt.UpdatePrompt()
defer hc.pause(true)
notifications, err := hc.GetUnreadNotifications()
if len(notifications) > 0 {--- a/mastodon.go
+++ b/mastodon.go
@@ -427,7 +427,7 @@
return
case *mastodon.NotificationEvent:
- hc.updatePrompt()
+ hc.prompt.UpdatePrompt()
if post.Notification.Status == nil { if hc.isPaused { hc.actionBuffer = append(hc.actionBuffer, func() {--- a/pages.go
+++ b/pages.go
@@ -37,6 +37,7 @@
client *mastodon.Client
loaded bool
}
+
// Thread status getter
// The API doesn't take a limit for this so we ignore the limit
func (getter *ThreadStatusGetter) Get(limit int) ([]*mastodon.Status, error) {--- /dev/null
+++ b/prompt.go
@@ -1,0 +1,95 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/ergochat/readline"
+)
+
+// Returns text to add to the status bar
+type PromptItem interface {+ Update() (string, bool, error)
+}
+
+// Renders a status bar with a prompt and PromptItems
+type PromptBar struct {+ prompt string
+ items []PromptItem
+ rl *readline.Instance
+}
+
+// Render the prompt and update the readline prompt
+func (pb *PromptBar) UpdatePrompt() {+ var sb strings.Builder
+
+ for item := range pb.items {+ status, print, err := pb.items[item].Update()
+ if err != nil {+ fmt.Printf("Error updating status item: %s\n", err)+ }
+ if print {+ sb.WriteString(status)
+ sb.WriteString(" ")+ }
+ }
+ sb.WriteString(pb.prompt)
+ pb.rl.SetPrompt(sb.String())
+}
+
+type NotificationCounter struct {+ *Hellclient
+}
+
+func (nc *NotificationCounter) Update() (string, bool, error) {+ unread, err := nc.client.GetUnreadNotifications(context.Background(), nil, nil, 0)
+ if err != nil {+ return "", false, err
+ }
+ if unread.Count == 0 {+ return "", false, nil
+ }
+ return fmt.Sprintf("ur:%d", unread.Count), true, nil+}
+
+// MultiLineIndicator shows when multi-line input mode is active.
+type MultiLineIndicator struct {+ *Hellclient
+}
+
+func (ml *MultiLineIndicator) Update() (string, bool, error) {+ if ml.multiLineMode {+ return "MULTILINE", true, nil
+ }
+ return "", false, nil
+}
+
+type PauseIndicator struct {+ *Hellclient
+}
+// Display whether the client is paused or not
+func (pi *PauseIndicator) Update() (string, bool, error) {+ if pi.isPaused {+ return "STREAM PAUSED", true, nil
+ }
+ return "", false, nil
+}
+
+// Display when you have attachments ready to post
+func (sam *StatusAttachmentHolder) Update() (string, bool, error) {+ if len(sam.attachments) < 1 {+ return "", false, nil
+ }
+ var sb strings.Builder
+
+ sb.WriteString("ATTACHED:[")+ for item := range sam.attachments {+ sb.WriteString(sam.attachments[item].Type)
+ if item != len(sam.attachments) {+ sb.WriteString(" ")+ }
+ }
+ sb.WriteString("]")+ return sb.String(), true, nil
+}
--
⑨