aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMoritz Poldrack <git@moritz.sh>2022-09-14 18:34:49 +0200
committerRobin Jarry <robin@jarry.cc>2022-09-14 20:51:51 +0200
commitef99ec17d63629fdc62bcc8ed6360a56bc7128fa (patch)
tree7769f26b313109cf05283c2d708a76159ef1f83d
parent9c11ab21c73cecf80a243d8dfb55b7793da7bb96 (diff)
downloadaerc-ef99ec17d63629fdc62bcc8ed6360a56bc7128fa.tar.gz
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>
-rw-r--r--CHANGELOG.md1
-rw-r--r--commands/history.go77
2 files changed, 78 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ac429dd..7454b2a2 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 1c0a1ec8..5cc0bfb3 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
+ }
+}