ref: 35959699c979b584314702e7d6f27c6cebf25c69
parent: a8234dc3c631ff8477654661683f852d61575218
author: penny <penny@limitedideas.org>
date: Sun Aug 10 13:04:04 EDT 2025
bring in the big client lock
--- a/hellclient.go
+++ b/hellclient.go
@@ -1,22 +1,37 @@
package main
import (
- "github.com/chzyer/readline"
+ "io"
"strings"
+ "sync"
+
+ "github.com/chzyer/readline"
+ "github.com/mattn/go-mastodon"
)
+var (
+ ErrInterrupt = readline.ErrInterrupt
+ EOF = io.EOF
+)
+
type Hellclient struct {- isPaused bool
- rl *readline.Instance
- pauseChan chan bool
+ isPaused bool
+ rl *readline.Instance
+ block sync.Mutex
+
+ //Only use these in one routine at a time unless you add a mutex
+ homeMap map[string]*mastodon.Status
+ debugMap map[string]interface{}}
func NewHellclient() (*Hellclient, error) { rl, err := readline.New("> ")+ homeMap := make(map[string]*mastodon.Status)
+ debugMap := make(map[string]interface{}) if err != nil {return nil, err
}
- return &Hellclient{rl: rl, pauseChan: make(chan bool)}, nil+ return &Hellclient{rl: rl, homeMap: homeMap, debugMap: debugMap, isPaused: false}, nil}
func (hc *Hellclient) updatePrompt() {@@ -30,12 +45,18 @@
func (hc *Hellclient) pause(on bool) {hc.isPaused = on
- hc.pauseChan <- hc.isPaused
hc.updatePrompt()
}
func (hc *Hellclient) togglepause() {hc.isPaused = !hc.isPaused
- hc.pauseChan <- hc.isPaused
hc.updatePrompt()
+}
+
+func (hc *Hellclient) lock() {+ hc.block.Lock()
+}
+
+func (hc *Hellclient) unlock() {+ hc.block.Unlock()
}
--- a/main.go
+++ b/main.go
@@ -36,8 +36,8 @@
io.Copy(rl.Stdout(), r)
}()
- homeMap := make(map[string]*mastodon.Status)
- debugMap := make(map[string]interface{})+ homeMap := hc.homeMap
+ debugMap := hc.debugMap
postref := "a"
var recentpost *mastodon.Status
@@ -48,204 +48,212 @@
return
}
- go StreamHomeTimeline(client, homeMap, hc.pauseChan)
+ go StreamHomeTimeline(client, homeMap, hc)
for {- line, err := rl.Readline()
+ func() {+ line, err := rl.Readline()
- if err != nil {- fmt.Println("Error:", err)- return
- }
- command, arguments := processInput(line)
-
- //if we didn't get a slash command then the user is just posting
- if command == "" && arguments != "" {- recentpost, err = postStatus(fmt.Sprintf(line), account, *client, "public")
if err != nil {- fmt.Println(err)
+ if err == EOF || err == ErrInterrupt {+ os.Exit(0)
+ }
+ fmt.Println("Error:", err)+ return
}
- continue
- }
+ command, arguments := processInput(line)
- index, content, _ := strings.Cut(arguments, " ")
- postItem, postOK := homeMap[index]
- debugItem, debugOK := debugMap[index]
-
- //Contextual commands that need to handle their own requirements
- switch command {- case "pause":
- hc.togglepause()
- continue
- case "resume":
- hc.pause(false)
- continue
- case "rm":
- if !postOK && recentpost != nil {- err = client.DeleteStatus(context.Background(), recentpost.ID)
+ //if we didn't get a slash command then the user is just posting
+ if command == "" && arguments != "" {+ recentpost, err = postStatus(fmt.Sprintf(line), account, *client, "public")
if err != nil {fmt.Println(err)
}
- recentpost = nil
- continue
+ return
}
- if !postOK {- fmt.Println("No recent status to delete or post index not valid")- continue
- }
- err = client.DeleteStatus(context.Background(), postItem.ID)
- if err != nil {- fmt.Println(err)
- }
- continue
- }
+
+ hc.lock()
+ defer hc.unlock()
- if arguments == "" {- fmt.Printf("%v requires an argument\n", command)- continue
- }
+ index, content, _ := strings.Cut(arguments, " ")
+ postItem, postOK := homeMap[index]
+ debugItem, debugOK := debugMap[index]
- //Commands that don't take an index
- switch command {- case "dm":
- recentpost, err = postStatus(arguments, account, *client, "direct")
- if err != nil {- fmt.Println(err)
+ //Contextual commands that need to handle their own requirements
+ switch command {+ case "pause":
+ hc.togglepause()
+ return
+ case "resume":
+ hc.pause(false)
+ return
+ case "rm":
+ if !postOK && recentpost != nil {+ err = client.DeleteStatus(context.Background(), recentpost.ID)
+ if err != nil {+ fmt.Println(err)
+ }
+ recentpost = nil
+ return
+ }
+ if !postOK {+ fmt.Println("No recent status to delete or post index not valid")+ return
+ }
+ err = client.DeleteStatus(context.Background(), postItem.ID)
+ if err != nil {+ fmt.Println(err)
+ }
+ return
}
- continue
- if !postOK && !debugOK {- fmt.Printf("\"%v\" not a valid index\n", index)- continue
- }
- }
- //Commands that accept debug indexes
- switch command {- case "examine":
- if postOK {- PrintObjectProperties(postItem, debugMap)
- continue
+ if arguments == "" {+ fmt.Printf("%v requires an argument\n", command)+ return
}
- PrintObjectProperties(debugItem, debugMap)
- continue
- }
- //if a user passes a debug index to a status command
- if !postOK {- fmt.Printf("\"%v\" not a valid post index\n", index)- continue
- }
- //Commands require status indexes
- switch command {- case "like":
- _, err := client.Favourite(context.Background(), postItem.ID)
- if err != nil {- printMastodonErr(err)
- } else {- fmt.Printf(formatFavorite(postItem, arguments) + "\n")
- }
- case "mark":
- _, err := client.Bookmark(context.Background(), postItem.ID)
- if err != nil {- printMastodonErr(err)
- } else {- fmt.Printf(formatBookmark(postItem, arguments) + "\n")
- }
- case "unmark":
- _, err := client.Unbookmark(context.Background(), postItem.ID)
- if err != nil {- printMastodonErr(err)
- } else {- fmt.Printf(formatUnbookmark(postItem, arguments) + "\n")
- }
- case "open":
- url := fmt.Sprintf("%v/statuses/%v", client.Config.Server, postItem.ID)- cmd := exec.Command("open", url, "-a", "Eldritch Café")- cmd.Run()
- case "preview":
- err := previewPostImages(postItem, "open -W -a \"Quick Look\"")
- if err != nil {- fmt.Printf("Image preview failed: %v\n", err)- }
- case "import":
- err := previewPostImages(postItem, "shortcuts run \"media collector\" --input-path")
- if err != nil {- fmt.Printf("Image preview failed: %v\n", err)- }
- case "download":
- savePostImages(postItem, "/Users/penny/Downloads/")
- case "reply":
- if currentUser.ID == postItem.Account.ID {- recentpost, err = postReply(content, account, *client, postItem.Visibility, postItem.ID)
+ //Commands that don't take an index
+ switch command {+ case "dm":
+ recentpost, err = postStatus(arguments, account, *client, "direct")
if err != nil {fmt.Println(err)
}
- continue
+ return
+ if !postOK && !debugOK {+ fmt.Printf("\"%v\" not a valid index\n", index)+ return
+ }
}
- recentpost, err = postReply("@"+getUserString(postItem)+" "+content, account, *client, postItem.Visibility, postItem.ID)- if err != nil {- fmt.Println(err)
+
+ //Commands that accept debug indexes
+ switch command {+ case "examine":
+ if postOK {+ PrintObjectProperties(postItem, debugMap)
+ return
+ }
+ PrintObjectProperties(debugItem, debugMap)
+ return
}
- continue
- case "rt":
- rtStatus, err := client.Reblog(context.Background(), postItem.ID)
- if err != nil {- fmt.Println(err)
- continue
+
+ //if a user passes a debug index to a status command
+ if !postOK {+ fmt.Printf("\"%v\" not a valid post index\n", index)+ return
}
- recentpost = rtStatus
- saveWorkRef(homeMap, rtStatus, postref)
- printPost("?"+postref, rtStatus)- postref = IncrementString(postref)
- case "parent":
- if postItem.InReplyToID == nil {- fmt.Printf("%v doesn't have a parent\n", index)- continue
- }
- parentStatus, _ := client.GetStatus(context.Background(), mastodon.ID(postItem.InReplyToID.(string)))
- saveWorkRef(homeMap, parentStatus, postref)
- printPost("?"+postref, parentStatus)- postref = IncrementString(postref)
- case "children":
- context, err := client.GetStatusContext(context.Background(), postItem.ID)
- if err != nil {- fmt.Println(err)
- continue
- }
- if len(context.Descendants) == 0 {- fmt.Printf("\"%v\" has no children")- }
- for post := range context.Descendants {- saveWorkRef(homeMap, context.Descendants[post], postref)
- printPost("?"+postref, context.Descendants[post])+ //Commands require status indexes
+ switch command {+ case "like":
+ _, err := client.Favourite(context.Background(), postItem.ID)
+ if err != nil {+ printMastodonErr(err)
+ } else {+ fmt.Printf(formatFavorite(postItem, arguments) + "\n")
+ }
+ case "mark":
+ _, err := client.Bookmark(context.Background(), postItem.ID)
+ if err != nil {+ printMastodonErr(err)
+ } else {+ fmt.Printf(formatBookmark(postItem, arguments) + "\n")
+ }
+ case "unmark":
+ _, err := client.Unbookmark(context.Background(), postItem.ID)
+ if err != nil {+ printMastodonErr(err)
+ } else {+ fmt.Printf(formatUnbookmark(postItem, arguments) + "\n")
+ }
+ case "open":
+ url := fmt.Sprintf("%v/statuses/%v", client.Config.Server, postItem.ID)+ cmd := exec.Command("open", url, "-a", "Eldritch Café")+ cmd.Run()
+ case "preview":
+ err := previewPostImages(postItem, "open -W -a \"Quick Look\"")
+ if err != nil {+ fmt.Printf("Image preview failed: %v\n", err)+ }
+ case "import":
+ err := previewPostImages(postItem, "shortcuts run \"media collector\" --input-path")
+ if err != nil {+ fmt.Printf("Image preview failed: %v\n", err)+ }
+ case "download":
+ savePostImages(postItem, "/Users/penny/Downloads/")
+ case "reply":
+ if currentUser.ID == postItem.Account.ID {+ recentpost, err = postReply(content, account, *client, postItem.Visibility, postItem.ID)
+ if err != nil {+ fmt.Println(err)
+ }
+ return
+ }
+ recentpost, err = postReply("@"+getUserString(postItem)+" "+content, account, *client, postItem.Visibility, postItem.ID)+ if err != nil {+ fmt.Println(err)
+ }
+ return
+ case "rt":
+ rtStatus, err := client.Reblog(context.Background(), postItem.ID)
+ if err != nil {+ fmt.Println(err)
+ return
+ }
+ recentpost = rtStatus
+ saveWorkRef(homeMap, rtStatus, postref)
+ printPost("?"+postref, rtStatus)postref = IncrementString(postref)
- }
- case "thread":
- context, err := client.GetStatusContext(context.Background(), postItem.ID)
- if err != nil {- fmt.Println(err)
- continue
- }
-
- for post := range context.Ancestors {- saveWorkRef(homeMap, context.Ancestors[post], postref)
- printPost("?"+postref, context.Ancestors[post])+ case "parent":
+ if postItem.InReplyToID == nil {+ fmt.Printf("%v doesn't have a parent\n", index)+ return
+ }
+ parentStatus, _ := client.GetStatus(context.Background(), mastodon.ID(postItem.InReplyToID.(string)))
+ saveWorkRef(homeMap, parentStatus, postref)
+ printPost("?"+postref, parentStatus)postref = IncrementString(postref)
- }
+ case "children":
+ context, err := client.GetStatusContext(context.Background(), postItem.ID)
+ if err != nil {+ fmt.Println(err)
+ return
+ }
+ if len(context.Descendants) == 0 {+ fmt.Printf("\"%v\" has no children")+ }
+ for post := range context.Descendants {+ saveWorkRef(homeMap, context.Descendants[post], postref)
+ printPost("?"+postref, context.Descendants[post])+ postref = IncrementString(postref)
+ }
+ case "thread":
+ context, err := client.GetStatusContext(context.Background(), postItem.ID)
+ if err != nil {+ fmt.Println(err)
+ return
+ }
- printPost(index, postItem)
+ for post := range context.Ancestors {+ saveWorkRef(homeMap, context.Ancestors[post], postref)
+ printPost("?"+postref, context.Ancestors[post])+ postref = IncrementString(postref)
+ }
- for post := range context.Descendants {- saveWorkRef(homeMap, context.Descendants[post], postref)
- printPost("?"+postref, context.Descendants[post])- postref = IncrementString(postref)
- }
- case "account":
- account := postItem.Account
- fmt.Printf(formatAccount(&account))
+ printPost(index, postItem)
- default:
- fmt.Printf("Unimplemented command \"%v\"?\"\n", command)- }
+ for post := range context.Descendants {+ saveWorkRef(homeMap, context.Descendants[post], postref)
+ printPost("?"+postref, context.Descendants[post])+ postref = IncrementString(postref)
+ }
+ case "account":
+ account := postItem.Account
+ fmt.Printf(formatAccount(&account))
+
+ default:
+ fmt.Printf("Unimplemented command \"%v\"?\"\n", command)+ }
+ } ()
}
}
--- a/mastodon.go
+++ b/mastodon.go
@@ -228,7 +228,7 @@
return
}
-func StreamHomeTimeline(client *mastodon.Client, postMap map[string]*mastodon.Status, pauseChan <-chan bool) {+func StreamHomeTimeline(client *mastodon.Client, postMap map[string]*mastodon.Status, hc *Hellclient) {ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -249,19 +249,6 @@
// Enter a loop to continuously listen for events from the event channel.
for { select {- case newPauseState := <-pauseChan:
- if newPauseState == isPaused {- continue
- }
- isPaused = newPauseState
-
- if !isPaused {- fmt.Println("Output resumed")- for _, action := range actionBuffer {- action()
- }
- actionBuffer = nil
- }
case event, ok := <-eventCh: // Read from the event channel, checking 'ok' for closure
if !ok {@@ -269,62 +256,92 @@
fmt.Println("Stream closed.\n")return // Exit the function
}
+ func() {+ func() {+ hc.lock()
+ defer hc.unlock()
+ newPauseState := hc.isPaused
+ if newPauseState == isPaused {+ return
+ }
+ isPaused = newPauseState
- switch post := event.(type) {- case *mastodon.UpdateEvent:
- if isPaused {- currentPostRef := postref
- capturedPost := post
- actionBuffer = append(actionBuffer, func() {- capturedPost.Status = printPost(currentPostRef, capturedPost.Status)
- })
- } else {- post.Status = printPost(postref, post.Status)
- }
- saveRef(postMap, post.Status, postref)
- idmap[post.Status.ID] = post.Status
- postref = IncrementString(postref)
+ if !isPaused {+ fmt.Println("Output resumed")+ for _, action := range actionBuffer {+ action()
+ }
+ actionBuffer = nil
+ }
+ }()
+ hc.lock()
+ defer hc.unlock()
+ switch post := event.(type) {+ case *mastodon.UpdateEvent:
+ if isPaused {+ currentPostRef := postref
+ capturedPost := *post
+ actionBuffer = append(actionBuffer, func() {+ capturedPost.Status = printPost(currentPostRef, capturedPost.Status)
+ })
+ } else {+ post.Status = printPost(postref, post.Status)
+ }
+ saveRef(postMap, post.Status, postref)
+ idmap[post.Status.ID] = post.Status
+ postref = IncrementString(postref)
- case *mastodon.UpdateEditEvent:
- if isPaused {- currentPostRef := postref
- capturedPost := post
- actionBuffer = append(actionBuffer, func() {- fmt.Println(formatEdit(capturedPost.Status, currentPostRef))
- })
- } else {- fmt.Println(formatEdit(post.Status, postref))
- }
- saveRef(postMap, post.Status, postref)
- postref = IncrementString(postref)
- idmap[post.Status.ID] = post.Status
-
- case *mastodon.DeleteEvent:
- deleted, ok := idmap[post.ID]
- //didn't have this in the cache
- if !ok {- capturedID := post.ID
+ case *mastodon.UpdateEditEvent:
if isPaused {+ currentPostRef := postref
+ capturedPost := post
actionBuffer = append(actionBuffer, func() {- fmt.Printf("Deleted: ID %v\n", capturedID)+ fmt.Println(formatEdit(capturedPost.Status, currentPostRef))
})
} else {- fmt.Printf("Deleted: ID %v\n", capturedID)+ fmt.Println(formatEdit(post.Status, postref))
}
- continue
- }
- if isPaused {- actionBuffer = append(actionBuffer, func() {+ saveRef(postMap, post.Status, postref)
+ postref = IncrementString(postref)
+ idmap[post.Status.ID] = post.Status
+
+ case *mastodon.DeleteEvent:
+ deleted, ok := idmap[post.ID]
+ //didn't have this in the cache
+ if !ok {+ capturedID := post.ID
+ if isPaused {+ actionBuffer = append(actionBuffer, func() {+ fmt.Printf("Deleted: ID %v\n", capturedID)+ })
+ } else {+ fmt.Printf("Deleted: ID %v\n", capturedID)+ }
+ return
+ }
+ if isPaused {+ actionBuffer = append(actionBuffer, func() {+ printPostDetailed("", deleted, "Deleted:")+ })
+ } else { printPostDetailed("", deleted, "Deleted:")- })
- } else {- printPostDetailed("", deleted, "Deleted:")- }
- continue
+ }
+ return
- case *mastodon.NotificationEvent:
- if post.Notification.Status == nil {- notification := fmt.Sprintf("Notification [%v] from <%v>\n", post.Notification.Type, post.Notification.Account.Acct)+ case *mastodon.NotificationEvent:
+ if post.Notification.Status == nil {+ notification := fmt.Sprintf("Notification [%v] from <%v>\n", post.Notification.Type, post.Notification.Account.Acct)+ if isPaused {+ actionBuffer = append(actionBuffer, func() {+ fmt.Printf(hyphenate(notification))
+ })
+ } else {+ fmt.Printf(hyphenate(notification))
+ }
+ return
+ }
+ _, plaintext = RenderPostPlaintext(post.Notification.Status, postref, "")
+ notification := fmt.Sprintf("Notification [%v] from <%v>: %v\n", post.Notification.Type, post.Notification.Account.Acct, plaintext) if isPaused { actionBuffer = append(actionBuffer, func() {fmt.Printf(hyphenate(notification))
@@ -332,31 +349,20 @@
} else {fmt.Printf(hyphenate(notification))
}
- continue
- }
- _, plaintext = RenderPostPlaintext(post.Notification.Status, postref, "")
- notification := fmt.Sprintf("Notification [%v] from <%v>: %v\n", post.Notification.Type, post.Notification.Account.Acct, plaintext)- if isPaused {- actionBuffer = append(actionBuffer, func() {- fmt.Printf(hyphenate(notification))
- })
- } else {- fmt.Printf(hyphenate(notification))
- }
- saveRef(postMap, post.Notification.Status, postref)
- postref = IncrementString(postref)
- default:
- // Catch any other unexpected event types.
- unhandledEvent := event
- if isPaused {- actionBuffer = append(actionBuffer, func() {+ saveRef(postMap, post.Notification.Status, postref)
+ postref = IncrementString(postref)
+ default:
+ // Catch any other unexpected event types.
+ unhandledEvent := event
+ if isPaused {+ actionBuffer = append(actionBuffer, func() {+ fmt.Printf("Unhandled event: %T\n", unhandledEvent)+ })
+ } else { fmt.Printf("Unhandled event: %T\n", unhandledEvent)- })
- } else {- fmt.Printf("Unhandled event: %T\n", unhandledEvent)+ }
}
- }
+ }()
}
}
}
-
--
⑨