Add command history and cycling

Aerc will keep track of the previous 1000 commands, which the user can
cycle through using the arrow keys while in the ex-line. Pressing up
will move backwards in history while pressing down will move forward.
This commit is contained in:
Galen Abell 2019-07-23 12:52:33 -04:00 committed by Drew DeVault
parent 67fb0938a6
commit 8635c70fda
7 changed files with 113 additions and 8 deletions

View File

@ -148,7 +148,7 @@ func main() {
return execCommand(aerc, ui, cmd) return execCommand(aerc, ui, cmd)
}, func(cmd string) []string { }, func(cmd string) []string {
return getCompletions(aerc, cmd) return getCompletions(aerc, cmd)
}) }, &commands.CmdHistory)
ui, err = libui.Initialize(conf, aerc) ui, err = libui.Initialize(conf, aerc)
if err != nil { if err != nil {

62
commands/history.go Normal file
View File

@ -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)
}

View File

@ -25,6 +25,10 @@ as the terminal emulator, '<c-x>' is used to bring up the command interface.
Different commands work in different contexts, depending on the kind of tab you Different commands work in different contexts, depending on the kind of tab you
have selected. 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 ## GLOBAL COMMANDS
These commands work in any context. These commands work in any context.
@ -113,7 +117,7 @@ message list, the message in the message viewer, etc).
*unread* *unread*
Marks the selected message as unread. Marks the selected message as unread.
*-t*: Toggle the selected message between read and unread. *-t*: Toggle the selected message between read and unread.
*unsubscribe* *unsubscribe*

13
lib/history.go Normal file
View File

@ -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()
}

View File

@ -11,6 +11,7 @@ import (
"github.com/google/shlex" "github.com/google/shlex"
"git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
libui "git.sr.ht/~sircmpwn/aerc/lib/ui" libui "git.sr.ht/~sircmpwn/aerc/lib/ui"
) )
@ -18,6 +19,7 @@ import (
type Aerc struct { type Aerc struct {
accounts map[string]*AccountView accounts map[string]*AccountView
cmd func(cmd []string) error cmd func(cmd []string) error
cmdHistory lib.History
complete func(cmd string) []string complete func(cmd string) []string
conf *config.AercConfig conf *config.AercConfig
focused libui.Interactive focused libui.Interactive
@ -31,7 +33,8 @@ type Aerc struct {
} }
func NewAerc(conf *config.AercConfig, logger *log.Logger, 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() tabs := libui.NewTabs()
@ -54,6 +57,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
accounts: make(map[string]*AccountView), accounts: make(map[string]*AccountView),
conf: conf, conf: conf,
cmd: cmd, cmd: cmd,
cmdHistory: cmdHistory,
complete: complete, complete: complete,
grid: grid, grid: grid,
logger: logger, logger: logger,
@ -323,6 +327,11 @@ func (aerc *Aerc) BeginExCommand() {
aerc.PushStatus(" "+err.Error(), 10*time.Second). aerc.PushStatus(" "+err.Error(), 10*time.Second).
Color(tcell.ColorDefault, tcell.ColorRed) 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.statusbar.Pop()
aerc.focus(previous) aerc.focus(previous)
}, func() { }, func() {
@ -330,7 +339,7 @@ func (aerc *Aerc) BeginExCommand() {
aerc.focus(previous) aerc.focus(previous)
}, func(cmd string) []string { }, func(cmd string) []string {
return aerc.complete(cmd) return aerc.complete(cmd)
}) }, aerc.cmdHistory)
aerc.statusbar.Push(exline) aerc.statusbar.Push(exline)
aerc.focus(exline) aerc.focus(exline)
} }

View File

@ -51,7 +51,8 @@ func NewComposer(conf *config.AercConfig,
defaults["From"] = acct.From defaults["From"] = acct.From
} }
layout, editors, focusable := buildComposeHeader(conf.Compose.HeaderLayout, defaults) layout, editors, focusable := buildComposeHeader(
conf.Compose.HeaderLayout, defaults)
header, headerHeight := layout.grid( header, headerHeight := layout.grid(
func(header string) ui.Drawable { return editors[header] }, func(header string) ui.Drawable { return editors[header] },
@ -90,7 +91,11 @@ func NewComposer(conf *config.AercConfig,
return c 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) editors = make(map[string]*headerEditor)
focusable = make([]ui.DrawableInteractive, 0) focusable = make([]ui.DrawableInteractive, 0)

View File

@ -3,6 +3,7 @@ package widgets
import ( import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
) )
@ -11,17 +12,20 @@ type ExLine struct {
cancel func() cancel func()
commit func(cmd string) commit func(cmd string)
tabcomplete func(cmd string) []string tabcomplete func(cmd string) []string
cmdHistory lib.History
input *ui.TextInput input *ui.TextInput
} }
func NewExLine(commit func(cmd string), cancel func(), 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(":") input := ui.NewTextInput("").Prompt(":")
exline := &ExLine{ exline := &ExLine{
cancel: cancel, cancel: cancel,
commit: commit, commit: commit,
tabcomplete: tabcomplete, tabcomplete: tabcomplete,
cmdHistory: cmdHistory,
input: input, input: input,
} }
input.OnInvalidate(func(d ui.Drawable) { input.OnInvalidate(func(d ui.Drawable) {
@ -47,10 +51,18 @@ func (ex *ExLine) Event(event tcell.Event) bool {
case *tcell.EventKey: case *tcell.EventKey:
switch event.Key() { switch event.Key() {
case tcell.KeyEnter: case tcell.KeyEnter:
cmd := ex.input.String()
ex.input.Focus(false) 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: case tcell.KeyEsc, tcell.KeyCtrlC:
ex.input.Focus(false) ex.input.Focus(false)
ex.cmdHistory.Reset()
ex.cancel() ex.cancel()
case tcell.KeyTab: case tcell.KeyTab:
complete := ex.tabcomplete(ex.input.StringLeft()) complete := ex.tabcomplete(ex.input.StringLeft())