summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorGalen Abell <galen@galenabell.com>2019-07-23 12:52:33 -0400
committerDrew DeVault <sir@cmpwn.com>2019-07-26 14:29:34 -0400
commit8635c70fda20b91f97c42f4e23e97bc01a14a89d (patch)
treeea70a40f7617782ca28060965ad253fa0e686161
parent67fb0938a66605a0b6a837005804637b348b250d (diff)
downloadaerc-8635c70fda20b91f97c42f4e23e97bc01a14a89d.tar.gz
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.
-rw-r--r--aerc.go2
-rw-r--r--commands/history.go62
-rw-r--r--doc/aerc.1.scd6
-rw-r--r--lib/history.go13
-rw-r--r--widgets/aerc.go13
-rw-r--r--widgets/compose.go9
-rw-r--r--widgets/exline.go16
7 files changed, 113 insertions, 8 deletions
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, '<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*
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())