mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-25 01:38:02 +02:00
Start work on history support for readline
This commit is contained in:
@@ -8,7 +8,9 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/shlex"
|
||||
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"kitty/tools/cli/markup"
|
||||
"kitty/tools/tui/loop"
|
||||
"kitty/tools/tui/readline"
|
||||
"kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
@@ -111,7 +114,7 @@ func print_basic_help() {
|
||||
fmt.Println(" ", "Exit this shell")
|
||||
}
|
||||
|
||||
func exec_command(cmdline string) bool {
|
||||
func exec_command(rl *readline.Readline, cmdline string) bool {
|
||||
parsed_cmdline, err := shlex.Split(cmdline)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Could not parse cmdline:", err)
|
||||
@@ -120,10 +123,15 @@ func exec_command(cmdline string) bool {
|
||||
if len(parsed_cmdline) == 0 {
|
||||
return true
|
||||
}
|
||||
hi := readline.HistoryItem{Timestamp: time.Now(), Cmd: cmdline, ExitCode: -1}
|
||||
switch parsed_cmdline[0] {
|
||||
case "exit":
|
||||
hi.ExitCode = 0
|
||||
rl.AddHistoryItem(hi)
|
||||
return false
|
||||
case "help":
|
||||
hi.ExitCode = 0
|
||||
defer rl.AddHistoryItem(hi)
|
||||
if len(parsed_cmdline) == 1 {
|
||||
print_basic_help()
|
||||
return true
|
||||
@@ -137,6 +145,7 @@ func exec_command(cmdline string) bool {
|
||||
r := EntryPoint(cli.NewRootCommand())
|
||||
sc := r.FindSubCommand(parsed_cmdline[1])
|
||||
if sc == nil {
|
||||
hi.ExitCode = 1
|
||||
fmt.Fprintln(os.Stderr, "No command named", formatter.BrightRed(parsed_cmdline[1]), ". Type help for a list of commands")
|
||||
} else {
|
||||
sc.ShowHelpWithCommandString(sc.Name)
|
||||
@@ -156,9 +165,15 @@ func exec_command(cmdline string) bool {
|
||||
cmdline = append(cmdline, parsed_cmdline...)
|
||||
cmd := exec.Cmd{Path: exe, Args: cmdline, Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr}
|
||||
err = cmd.Run()
|
||||
hi.Duration = time.Now().Sub(hi.Timestamp)
|
||||
hi.ExitCode = 0
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
hi.ExitCode = exitError.ExitCode()
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
rl.AddHistoryItem(hi)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -167,14 +182,17 @@ func shell_main(cmd *cli.Command, args []string) (int, error) {
|
||||
formatter = markup.New(true)
|
||||
fmt.Println("Welcome to the kitty shell!")
|
||||
fmt.Println("Use", formatter.Green("help"), "for assistance or", formatter.Green("exit"), "to quit.")
|
||||
rl := readline.New(nil, readline.RlInit{Prompt: prompt})
|
||||
rl := readline.New(nil, readline.RlInit{Prompt: prompt, HistoryPath: filepath.Join(utils.CacheDir(), "shell.history.json")})
|
||||
defer func() {
|
||||
rl.Shutdown()
|
||||
}()
|
||||
for {
|
||||
rc, err := shell_loop(rl, true)
|
||||
if err != nil {
|
||||
if err == ErrExec {
|
||||
cmdline := rl.AllText()
|
||||
cmdline = strings.ReplaceAll(cmdline, "\\\n", "")
|
||||
if !exec_command(cmdline) {
|
||||
if !exec_command(rl, cmdline) {
|
||||
return 0, nil
|
||||
}
|
||||
continue
|
||||
|
||||
@@ -17,6 +17,8 @@ const PROMPT_MARK = "\x1b]133;"
|
||||
|
||||
type RlInit struct {
|
||||
Prompt string
|
||||
HistoryPath string
|
||||
HistoryCount int
|
||||
ContinuationPrompt string
|
||||
EmptyContinuationPrompt bool
|
||||
DontMarkPrompts bool
|
||||
@@ -54,6 +56,7 @@ type Readline struct {
|
||||
continuation_prompt_len int
|
||||
mark_prompts bool
|
||||
loop *loop.Loop
|
||||
history *History
|
||||
|
||||
// The number of lines after the initial line on the screen
|
||||
cursor_y int
|
||||
@@ -65,9 +68,13 @@ type Readline struct {
|
||||
}
|
||||
|
||||
func New(loop *loop.Loop, r RlInit) *Readline {
|
||||
hc := r.HistoryCount
|
||||
if hc == 0 {
|
||||
hc = 8192
|
||||
}
|
||||
ans := &Readline{
|
||||
prompt: r.Prompt, prompt_len: wcswidth.Stringwidth(r.Prompt), mark_prompts: !r.DontMarkPrompts,
|
||||
loop: loop, lines: []string{""},
|
||||
loop: loop, lines: []string{""}, history: NewHistory(r.HistoryPath, hc),
|
||||
}
|
||||
if r.ContinuationPrompt != "" || !r.EmptyContinuationPrompt {
|
||||
ans.continuation_prompt = r.ContinuationPrompt
|
||||
@@ -83,6 +90,14 @@ func New(loop *loop.Loop, r RlInit) *Readline {
|
||||
return ans
|
||||
}
|
||||
|
||||
func (self *Readline) Shutdown() {
|
||||
self.history.Shutdown()
|
||||
}
|
||||
|
||||
func (self *Readline) AddHistoryItem(hi HistoryItem) {
|
||||
self.history.add_item(hi)
|
||||
}
|
||||
|
||||
func (self *Readline) ChangeLoopAndResetText(lp *loop.Loop) {
|
||||
self.loop = lp
|
||||
self.lines = []string{""}
|
||||
|
||||
149
tools/tui/readline/history.go
Normal file
149
tools/tui/readline/history.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// 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"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
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
|
||||
}
|
||||
@@ -102,6 +102,28 @@ func ConfigDir() string {
|
||||
return config_dir
|
||||
}
|
||||
|
||||
var cache_dir string
|
||||
|
||||
func CacheDir() string {
|
||||
if cache_dir != "" {
|
||||
return cache_dir
|
||||
}
|
||||
candidate := ""
|
||||
if edir := os.Getenv("KITTY_CACHE_DIRECTORY"); edir != "" {
|
||||
candidate = Abspath(Expanduser(edir))
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
candidate = Expanduser("~/Library/Caches/kitty")
|
||||
} else {
|
||||
candidate = os.Getenv("XDG_CACHE_HOME")
|
||||
if candidate == "" {
|
||||
candidate = "~/.cache"
|
||||
}
|
||||
candidate = filepath.Join(Expanduser(candidate), "kitty")
|
||||
}
|
||||
os.MkdirAll(candidate, 0o755)
|
||||
return candidate
|
||||
}
|
||||
|
||||
type Walk_callback func(path, abspath string, d fs.DirEntry, err error) error
|
||||
|
||||
func transform_symlink(path string) string {
|
||||
|
||||
Reference in New Issue
Block a user