ef99ec17d6
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>
141 lines
3 KiB
Go
141 lines
3 KiB
Go
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
|
|
//
|
|
// most recent command is at the end of the list,
|
|
// least recent is index 0
|
|
cmdList []string
|
|
|
|
// current placement in list
|
|
current int
|
|
|
|
// initialize history storage
|
|
initHistfile sync.Once
|
|
histfile io.ReadWriter
|
|
}
|
|
|
|
// 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) {
|
|
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:]
|
|
}
|
|
|
|
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
|
|
// 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 {
|
|
h.initHistfile.Do(h.initialize)
|
|
|
|
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 {
|
|
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)"
|
|
}
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|