shithub: hell

Download patch

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
+}
--