aboutsummaryrefslogblamecommitdiffstats
path: root/commands/history.go
blob: 0a7d68942f831e8d619d2379c6b20bceefda6bc8 (plain) (tree)
1
2
3
4
5
6
7
8
9
10

                





               

              
                                        
                                        

 








                                                         



                                     








                                                 

                                       




                                                     

                                                                      

                                
         









                                                             

                                       












                                                             

                                       












                                                                 




                                          
                                                    
                                                            
                                                                                   









                                                                    
                                                                  
































                                                                                                   
package commands

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os"
	"sync"

	"git.sr.ht/~rjarry/aerc/lib/log"
	"git.sr.ht/~rjarry/aerc/lib/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 := xdg.CachePath("aerc", "history")
	if _, err := os.Stat(histPath); os.IsNotExist(err) {
		_ = os.MkdirAll(xdg.CachePath("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 {
		log.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
	}
}