history.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package readline
  3. import (
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "os"
  8. "strings"
  9. "time"
  10. "kitty/tools/cli"
  11. "kitty/tools/utils"
  12. "kitty/tools/utils/shlex"
  13. "kitty/tools/wcswidth"
  14. )
  15. var _ = fmt.Print
  16. type HistoryItem struct {
  17. Cmd string `json:"cmd"`
  18. Cwd string `json:"cwd,omitempty"`
  19. Timestamp time.Time `json:"timestamp"`
  20. Duration time.Duration `json:"duration,omitempty"`
  21. ExitCode int `json:"exit_code"`
  22. }
  23. type HistoryMatches struct {
  24. items []HistoryItem
  25. prefix string
  26. current_idx int
  27. original_input_state InputState
  28. }
  29. type HistorySearch struct {
  30. query string
  31. tokens []string
  32. items []*HistoryItem
  33. current_idx int
  34. backwards bool
  35. original_input_state InputState
  36. }
  37. type History struct {
  38. file_path string
  39. file *os.File
  40. max_items int
  41. items []HistoryItem
  42. cmd_map map[string]int
  43. }
  44. func map_from_items(items []HistoryItem) map[string]int {
  45. pmap := make(map[string]int, len(items))
  46. for i, hi := range items {
  47. pmap[hi.Cmd] = i
  48. }
  49. return pmap
  50. }
  51. func (self *History) add_item(x HistoryItem) bool {
  52. existing, found := self.cmd_map[x.Cmd]
  53. if found {
  54. if self.items[existing].Timestamp.Before(x.Timestamp) {
  55. self.items[existing] = x
  56. return true
  57. }
  58. return false
  59. }
  60. self.cmd_map[x.Cmd] = len(self.items)
  61. self.items = append(self.items, x)
  62. return true
  63. }
  64. func (self *History) merge_items(items ...HistoryItem) {
  65. if len(self.items) == 0 {
  66. self.items = items
  67. self.cmd_map = map_from_items(self.items)
  68. return
  69. }
  70. if len(items) == 0 {
  71. return
  72. }
  73. changed := false
  74. for _, x := range items {
  75. if self.add_item(x) {
  76. changed = true
  77. }
  78. }
  79. if !changed {
  80. return
  81. }
  82. self.items = utils.StableSort(self.items, func(a, b HistoryItem) int {
  83. return a.Timestamp.Compare(b.Timestamp)
  84. })
  85. if len(self.items) > self.max_items {
  86. self.items = self.items[len(self.items)-self.max_items:]
  87. }
  88. self.cmd_map = map_from_items(self.items)
  89. }
  90. func (self *History) Write() {
  91. if self.file == nil {
  92. return
  93. }
  94. self.file.Seek(0, 0)
  95. utils.LockFileExclusive(self.file)
  96. defer utils.UnlockFile(self.file)
  97. data, err := io.ReadAll(self.file)
  98. if err != nil {
  99. return
  100. }
  101. var items []HistoryItem
  102. err = json.Unmarshal(data, &items)
  103. if err != nil {
  104. self.merge_items(items...)
  105. }
  106. ndata, err := json.MarshalIndent(self.items, "", " ")
  107. if err != nil {
  108. return
  109. }
  110. self.file.Truncate(int64(len(ndata)))
  111. self.file.Seek(0, 0)
  112. self.file.Write(ndata)
  113. }
  114. func (self *History) Read() {
  115. if self.file == nil {
  116. return
  117. }
  118. self.file.Seek(0, 0)
  119. utils.LockFileShared(self.file)
  120. data, err := io.ReadAll(self.file)
  121. utils.UnlockFile(self.file)
  122. if err != nil {
  123. return
  124. }
  125. var items []HistoryItem
  126. err = json.Unmarshal(data, &items)
  127. if err == nil {
  128. self.merge_items(items...)
  129. }
  130. }
  131. func (self *History) AddItem(cmd string, duration time.Duration) {
  132. self.merge_items(HistoryItem{Cmd: cmd, Duration: duration, Timestamp: time.Now()})
  133. }
  134. func (self *History) Shutdown() {
  135. if self.file != nil {
  136. self.Write()
  137. self.file.Close()
  138. self.file = nil
  139. }
  140. }
  141. func NewHistory(path string, max_items int) *History {
  142. ans := History{items: []HistoryItem{}, cmd_map: map[string]int{}, max_items: max_items}
  143. if path != "" {
  144. ans.file_path = path
  145. f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600)
  146. if err == nil {
  147. ans.file = f
  148. } else {
  149. fmt.Fprintln(os.Stderr, "Failed to open history file at:", path, "with error:", err)
  150. }
  151. }
  152. ans.Read()
  153. return &ans
  154. }
  155. func (self *History) find_prefix_matches(prefix, current_command string, input_state InputState) *HistoryMatches {
  156. ans := HistoryMatches{items: make([]HistoryItem, 0, len(self.items)+1), prefix: prefix, original_input_state: input_state}
  157. if prefix == "" {
  158. ans.items = ans.items[:len(self.items)]
  159. copy(ans.items, self.items)
  160. } else {
  161. for _, x := range self.items {
  162. if strings.HasPrefix(x.Cmd, prefix) {
  163. ans.items = append(ans.items, x)
  164. }
  165. }
  166. }
  167. ans.items = append(ans.items, HistoryItem{Cmd: current_command})
  168. ans.current_idx = len(ans.items) - 1
  169. return &ans
  170. }
  171. func (self *Readline) create_history_matches() {
  172. if self.last_action_was_history_movement() && self.history_matches != nil {
  173. return
  174. }
  175. prefix := self.text_upto_cursor_pos()
  176. self.history_matches = self.history.find_prefix_matches(prefix, self.AllText(), self.input_state.copy())
  177. }
  178. func (self *Readline) last_action_was_history_movement() bool {
  179. switch self.last_action {
  180. case ActionHistoryLast, ActionHistoryFirst, ActionHistoryNext, ActionHistoryPrevious:
  181. return true
  182. default:
  183. return false
  184. }
  185. }
  186. func (self *HistoryMatches) apply(rl *Readline) bool {
  187. if self.current_idx >= len(self.items) || self.current_idx < 0 {
  188. return false
  189. }
  190. if self.current_idx == len(self.items)-1 {
  191. rl.input_state = self.original_input_state.copy()
  192. } else {
  193. item := self.items[self.current_idx]
  194. rl.input_state.lines = utils.Splitlines(item.Cmd)
  195. if len(rl.input_state.lines) == 0 {
  196. rl.input_state.lines = []string{""}
  197. }
  198. idx := len(rl.input_state.lines) - 1
  199. rl.input_state.cursor = Position{Y: idx, X: len(rl.input_state.lines[idx])}
  200. }
  201. return true
  202. }
  203. func (self *HistoryMatches) first(rl *Readline) bool {
  204. self.current_idx = 0
  205. return self.apply(rl)
  206. }
  207. func (self *HistoryMatches) last(rl *Readline) bool {
  208. self.current_idx = max(0, len(self.items)-1)
  209. return self.apply(rl)
  210. }
  211. func (self *HistoryMatches) previous(num uint, rl *Readline) bool {
  212. if self.current_idx > 0 {
  213. self.current_idx = max(0, self.current_idx-int(num))
  214. return self.apply(rl)
  215. }
  216. return false
  217. }
  218. func (self *HistoryMatches) next(num uint, rl *Readline) bool {
  219. if self.current_idx+1 < len(self.items) {
  220. self.current_idx = min(len(self.items)-1, self.current_idx+int(num))
  221. return self.apply(rl)
  222. }
  223. return false
  224. }
  225. func (self *Readline) create_history_search(backwards bool, num uint) {
  226. self.history_search = &HistorySearch{backwards: backwards, original_input_state: self.input_state.copy()}
  227. self.push_keyboard_map(history_search_shortcuts())
  228. self.markup_history_search()
  229. }
  230. func (self *Readline) end_history_search(accept bool) {
  231. if accept && self.history_search.current_idx < len(self.history_search.items) {
  232. self.input_state.lines = utils.Splitlines(self.history_search.items[self.history_search.current_idx].Cmd)
  233. self.input_state.cursor.Y = len(self.input_state.lines) - 1
  234. self.input_state.cursor.X = len(self.input_state.lines[self.input_state.cursor.Y])
  235. } else {
  236. self.input_state = self.history_search.original_input_state
  237. }
  238. self.input_state.cursor = *self.ensure_position_in_bounds(&self.input_state.cursor)
  239. self.pop_keyboard_map()
  240. self.history_search = nil
  241. }
  242. func (self *Readline) markup_history_search() {
  243. if len(self.history_search.items) == 0 {
  244. if len(self.history_search.tokens) == 0 {
  245. self.input_state.lines = []string{""}
  246. } else {
  247. self.input_state.lines = []string{"No matches for: " + self.history_search.query}
  248. }
  249. self.input_state.cursor = Position{X: wcswidth.Stringwidth(self.input_state.lines[0])}
  250. return
  251. }
  252. lines := utils.Splitlines(self.history_search.items[self.history_search.current_idx].Cmd)
  253. cursor := Position{Y: len(lines)}
  254. for _, tok := range self.history_search.tokens {
  255. for i, line := range lines {
  256. if idx := strings.Index(line, tok); idx > -1 {
  257. q := Position{Y: i, X: idx}
  258. if q.Less(cursor) {
  259. cursor = q
  260. }
  261. break
  262. }
  263. }
  264. }
  265. self.input_state.lines = lines
  266. self.input_state.cursor = *self.ensure_position_in_bounds(&cursor)
  267. }
  268. func (self *Readline) remove_text_from_history_search(num uint) uint {
  269. l := len(self.history_search.query)
  270. nl := max(0, l-int(num))
  271. self.history_search.query = self.history_search.query[:nl]
  272. num_removed := uint(l - nl)
  273. self.add_text_to_history_search("") // update the search results
  274. return num_removed
  275. }
  276. func (self *Readline) history_search_highlighter(text string, x, y int) string {
  277. if len(self.history_search.items) == 0 {
  278. return text
  279. }
  280. lines := utils.Splitlines(text)
  281. for _, tok := range self.history_search.tokens {
  282. for i, line := range lines {
  283. if idx := strings.Index(line, tok); idx > -1 {
  284. lines[i] = line[:idx] + self.fmt_ctx.Green(tok) + line[idx+len(tok):]
  285. break
  286. }
  287. }
  288. }
  289. return strings.Join(lines, "\n")
  290. }
  291. func (self *Readline) add_text_to_history_search(text string) {
  292. self.history_search.query += text
  293. tokens, err := shlex.Split(self.history_search.query)
  294. if err != nil {
  295. tokens = strings.Split(self.history_search.query, " ")
  296. }
  297. self.history_search.tokens = tokens
  298. var current_item *HistoryItem
  299. if len(self.history_search.items) > 0 {
  300. current_item = self.history_search.items[self.history_search.current_idx]
  301. }
  302. if len(self.history_search.tokens) == 0 {
  303. self.history_search.items = []*HistoryItem{}
  304. } else {
  305. items := make([]*HistoryItem, len(self.history.items))
  306. for i := range self.history.items {
  307. items[i] = &self.history.items[i]
  308. }
  309. for _, token := range self.history_search.tokens {
  310. matches := make([]*HistoryItem, 0, len(items))
  311. for _, item := range items {
  312. if strings.Contains(item.Cmd, token) {
  313. matches = append(matches, item)
  314. }
  315. }
  316. items = matches
  317. }
  318. self.history_search.items = items
  319. }
  320. idx := -1
  321. for i, item := range self.history_search.items {
  322. if item == current_item {
  323. idx = i
  324. break
  325. }
  326. }
  327. if idx == -1 {
  328. if self.history_search.backwards {
  329. idx = len(self.history_search.items) - 1
  330. } else {
  331. idx = 0
  332. }
  333. }
  334. self.history_search.current_idx = max(0, idx)
  335. self.markup_history_search()
  336. }
  337. func (self *Readline) next_history_search(backwards bool, num uint) bool {
  338. ni := self.history_search.current_idx
  339. self.history_search.backwards = backwards
  340. if len(self.history_search.items) == 0 {
  341. return false
  342. }
  343. if backwards {
  344. ni = max(0, ni-int(num))
  345. } else {
  346. ni = min(ni+int(num), len(self.history_search.items)-1)
  347. }
  348. if ni == self.history_search.current_idx {
  349. return false
  350. }
  351. self.history_search.current_idx = ni
  352. self.markup_history_search()
  353. return true
  354. }
  355. func (self *Readline) history_search_prompt() string {
  356. ans := "↑"
  357. if !self.history_search.backwards {
  358. ans = "↓"
  359. }
  360. failed := len(self.history_search.tokens) > 0 && len(self.history_search.items) == 0
  361. if failed {
  362. ans = self.fmt_ctx.BrightRed(ans)
  363. } else {
  364. ans = self.fmt_ctx.Green(ans)
  365. }
  366. return fmt.Sprintf("history %s: ", ans)
  367. }
  368. func (self *Readline) history_completer(before_cursor, after_cursor string) (ans *cli.Completions) {
  369. ans = cli.NewCompletions()
  370. if before_cursor != "" {
  371. var words_before_cursor []string
  372. words_before_cursor, ans.CurrentWordIdx = shlex.SplitForCompletion(before_cursor)
  373. idx := len(words_before_cursor)
  374. if idx > 0 {
  375. idx--
  376. }
  377. seen := utils.NewSet[string](16)
  378. mg := ans.AddMatchGroup("History")
  379. for _, x := range self.history.items {
  380. if strings.HasPrefix(x.Cmd, before_cursor) {
  381. words, _ := shlex.SplitForCompletion(x.Cmd)
  382. if idx < len(words) {
  383. word := words[idx]
  384. desc := ""
  385. if !seen.Has(word) {
  386. if word != x.Cmd {
  387. desc = x.Cmd
  388. }
  389. mg.AddMatch(word, desc)
  390. seen.Add(word)
  391. }
  392. }
  393. }
  394. }
  395. }
  396. return
  397. }