~sircmpwn/aerc

2a0961701c4cabecc53d134ed1782e5612e64580 — Gregory Mullen 1 year, 9 months ago 177651b
Implement basic tab completion support

Tab completion currently only works on commands. Contextual completion
will be added in the future.
M aerc.go => aerc.go +40 -20
@@ 51,6 51,41 @@ func getCommands(selected libui.Drawable) []*commands.Commands {
	}
}

func execCommand(aerc *widgets.Aerc, ui *libui.UI, cmd string) error {
	cmds := getCommands((*aerc).SelectedTab())
	for i, set := range cmds {
		err := set.ExecuteCommand(aerc, cmd)
		if _, ok := err.(commands.NoSuchCommand); ok {
			if i == len(cmds)-1 {
				return err
			}
			continue
		} else if _, ok := err.(commands.ErrorExit); ok {
			ui.Exit()
			return nil
		} else if err != nil {
			return err
		} else {
			break
		}
	}
	return nil
}

func getCompletions(aerc *widgets.Aerc, cmd string) []string {
	cmds := getCommands((*aerc).SelectedTab())
	completions := make([]string, 0)
	for _, set := range cmds {
		opts := set.GetCompletions(aerc, cmd)
		if len(opts) > 0 {
			for _, opt := range opts {
				completions = append(completions, opt)
			}
		}
	}
	return completions
}

var (
	Prefix   string
	ShareDir string


@@ 96,27 131,12 @@ func main() {
		aerc *widgets.Aerc
		ui   *libui.UI
	)

	aerc = widgets.NewAerc(conf, logger, func(cmd string) error {
		cmds := getCommands(aerc.SelectedTab())
		for i, set := range cmds {
			err := set.ExecuteCommand(aerc, cmd)
			if _, ok := err.(commands.NoSuchCommand); ok {
				if i == len(cmds)-1 {
					return err
				} else {
					continue
				}
			} else if _, ok := err.(commands.ErrorExit); ok {
				ui.Exit()
				return nil
			} else if err != nil {
				return err
			} else {
				break
			}
		}
		return nil
	})
			return execCommand(aerc, ui, cmd)
		}, func(cmd string) []string {
			return getCompletions(aerc, cmd)
		})

	ui, err = libui.Initialize(conf, aerc)
	if err != nil {

M commands/account/account.go => commands/account/account.go +2 -2
@@ 8,9 8,9 @@ var (
	AccountCommands *commands.Commands
)

func register(name string, cmd commands.AercCommand) {
func register(cmd commands.Command) {
	if AccountCommands == nil {
		AccountCommands = commands.NewCommands()
	}
	AccountCommands.Register(name, cmd)
	AccountCommands.Register(cmd)
}

M commands/account/cf.go => commands/account/cf.go +12 -2
@@ 10,12 10,22 @@ var (
	history map[string]string
)

type ChangeFolder struct{}

func init() {
	history = make(map[string]string)
	register("cf", ChangeFolder)
	register(ChangeFolder{})
}

func (_ ChangeFolder) Aliases() []string {
	return []string{"cf"}
}

func (_ ChangeFolder) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func ChangeFolder(aerc *widgets.Aerc, args []string) error {
func (_ ChangeFolder) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 2 {
		return errors.New("Usage: cf <folder>")
	}

M commands/account/compose.go => commands/account/compose.go +12 -2
@@ 6,12 6,22 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Compose struct{}

func init() {
	register("compose", Compose)
	register(Compose{})
}

func (_ Compose) Aliases() []string {
	return []string{"compose"}
}

func (_ Compose) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

// TODO: Accept arguments for default headers, message body
func Compose(aerc *widgets.Aerc, args []string) error {
func (_ Compose) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: compose")
	}

M commands/account/mkdir.go => commands/account/mkdir.go +12 -2
@@ 10,11 10,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

type MakeDir struct{}

func init() {
	register("mkdir", Mkdir)
	register(MakeDir{})
}

func (_ MakeDir) Aliases() []string {
	return []string{"mkdir"}
}

func (_ MakeDir) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Mkdir(aerc *widgets.Aerc, args []string) error {
func (_ MakeDir) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 2 {
		return errors.New("Usage: :mkdir <name>")
	}

M commands/account/next-folder.go => commands/account/next-folder.go +14 -5
@@ 8,16 8,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type NextPrevFolder struct{}

func init() {
	register("next-folder", NextPrevFolder)
	register("prev-folder", NextPrevFolder)
	register(NextPrevFolder{})
}

func nextPrevFolderUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [n]", cmd))
func (_ NextPrevFolder) Aliases() []string {
	return []string{"next-folder", "prev-folder"}
}

func (_ NextPrevFolder) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func NextPrevFolder(aerc *widgets.Aerc, args []string) error {
func (_ NextPrevFolder) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) > 2 {
		return nextPrevFolderUsage(args[0])
	}


@@ 44,3 49,7 @@ func NextPrevFolder(aerc *widgets.Aerc, args []string) error {
	}
	return nil
}

func nextPrevFolderUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [n]", cmd))
}

M commands/account/next-result.go => commands/account/next-result.go +14 -5
@@ 7,16 7,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type NextPrevResult struct{}

func init() {
	register("next-result", NextPrevResult)
	register("prev-result", NextPrevResult)
	register(NextPrevResult{})
}

func nextPrevResultUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [<n>[%%]]", cmd))
func (_ NextPrevResult) Aliases() []string {
	return []string{"next-result", "prev-result"}
}

func (_ NextPrevResult) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func NextPrevResult(aerc *widgets.Aerc, args []string) error {
func (_ NextPrevResult) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) > 1 {
		return nextPrevResultUsage(args[0])
	}


@@ 39,3 44,7 @@ func NextPrevResult(aerc *widgets.Aerc, args []string) error {
	}
	return nil
}

func nextPrevResultUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [<n>[%%]]", cmd))
}

M commands/account/next.go => commands/account/next.go +14 -7
@@ 9,18 9,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type NextPrevMsg struct{}

func init() {
	register("next", NextPrevMessage)
	register("next-message", NextPrevMessage)
	register("prev", NextPrevMessage)
	register("prev-message", NextPrevMessage)
	register(NextPrevMsg{})
}

func nextPrevMessageUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [<n>[%%]]", cmd))
func (_ NextPrevMsg) Aliases() []string {
	return []string{"next", "next-message", "prev", "prev-message"}
}

func (_ NextPrevMsg) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func NextPrevMessage(aerc *widgets.Aerc, args []string) error {
func (_ NextPrevMsg) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) > 2 {
		return nextPrevMessageUsage(args[0])
	}


@@ 63,3 66,7 @@ func NextPrevMessage(aerc *widgets.Aerc, args []string) error {
	}
	return nil
}

func nextPrevMessageUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [<n>[%%]]", cmd))
}

M commands/account/pipe.go => commands/account/pipe.go +12 -2
@@ 8,11 8,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Pipe struct{}

func init() {
	register("pipe", Pipe)
	register(Pipe{})
}

func (_ Pipe) Aliases() []string {
	return []string{"pipe"}
}

func (_ Pipe) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Pipe(aerc *widgets.Aerc, args []string) error {
func (_ Pipe) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) < 2 {
		return errors.New("Usage: :pipe <cmd> [args...]")
	}

M commands/account/search.go => commands/account/search.go +12 -3
@@ 9,12 9,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type SearchFilter struct{}

func init() {
	register("search", SearchFilter)
	//register("filter", SearchFilter) // TODO
	register(SearchFilter{})
}

func (_ SearchFilter) Aliases() []string {
	return []string{"search"}
}

func (_ SearchFilter) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func SearchFilter(aerc *widgets.Aerc, args []string) error {
func (_ SearchFilter) Execute(aerc *widgets.Aerc, args []string) error {
	var (
		criteria *imap.SearchCriteria = imap.NewSearchCriteria()
	)

M commands/account/select.go => commands/account/select.go +12 -3
@@ 7,12 7,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type SelectMessage struct{}

func init() {
	register("select", SelectMessage)
	register("select-message", SelectMessage)
	register(SelectMessage{})
}

func (_ SelectMessage) Aliases() []string {
	return []string{"select", "select-message"}
}

func (_ SelectMessage) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func SelectMessage(aerc *widgets.Aerc, args []string) error {
func (_ SelectMessage) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 2 {
		return errors.New("Usage: :select-message <n>")
	}

M commands/account/view.go => commands/account/view.go +12 -3
@@ 6,12 6,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type ViewMessage struct{}

func init() {
	register("view", ViewMessage)
	register("view-message", ViewMessage)
	register(ViewMessage{})
}

func (_ ViewMessage) Aliases() []string {
	return []string{"view-message", "view"}
}

func (_ ViewMessage) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func ViewMessage(aerc *widgets.Aerc, args []string) error {
func (_ ViewMessage) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: view-message")
	}

M commands/cd.go => commands/cd.go +12 -2
@@ 12,11 12,21 @@ var (
	previousDir string
)

type ChangeDirectory struct{}

func init() {
	register("cd", ChangeDirectory)
	register(ChangeDirectory{})
}

func (_ ChangeDirectory) Aliases() []string {
	return []string{"cd"}
}

func (_ ChangeDirectory) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func ChangeDirectory(aerc *widgets.Aerc, args []string) error {
func (_ ChangeDirectory) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) < 1 || len(args) > 2 {
		return errors.New("Usage: cd [directory]")
	}

M commands/commands.go => commands/commands.go +69 -9
@@ 2,27 2,47 @@ package commands

import (
	"errors"
	"strings"

	"github.com/google/shlex"

	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type AercCommand func(aerc *widgets.Aerc, args []string) error
type Command interface {
	Aliases() []string
	Execute(*widgets.Aerc, []string) error
	Complete(*widgets.Aerc, []string) []string
}

type Commands map[string]AercCommand
type Commands map[string]Command

func NewCommands() *Commands {
	cmds := Commands(make(map[string]AercCommand))
	cmds := Commands(make(map[string]Command))
	return &cmds
}

func (cmds *Commands) dict() map[string]AercCommand {
	return map[string]AercCommand(*cmds)
func (cmds *Commands) dict() map[string]Command {
	return map[string]Command(*cmds)
}

func (cmds *Commands) Register(name string, cmd AercCommand) {
	cmds.dict()[name] = cmd
func (cmds *Commands) Names() []string {
	names := make([]string, 0)

	for k := range cmds.dict() {
		names = append(names, k)
	}
	return names
}

func (cmds *Commands) Register(cmd Command) {
	// TODO enforce unique aliases, until then, duplicate each
	if len(cmd.Aliases()) < 1 {
		return
	}
	for _, alias := range cmd.Aliases() {
		cmds.dict()[alias] = cmd
	}
}

type NoSuchCommand string


@@ 43,8 63,48 @@ func (cmds *Commands) ExecuteCommand(aerc *widgets.Aerc, cmd string) error {
	if len(args) == 0 {
		return errors.New("Expected a command.")
	}
	if fn, ok := cmds.dict()[args[0]]; ok {
		return fn(aerc, args)
	if cmd, ok := cmds.dict()[args[0]]; ok {
		return cmd.Execute(aerc, args)
	}
	return NoSuchCommand(args[0])
}

func (cmds *Commands) GetCompletions(aerc *widgets.Aerc, cmd string) []string {
	args, err := shlex.Split(cmd)
	if err != nil {
		return nil
	}

	if len(args) == 0 {
		return nil
	}

	if len(args) > 1 {
		if cmd, ok := cmds.dict()[args[0]]; ok {
			completions := cmd.Complete(aerc, args[1:])
			if completions != nil && len(completions) == 0 {
				return nil
			}

			options := make([]string, 0)
			for _, option := range completions {
				options = append(options, args[0]+" "+option)
			}
			return options
		}
		return nil
	}

	names := cmds.Names()
	options := make([]string, 0)
	for _, name := range names {
		if strings.HasPrefix(name, args[0]) {
			options = append(options, name)
		}
	}

	if len(options) > 0 {
		return options
	}
	return nil
}

M commands/compose/abort.go => commands/compose/abort.go +12 -2
@@ 6,11 6,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Abort struct{}

func init() {
	register("abort", CommandAbort)
	register(Abort{})
}

func (_ Abort) Aliases() []string {
	return []string{"abort"}
}

func (_ Abort) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func CommandAbort(aerc *widgets.Aerc, args []string) error {
func (_ Abort) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: abort")
	}

M commands/compose/compose.go => commands/compose/compose.go +2 -2
@@ 8,9 8,9 @@ var (
	ComposeCommands *commands.Commands
)

func register(name string, cmd commands.AercCommand) {
func register(cmd commands.Command) {
	if ComposeCommands == nil {
		ComposeCommands = commands.NewCommands()
	}
	ComposeCommands.Register(name, cmd)
	ComposeCommands.Register(cmd)
}

M commands/compose/edit.go => commands/compose/edit.go +12 -2
@@ 6,11 6,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Edit struct{}

func init() {
	register("edit", CommandEdit)
	register(Edit{})
}

func (_ Edit) Aliases() []string {
	return []string{"edit"}
}

func (_ Edit) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func CommandEdit(aerc *widgets.Aerc, args []string) error {
func (_ Edit) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: edit")
	}

M commands/compose/next-field.go => commands/compose/next-field.go +14 -5
@@ 7,16 7,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type NextPrevField struct{}

func init() {
	register("next-field", NextPrevField)
	register("prev-field", NextPrevField)
	register(NextPrevField{})
}

func nextPrevFieldUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s", cmd))
func (_ NextPrevField) Aliases() []string {
	return []string{"next-field", "prev-field"}
}

func (_ NextPrevField) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func NextPrevField(aerc *widgets.Aerc, args []string) error {
func (_ NextPrevField) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) > 2 {
		return nextPrevFieldUsage(args[0])
	}


@@ 28,3 33,7 @@ func NextPrevField(aerc *widgets.Aerc, args []string) error {
	}
	return nil
}

func nextPrevFieldUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s", cmd))
}

M commands/compose/send.go => commands/compose/send.go +13 -3
@@ 20,13 20,23 @@ import (
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

type Send struct{}

func init() {
	register("send", SendMessage)
	register(Send{})
}

func (_ Send) Aliases() []string {
	return []string{"send"}
}

func (_ Send) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func SendMessage(aerc *widgets.Aerc, args []string) error {
func (_ Send) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) > 1 {
		return errors.New("Usage: send-message")
		return errors.New("Usage: send")
	}
	composer, _ := aerc.SelectedTab().(*widgets.Composer)
	config := composer.Config()

M commands/global.go => commands/global.go +2 -2
@@ 4,9 4,9 @@ var (
	GlobalCommands *Commands
)

func register(name string, cmd AercCommand) {
func register(cmd Command) {
	if GlobalCommands == nil {
		GlobalCommands = NewCommands()
	}
	GlobalCommands.Register(name, cmd)
	GlobalCommands.Register(cmd)
}

M commands/help.go => commands/help.go +13 -3
@@ 6,16 6,26 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Help struct{}

func init() {
	register("help", Help)
	register(Help{})
}

func (_ Help) Aliases() []string {
	return []string{"help"}
}

func (_ Help) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Help(aerc *widgets.Aerc, args []string) error {
func (_ Help) Execute(aerc *widgets.Aerc, args []string) error {
	page := "aerc"
	if len(args) == 2 {
		page = "aerc-" + args[1]
	} else if len(args) > 2 {
		return errors.New("Usage: help [topic]")
	}
	return Term(aerc, []string{"term", "man", page})
	return TermCore(aerc, []string{"term", "man", page})
}

M commands/msg/archive.go => commands/msg/archive.go +12 -2
@@ 18,11 18,21 @@ const (
	ARCHIVE_MONTH = "month"
)

type Archive struct{}

func init() {
	register("archive", Archive)
	register(Archive{})
}

func (_ Archive) Aliases() []string {
	return []string{"archive"}
}

func (_ Archive) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Archive(aerc *widgets.Aerc, args []string) error {
func (_ Archive) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 2 {
		return errors.New("Usage: archive <flat|year|month>")
	}

M commands/msg/copy.go => commands/msg/copy.go +12 -3
@@ 11,12 11,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

type Copy struct{}

func init() {
	register("cp", Copy)
	register("copy", Copy)
	register(Copy{})
}

func (_ Copy) Aliases() []string {
	return []string{"copy"}
}

func (_ Copy) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Copy(aerc *widgets.Aerc, args []string) error {
func (_ Copy) Execute(aerc *widgets.Aerc, args []string) error {
	opts, optind, err := getopt.Getopts(args, "p")
	if err != nil {
		return err

M commands/msg/delete.go => commands/msg/delete.go +12 -3
@@ 10,12 10,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

type Delete struct{}

func init() {
	register("delete", DeleteMessage)
	register("delete-message", DeleteMessage)
	register(Delete{})
}

func (_ Delete) Aliases() []string {
	return []string{"delete", "delete-message"}
}

func (_ Delete) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func DeleteMessage(aerc *widgets.Aerc, args []string) error {
func (_ Delete) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: :delete")
	}

M commands/msg/move.go => commands/msg/move.go +12 -3
@@ 11,12 11,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

type Move struct{}

func init() {
	register("mv", Move)
	register("move", Move)
	register(Move{})
}

func (_ Move) Aliases() []string {
	return []string{"mv", "move"}
}

func (_ Move) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Move(aerc *widgets.Aerc, args []string) error {
func (_ Move) Execute(aerc *widgets.Aerc, args []string) error {
	opts, optind, err := getopt.Getopts(args, "p")
	if err != nil {
		return err

M commands/msg/msg.go => commands/msg/msg.go +2 -2
@@ 8,9 8,9 @@ var (
	MessageCommands *commands.Commands
)

func register(name string, cmd commands.AercCommand) {
func register(cmd commands.Command) {
	if MessageCommands == nil {
		MessageCommands = commands.NewCommands()
	}
	MessageCommands.Register(name, cmd)
	MessageCommands.Register(cmd)
}

M commands/msg/read.go => commands/msg/read.go +12 -3
@@ 10,12 10,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

type Read struct{}

func init() {
	register("read", Read)
	register("unread", Read)
	register(Read{})
}

func (_ Read) Aliases() []string {
	return []string{"read", "unread"}
}

func (_ Read) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Read(aerc *widgets.Aerc, args []string) error {
func (_ Read) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: " + args[0])
	}

M commands/msg/reply.go => commands/msg/reply.go +12 -3
@@ 18,12 18,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type reply struct{}

func init() {
	register("reply", Reply)
	register("forward", Reply)
	register(reply{})
}

func (_ reply) Aliases() []string {
	return []string{"reply", "forward"}
}

func (_ reply) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Reply(aerc *widgets.Aerc, args []string) error {
func (_ reply) Execute(aerc *widgets.Aerc, args []string) error {
	opts, optind, err := getopt.Getopts(args, "aq")
	if err != nil {
		return err

M commands/msgview/close.go => commands/msgview/close.go +12 -2
@@ 6,11 6,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Close struct{}

func init() {
	register("close", CommandClose)
	register(Close{})
}

func (_ Close) Aliases() []string {
	return []string{"close"}
}

func (_ Close) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func CommandClose(aerc *widgets.Aerc, args []string) error {
func (_ Close) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: close")
	}

M commands/msgview/msgview.go => commands/msgview/msgview.go +2 -2
@@ 8,9 8,9 @@ var (
	MessageViewCommands *commands.Commands
)

func register(name string, cmd commands.AercCommand) {
func register(cmd commands.Command) {
	if MessageViewCommands == nil {
		MessageViewCommands = commands.NewCommands()
	}
	MessageViewCommands.Register(name, cmd)
	MessageViewCommands.Register(cmd)
}

M commands/msgview/next-part.go => commands/msgview/next-part.go +14 -5
@@ 8,16 8,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type NextPrevPart struct{}

func init() {
	register("next-part", NextPrevPart)
	register("prev-part", NextPrevPart)
	register(NextPrevPart{})
}

func nextPrevPartUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [n]", cmd))
func (_ NextPrevPart) Aliases() []string {
	return []string{"next-part", "prev-part"}
}

func (_ NextPrevPart) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func NextPrevPart(aerc *widgets.Aerc, args []string) error {
func (_ NextPrevPart) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) > 2 {
		return nextPrevPartUsage(args[0])
	}


@@ 41,3 46,7 @@ func NextPrevPart(aerc *widgets.Aerc, args []string) error {
	}
	return nil
}

func nextPrevPartUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [n]", cmd))
}

M commands/msgview/next.go => commands/msgview/next.go +12 -5
@@ 6,14 6,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type NextPrevMsg struct{}

func init() {
	register("next", NextPrevMessage)
	register("next-message", NextPrevMessage)
	register("prev", NextPrevMessage)
	register("prev-message", NextPrevMessage)
	register(NextPrevMsg{})
}

func (_ NextPrevMsg) Aliases() []string {
	return []string{"next", "next-message", "prev", "prev-message"}
}

func (_ NextPrevMsg) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func NextPrevMessage(aerc *widgets.Aerc, args []string) error {
func (_ NextPrevMsg) Execute(aerc *widgets.Aerc, args []string) error {
	mv, _ := aerc.SelectedTab().(*widgets.MessageViewer)
	acct := mv.SelectedAccount()
	store := mv.Store()

M commands/msgview/open.go => commands/msgview/open.go +12 -2
@@ 14,11 14,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Open struct{}

func init() {
	register("open", Open)
	register(Open{})
}

func (_ Open) Aliases() []string {
	return []string{"open"}
}

func (_ Open) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Open(aerc *widgets.Aerc, args []string) error {
func (_ Open) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: open")
	}

M commands/msgview/pipe.go => commands/msgview/pipe.go +12 -2
@@ 12,11 12,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Pipe struct{}

func init() {
	register("pipe", Pipe)
	register(Pipe{})
}

func (_ Pipe) Aliases() []string {
	return []string{"pipe"}
}

func (_ Pipe) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Pipe(aerc *widgets.Aerc, args []string) error {
func (_ Pipe) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) < 2 {
		return errors.New("Usage: :pipe <cmd> [args...]")
	}

M commands/msgview/save.go => commands/msgview/save.go +14 -2
@@ 16,19 16,31 @@ import (
	"github.com/mitchellh/go-homedir"
)

type Save struct{}

func init() {
	register("save", Save)
	register(Save{})
}

func (_ Save) Aliases() []string {
	return []string{"save"}
}

func (_ Save) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Save(aerc *widgets.Aerc, args []string) error {
func (_ Save) Execute(aerc *widgets.Aerc, args []string) error {
	opts, optind, err := getopt.Getopts(args, "p")
	if err != nil {
		return err
	}

	var (
		mkdirs bool
		path   string
	)

	for _, opt := range opts {
		switch opt.Option {
		case 'p':

M commands/msgview/toggle-headers.go => commands/msgview/toggle-headers.go +14 -4
@@ 7,15 7,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type ToggleHeaders struct{}

func init() {
	register("toggle-headers", ToggleHeaders)
	register(ToggleHeaders{})
}

func toggleHeadersUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s", cmd))
func (_ ToggleHeaders) Aliases() []string {
	return []string{"toggle-headers"}
}

func (_ ToggleHeaders) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func ToggleHeaders(aerc *widgets.Aerc, args []string) error {
func (_ ToggleHeaders) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) > 1 {
		return toggleHeadersUsage(args[0])
	}


@@ 23,3 29,7 @@ func ToggleHeaders(aerc *widgets.Aerc, args []string) error {
	mv.ToggleHeaders()
	return nil
}

func toggleHeadersUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s", cmd))
}

M commands/new-account.go => commands/new-account.go +12 -2
@@ 7,11 7,21 @@ import (
	"git.sr.ht/~sircmpwn/getopt"
)

type NewAccount struct{}

func init() {
	register("new-account", CommandNewAccount)
	register(NewAccount{})
}

func (_ NewAccount) Aliases() []string {
	return []string{"new-account"}
}

func (_ NewAccount) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func CommandNewAccount(aerc *widgets.Aerc, args []string) error {
func (_ NewAccount) Execute(aerc *widgets.Aerc, args []string) error {
	opts, _, err := getopt.Getopts(args, "t")
	if err != nil {
		return errors.New("Usage: new-account [-t]")

M commands/next-tab.go => commands/next-tab.go +14 -5
@@ 8,16 8,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type NextPrevTab struct{}

func init() {
	register("next-tab", NextPrevTab)
	register("prev-tab", NextPrevTab)
	register(NextPrevTab{})
}

func nextPrevTabUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [n]", cmd))
func (_ NextPrevTab) Aliases() []string {
	return []string{"next-tab", "prev-tab"}
}

func (_ NextPrevTab) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func NextPrevTab(aerc *widgets.Aerc, args []string) error {
func (_ NextPrevTab) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) > 2 {
		return nextPrevTabUsage(args[0])
	}


@@ 40,3 45,7 @@ func NextPrevTab(aerc *widgets.Aerc, args []string) error {
	}
	return nil
}

func nextPrevTabUsage(cmd string) error {
	return errors.New(fmt.Sprintf("Usage: %s [n]", cmd))
}

M commands/pwd.go => commands/pwd.go +12 -2
@@ 8,11 8,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type PrintWorkDir struct{}

func init() {
	register("pwd", PrintWorkDirectory)
	register(PrintWorkDir{})
}

func (_ PrintWorkDir) Aliases() []string {
	return []string{"pwd"}
}

func (_ PrintWorkDir) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func PrintWorkDirectory(aerc *widgets.Aerc, args []string) error {
func (_ PrintWorkDir) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: pwd")
	}

M commands/quit.go => commands/quit.go +12 -2
@@ 6,8 6,18 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Quit struct{}

func init() {
	register("quit", CommandQuit)
	register(Quit{})
}

func (_ Quit) Aliases() []string {
	return []string{"quit", "exit"}
}

func (_ Quit) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

type ErrorExit int


@@ 16,7 26,7 @@ func (err ErrorExit) Error() string {
	return "exit"
}

func CommandQuit(aerc *widgets.Aerc, args []string) error {
func (_ Quit) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: quit")
	}

M commands/term.go => commands/term.go +17 -2
@@ 10,11 10,22 @@ import (
	"github.com/riywo/loginshell"
)

type Term struct{}

func init() {
	register("term", Term)
	register(Term{})
}

func (_ Term) Aliases() []string {
	return []string{"terminal", "term"}
}

func (_ Term) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func Term(aerc *widgets.Aerc, args []string) error {
// The help command is an alias for `term man` thus Term requires a simple func
func TermCore(aerc *widgets.Aerc, args []string) error {
	if len(args) == 1 {
		shell, err := loginshell.Shell()
		if err != nil {


@@ 43,3 54,7 @@ func Term(aerc *widgets.Aerc, args []string) error {
	}
	return nil
}

func (_ Term) Execute(aerc *widgets.Aerc, args []string) error {
	return TermCore(aerc, args)
}

M commands/terminal/close.go => commands/terminal/close.go +12 -2
@@ 6,11 6,21 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Close struct{}

func init() {
	register("close", CommandClose)
	register(Close{})
}

func (_ Close) Aliases() []string {
	return []string{"close"}
}

func (_ Close) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func CommandClose(aerc *widgets.Aerc, args []string) error {
func (_ Close) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: close")
	}

M commands/terminal/terminal.go => commands/terminal/terminal.go +2 -2
@@ 8,9 8,9 @@ var (
	TerminalCommands *commands.Commands
)

func register(name string, cmd commands.AercCommand) {
func register(cmd commands.Command) {
	if TerminalCommands == nil {
		TerminalCommands = commands.NewCommands()
	}
	TerminalCommands.Register(name, cmd)
	TerminalCommands.Register(cmd)
}

M lib/ui/textinput.go => lib/ui/textinput.go +8 -0
@@ 46,6 46,14 @@ func (ti *TextInput) String() string {
	return string(ti.text)
}

func (ti *TextInput) StringLeft() string {
	return string(ti.text[:ti.index])
}

func (ti *TextInput) StringRight() string {
	return string(ti.text[ti.index:])
}

func (ti *TextInput) Set(value string) {
	ti.text = []rune(value)
	ti.index = len(ti.text)

M widgets/aerc.go => widgets/aerc.go +5 -1
@@ 14,6 14,7 @@ import (
type Aerc struct {
	accounts    map[string]*AccountView
	cmd         func(cmd string) error
	complete    func(cmd string) []string
	conf        *config.AercConfig
	focused     libui.Interactive
	grid        *libui.Grid


@@ 26,7 27,7 @@ type Aerc struct {
}

func NewAerc(conf *config.AercConfig, logger *log.Logger,
	cmd func(cmd string) error) *Aerc {
	cmd func(cmd string) error, complete func(cmd string) []string) *Aerc {

	tabs := libui.NewTabs()



@@ 49,6 50,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
		accounts:   make(map[string]*AccountView),
		conf:       conf,
		cmd:        cmd,
		complete:   complete,
		grid:       grid,
		logger:     logger,
		statusbar:  statusbar,


@@ 289,6 291,8 @@ func (aerc *Aerc) BeginExCommand() {
	}, func() {
		aerc.statusbar.Pop()
		aerc.focus(previous)
	}, func(cmd string) []string {
		return aerc.complete(cmd)
	})
	aerc.statusbar.Push(exline)
	aerc.focus(exline)

M widgets/dirlist.go => widgets/dirlist.go +4 -0
@@ 40,6 40,10 @@ func NewDirectoryList(acctConf *config.AccountConfig, uiConf *config.UIConfig,
	return dirlist
}

func (dirlist *DirectoryList) List() []string {
	return dirlist.dirs
}

func (dirlist *DirectoryList) UpdateList(done func(dirs []string)) {
	var dirs []string
	dirlist.worker.PostAction(

M widgets/exline.go => widgets/exline.go +17 -7
@@ 8,17 8,21 @@ import (

type ExLine struct {
	ui.Invalidatable
	cancel func()
	commit func(cmd string)
	input  *ui.TextInput
	cancel      func()
	commit      func(cmd string)
	tabcomplete func(cmd string) []string
	input       *ui.TextInput
}

func NewExLine(commit func(cmd string), cancel func()) *ExLine {
func NewExLine(commit func(cmd string), cancel func(),
	tabcomplete func(cmd string) []string) *ExLine {

	input := ui.NewTextInput("").Prompt(":")
	exline := &ExLine{
		cancel: cancel,
		commit: commit,
		input:  input,
		cancel:      cancel,
		commit:      commit,
		tabcomplete: tabcomplete,
		input:       input,
	}
	input.OnInvalidate(func(d ui.Drawable) {
		exline.Invalidate()


@@ 48,6 52,12 @@ func (ex *ExLine) Event(event tcell.Event) bool {
		case tcell.KeyEsc, tcell.KeyCtrlC:
			ex.input.Focus(false)
			ex.cancel()
		case tcell.KeyTab:
			complete := ex.tabcomplete(ex.input.StringLeft())
			if len(complete) == 1 {
				ex.input.Set(complete[0] + " " + ex.input.StringRight())
			}
			ex.Invalidate()
		default:
			return ex.input.Event(event)
		}