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:
parent
67fb0938a6
commit
8635c70fda
7 changed files with 113 additions and 8 deletions
2
aerc.go
2
aerc.go
|
@ -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
62
commands/history.go
Normal 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)
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
13
lib/history.go
Normal file
13
lib/history.go
Normal 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()
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in a new issue