shithub: hell

ref: 40b380a7a4bb3ded2d29cf0a415b49660dc7c454
dir: /commands.go/

View raw version
package main

import (
	"fmt"
	"runtime/debug"
	"strings"
	"time"
	"context"
	"strconv"
	
	"github.com/k3a/html2text"
	mastodon "codeberg.org/penny64/hellclient-go-mastodon"
)

func processInput(input string, commands []string) (command string, arguments string, found bool) {

	if input == "" {
		command = ""
		arguments = ""
		return
	}

	if input[0] != '/' {
		command = ""
		arguments = input
		return
	}

	inputcommand, _, hasargument := strings.Cut(input[1:], " ")

	if !hasargument {
		inputcommand = input[1:]
	}

	for _, choice := range commands {
		if strings.HasPrefix(choice, inputcommand) {
			command = choice
			_, arguments, _ = strings.Cut(input, " ")
			found = true
			break
		}
	}

	if command == "" {
		command = inputcommand
	}

	return
}

func pullParameter(input string) (parameter string, remainder string) {
	if strings.HasPrefix(input, "?") {
		parameter, _, _ = strings.Cut(input, " ")
		if len(parameter) == len(input) {
			remainder = ""
			parameter = input
		} else {
			remainder = input[len(parameter)+1:]
		}
	} else {
		return "", input
	}

	prefix, _, cut := strings.Cut(parameter, "\"")

	if cut {
		quote, _, _ := strings.Cut(input[len(prefix)+1:], "\"")
		parameter = prefix + quote
		if len(parameter)+3 <= len(input) {
			remainder = input[len(parameter)+3:]
		} else {
			remainder = input[len(parameter)+2:]
		}
	}
	return
}

func splitParameter(input string) (parameter []string) {
	if len(input) < 2 {
		//nothing here
		return nil
	}
	input = input[1:]
	key, value, _ := strings.Cut(input, "=")
	return []string{key, value}
}

func extractInputParameters(input string) (remainder string, parameters [][]string) {
	parameter := " "
	remainder = input
	for parameter != "" {
		parameter, remainder = pullParameter(remainder)
		if parameter != " " && parameter != "" {
			parameters = append(parameters, splitParameter(parameter))
		}
	}
	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 {
			if data.account != nil {
				if data.status.Account.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
	}
	if index == "@" {
		loader.lastindex = index
		loader.lastaccount = loader.hc.currentuser
	}

	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]
		content = arguments
	}
	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 free
}

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}
	hc.pause(true)
	return hc.page.Pages(1)
}

type likescmd struct {
	*Hellclient
}

func (cmd *likescmd) name() string {
	return "lp"
}

func (cmd *likescmd) flags() cmdflag {
	return free
}

func (cmd *likescmd) result(data *cmddata) string {
	hc := cmd.Hellclient
	hc.page = &Page{}
	getter := &BasicStatusGetter{getter: hc.client.GetFavourites}
	hc.page.loader = &StatusPages{hc: hc, getter: getter}
	hc.pause(true)
	return hc.page.Pages(1)
}

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
	fmt.Println(data.raw_argument)
	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()
			num, _ := strconv.Atoi(data.content)
			num = -num
			return fmt.Sprint(hc.page.Pages(num))
		}
		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()
			num, _ := strconv.Atoi(data.content)
			return hc.page.Pages(num)
			
		}
		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.Pages(1)

		}
		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.Pages(1)
	}
	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.Pages(1)
	}
	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.Pages(1)

	}
	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.Pages(1)

	}
	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.Pages(1)

	}
	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 {
		replyLine := ""
		if data.status.InReplyToAccountID != nil {
			var account *mastodon.Account
			var err error
			getaccount := func() {
				ID := mastodon.ID(data.status.InReplyToAccountID.(string))
				account, err = hc.client.GetAccount(context.Background(), ID)
			}
			hc.dispatchAnon(getaccount).Wait()
			if err == nil {
				replyLine = fmt.Sprintf("Reply to %s\n", account.Acct)
			}
		}
		renderLine := fmt.Sprintf("$index $display_name $username_full $content $media_descriptions\n%s$detail_line", replyLine)
		line, _ := data.templater.render(renderLine)
		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 {
		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.Pages(1)
	}
	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.Pages(1)
	}
	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.Pages(1)
	}
	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
}

//self printer fixme
func (hc *Hellclient) examinecmd() cmder {
	cmd := &basiccmd{}
	cmd.hc = hc
	cmd.bname = "examine"
	cmd.bflags = free
	cmd.doer = func(data *cmddata) string {
		if data.found_index || data.dot_index {
			if data.status != nil {
				hc.PrintObjectProperties(data.status)
				return ""
			}
			hc.PrintObjectProperties(data.account)
			return ""
		}
		debugItem, debugOK := hc.debugMap[data.index]
		if debugOK {
			hc.PrintObjectProperties(debugItem)
		}
		return ""
	}
	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},
		&likescmd{hc}, //lp
		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),
		hc.examinecmd(),

	}
	return cmdarray
}