~sircmpwn/aerc

8635c70fda20b91f97c42f4e23e97bc01a14a89d — Galen Abell 1 year, 23 days 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())