8635c70fda20b91f97c42f4e23e97bc01a14a89d — Galen Abell 3 months ago 67fb093
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.
7 files changed, 113 insertions(+), 8 deletions(-)

M aerc.go
A commands/history.go
M doc/aerc.1.scd
A lib/history.go
M widgets/aerc.go
M widgets/compose.go
M widgets/exline.go
M aerc.go => aerc.go +1 -1
@@ 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 {

A commands/history.go => commands/history.go +62 -0
@@ 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)
+}

M doc/aerc.1.scd => doc/aerc.1.scd +5 -1
@@ 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
 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*

A lib/history.go => lib/history.go +13 -0
@@ 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()
+}

M widgets/aerc.go => widgets/aerc.go +11 -2
@@ 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)
 }

M widgets/compose.go => widgets/compose.go +7 -2
@@ 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)
 

M widgets/exline.go => widgets/exline.go +14 -2
@@ 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())