kitty/tools/tui/readline/history.go
2022-11-14 15:42:07 +05:30

151 lines
3.1 KiB
Go

// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package readline
import (
"encoding/json"
"fmt"
"io"
"os"
"time"
"kitty/tools/utils"
)
var _ = fmt.Print
type HistoryItem struct {
Cmd string `json:"cmd"`
Cwd string `json:"cwd,omitempty"`
Timestamp time.Time `json:"timestamp"`
Duration time.Duration `json:"duration,omitempty"`
ExitCode int `json:"exit_code"`
}
type History struct {
file_path string
file *os.File
max_items int
items []HistoryItem
cmd_map map[string]int
}
func map_from_items(items []HistoryItem) map[string]int {
pmap := make(map[string]int, len(items))
for i, hi := range items {
pmap[hi.Cmd] = i
}
return pmap
}
func (self *History) add_item(x HistoryItem) bool {
existing, found := self.cmd_map[x.Cmd]
if found {
if self.items[existing].Timestamp.Before(x.Timestamp) {
self.items[existing] = x
return true
}
return false
}
self.cmd_map[x.Cmd] = len(self.items)
self.items = append(self.items, x)
return true
}
func (self *History) merge_items(items ...HistoryItem) {
if len(self.items) == 0 {
self.items = items
self.cmd_map = map_from_items(self.items)
return
}
if len(items) == 0 {
return
}
changed := false
for _, x := range items {
if self.add_item(x) {
changed = true
}
}
if !changed {
return
}
self.items = utils.StableSort(self.items, func(a, b HistoryItem) bool {
return a.Timestamp.Before(b.Timestamp)
})
if len(self.items) > self.max_items {
self.items = self.items[len(self.items)-self.max_items:]
}
self.cmd_map = map_from_items(self.items)
}
func (self *History) Write() {
if self.file == nil {
return
}
self.file.Seek(0, 0)
utils.LockFileExclusive(self.file)
defer utils.UnlockFile(self.file)
data, err := io.ReadAll(self.file)
if err != nil {
return
}
var items []HistoryItem
err = json.Unmarshal(data, &items)
if err != nil {
self.merge_items(items...)
}
ndata, err := json.MarshalIndent(self.items, "", " ")
if err != nil {
return
}
self.file.Truncate(int64(len(ndata)))
self.file.Seek(0, 0)
self.file.Write(ndata)
}
func (self *History) Read() {
if self.file == nil {
return
}
self.file.Seek(0, 0)
utils.LockFileShared(self.file)
data, err := io.ReadAll(self.file)
utils.UnlockFile(self.file)
if err != nil {
return
}
var items []HistoryItem
err = json.Unmarshal(data, &items)
if err != nil {
self.merge_items(items...)
}
}
func (self *History) AddItem(cmd string, duration time.Duration) {
self.merge_items(HistoryItem{Cmd: cmd, Duration: duration, Timestamp: time.Now()})
}
func (self *History) Shutdown() {
if self.file != nil {
self.Write()
self.file.Close()
self.file = nil
}
}
func NewHistory(path string, max_items int) *History {
ans := History{items: []HistoryItem{}, cmd_map: map[string]int{}, max_items: max_items}
if path != "" {
ans.file_path = path
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600)
if err == nil {
ans.file = f
} else {
fmt.Fprintln(os.Stderr, "Failed to open history file at:", path, "with error:", err)
}
}
ans.Read()
return &ans
}