diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ac429d..7454b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support for bindings with the Alt modifier. - Zoxide support with `:z`. - Hide local timezone with `send-as-utc = true` in `accounts.conf`. +- Persistent command history in `~/.cache/aerc/history`. ### Changed diff --git a/commands/history.go b/commands/history.go index 1c0a1ec..5cc0bfb 100644 --- a/commands/history.go +++ b/commands/history.go @@ -1,5 +1,18 @@ package commands +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "path" + "sync" + + "git.sr.ht/~rjarry/aerc/logging" + "github.com/kyoh86/xdg" +) + type cmdHistory struct { // rolling buffer of prior commands // @@ -9,6 +22,10 @@ type cmdHistory struct { // current placement in list current int + + // initialize history storage + initHistfile sync.Once + histfile io.ReadWriter } // number of commands to keep in history @@ -18,6 +35,8 @@ const cmdLimit = 1000 var CmdHistory = cmdHistory{} func (h *cmdHistory) Add(cmd string) { + h.initHistfile.Do(h.initialize) + // if we're at cap, cut off the first element if len(h.cmdList) >= cmdLimit { h.cmdList = h.cmdList[1:] @@ -25,6 +44,8 @@ func (h *cmdHistory) Add(cmd string) { if len(h.cmdList) == 0 || h.cmdList[len(h.cmdList)-1] != cmd { h.cmdList = append(h.cmdList, cmd) + + h.writeHistory() } // whenever we add a new command, reset the current @@ -36,6 +57,8 @@ func (h *cmdHistory) Add(cmd string) { // Since the list is reverse-order, this will return elements // increasingly towards index 0. func (h *cmdHistory) Prev() string { + h.initHistfile.Do(h.initialize) + if h.current <= 0 || len(h.cmdList) == 0 { h.current = -1 return "(Already at beginning)" @@ -49,6 +72,8 @@ func (h *cmdHistory) Prev() string { // Since the list is reverse-order, this will return elements // increasingly towards index len(cmdList). func (h *cmdHistory) Next() string { + h.initHistfile.Do(h.initialize) + if h.current >= len(h.cmdList)-1 || len(h.cmdList) == 0 { h.current = len(h.cmdList) return "(Already at end)" @@ -62,3 +87,55 @@ func (h *cmdHistory) Next() string { func (h *cmdHistory) Reset() { h.current = len(h.cmdList) } + +func (h *cmdHistory) initialize() { + var err error + openFlags := os.O_RDWR | os.O_EXCL + + histPath := path.Join(xdg.CacheHome(), "aerc", "history") + if _, err := os.Stat(histPath); os.IsNotExist(err) { + _ = os.MkdirAll(path.Join(xdg.CacheHome(), "aerc"), 0o700) // caught by OpenFile + openFlags |= os.O_CREATE + } + + // O_EXCL to make sure that only one aerc writes to the file + h.histfile, err = os.OpenFile( + histPath, + openFlags, + 0o600, + ) + if err != nil { + logging.Errorf("failed to open history file: %v", err) + // basically mirror the old behavior + h.histfile = bytes.NewBuffer([]byte{}) + return + } + + s := bufio.NewScanner(h.histfile) + + for s.Scan() { + h.cmdList = append(h.cmdList, s.Text()) + } + + h.Reset() +} + +func (h *cmdHistory) writeHistory() { + if fh, ok := h.histfile.(*os.File); ok { + err := fh.Truncate(0) + if err != nil { + // if we can't delete it, don't break it. + return + } + _, err = fh.Seek(0, io.SeekStart) + if err != nil { + // if we can't delete it, don't break it. + return + } + for _, entry := range h.cmdList { + fmt.Fprintln(fh, entry) + } + + fh.Sync() //nolint:errcheck // if your computer can't sync you're in bigger trouble + } +}