shithub: hell

Download patch

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