summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorGregory Mullen <greg@cmdline.org>2019-06-27 10:33:11 -0700
committerDrew DeVault <sir@cmpwn.com>2019-06-29 14:24:19 -0400
commit2a0961701c4cabecc53d134ed1782e5612e64580 (patch)
tree57952ac82fb7104113ca7fc0e25dc3d225f77ea7
parent177651bddab145c8a56cdfeb0d57b5fd95a6d0e2 (diff)
downloadaerc-2a0961701c4cabecc53d134ed1782e5612e64580.tar.gz
Implement basic tab completion support
Tab completion currently only works on commands. Contextual completion
will be added in the future.
-rw-r--r--aerc.go60
-rw-r--r--commands/account/account.go4
-rw-r--r--commands/account/cf.go14
-rw-r--r--commands/account/compose.go14
-rw-r--r--commands/account/mkdir.go14
-rw-r--r--commands/account/next-folder.go19
-rw-r--r--commands/account/next-result.go19
-rw-r--r--commands/account/next.go21
-rw-r--r--commands/account/pipe.go14
-rw-r--r--commands/account/search.go15
-rw-r--r--commands/account/select.go15
-rw-r--r--commands/account/view.go15
-rw-r--r--commands/cd.go14
-rw-r--r--commands/commands.go78
-rw-r--r--commands/compose/abort.go14
-rw-r--r--commands/compose/compose.go4
-rw-r--r--commands/compose/edit.go14
-rw-r--r--commands/compose/next-field.go19
-rw-r--r--commands/compose/send.go16
-rw-r--r--commands/global.go4
-rw-r--r--commands/help.go16
-rw-r--r--commands/msg/archive.go14
-rw-r--r--commands/msg/copy.go15
-rw-r--r--commands/msg/delete.go15
-rw-r--r--commands/msg/move.go15
-rw-r--r--commands/msg/msg.go4
-rw-r--r--commands/msg/read.go15
-rw-r--r--commands/msg/reply.go15
-rw-r--r--commands/msgview/close.go14
-rw-r--r--commands/msgview/msgview.go4
-rw-r--r--commands/msgview/next-part.go19
-rw-r--r--commands/msgview/next.go17
-rw-r--r--commands/msgview/open.go14
-rw-r--r--commands/msgview/pipe.go14
-rw-r--r--commands/msgview/save.go16
-rw-r--r--commands/msgview/toggle-headers.go18
-rw-r--r--commands/new-account.go14
-rw-r--r--commands/next-tab.go19
-rw-r--r--commands/pwd.go14
-rw-r--r--commands/quit.go14
-rw-r--r--commands/term.go19
-rw-r--r--commands/terminal/close.go14
-rw-r--r--commands/terminal/terminal.go4
-rw-r--r--lib/ui/textinput.go8
-rw-r--r--widgets/aerc.go6
-rw-r--r--widgets/dirlist.go4
-rw-r--r--widgets/exline.go24
47 files changed, 598 insertions, 154 deletions
diff --git a/aerc.go b/aerc.go
index 20e2bb1..a248e18 100644
--- a/aerc.go
+++ b/aerc.go
@@ -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 {
diff --git a/commands/account/account.go b/commands/account/account.go
index c590c8a..9c90087 100644
--- a/commands/account/account.go
+++ b/commands/account/account.go
@@ -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)
 }
diff --git a/commands/account/cf.go b/commands/account/cf.go
index 2816473..197e956 100644
--- a/commands/account/cf.go
+++ b/commands/account/cf.go
@@ -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>")
 	}
diff --git a/commands/account/compose.go b/commands/account/compose.go
index aeb415e..cafba78 100644
--- a/commands/account/compose.go
+++ b/commands/account/compose.go
@@ -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")
 	}
diff --git a/commands/account/mkdir.go b/commands/account/mkdir.go
index d245821..be9b14a 100644
--- a/commands/account/mkdir.go
+++ b/commands/account/mkdir.go
@@ -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>")
 	}
diff --git a/commands/account/next-folder.go b/commands/account/next-folder.go
index 6ad3d54..414e606 100644
--- a/commands/account/next-folder.go
+++ b/commands/account/next-folder.go
@@ -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))
+}
diff --git a/commands/account/next-result.go b/commands/account/next-result.go
index d89de56..24d53be 100644
--- a/commands/account/next-result.go
+++ b/commands/account/next-result.go
@@ -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))
+}
diff --git a/commands/account/next.go b/commands/account/next.go
index 3b9260c..f306b48 100644
--- a/commands/account/next.go
+++ b/commands/account/next.go
@@ -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))
+}
diff --git a/commands/account/pipe.go b/commands/account/pipe.go
index d3cc80a..a68ef64 100644
--- a/commands/account/pipe.go
+++ b/commands/account/pipe.go
@@ -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...]")
 	}
diff --git a/commands/account/search.go b/commands/account/search.go
index 513ad43..a8640dc 100644
--- a/commands/account/search.go
+++ b/commands/account/search.go
@@ -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()
 	)
diff --git a/commands/account/select.go b/commands/account/select.go
index 707f6c9..70e08ac 100644
--- a/commands/account/select.go
+++ b/commands/account/select.go
@@ -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>")
 	}
diff --git a/commands/account/view.go b/commands/account/view.go
index f7f3ec6..cec65aa 100644
--- a/commands/account/view.go
+++ b/commands/account/view.go
@@ -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")
 	}
diff --git a/commands/cd.go b/commands/cd.go
index fb495d7..8c9cb21 100644
--- a/commands/cd.go
+++ b/commands/cd.go
@@ -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]")
 	}
diff --git a/commands/commands.go b/commands/commands.go
index 04462d2..8d50b41 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -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
+}
diff --git a/commands/compose/abort.go b/commands/compose/abort.go
index c60793c..4c121d7 100644
--- a/commands/compose/abort.go
+++ b/commands/compose/abort.go
@@ -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")
 	}
diff --git a/commands/compose/compose.go b/commands/compose/compose.go
index 35a2ed7..d61696e 100644
--- a/commands/compose/compose.go
+++ b/commands/compose/compose.go
@@ -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)
 }
diff --git a/commands/compose/edit.go b/commands/compose/edit.go
index 18ba481..e888350 100644
--- a/commands/compose/edit.go
+++ b/commands/compose/edit.go
@@ -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")
 	}
diff --git a/commands/compose/next-field.go b/commands/compose/next-field.go
index a10aa32..3496dfd 100644
--- a/commands/compose/next-field.go
+++ b/commands/compose/next-field.go
@@ -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))
+}
diff --git a/commands/compose/send.go b/commands/compose/send.go
index c4bff11..26df82a 100644
--- a/commands/compose/send.go
+++ b/commands/compose/send.go
@@ -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()
diff --git a/commands/global.go b/commands/global.go
index c24869a..459192f 100644
--- a/commands/global.go
+++ b/commands/global.go
@@ -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)
 }
diff --git a/commands/help.go b/commands/help.go
index e269fcf..c4ed4ff 100644
--- a/commands/help.go
+++ b/commands/help.go
@@ -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})
 }
diff --git a/commands/msg/archive.go b/commands/msg/archive.go
index 4fe7330..40fb48b 100644
--- a/commands/msg/archive.go
+++ b/commands/msg/archive.go
@@ -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>")
 	}
diff --git a/commands/msg/copy.go b/commands/msg/copy.go
index 0735e98..4d65d24 100644
--- a/commands/msg/copy.go
+++ b/commands/msg/copy.go
@@ -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
diff --git a/commands/msg/delete.go b/commands/msg/delete.go
index ee3dd29..5a72fc9 100644
--- a/commands/msg/delete.go
+++ b/commands/msg/delete.go
@@ -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")
 	}
diff --git a/commands/msg/move.go b/commands/msg/move.go
index 2367076..45199b6 100644
--- a/commands/msg/move.go
+++ b/commands/msg/move.go
@@ -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
diff --git a/commands/msg/msg.go b/commands/msg/msg.go
index 73755aa..ecf2102 100644
--- a/commands/msg/msg.go
+++ b/commands/msg/msg.go
@@ -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)
 }
diff --git a/commands/msg/read.go b/commands/msg/read.go
index 9844797..db463f1 100644
--- a/commands/msg/read.go
+++ b/commands/msg/read.go
@@ -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])
 	}
diff --git a/commands/msg/reply.go b/commands/msg/reply.go
index 51f6584..7a64d21 100644
--- a/commands/msg/reply.go
+++ b/commands/msg/reply.go
@@ -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
diff --git a/commands/msgview/close.go b/commands/msgview/close.go
index 4ce15c4..6a7eb0a 100644
--- a/commands/msgview/close.go
+++ b/commands/msgview/close.go
@@ -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")
 	}
diff --git a/commands/msgview/msgview.go b/commands/msgview/msgview.go
index f0e42ad..d60549e 100644
--- a/commands/msgview/msgview.go
+++ b/commands/msgview/msgview.go
@@ -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)
 }
diff --git a/commands/msgview/next-part.go b/commands/msgview/next-part.go
index fcf8f19..8f25e02 100644
--- a/commands/msgview/next-part.go
+++ b/commands/msgview/next-part.go
@@ -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))
+}
diff --git a/commands/msgview/next.go b/commands/msgview/next.go
index 0c86839..82fb12f 100644
--- a/commands/msgview/next.go
+++ b/commands/msgview/next.go
@@ -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()
diff --git a/commands/msgview/open.go b/commands/msgview/open.go
index 1a33cec..d1b3238 100644
--- a/commands/msgview/open.go
+++ b/commands/msgview/open.go
@@ -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")
 	}
diff --git a/commands/msgview/pipe.go b/commands/msgview/pipe.go
index a84cdf5..56c125b 100644
--- a/commands/msgview/pipe.go
+++ b/commands/msgview/pipe.go
@@ -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...]")
 	}
diff --git a/commands/msgview/save.go b/commands/msgview/save.go
index 59d94b2..93fa83f 100644
--- a/commands/msgview/save.go
+++ b/commands/msgview/save.go
@@ -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':
diff --git a/commands/msgview/toggle-headers.go b/commands/msgview/toggle-headers.go
index fc29042..d9d7eba 100644
--- a/commands/msgview/toggle-headers.go
+++ b/commands/msgview/toggle-headers.go
@@ -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))
+}
diff --git a/commands/new-account.go b/commands/new-account.go
index 9e70dec..8d2fef5 100644
--- a/commands/new-account.go
+++ b/commands/new-account.go
@@ -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]")
diff --git a/commands/next-tab.go b/commands/next-tab.go
index fb5b664..aa6e1dc 100644
--- a/commands/next-tab.go
+++ b/commands/next-tab.go
@@ -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))
+}
diff --git a/commands/pwd.go b/commands/pwd.go
index b4f3eb1..4903c29 100644
--- a/commands/pwd.go
+++ b/commands/pwd.go
@@ -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")
 	}
diff --git a/commands/quit.go b/commands/quit.go
index c9d83b7..535ee61 100644
--- a/commands/quit.go
+++ b/commands/quit.go
@@ -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")
 	}
diff --git a/commands/term.go b/commands/term.go
index 3f70d67..8575019 100644
--- a/commands/term.go
+++ b/commands/term.go
@@ -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)
+}
diff --git a/commands/terminal/close.go b/commands/terminal/close.go
index 0ea7a5a..35c4799 100644
--- a/commands/terminal/close.go
+++ b/commands/terminal/close.go
@@ -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")
 	}
diff --git a/commands/terminal/terminal.go b/commands/terminal/terminal.go
index fb1583f..710d796 100644
--- a/commands/terminal/terminal.go
+++ b/commands/terminal/terminal.go
@@ -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)
 }
diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go
index 892646d..a3127e5 100644
--- a/lib/ui/textinput.go
+++ b/lib/ui/textinput.go
@@ -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)
diff --git a/widgets/aerc.go b/widgets/aerc.go
index 8aa1e2c..ade56d1 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -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)
diff --git a/widgets/dirlist.go b/widgets/dirlist.go
index 2b4773a..71cf79d 100644
--- a/widgets/dirlist.go
+++ b/widgets/dirlist.go
@@ -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(
diff --git a/widgets/exline.go b/widgets/exline.go
index ff18d13..e984ee1 100644
--- a/widgets/exline.go
+++ b/widgets/exline.go
@@ -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)
 		}