shithub: hell

Download patch

ref: 7eeca4c25afb01df3f134237014f9583975fc608
parent: c2fc0147298b582aeb55eaed59f9ac0c4c2860cf
parent: cb96e45d2b5094c69a4cb0567ca2391d79533d00
author: penny64 <penny64@noreply.codeberg.org>
date: Wed Nov 5 21:29:20 EST 2025

Merge pull request 'Create a CMD processing system and port every command to it' (#14) from cmd_structure into main

Reviewed-on: https://codeberg.org/penny64/hellclient/pulls/14

--- a/commands.go
+++ b/commands.go
@@ -1,12 +1,20 @@
 package main
 
 import (
+	"fmt"
+	"runtime/debug"
 	"strings"
+	"time"
+	"context"
+	"strconv"
+	
+	"github.com/k3a/html2text"
+	mastodon "codeberg.org/penny64/hellclient-go-mastodon"
 )
 
 var commands = []string{"examine", "reply", "like", "thread", "open", "prev", "download", "dm", "rt", "hrt", "parent", "children", "rm", "mark", "unmark", "account", "import", "pause", "resume", "url", "fpost", "ufpost", "edit", "notice", "stats", "next", "view", "bookmarks", "follow", "unfollow", "likes", "help", "reload", "attach", "detach", "pinned", "cat", "play", "translate", "read", "version", "local", "public", "block", "unblock", "unlike", "home", "page", "profile"}
 
-func processInput(input string) (command string, arguments string, found bool) {
+func processInput(input string, commands []string) (command string, arguments string, found bool) {
 
 	if input == "" {
 		command = ""
@@ -89,4 +97,1179 @@
 		}
 	}
 	return
+}
+
+type cmdflag uint8
+
+const (
+	free cmdflag = 1 << iota
+	status
+	account
+	argument
+	// Resolve @user@domain stringfs for us
+	acc_resolve
+	// Load user's own most recent post if they have one
+	want_recent
+	// Set up a templater for us
+	templater
+	// Load url from url index
+	load_url
+)
+
+type cmdloader struct {
+	hc          *Hellclient
+	lastindex   string
+	lastaccount *mastodon.Account
+	commands    []string
+	cmdmap      map[string]cmder
+}
+
+func (loader *cmdloader) init(cmds []cmder) {
+	loader.cmdmap = make(map[string]cmder)
+	for i, _ := range cmds {
+		name := cmds[i].name()
+		loader.commands = append(loader.commands, name)
+		loader.cmdmap[name] = cmds[i]
+	}
+}
+
+// Cases
+// Return loaded account if tag was found or was empty but loaded
+// Return loaded account if index was empty
+// Load lastaccount if it exists or try to look one up, return account or nil
+func (data *cmddata) lookupAccount(loader *cmdloader) *mastodon.Account {
+
+	if data.found_index {
+		return data.account
+	}
+	if data.index == "" && data.account != nil {
+		return data.account
+	}
+
+	account := loader.lastaccount
+	if account == nil {
+		account = loader.hc.resolveAccount(data.index)
+		if account == nil {
+			return nil
+		}
+	}
+
+	loader.lastaccount = account
+	loader.lastindex = ""
+	return account
+}
+
+func (loader *cmdloader) run(line string) (string, error) {
+	data, err := loader.processLine(line)
+	if err != nil {
+		return "", nil
+	}
+	hc := loader.hc
+	if !data.cmd_found {
+		return "", fmt.Errorf("Command %s not found.\n", data.command)
+	}
+	flags := loader.cmdmap[data.command].flags()
+	
+	// Command needs us to resolve @user@domain targets as indexes
+	if flags&acc_resolve != 0 {
+		data.account = data.lookupAccount(loader)
+		if data.status != nil && data.status.ID != data.account.ID {
+			data.status = nil
+		}
+	}
+	
+	// Command wants us to load the user's most recently made status
+	if flags&want_recent != 0 && !data.found_index {
+		if hc.recentpost != nil {
+			data.status = hc.recentpost
+			data.account = &hc.recentpost.Account
+		}
+	}
+
+	// Command needs us to set up a post templater
+	if flags&templater != 0 && data.status != nil {
+		formatter := &StatusFormatter{prefs: hc.preferences, status: data.status, postContext: hc.ctxref, localindex: data.index}
+		templater := newStatusTemplateRenderer(formatter)
+		data.templater = templater
+	}
+
+	// Command needs targeturl set
+	if flags&load_url != 0 && data.status != nil {
+		urlindex, err := strconv.Atoi(data.content)
+		if err != nil {
+			urlindex = 1
+		}
+		if urlindex <= len(hc.homeref.urlmap[data.index]) {
+			data.targeturl = hc.homeref.urlmap[data.index][urlindex-1]
+		}
+	}
+		
+	err = data.checkReqs(loader.cmdmap[data.command].flags())
+	if err != nil {
+		return "", err
+	}
+	return loader.cmdmap[data.command].result(data), nil
+}
+
+func (loader *cmdloader) processLine(line string) (*cmddata, error) {
+	command, arguments, found := processInput(line, loader.commands)
+	index, content, _ := strings.Cut(arguments, " ")
+	// "." refers to most recently acted on status
+	// but if we have an account set, we don't want to set an index!
+	var dot_index bool
+	if index == "." {
+		dot_index = true
+	}
+	if index == "." && loader.lastaccount == nil {
+		index = loader.lastindex
+	}
+
+	postItem, postOK := loader.hc.homeMap[index]
+	foundindex := false
+	//If there's no index selected load the last post we operated on
+	if postOK {
+		foundindex = true
+		loader.lastindex = index
+		loader.lastaccount = nil
+	} else {
+		postItem, postOK = loader.hc.homeMap[loader.lastindex]
+	}
+	var reblogger *mastodon.Status
+	//Okay now see if the post we end up with is a reblog
+	if postOK {
+		if postItem.Reblog != nil {
+			reblogger = postItem
+			postItem = postItem.Reblog
+		}
+	}
+
+	cmdctx := &cmddata{
+		command:      command,
+		content:      content,
+		raw_argument: arguments,
+		dot_index:    dot_index,
+		found_index:  foundindex,
+		index:        index,
+		cmd_found:    found,
+	}
+	if loader.lastaccount != nil {
+		cmdctx.account = loader.lastaccount
+	}
+	if postOK {
+		cmdctx.status = postItem
+		cmdctx.account = &postItem.Account
+		cmdctx.reblogger = reblogger
+
+	}
+
+	return cmdctx, nil
+}
+
+type cmddata struct {
+	status       *mastodon.Status
+	account      *mastodon.Account
+	reblogger    *mastodon.Status
+	raw_argument string
+	command      string
+	cmd_found    bool
+	content      string
+	found_index  bool
+	dot_index    bool
+	index        string
+	targeturl string
+	templater    *templateRenderer
+}
+
+
+type cmder interface {
+	name() string
+	flags() cmdflag
+	result(data *cmddata) string
+}
+
+
+// return an error message if cmddata matches cmders flags
+func (cmd *cmddata) checkReqs(flags cmdflag) (err error) {
+	if flags&free != 0 {
+		return nil
+	}
+	if (flags&status != 0) && (flags&account != 0) {
+		if cmd.status == nil && cmd.account == nil {
+			return fmt.Errorf("%s requires a status or an account\n", cmd.command)
+		}
+	} else {
+		if (flags&status != 0) && (cmd.status == nil) {
+			return fmt.Errorf("%s requires a status\n", cmd.command)
+		}
+		if (flags&account != 0) && (cmd.account == nil) {
+			return fmt.Errorf("%s requires an account\n", cmd.command)
+		}
+	}
+
+	if (flags&argument != 0) && (cmd.raw_argument == "") {
+		return fmt.Errorf("%s requires an argument\n", cmd.command)
+	}
+
+	if (flags&load_url != 0) && (cmd.targeturl == "") {
+		return fmt.Errorf("%s: bad url index\n", cmd.command)
+	}
+
+	return nil
+}
+
+type dmcmd struct {
+	*Hellclient
+}
+
+func (cmd *dmcmd) name() string {
+	return "dm"
+}
+
+func (cmd *dmcmd) flags() cmdflag {
+	return argument
+}
+
+func (cmd *dmcmd) result(data *cmddata) string {
+	hc := cmd.Hellclient
+	if data.raw_argument != "" {
+		hc.dispatchStatus(data.raw_argument, "direct")
+		return ""
+	}
+	hc.page = &Page{}
+	getter := &BasicStatusGetter{getter: hc.client.GetTimelineDirect}
+	hc.page.loader = &StatusPages{hc: hc, getter: getter}
+	fmt.Print(hc.page.String())
+	hc.pause(true)
+	return ""
+}
+
+type profilecmd struct {
+	*Hellclient
+	*cmdloader
+}
+
+func (cmd *profilecmd) name() string {
+	return "profile"
+}
+
+func (cmd *profilecmd) flags() cmdflag {
+	return account | acc_resolve
+}
+
+func (cmd *profilecmd) result(data *cmddata) string {
+	hc := cmd.Hellclient
+	account := data.account
+	if account == nil {
+		return fmt.Sprintf("Account lookup failed.\n")
+	}
+	return fmt.Sprint(hc.formatAccount(account))
+}
+
+type helpcmd struct {
+	*Hellclient
+}
+
+func (cmd *helpcmd) name() string {
+	return "help"
+}
+
+func (cmd *helpcmd) flags() cmdflag {
+	return free
+}
+
+func (cmd *helpcmd) result(data *cmddata) string {
+	return fmt.Sprintln(hyphenate(helpString(cmd.configPath)))
+}
+
+type reloadcmd struct {
+	*Hellclient
+}
+
+func (cmd *reloadcmd) flags() cmdflag {
+	return free
+}
+
+func (cmd *reloadcmd) name() string {
+	return "reload"
+}
+
+func (cmd *reloadcmd) result(data *cmddata) string {
+	account, _, err := loadConfig()
+	if err != nil {
+		return fmt.Sprintf("Error reloading config: %s\n", err)
+	}
+	cmd.preferences = &account.Preferences
+	return fmt.Sprintln("Successfully reloaded preferences")
+
+}
+
+type detachcmd struct {
+	*Hellclient
+}
+
+func (cmd *detachcmd) flags() cmdflag {
+	return free
+}
+
+func (cmd *detachcmd) name() string {
+	return "detach"
+}
+
+func (cmd *detachcmd) result(data *cmddata) string {
+	cmd.attacher.clearAttachments()
+	cmd.prompt.UpdatePrompt()
+	return ""
+}
+
+type attachcmd struct {
+	*Hellclient
+}
+
+func (cmd *attachcmd) flags() cmdflag {
+	return free
+}
+
+func (cmd *attachcmd) name() string {
+	return "attach"
+}
+
+func (cmd *attachcmd) result(data *cmddata) string {
+	hc := cmd.Hellclient
+	var err error
+	filename := data.content
+	if filename == "" {
+		filename, err = pickFilename(hc.preferences.FilePicker)
+	}
+	if err != nil {
+		return fmt.Sprintf("File picking error: %s\n", err)
+	}
+	err = hc.attacher.uploadAttachment(filename)
+	if err != nil {
+		return fmt.Sprintf("Upload error: %s\n", err)
+	}
+	hc.prompt.UpdatePrompt()
+	return ""
+}
+
+type statscmd struct {
+	*Hellclient
+}
+
+func (cmd *statscmd) flags() cmdflag {
+	return free
+}
+
+func (cmd *statscmd) name() string {
+	return "stats"
+}
+
+func (cmd *statscmd) result(data *cmddata) string {
+	hc := cmd.Hellclient
+	hc.stats.slock.Lock()
+	var sb strings.Builder
+	sb.WriteString(fmt.Sprintf("API Calls: %d\n", hc.stats.APICalls))
+	sb.WriteString(fmt.Sprintf("Statuses Received: %d\n", hc.stats.IncomingStatuses))
+	sb.WriteString("Statuses per hour:")
+	sb.WriteString(fmt.Sprintf("%.2f\n", float32(hc.stats.IncomingStatuses)/(float32(time.Since(hc.stats.StartedTime))/float32(time.Hour))))
+	sb.WriteString(fmt.Sprintf("Started At: %s\n", hc.stats.StartedTime))
+	timeSince := time.Since(hc.stats.StartedTime)
+	timeSince = timeSince.Round(time.Second)
+	sb.WriteString(fmt.Sprintf("Runtime: %s\n", timeSince.String()))
+	hc.stats.slock.Unlock()
+	return sb.String()
+}
+
+type basiccmd struct {
+	hc     *Hellclient
+	bname  string
+	bflags cmdflag
+	doer   func(*cmddata) string
+}
+
+func (cmd *basiccmd) flags() cmdflag {
+	return cmd.bflags
+}
+
+func (cmd *basiccmd) name() string {
+	return cmd.bname
+}
+
+func (cmd *basiccmd) result(data *cmddata) string {
+	return cmd.doer(data)
+}
+
+func (hc *Hellclient) versioncmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "version"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		if buildInfo, ok := debug.ReadBuildInfo(); ok {
+			return fmt.Sprintf("%+v\n", buildInfo)
+		}
+		return fmt.Sprintf("No version information available.")
+	}
+	return cmd
+}
+
+func (hc *Hellclient) prevcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "prev"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		if hc.page != nil {
+			hc.page.Prev()
+			return fmt.Sprint(hc.page.String())
+		}
+		return fmt.Sprintf("No page loaded")
+	}
+	return cmd
+}
+
+func (hc *Hellclient) nextcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "next"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		if hc.page != nil {
+			hc.page.Next()
+			return hc.page.String()
+		}
+		return fmt.Sprintf("No page loaded")
+	}
+	return cmd
+}
+
+func (hc *Hellclient) pagecmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "page"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		if hc.page != nil {
+			return hc.page.String()
+		}
+		return fmt.Sprintf("No page loaded")
+	}
+	return cmd
+}
+
+func (hc *Hellclient) bookmarkscmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "bookmarks"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		hc.pause(true)
+		hc.page = &Page{}
+		getter := &BasicStatusGetter{getter: hc.client.GetBookmarks}
+		hc.page.loader = &StatusPages{hc: hc, getter: getter}
+		return hc.page.String()
+	}
+	return cmd
+}
+
+func (hc *Hellclient) readcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "read"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		defer hc.prompt.UpdatePrompt()
+		notifications, err := hc.GetUnreadNotifications()
+		if err != nil {
+			return fmt.Sprintf("%s\n", err)
+		}
+		if len(notifications) > 0 {
+			err = hc.SetNotificationsRead(notifications[len(notifications)-1].ID)
+			if err != nil {
+				return fmt.Sprintf("%s\n", err)
+			}
+		}
+		return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) noticecmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "notice"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		defer hc.prompt.UpdatePrompt()
+		defer hc.pause(true)
+		notifications, err := hc.GetUnreadNotifications()
+		if len(notifications) > 0 {
+			hc.PrintNotifications(notifications)
+			err = hc.SetNotificationsRead(notifications[len(notifications)-1].ID)
+			if err != nil {
+				return fmt.Sprintf("%s\n", err)
+			}
+			return ""
+		}
+		hc.page = &Page{}
+		hc.page.loader = &NotificationPages{hc: hc}
+		return hc.page.String()
+	}
+	return cmd
+}
+
+func (hc *Hellclient) pausecmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "pause"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		hc.togglepause()
+		return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) resumecmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "resume"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		hc.pause(false)
+		return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) replycmd() cmder {
+	cmd := &basiccmd{}
+	cmd.bname = "reply"
+	cmd.bflags = status
+	cmd.doer = func(data *cmddata) string {
+		if data.status != nil {
+			hc.dispatchReply(data.content, data.status.SpoilerText, data.status.ID, data.status)
+		}
+		return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) rmcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "rm"
+	cmd.bflags = status | want_recent
+	cmd.doer = func(data *cmddata) string {
+		if data.status != nil {
+			var err error
+			deletefunc := func(job *GenericJob) {
+				err = hc.client.DeleteStatus(context.Background(), data.status.ID)
+			}
+			deleteJob := hc.dispatchFunc(deletefunc)
+			deleteJob.Wait()
+			if err != nil {
+				return fmt.Sprintln(err)
+			}
+		}
+		if !data.found_index {
+			hc.recentpost = nil
+		}
+		return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) homecmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "home"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		hc.page = &Page{}
+		getter := &BasicStatusGetter{getter: hc.client.GetTimelineHome}
+		hc.page.loader = &StatusPages{hc: hc, getter: getter}
+		hc.pause(true)
+		return hc.page.String()
+
+	}
+	return cmd
+}
+
+func (hc *Hellclient) localcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "local"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		hc.page = &Page{}
+		localAPI := func(ctx context.Context, pg *mastodon.Pagination) ([]*mastodon.Status, error) {
+					return hc.client.GetTimelinePublic(ctx, true, pg)
+		}
+		getter := &BasicStatusGetter{getter: localAPI}
+		hc.page.loader = &StatusPages{hc: hc, getter: getter}
+		hc.pause(true)
+		return hc.page.String()
+
+	}
+	return cmd
+}
+
+func (hc *Hellclient) publiccmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "public"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		hc.page = &Page{}
+		localAPI := func(ctx context.Context, pg *mastodon.Pagination) ([]*mastodon.Status, error) {
+					return hc.client.GetTimelinePublic(ctx, false, pg)
+		}
+		getter := &BasicStatusGetter{getter: localAPI}
+		hc.page.loader = &StatusPages{hc: hc, getter: getter}
+		hc.pause(true)
+		return hc.page.String()
+
+	}
+	return cmd
+}
+
+func (hc *Hellclient) catcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "cat"
+	cmd.bflags = status | templater
+	cmd.doer = func(data *cmddata) string {
+		line, _ := data.templater.render("$index $display_name $username_full $content $media_descriptions\n$detail_line")
+		return fmt.Sprint(line)
+	}
+	return cmd
+}
+
+//Untested and undocumtend my intance doesn't do this
+//Don't even have the renderer set up here
+func (hc *Hellclient) translatecmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "translate"
+	cmd.bflags = status | templater
+	cmd.doer = func(data *cmddata) string {
+		line, _ := data.templater.render("$index $display_name $username_full $content $media_descriptions\n$detail_line")
+		return fmt.Sprint(line)
+		translated, err := hc.client.TranslateStatus(context.Background(), data.status.ID)
+			if err != nil {
+				return fmt.Sprintf("Translation error: %s\n", err)
+
+			}
+		hc.PrintObjectProperties(translated)
+		return ""
+	}
+	return cmd
+}
+
+type likecmd struct {
+	*Hellclient
+	islike bool
+}
+
+func (like *likecmd) name() string {
+	if like.islike {
+		return "like"
+	}
+	return "unlike"
+}
+
+func (like *likecmd) flags() cmdflag  {
+	return status | templater
+}
+
+func (like *likecmd) result(data *cmddata) string {
+	var likefunc func()
+	var err error
+	var verb string
+	hc := like.Hellclient
+	if like.islike {
+		likefunc = func() {
+			verb = "Favourited"
+			_, err = hc.client.Favourite(context.Background(), data.status.ID)
+		}
+	} else {
+		likefunc = func() {
+			verb = "Unfavourited"
+			_, err = hc.client.Unfavourite(context.Background(), data.status.ID)
+		}
+	}
+	hc.dispatchAnon(likefunc).Wait()
+	if err != nil {
+		return fmt.Sprint("err: %s\n", err)
+	}
+	line, _ := data.templater.render(fmt.Sprintf("%s: $standard_status", verb))
+	return line
+}
+
+func (hc *Hellclient) markcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "mark"
+	cmd.bflags = status | templater
+	cmd.doer = func(data *cmddata) string {
+		var err error
+		markfunc := func() { _, err = hc.client.Bookmark(context.Background(), data.status.ID) }
+		hc.dispatchAnon(markfunc).Wait()
+		if err != nil {
+			return fmt.Sprintf("err: %s\n", err)
+		} 
+		line, _ := data.templater.render("Bookmarked: $index $username $content $media_descriptions\n")
+		return line
+	}
+	return cmd
+}
+
+func (hc *Hellclient) unmarkcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "unmark"
+	cmd.bflags = status | templater
+	cmd.doer = func(data *cmddata) string {
+		var err error
+		var postCopy *mastodon.Status
+		unmarkfunc := func() {
+			postCopy, err = hc.client.GetStatus(context.Background(), data.status.ID)
+		}
+		hc.dispatchAnon(unmarkfunc).Wait()
+
+		if err != nil {
+			return fmt.Sprintf("err: %s\n", err)
+		}
+
+		if !postCopy.Bookmarked.(bool) {
+			return "Status not bookmarked.\n"
+		}
+		
+		unmarkfunc = func() { _, err = hc.client.Unbookmark(context.Background(), data.status.ID) }
+		hc.dispatchAnon(unmarkfunc).Wait()
+		if err != nil {
+			return fmt.Sprintf("err: %s\n", err)
+		} 
+		line, _ := data.templater.render("Unbookmarked: $index $username $content $media_descriptions\n")
+		return line
+	}
+	return cmd
+}
+
+func (hc *Hellclient) statusurlcmd(name string, command *string,) cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = name
+	cmd.bflags = status
+	cmd.doer = func(data *cmddata) string {
+		url := fmt.Sprintf("%v/statuses/%v", hc.client.Config.Server, data.status.ID)
+		openItemInOS(*command, url)
+		return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) urlcmd(name string, command *string) cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = name
+	cmd.bflags = load_url | status
+	cmd.doer = func(data *cmddata) string {
+		openItemInOS(*command, data.targeturl)
+		return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) mediacmd(name string, command *string) cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = name
+	cmd.bflags = status
+	cmd.doer = func(data *cmddata) string {
+		err := hc.previewPostImages(data.status, *command)
+		if err != nil {
+			return fmt.Sprintf("err: %s\n", err)
+		}
+		return ""
+	}
+	return cmd
+}
+
+//this is using printf fixme
+func (hc *Hellclient) downloadcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "download"
+	cmd.bflags = status
+	cmd.doer = func(data *cmddata) string {
+		savePostImages(data.status, hc.preferences.Save_Location)
+		return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) hrtcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "hrt"
+	cmd.bflags = account | acc_resolve
+	cmd.doer = func(data *cmddata) string {
+		var account *mastodon.Account
+		if data.reblogger != nil {
+			account = &data.reblogger.Account
+		} else {
+			account = data.account
+		}
+		relationships, err := hc.client.GetAccountRelationships(context.Background(), []string{string(account.ID)})
+		if err != nil {
+			return fmt.Sprintf("err loading relationships: %s\n", err)
+		}
+		relationship := relationships[0]
+		if !relationship.Following {
+			return fmt.Sprintf("can't filter rts from user you don't follow!\n")
+		}
+		if relationship.ShowingReblogs {
+			_, err := hc.client.AccountFollowDetailed(context.Background(), account.ID, true, relationship.Notifying)
+			if err != nil {
+				return "Error updating settings\n"
+				}
+			return fmt.Sprintf("No longer showing RTs from <%s>\n", account.Acct)
+		}
+		_, err = hc.client.AccountFollowDetailed(context.Background(), account.ID, false, relationship.Notifying)
+		if err != nil {
+			return "Error updating settings\n"
+		}
+		return fmt.Sprintf("Now showing RTs from <%s>\n", account.Acct)
+	}
+	return cmd
+}
+
+func (hc *Hellclient) rtcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "rt"
+	cmd.bflags = status
+	cmd.doer = func(data *cmddata) string {
+		var status *mastodon.Status
+		var err error
+		rtfunc := func() {
+			status, err = hc.client.Reblog(context.Background(), data.status.ID)
+			hc.recentpost = status
+		}
+		hc.dispatchAnon(rtfunc).Wait()
+		hc.printAndIncrement(hc.ctxref, status)
+		if err != nil {
+			return fmt.Sprintf("err: %s\n", err)
+		}
+		return ""
+	}
+	return cmd
+}
+
+//another printer fixme
+func (hc *Hellclient) parentcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "parent"
+	cmd.bflags = status
+	cmd.doer = func(data *cmddata) string {
+		parentfunc := func() {
+			if data.status.InReplyToID == nil {
+				fmt.Printf("%v doesn't have a parent\n", data.index)
+				return
+			}
+			parentStatus, _ := hc.client.GetStatus(context.Background(), mastodon.ID(data.status.InReplyToID.(string)))
+			hc.printAndIncrement(hc.ctxref, parentStatus)
+			return
+		}
+		hc.dispatchAnon(parentfunc).Wait()
+		return ""
+	}
+	return cmd
+}
+
+//another printer fixme
+func (hc *Hellclient) childrencmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "children"
+	cmd.bflags = status
+	cmd.doer = func(data *cmddata) string {
+		childfunc := func() {
+			context, err := hc.client.GetStatusContext(context.Background(), data.status.ID)
+			if err != nil {
+				fmt.Println(err)
+				return
+			}
+			if len(context.Descendants) == 0 {
+				fmt.Printf("\"%s\" has no children\n", data.index)
+			}
+			for post := range context.Descendants {
+				hc.printAndIncrement(hc.ctxref, context.Descendants[post])
+			}
+			return
+			}
+			hc.dispatchAnon(childfunc).Wait()
+			return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) editcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "edit"
+	cmd.bflags = status
+	cmd.doer = func(data *cmddata) string {
+		var err error
+		if data.content == "" || data.content == " " {
+			if data.status.Account.ID != hc.currentuser.ID {
+				return fmt.Sprintf("cannot edit other's statuses!\n")
+			}
+			fixedHTML, err := prepareForEdit(data.status)
+			if err != nil {
+				return fmt.Sprintf("Error loading post HTML: %s\n", err)
+			}
+			hc.rl.SetDefault(fmt.Sprintf("/edit %v %v", data.index, html2text.HTML2TextWithOptions(fixedHTML, html2text.WithUnixLineBreaks())))
+			return ""
+		}
+		var MediaIDs []mastodon.ID
+			for _, media := range data.status.MediaAttachments {
+				MediaIDs = append(MediaIDs, media.ID)
+			}
+			toot := &mastodon.Toot{
+				Status:      data.content,
+				MediaIDs:    MediaIDs,
+				Sensitive:   data.status.Sensitive,
+				SpoilerText: data.status.SpoilerText,
+				Visibility:  data.status.Visibility,
+				Language:    data.status.Language,
+			}
+			if data.status.InReplyToID != nil {
+				id := mastodon.ID(data.status.InReplyToID.(string))
+				toot.InReplyToID = id
+			}
+			editfunc := func() {
+				_, err = hc.client.UpdateStatus(context.Background(), toot, data.status.ID)
+			}
+			hc.dispatchAnon(editfunc).Wait()
+			if err != nil {
+				return fmt.Sprintf("err: %s\n", err)
+			}
+			return ""
+	}
+	return cmd
+}
+
+func (hc *Hellclient) threadcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "thread"
+	cmd.bflags = free
+	cmd.doer = func(data *cmddata) string {
+		hc.pause(true)
+		hc.page = &Page{disablereverse: true}
+		getter := &ThreadStatusGetter{target: data.status, client: hc.client}
+		hc.page.loader = &StatusPages{hc: hc, getter: getter}
+		return hc.page.String()
+	}
+	return cmd
+}
+
+func (hc *Hellclient) pinnedcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "pinned"
+	cmd.bflags = account | acc_resolve
+	cmd.doer = func(data *cmddata) string {
+		hc.pause(true)
+		hc.page = &Page{}
+		hc.page.itembuffer = new([]PageItem)
+		getter := &PinnedStatusGetter{client: hc.client, ID: data.account.ID}
+		hc.page.loader = &StatusPages{hc: hc, getter: getter}
+		return hc.page.String()
+	}
+	return cmd
+}
+
+func (hc *Hellclient) accountcmd() cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = "account"
+	cmd.bflags = account | acc_resolve
+	cmd.doer = func(data *cmddata) string {
+		hc.pause(true)
+		hc.page = &Page{}
+		hc.page.itembuffer = new([]PageItem)
+		getter := &AccountStatusGetter{client: hc.client, ID: data.account.ID}
+		hc.page.loader = &StatusPages{hc: hc, getter: getter}
+		return hc.page.String()
+	}
+	return cmd
+}
+
+func (hc *Hellclient) followcmd() cmder {
+		cmd := &basiccmd{}
+		cmd.hc = hc
+		cmd.bname = "follow"
+		cmd.bflags = account | acc_resolve
+		cmd.doer = func(data *cmddata) string {
+			account := data.account
+			var err error
+			var relationship *mastodon.Relationship
+			followfunc := func(job *GenericJob) { relationship, err = hc.client.AccountFollow(context.Background(), account.ID) }
+			followjob := hc.dispatchFunc(followfunc)
+			followjob.Wait()
+			if err != nil {
+				return fmt.Sprintf("Error requesting follow: %s\n", err)
+			}
+			if relationship.Following {
+				return fmt.Sprintf("Successfully followed %s\n", account.Acct)
+			}
+			if relationship.Requested {
+				return fmt.Sprintf("Follow request sent to %s\n", account.Acct)
+			}
+			return "No error but no follow or request returned in response.\n"
+		}
+		return cmd
+}
+
+func (hc *Hellclient) unfollowcmd() cmder {
+		cmd := &basiccmd{}
+		cmd.hc = hc
+		cmd.bname = "unfollow"
+		cmd.bflags = account | acc_resolve
+		cmd.doer = func(data *cmddata) string {
+			var relationship *mastodon.Relationship
+			account := data.account
+			var err error
+			unfollowfunc := func(job *GenericJob) { relationship, err = hc.client.AccountUnfollow(context.Background(), account.ID) }
+			unfollowjob := hc.dispatchFunc(unfollowfunc)
+			unfollowjob.Wait()
+			if err != nil {
+				return fmt.Sprintf("Error unfollowing account: %s\n", err)
+			}
+			if !relationship.Following {
+				return fmt.Sprintf("Successfully unfollowed %s\n", data.index)
+			}
+			return "No error but account is still followed in response\n"
+		}
+		return cmd
+}
+
+func (hc *Hellclient) filtercmd(name string, filter bool) cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = name
+	cmd.bflags = status | templater
+	cmd.doer = func(data *cmddata) string {
+		var err error
+		if filter {
+			_, err = hc.filterStatus(data.status)
+			if err != nil {
+				return fmt.Sprintf("Error filtering post: %v\n", err)
+			}
+			url := fmt.Sprintf("%v/statuses/%v", hc.client.Config.Server, data.status.ID)
+			return fmt.Sprintf("Filtered %v\n", url)
+		}
+		_, err = hc.unfilterStatus(data.status)
+		if err != nil {
+			return fmt.Sprintf("Error unfiltering post: %v\n", err)
+		}
+		line, _ := data.templater.render("Unfiltered: $standard_or_subject\n")
+		return line
+	}
+	return cmd
+}
+
+func (hc *Hellclient) blockcmd(name string, block bool) cmder {
+	cmd := &basiccmd{}
+	cmd.hc = hc
+	cmd.bname = name
+	cmd.bflags = account | acc_resolve
+	cmd.doer = func(data *cmddata) string {
+		var err error
+		var result string
+		var blockfunc func()
+		if block {
+			blockfunc = func() {
+					_, err = hc.client.AccountBlock(context.Background(), data.account.ID)
+					if err != nil {
+						result = fmt.Sprintf("Error blocking account: %s.\n", data.account.Acct)
+						return
+					}
+					result = fmt.Sprintf("Account blocked: %s\n", data.account.Acct)
+					return
+			}
+		} else {
+			blockfunc = func() {
+					_, err := hc.client.AccountUnblock(context.Background(), data.account.ID)
+					if err != nil {
+						result = fmt.Sprintf("Error unblocking account: %s.\n", data.account.Acct)
+					}
+					result = fmt.Sprintf("Account unblocked: %s\n", data.account.Acct)
+					return
+			}
+		}
+		hc.dispatchAnon(blockfunc).Wait()
+		return result
+	}
+	return cmd
+}
+
+// Commmands are lazy evaluated in this order
+// Single/two letter matches need to match the most common commands
+func (hc *Hellclient) newCmdArray() []cmder {
+	cmdarray := []cmder{
+		&likecmd{hc, true},
+		&likecmd{hc, false},
+		hc.markcmd(),
+		hc.unmarkcmd(),
+		hc.translatecmd(),
+		hc.catcmd(),
+		hc.childrencmd(),
+		hc.replycmd(),
+		hc.rtcmd(),
+		hc.rmcmd(),
+		&reloadcmd{hc},
+		&profilecmd{hc, hc.cmdload},
+		hc.publiccmd(),
+		hc.pagecmd(),
+		hc.parentcmd(),
+		hc.pinnedcmd(),
+		&dmcmd{hc},
+		hc.downloadcmd(),
+		&helpcmd{hc},
+		&detachcmd{hc},
+		hc.accountcmd(),
+		&attachcmd{hc},
+		&statscmd{hc},
+		hc.versioncmd(),
+		hc.prevcmd(),
+		hc.nextcmd(),
+		hc.bookmarkscmd(),
+		hc.blockcmd("block", true),
+		hc.readcmd(),
+		hc.noticecmd(),
+		hc.pausecmd(),
+		hc.resumecmd(),
+		hc.homecmd(),
+		hc.hrtcmd(),
+		hc.localcmd(),
+		hc.statusurlcmd("open", &hc.preferences.Browser),
+		hc.urlcmd("url", &hc.preferences.Browser),
+		hc.filtercmd("ufpost", false),
+		hc.unfollowcmd(),
+		hc.blockcmd("unblock", false),
+		hc.urlcmd("play", &hc.preferences.MediaPlayer),
+		hc.mediacmd("view", &hc.preferences.ImageViewer),
+		hc.mediacmd("import", &hc.preferences.MediaImport),
+		hc.editcmd(),
+		hc.threadcmd(),
+		hc.followcmd(),
+		hc.filtercmd("fpost", true),
+
+	}
+	return cmdarray
 }
--- a/hellclient.go
+++ b/hellclient.go
@@ -34,6 +34,7 @@
 	preferences   *Hellprefs
 	multiLineMode bool
 	configPath    string
+	cmdload    *cmdloader
 
 	//Global status map for status indexes
 	//Needs to be converted to a postref struct
@@ -126,6 +127,10 @@
 		hc.prompt.UpdatePrompt()
 		//start up post dispatcher
 		go hc.clientDispatch()
+
+		//command processor
+		hc.cmdload = &cmdloader{hc: &hc}
+		hc.cmdload.init(hc.newCmdArray())
 
 		markers, err := hc.client.GetTimelineMarkers(context.Background(), []string{"home"})
 		if err != nil {
--- a/main.go
+++ b/main.go
@@ -1,15 +1,7 @@
 package main
 
 import (
-	"context"
 	"fmt"
-	"runtime/debug"
-	"strconv"
-	"strings"
-	"time"
-
-	mastodon "codeberg.org/penny64/hellclient-go-mastodon"
-	"github.com/k3a/html2text"
 )
 
 func main() {
@@ -22,21 +14,15 @@
 	rl := hc.rl
 	client := *hc.client
 	enablePipeHack(rl)
+	go StreamHomeTimeline(&client, hc.homeMap, hc)
 
-	homeMap := hc.homeMap
-	debugMap := hc.debugMap
-	lastindex := ""
-	recentpost := &hc.recentpost
-	var lastaccount *mastodon.Account
-
-
-	go StreamHomeTimeline(&client, homeMap, hc)
-
 	for {
 		func() {
 			line, err := rl.Readline()
+			hc.lock()
+			defer hc.unlock()
 
-			command, arguments, found := processInput(line)
+			command, arguments, _ := processInput(line, []string{})
 
 			//empty line
 			if command == "" && arguments == "" && err == nil {
@@ -54,26 +40,21 @@
 				return
 			}
 
+			result, err := hc.cmdload.run(line)
+			if err != nil {
+				fmt.Print(err)
+			}
+			fmt.Print(result)
+			return
+
 			if !found {
 				fmt.Printf("Command not found: \"%s\"\n", command)
 				return
 			}
 
-			hc.lock()
-			defer hc.unlock()
-
 			index, content, _ := strings.Cut(arguments, " ")
 			
-			
-
-			// "." refers to most recently acted on status
-			// but if we have an account set, we don't want to set an index!
-			if index == "." && lastaccount == nil {
-				index = lastindex
-			}
-
 			postItem, postOK := homeMap[index]
-			debugItem := debugMap[index]
 
 			//Wether we got a post index or not
 			foundindex := false
@@ -81,21 +62,25 @@
 			if postOK {
 				foundindex = true
 				lastindex = index
-				lastaccount = nil
 			} else {
 				postItem, postOK = homeMap[lastindex]
 			}
-
-
-			var reblogger *mastodon.Status
+			
 			//Okay now see if the post we end up with is a reblog
 			if postOK {
 				if postItem.Reblog != nil {
-					reblogger = postItem
 					postItem = postItem.Reblog
 					
 				}
 			}
+			cmdctx, _ := hc.cmdload.processLine(line)
+			hc.PrintObjectProperties(cmdctx.account)
+			hc.PrintObjectProperties(cmdctx.status)
+			hc.PrintObjectProperties(cmdctx.reblogger)
+			fmt.Println(cmdctx.raw_argument)
+			fmt.Println(cmdctx.command)
+			fmt.Println(cmdctx.content)
+			fmt.Println(cmdctx.index)
 
 			accByNameOrRef := func() (account *mastodon.Account)  {
 				if lastaccount != nil {
@@ -112,569 +97,11 @@
 				lastaccount = account
 				return
 			}
-
-			//Contextual commands that need to handle their own requirements
-			switch command {
-			case "detach":
-				hc.attacher.clearAttachments()
-				hc.prompt.UpdatePrompt()
-				return
-			case "attach":
-				if arguments == "" {
-					arguments, err = pickFilename(hc.preferences.FilePicker)
-				}
-				if err != nil {
-					fmt.Printf("File picking error: %s\n", err)
-					return
-				}
-				err := hc.attacher.uploadAttachment(arguments)
-				if err != nil {
-					fmt.Printf("Upload error: %s\n", err)
-					return
-				}
-				hc.prompt.UpdatePrompt()
-				return
-			case "help":
-				fmt.Println(hyphenate(helpString(hc.configPath)))
-				return
-			case "reload":
-				account, _, err := loadConfig()
-				if err != nil {
-					fmt.Printf("Error reloading config: %s\n", err)
-					return
-				}
-				fmt.Println("Successfully reloaded preferences")
-				hc.preferences = &account.Preferences
-				return
-			case "stats":
-				hc.stats.slock.Lock()
-				var sb strings.Builder
-				sb.WriteString(fmt.Sprintf("API Calls: %d\n", hc.stats.APICalls))
-				sb.WriteString(fmt.Sprintf("Statuses Received: %d\n", hc.stats.IncomingStatuses))
-				sb.WriteString("Statuses per hour:")
-				sb.WriteString(fmt.Sprintf("%.2f\n", float32(hc.stats.IncomingStatuses)/(float32(time.Since(hc.stats.StartedTime))/float32(time.Hour))))
-				sb.WriteString(fmt.Sprintf("Started At: %s\n", hc.stats.StartedTime))
-				timeSince := time.Since(hc.stats.StartedTime)
-				timeSince = timeSince.Round(time.Second)
-				sb.WriteString(fmt.Sprintf("Runtime: %s\n", timeSince.String()))
-				fmt.Print(sb.String())
-				hc.stats.slock.Unlock()
-				return
-			case "version":
-				if buildInfo, ok := debug.ReadBuildInfo(); ok {
-					fmt.Printf("%+v\n", buildInfo)
-					return
-				}
-				fmt.Printf("No version information available.")
-				return
-			case "prev":
-				if hc.page != nil {
-					hc.page.Prev()
-					fmt.Print(hc.page.String())
-				}
-				return
-			case "next":
-				if hc.page != nil {
-					hc.page.Next()
-					fmt.Print(hc.page.String())
-				}
-				return
-			case "page":
-				if hc.page != nil {
-					fmt.Print(hc.page.String())
-				}
-				return
-			case "bookmarks":
-				hc.pause(true)
-				hc.page = &Page{}
-				getter := &BasicStatusGetter{getter: hc.client.GetBookmarks}
-				hc.page.loader = &StatusPages{hc: hc, getter: getter}
-				fmt.Print(hc.page.String())
-				return
-			case "likes":
-				hc.pause(true)
-				hc.page = &Page{}
-				getter := &BasicStatusGetter{getter: hc.client.GetFavourites}
-				hc.page.loader = &StatusPages{hc: hc, getter: getter}
-				fmt.Print(hc.page.String())
-				return
-			case "read":
-				defer hc.prompt.UpdatePrompt()
-				notifications, err := hc.GetUnreadNotifications()
-				if err != nil {
-					fmt.Print(err)
-				}
-				if len(notifications) > 0 {
-					err = hc.SetNotificationsRead(notifications[len(notifications)-1].ID)
-					if err != nil {
-						fmt.Print(err)
-					}
-				}
-				return
-			case "notice":
-				defer hc.prompt.UpdatePrompt()
-				defer hc.pause(true)
-				notifications, err := hc.GetUnreadNotifications()
-				if len(notifications) > 0 {
-					hc.PrintNotifications(notifications)
-					err = hc.SetNotificationsRead(notifications[len(notifications)-1].ID)
-					if err != nil {
-						fmt.Print(err)
-					}
-					return
-				}
-				hc.page = &Page{}
-				hc.page.loader = &NotificationPages{hc: hc}
-				fmt.Print(hc.page.String())
-				if err != nil {
-					fmt.Print(err)
-				}
-				return
-			case "pause":
-				hc.togglepause()
-				return
-			case "resume":
-				hc.pause(false)
-				return
-			case "rm":
-				if !foundindex && *recentpost != nil {
-					deletefunc := func(job *GenericJob) {
-						err = client.DeleteStatus(context.Background(), (*recentpost).ID)
-					}
-					deleteJob := hc.dispatchFunc(deletefunc)
-					deleteJob.Wait()
-					if err != nil {
-						fmt.Println(err)
-					}
-					*recentpost = nil
-					return
-				}
-				if !postOK {
-					fmt.Println("No recent status to delete or post index not valid")
-					return
-				}
-				deletefunc := func(job *GenericJob) {
-					err = client.DeleteStatus(context.Background(), (*postItem).ID)
-				}
-				deleteJob := hc.dispatchFunc(deletefunc)
-				deleteJob.Wait()
-				if err != nil {
-					fmt.Println(err)
-				}
-				return
-			case "dm":
-				if arguments != "" {
-					hc.dispatchStatus(arguments, "direct")
-					return
-				}
-				hc.page = &Page{}
-				getter := &BasicStatusGetter{getter: hc.client.GetTimelineDirect}
-				hc.page.loader = &StatusPages{hc: hc, getter: getter}
-				fmt.Print(hc.page.String())
-				hc.pause(true)
-				return
-			case "home":
-				hc.page = &Page{}
-				getter := &BasicStatusGetter{getter: hc.client.GetTimelineHome}
-				hc.page.loader = &StatusPages{hc: hc, getter: getter}
-				fmt.Print(hc.page.String())
-				hc.pause(true)
-				return
-			case "local":
-				hc.page = &Page{}
-				localAPI := func(ctx context.Context, pg *mastodon.Pagination) ([]*mastodon.Status, error) {
-					return hc.client.GetTimelinePublic(ctx, true, pg)
-				}
-				getter := &BasicStatusGetter{getter: localAPI}
-				hc.page.loader = &StatusPages{hc: hc, getter: getter}
-				fmt.Print(hc.page.String())
-				hc.pause(true)
-				return
-			case "public":
-				hc.page = &Page{}
-				localAPI := func(ctx context.Context, pg *mastodon.Pagination) ([]*mastodon.Status, error) {
-					return hc.client.GetTimelinePublic(ctx, false, pg)
-				}
-				getter := &BasicStatusGetter{getter: localAPI}
-				hc.page.loader = &StatusPages{hc: hc, getter: getter}
-				fmt.Print(hc.page.String())
-				hc.pause(true)
-				return
-			}
-
-			if arguments == "" && !postOK && lastaccount == nil {
-				fmt.Printf("%v requires an argument\n", command)
-				return
-			}
-
-			//Commands that don't take an index
-			switch command {
-			}
-
-			//Commands that accept debug indexes
-			switch command {
-			case "examine":
-				if foundindex {
-					hc.PrintObjectProperties(postItem)
-					return
-				}
-				hc.PrintObjectProperties(debugItem)
-				return
-			}
 			formatter := &StatusFormatter{prefs: hc.preferences, status: postItem, postContext: hc.ctxref, localindex: index}
 			templater := newStatusTemplateRenderer(formatter)
 
 			//Commands require status indexes
 			switch command {
-			case "cat":
-				if !postOK {
-					fmt.Println("cat: no valid status")
-					return
-				}
-				if index == "" {
-					index = lastindex
-				}
-				line, _ := templater.render("$index $display_name $username_full $content $media_descriptions\n$detail_line")
-				fmt.Print(line)
-				return
-			case "translate":
-				if !postOK {
-					fmt.Println("translate: no valid status")
-					return
-				}
-				if index == "" {
-					index = lastindex
-				}
-				translated, err := client.TranslateStatus(context.Background(), postItem.ID)
-				if err != nil {
-					fmt.Printf("Translation error: %s\n", err)
-					return
-				}
-				hc.PrintObjectProperties(translated)
-			case "like":
-				likefunc := func() {
-					_, err = client.Favourite(context.Background(), postItem.ID)
-				}
-				hc.dispatchAnon(likefunc).Wait()
-				if err != nil {
-					printMastodonErr(err)
-				} else {
-					line, _ := templater.render("Favourited: $standard_status")
-					fmt.Print(line)
-				}
-				return
-			case "unlike":
-				likefunc := func() {
-					_, err = client.Unfavourite(context.Background(), postItem.ID)
-				}
-				hc.dispatchAnon(likefunc).Wait()
-				if err != nil {
-					printMastodonErr(err)
-				} else {
-					line, _ := templater.render("Unfavourited: $standard_status")
-					fmt.Print(line)
-				}
-				return
-			case "mark":
-				markfunc := func() { _, err = client.Bookmark(context.Background(), postItem.ID) }
-				hc.dispatchAnon(markfunc).Wait()
-				if err != nil {
-					printMastodonErr(err)
-				} else {
-					line, _ := templater.render("Bookmarked: $index $username $content $media_descriptions\n")
-					fmt.Print(line)
-				}
-				return
-			case "unmark":
-				var postCopy *mastodon.Status
-				unmarkfunc := func() {
-					postCopy, err = client.GetStatus(context.Background(), postItem.ID)
-				}
-				hc.dispatchAnon(unmarkfunc).Wait()
-				if err != nil {
-					fmt.Printf("Error removing bookmark: %s\n", err)
-					return
-				}
-				if !postCopy.Bookmarked.(bool) {
-					fmt.Printf("Post not bookmarked.\n")
-					return
-				}
-				hc.dispatchAnon(func() { _, err = client.Unbookmark(context.Background(), postItem.ID) }).Wait()
-				if err != nil {
-					printMastodonErr(err)
-				} else {
-					line, _ := templater.render("Unbookmarked: $index $username $content $media_descriptions\n")
-					fmt.Print(line)
-				}
-				return
-			case "open":
-				url := fmt.Sprintf("%v/statuses/%v", client.Config.Server, postItem.ID)
-				openItemInOS(hc.preferences.Browser, url)
-				return
-			case "url":
-				_, indexstr, _ := strings.Cut(arguments, " ")
-				urlindex, err := strconv.Atoi(indexstr)
-				if err != nil {
-					urlindex = 1
-				}
-				if urlindex > len(hc.homeref.urlmap[index]) {
-					fmt.Printf("Bad url index\n")
-					return
-				}
-				openItemInOS(hc.preferences.Browser, hc.homeref.urlmap[index][urlindex-1])
-				return
-			case "play":
-				_, indexstr, _ := strings.Cut(arguments, " ")
-				urlindex, err := strconv.Atoi(indexstr)
-				if err != nil {
-					urlindex = 1
-				}
-				if urlindex > len(hc.homeref.urlmap[index]) {
-					fmt.Printf("Bad url index\n")
-					return
-				}
-				openItemInOS(hc.preferences.MediaPlayer, hc.homeref.urlmap[index][urlindex-1])
-				return
-			case "view":
-				err := hc.previewPostImages(postItem, hc.preferences.ImageViewer)
-				if err != nil {
-					fmt.Printf("Image preview failed: %v\n", err)
-				}
-				return
-			case "import":
-				err := hc.previewPostImages(postItem, hc.preferences.MediaImport)
-				if err != nil {
-					fmt.Printf("Image preview failed: %v\n", err)
-				}
-				return
-			case "download":
-				savePostImages(postItem, hc.preferences.Save_Location)
-				return
-			case "hrt":
-				// We want to filter RTs from the RTer
-				if reblogger != nil {
-					postItem = reblogger
-				}
-				account := accByNameOrRef()
-				if account == nil {
-					fmt.Printf("Account lookup failed.\n")
-					return
-				}
-				relationships, err := hc.client.GetAccountRelationships(context.Background(), []string{string(account.ID)})
-				if err != nil {
-					fmt.Printf("Error loading relationship.\n")
-					return
-				}
-				relationship := relationships[0]
-				if !relationship.Following {
-					fmt.Printf("can't filter rts from user you don't follow!\n")
-					return
-				}
-				if relationship.ShowingReblogs {
-					_, err := hc.client.AccountFollowDetailed(context.Background(), account.ID, true, relationship.Notifying)
-					if err != nil {
-						fmt.Printf("Error updating settings\n")
-						return
-					}
-					fmt.Printf("No longer showing RTs from <%s>\n", account.Acct)
-					return
-				}
-				//Turn them back on if they were off!
-				_, err = hc.client.AccountFollowDetailed(context.Background(), account.ID, false, relationship.Notifying)
-				if err != nil {
-					fmt.Printf("Error updating settings\n")
-					return
-				}
-				fmt.Printf("Now showing RTs from <%s>\n", account.Acct)
-				return
-			case "rt":
-				rtfunc := func() {
-					rtStatus, err := client.Reblog(context.Background(), postItem.ID)
-					if err != nil {
-						fmt.Println(err)
-						return
-					}
-					*recentpost = rtStatus
-					hc.printAndIncrement(hc.ctxref, rtStatus)
-					return
-				}
-				hc.dispatchAnon(rtfunc).Wait()
-				return
-			case "parent":
-				parentfunc := func() {
-					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)))
-					hc.printAndIncrement(hc.ctxref, parentStatus)
-					return
-				}
-				hc.dispatchAnon(parentfunc).Wait()
-				return
-			case "children":
-				childfunc := func() {
-					context, err := client.GetStatusContext(context.Background(), postItem.ID)
-					if err != nil {
-						fmt.Println(err)
-						return
-					}
-					if len(context.Descendants) == 0 {
-						fmt.Printf("\"%s\" has no children\n", index)
-					}
-					for post := range context.Descendants {
-						hc.printAndIncrement(hc.ctxref, context.Descendants[post])
-					}
-					return
-				}
-				hc.dispatchAnon(childfunc).Wait()
-				return
-			case "edit":
-				if content == "" || content == " " {
-					if (postItem == nil) || postItem.Account.ID != hc.currentuser.ID {
-						fmt.Printf("cannot edit other's statuses!\n")
-						return
-					}
-					fixedHTML, err := prepareForEdit(postItem)
-					if err != nil {
-						fmt.Printf("Error loading post HTML: %s\n", err)
-						return
-					}
-					rl.SetDefault(fmt.Sprintf("/edit %v %v", index, html2text.HTML2TextWithOptions(fixedHTML, html2text.WithUnixLineBreaks())))
-					return
-				}
-				var MediaIDs []mastodon.ID
-				for _, media := range postItem.MediaAttachments {
-					MediaIDs = append(MediaIDs, media.ID)
-				}
-				toot := &mastodon.Toot{
-					Status:      content,
-					MediaIDs:    MediaIDs,
-					Sensitive:   postItem.Sensitive,
-					SpoilerText: postItem.SpoilerText,
-					Visibility:  postItem.Visibility,
-					Language:    postItem.Language,
-				}
-				if postItem.InReplyToID != nil {
-					id := mastodon.ID(postItem.InReplyToID.(string))
-					toot.InReplyToID = id
-				}
-				editfunc := func() {
-					_, err = client.UpdateStatus(context.Background(), toot, postItem.ID)
-					if err != nil {
-						fmt.Println(err)
-						return
-					}
-				}
-				hc.dispatchAnon(editfunc).Wait()
-				return
-			case "thread":
-				hc.pause(true)
-				hc.page = &Page{disablereverse: true}
-				getter := &ThreadStatusGetter{target: postItem, client: hc.client}
-				hc.page.loader = &StatusPages{hc: hc, getter: getter}
-				fmt.Print(hc.page.String())
-				return
-			case "pinned":
-				var account *mastodon.Account
-				if foundindex || index == "" {
-					account = &postItem.Account
-				} else {
-					account = hc.resolveAccount(index)
-					if account == nil {
-						return
-					}
-				}
-
-				hc.pause(true)
-				hc.page = &Page{}
-				hc.page.itembuffer = new([]PageItem)
-				getter := &PinnedStatusGetter{client: hc.client, ID: account.ID}
-				hc.page.loader = &StatusPages{hc: hc, getter: getter}
-				fmt.Print(hc.page.String())
-				return
-			case "profile":
-				account := accByNameOrRef()
-					if account == nil {
-						fmt.Printf("Account lookup failed.\n")
-						return
-					}
-				fmt.Print(hc.formatAccount(account))
-				return
-			case "account":
-				account := accByNameOrRef()
-				if account == nil {
-					fmt.Printf("Account lookup failed.\n")
-					return
-				}
-				hc.pause(true)
-				hc.page = &Page{}
-				hc.page.itembuffer = new([]PageItem)
-				getter := &AccountStatusGetter{client: hc.client, ID: account.ID}
-				hc.page.loader = &StatusPages{hc: hc, getter: getter}
-				fmt.Print(hc.page.String())
-				return
-			case "unfollow":
-				account := accByNameOrRef()
-				if account == nil {
-					fmt.Printf("Account lookup failed.")
-					return
-				}
-				var relationship *mastodon.Relationship
-				unfollowfunc := func(job *GenericJob) { relationship, err = hc.client.AccountUnfollow(context.Background(), account.ID) }
-				unfollowjob := hc.dispatchFunc(unfollowfunc)
-				unfollowjob.Wait()
-				if err != nil {
-					fmt.Printf("Error unfollowing account: %s\n", err)
-					return
-				}
-				if !relationship.Following {
-					fmt.Printf("Successfully unfollowed %s\n", index)
-					return
-				}
-				fmt.Printf("Unknown failure, account is still followed\n")
-				return
-
-			case "follow":				
-				account := accByNameOrRef()
-				if account == nil {
-					fmt.Printf("Account lookup failed.\n")
-					return
-				}
-				var relationship *mastodon.Relationship
-				followfunc := func(job *GenericJob) { relationship, err = hc.client.AccountFollow(context.Background(), account.ID) }
-				followjob := hc.dispatchFunc(followfunc)
-				followjob.Wait()
-				if err != nil {
-					fmt.Printf("Error requesting follow: %s\n", err)
-					return
-				}
-				if relationship.Following {
-					fmt.Printf("Successfully followed %s\n", account.Acct)
-					return
-				}
-				if relationship.Requested {
-					fmt.Printf("Follow request sent to %s\n", account.Acct)
-					return
-				}
-			case "fpost":
-				_, err := hc.filterStatus(postItem)
-				if err != nil {
-					fmt.Printf("Error filtering post: %v\n", err)
-					return
-				}
-				url := fmt.Sprintf("%v/statuses/%v", client.Config.Server, postItem.ID)
-				fmt.Printf("Filtered %v\n", url)
-				return
-			case "ufpost":
-				_, err := hc.unfilterStatus(postItem)
-				if err != nil {
-					fmt.Printf("Error unfiltering post: %v\n", err)
-					return
-				}
-				line, _ := templater.render("Unfiltered: %s> $standard_or_subject\n")
-				fmt.Print(line)
-				return
 			case "block":
 				var account *mastodon.Account
 				if foundindex || index == "" {
@@ -724,13 +151,6 @@
 				fmt.Printf("\"%v\" requires an argument\n", command)
 				return
 			}
-
-			switch command {
-			case "reply":
-				hc.dispatchReply(content, postItem.SpoilerText, postItem.ID, postItem)
-				return
-			}
-
 		}()
 	}
 }
--