7b45392a4aa110106e43bf98eb59d183a5ff8870 — Gregory Mullen 4 months ago ce475e4
Implement basic tab completion support

The move command is the only one currently implemented
M aerc.go => aerc.go +30 -17
@@ 78,27 78,40 @@ 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 {
+ 
+ 	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
+ 					}
+ 					continue
+ 				} else if _, ok := err.(commands.ErrorExit); ok {
+ 					ui.Exit()
+ 					return nil
+ 				} else if err != nil {
  					return err
  				} else {
- 					continue
+ 					break
  				}
- 			} else if _, ok := err.(commands.ErrorExit); ok {
- 				ui.Exit()
- 				return nil
- 			} else if err != nil {
- 				return err
- 			} else {
- 				break
  			}
- 		}
- 		return nil
- 	})
+ 			return nil
+ 		}, func (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
+ 		})
  
  	ui, err = libui.Initialize(conf, aerc)
  	if err != nil {

M commands/account/account.go => commands/account/account.go +2 -2
@@ 8,9 8,9 @@ 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 @@ 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 -3
@@ 6,13 6,22 @@ "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 {
- 	if len(args) != 1 {
+ func (_ Compose) Execute (aerc *widgets.Aerc, args []string) error {	if len(args) != 1 {
  		return errors.New("Usage: compose")
  	}
  	acct := aerc.SelectedAccount()

M commands/account/mkdir.go => commands/account/mkdir.go +12 -2
@@ 10,11 10,21 @@ "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 @@ "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 @@ }
  	return nil
  }
+ 
+ func nextPrevFolderUsage(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 @@ "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])
  	}


@@ 57,3 60,7 @@ }
  	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 @@ "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/select.go => commands/account/select.go +12 -3
@@ 7,12 7,21 @@ "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-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 @@ "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 @@ 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 +70 -9
@@ 2,27 2,47 @@   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) Names() []string {
+ 	names := make([]string, 0)
+ 
+ 	for k := range cmds.dict() {
+ 		names = append(names, k)
+ 	}
+ 	return names
  }
  
- func (cmds *Commands) Register(name string, cmd AercCommand) {
- 	cmds.dict()[name] = cmd
+ 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,49 @@ 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 @@ "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 @@ 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 @@ "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 @@ "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 @@ }
  	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 @@ "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 @@ 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 @@ "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 @@ 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 @@ "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 +11 -3
@@ 9,13 9,21 @@ "git.sr.ht/~sircmpwn/aerc/widgets"
  	"git.sr.ht/~sircmpwn/aerc/worker/types"
  )
+ type Delete struct{}
  
  func init() {
- 	register("delete", DeleteMessage)
- 	register("delete-message", DeleteMessage)
+ 	register(Delete{})
  }
  
- func DeleteMessage(aerc *widgets.Aerc, args []string) error {
+ func (_ Delete) Aliases() []string {
+ 	return []string{"delete"}
+ }
+ 
+ func (_ Delete) Complete(aerc *widgets.Aerc, args []string) []string {
+ 	return nil
+ }
+ 
+ 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 +26 -3
@@ 3,6 3,7 @@ import (
  	"errors"
  	"time"
+ 	"strings"
  
  	"git.sr.ht/~sircmpwn/getopt"
  	"github.com/gdamore/tcell"


@@ 11,12 12,34 @@ "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{"move"}
+ }
+ 
+ const caps string = "ABCDEFGHIJKLMNOPQRSTUVXYZ"
+ 
+ func (_ Move) Complete(aerc *widgets.Aerc, args []string) []string {
+ 	out := make([]string, 0)
+ 	for _, dir := range aerc.SelectedAccount().Directories().List() {
+ 		test := dir
+ 		if strings.IndexAny(args[0], caps) == -1 {
+ 			test = strings.ToLower(dir)
+ 		}
+ 
+ 		if strings.HasPrefix(test, args[0]) {
+ 			out = append(out, dir)
+ 		}
+ 	}
+ 	return out
  }
  
- 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 @@ 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 @@ "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 @@ "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 @@ "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 @@ 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 @@ "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 @@ }
  	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 @@ "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 @@ "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 @@ "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
@@ 15,19 15,31 @@ "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 +13 -4
@@ 6,16 6,21 @@   	"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 28,7 @@ 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 @@ "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 +15 -5
@@ 8,16 8,21 @@ "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,8 @@ }
  	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 @@ "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 @@ "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 @@ 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 @@ "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 @@ }
  	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 @@ "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 @@ 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 @@ 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 +13 -9
@@ 14,6 14,7 @@ type Aerc struct {
  	accounts    map[string]*AccountView
  	cmd         func(cmd string) error
+ 	tabcomplete func(cmd string) []string
  	conf        *config.AercConfig
  	focused     libui.Interactive
  	grid        *libui.Grid


@@ 26,7 27,7 @@ }
  
  func NewAerc(conf *config.AercConfig, logger *log.Logger,
- 	cmd func(cmd string) error) *Aerc {
+ 	cmd func(cmd string) error, tabcomplete func(cmd string) []string) *Aerc {
  
  	tabs := libui.NewTabs()
  


@@ 46,14 47,15 @@ grid.AddChild(statusbar).At(2, 0)
  
  	aerc := &Aerc{
- 		accounts:   make(map[string]*AccountView),
- 		conf:       conf,
- 		cmd:        cmd,
- 		grid:       grid,
- 		logger:     logger,
- 		statusbar:  statusbar,
- 		statusline: statusline,
- 		tabs:       tabs,
+ 		accounts:    make(map[string]*AccountView),
+ 		conf:        conf,
+ 		cmd:         cmd,
+ 		tabcomplete: tabcomplete,
+ 		grid:        grid,
+ 		logger:      logger,
+ 		statusbar:   statusbar,
+ 		statusline:  statusline,
+ 		tabs:        tabs,
  	}
  
  	for i, acct := range conf.Accounts {


@@ 285,6 287,8 @@ }, func() {
  		aerc.statusbar.Pop()
  		aerc.focus(previous)
+ 	}, func(cmd string) []string {
+ 		return aerc.tabcomplete(cmd)
  	})
  	aerc.statusbar.Push(exline)
  	aerc.focus(exline)

M widgets/dirlist.go => widgets/dirlist.go +4 -0
@@ 40,6 40,10 @@ 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 +12 -1
@@ 10,15 10,20 @@ ui.Invalidatable
  	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,
+ 		tabcomplete: tabcomplete,
  		input:  input,
+ 
  	}
  	input.OnInvalidate(func(d ui.Drawable) {
  		exline.Invalidate()


@@ 48,6 53,12 @@ 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)
  		}