history: store command history in a file

Losing your progress in case of a crash, or when accidentally closing
aerc is annoying and costs time. This can be drastically reduced by
keeping a persistent history.

Write commands to XDG_CACHE_DIR/aerc/histfile when they are run and load
them when needed. If another instance of aerc is already writing the
file, fall back to the current model, where the history is kept in
memory.

Signed-off-by: Moritz Poldrack <git@moritz.sh>
Acked-by: Robin Jarry <robin@jarry.cc>
Acked-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Moritz Poldrack 2022-09-14 18:34:49 +02:00 committed by Robin Jarry
parent 9c11ab21c7
commit ef99ec17d6
2 changed files with 78 additions and 0 deletions

View file

@ -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

View file

@ -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
}
}