diff --git a/aerc.go b/aerc.go index 2420b44..033de7b 100644 --- a/aerc.go +++ b/aerc.go @@ -148,7 +148,7 @@ func main() { return execCommand(aerc, ui, cmd) }, func(cmd string) []string { return getCompletions(aerc, cmd) - }) + }, &commands.CmdHistory) ui, err = libui.Initialize(conf, aerc) if err != nil { diff --git a/commands/history.go b/commands/history.go new file mode 100644 index 0000000..77bb155 --- /dev/null +++ b/commands/history.go @@ -0,0 +1,62 @@ +package commands + +type cmdHistory struct { + // rolling buffer of prior commands + // + // most recent command is at the end of the list, + // least recent is index 0 + cmdList []string + + // current placement in list + current int +} + +// number of commands to keep in history +const cmdLimit = 1000 + +// CmdHistory is the history of executed commands +var CmdHistory = cmdHistory{} + +func (h *cmdHistory) Add(cmd string) { + // if we're at cap, cut off the first element + if len(h.cmdList) >= cmdLimit { + h.cmdList = h.cmdList[1:] + } + + h.cmdList = append(h.cmdList, cmd) + + // whenever we add a new command, reset the current + // pointer to the "beginning" of the list + h.Reset() +} + +// Prev returns the previous command in history. +// Since the list is reverse-order, this will return elements +// increasingly towards index 0. +func (h *cmdHistory) Prev() string { + if h.current <= 0 || len(h.cmdList) == 0 { + h.current = -1 + return "(Already at beginning)" + } + h.current-- + + return h.cmdList[h.current] +} + +// Next returns the next command in history. +// Since the list is reverse-order, this will return elements +// increasingly towards index len(cmdList). +func (h *cmdHistory) Next() string { + if h.current >= len(h.cmdList)-1 || len(h.cmdList) == 0 { + h.current = len(h.cmdList) + return "(Already at end)" + } + h.current++ + + return h.cmdList[h.current] +} + +// Reset the current pointer to the beginning of history. +func (h *cmdHistory) Reset() { + h.current = len(h.cmdList) +} diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd index 0c5d7f5..44fb020 100644 --- a/doc/aerc.1.scd +++ b/doc/aerc.1.scd @@ -25,6 +25,10 @@ as the terminal emulator, '' is used to bring up the command interface. Different commands work in different contexts, depending on the kind of tab you have selected. +Aerc stores a history of commands, which can be cycled through in command mode. +Pressing the up key cycles backwards in history, while pressing down cycles +forwards. + ## GLOBAL COMMANDS These commands work in any context. @@ -113,7 +117,7 @@ message list, the message in the message viewer, etc). *unread* Marks the selected message as unread. - + *-t*: Toggle the selected message between read and unread. *unsubscribe* diff --git a/lib/history.go b/lib/history.go new file mode 100644 index 0000000..abc081f --- /dev/null +++ b/lib/history.go @@ -0,0 +1,13 @@ +package lib + +// History represents a list of elements ordered by time. +type History interface { + // Add a new element to the history + Add(string) + // Get the next element in history + Next() string + // Get the previous element in history + Prev() string + // Reset the current location in history + Reset() +} diff --git a/widgets/aerc.go b/widgets/aerc.go index 050ba77..458c2f9 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -11,6 +11,7 @@ import ( "github.com/google/shlex" "git.sr.ht/~sircmpwn/aerc/config" + "git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/lib/ui" libui "git.sr.ht/~sircmpwn/aerc/lib/ui" ) @@ -18,6 +19,7 @@ import ( type Aerc struct { accounts map[string]*AccountView cmd func(cmd []string) error + cmdHistory lib.History complete func(cmd string) []string conf *config.AercConfig focused libui.Interactive @@ -31,7 +33,8 @@ type Aerc struct { } func NewAerc(conf *config.AercConfig, logger *log.Logger, - cmd func(cmd []string) error, complete func(cmd string) []string) *Aerc { + cmd func(cmd []string) error, complete func(cmd string) []string, + cmdHistory lib.History) *Aerc { tabs := libui.NewTabs() @@ -54,6 +57,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger, accounts: make(map[string]*AccountView), conf: conf, cmd: cmd, + cmdHistory: cmdHistory, complete: complete, grid: grid, logger: logger, @@ -323,6 +327,11 @@ func (aerc *Aerc) BeginExCommand() { aerc.PushStatus(" "+err.Error(), 10*time.Second). Color(tcell.ColorDefault, tcell.ColorRed) } + // only add to history if this is an unsimulated command, + // ie one not executed from a keybinding + if aerc.simulating == 0 { + aerc.cmdHistory.Add(cmd) + } aerc.statusbar.Pop() aerc.focus(previous) }, func() { @@ -330,7 +339,7 @@ func (aerc *Aerc) BeginExCommand() { aerc.focus(previous) }, func(cmd string) []string { return aerc.complete(cmd) - }) + }, aerc.cmdHistory) aerc.statusbar.Push(exline) aerc.focus(exline) } diff --git a/widgets/compose.go b/widgets/compose.go index b45892f..4f6f7a1 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -51,7 +51,8 @@ func NewComposer(conf *config.AercConfig, defaults["From"] = acct.From } - layout, editors, focusable := buildComposeHeader(conf.Compose.HeaderLayout, defaults) + layout, editors, focusable := buildComposeHeader( + conf.Compose.HeaderLayout, defaults) header, headerHeight := layout.grid( func(header string) ui.Drawable { return editors[header] }, @@ -90,7 +91,11 @@ func NewComposer(conf *config.AercConfig, return c } -func buildComposeHeader(layout HeaderLayout, defaults map[string]string) (newLayout HeaderLayout, editors map[string]*headerEditor, focusable []ui.DrawableInteractive) { +func buildComposeHeader(layout HeaderLayout, defaults map[string]string) ( + newLayout HeaderLayout, + editors map[string]*headerEditor, + focusable []ui.DrawableInteractive, +) { editors = make(map[string]*headerEditor) focusable = make([]ui.DrawableInteractive, 0) diff --git a/widgets/exline.go b/widgets/exline.go index e984ee1..b7b4e3d 100644 --- a/widgets/exline.go +++ b/widgets/exline.go @@ -3,6 +3,7 @@ package widgets import ( "github.com/gdamore/tcell" + "git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/lib/ui" ) @@ -11,17 +12,20 @@ type ExLine struct { cancel func() commit func(cmd string) tabcomplete func(cmd string) []string + cmdHistory lib.History input *ui.TextInput } func NewExLine(commit func(cmd string), cancel func(), - tabcomplete func(cmd string) []string) *ExLine { + tabcomplete func(cmd string) []string, + cmdHistory lib.History) *ExLine { input := ui.NewTextInput("").Prompt(":") exline := &ExLine{ cancel: cancel, commit: commit, tabcomplete: tabcomplete, + cmdHistory: cmdHistory, input: input, } input.OnInvalidate(func(d ui.Drawable) { @@ -47,10 +51,18 @@ func (ex *ExLine) Event(event tcell.Event) bool { case *tcell.EventKey: switch event.Key() { case tcell.KeyEnter: + cmd := ex.input.String() ex.input.Focus(false) - ex.commit(ex.input.String()) + ex.commit(cmd) + case tcell.KeyUp: + ex.input.Set(ex.cmdHistory.Prev()) + ex.Invalidate() + case tcell.KeyDown: + ex.input.Set(ex.cmdHistory.Next()) + ex.Invalidate() case tcell.KeyEsc, tcell.KeyCtrlC: ex.input.Focus(false) + ex.cmdHistory.Reset() ex.cancel() case tcell.KeyTab: complete := ex.tabcomplete(ex.input.StringLeft())