// License: GPLv3 Copyright: 2022, Kovid Goyal, package readline import ( "container/list" "fmt" "strings" "kitty/tools/cli/markup" "kitty/tools/tui/loop" "kitty/tools/wcswidth" ) var _ = fmt.Print const ST = "\x1b\\" const PROMPT_MARK = "\x1b]133;" type RlInit struct { Prompt string HistoryPath string HistoryCount int ContinuationPrompt string EmptyContinuationPrompt bool DontMarkPrompts bool } type Position struct { X int Y int } func (self Position) Less(other Position) bool { return self.Y < other.Y || (self.Y == other.Y && self.X < other.X) } type Action uint const ( ActionNil Action = iota ActionBackspace ActionDelete ActionMoveToStartOfLine ActionMoveToEndOfLine ActionMoveToStartOfDocument ActionMoveToEndOfDocument ActionMoveToEndOfWord ActionMoveToStartOfWord ActionCursorLeft ActionCursorRight ActionEndInput ActionAcceptInput ActionCursorUp ActionHistoryPreviousOrCursorUp ActionCursorDown ActionHistoryNextOrCursorDown ActionHistoryNext ActionHistoryPrevious ActionHistoryFirst ActionHistoryLast ActionHistoryIncrementalSearchBackwards ActionHistoryIncrementalSearchForwards ActionClearScreen ActionAddText ActionAbortCurrentLine ActionStartKillActions ActionKillToEndOfLine ActionKillToStartOfLine ActionKillNextWord ActionKillPreviousWord ActionKillPreviousSpaceDelimitedWord ActionEndKillActions ActionYank ActionPopYank ActionNumericArgumentDigit0 ActionNumericArgumentDigit1 ActionNumericArgumentDigit2 ActionNumericArgumentDigit3 ActionNumericArgumentDigit4 ActionNumericArgumentDigit5 ActionNumericArgumentDigit6 ActionNumericArgumentDigit7 ActionNumericArgumentDigit8 ActionNumericArgumentDigit9 ActionNumericArgumentDigitMinus ) type kill_ring struct { items *list.List } func (self *kill_ring) append_to_existing_item(text string) { e := self.items.Front() if e == nil { self.add_new_item(text) } e.Value = e.Value.(string) + text } func (self *kill_ring) add_new_item(text string) { if text != "" { self.items.PushFront(text) } } func (self *kill_ring) yank() string { e := self.items.Front() if e == nil { return "" } return e.Value.(string) } func (self *kill_ring) pop_yank() string { e := self.items.Front() if e == nil { return "" } self.items.MoveToBack(e) return self.yank() } func (self *kill_ring) clear() { self.items = self.items.Init() } type Prompt struct { Text string Length int } type Readline struct { prompt, continuation_prompt Prompt mark_prompts bool loop *loop.Loop history *History kill_ring kill_ring // The number of lines after the initial line on the screen cursor_y int screen_width int // Input lines lines []string // The cursor position in the text cursor Position last_yank_extent struct { start, end Position } bracketed_paste_buffer strings.Builder last_action Action history_matches *HistoryMatches history_search *HistorySearch keyboard_state KeyboardState fmt_ctx *markup.Context text_to_be_added string } func (self *Readline) make_prompt(text string, is_secondary bool) Prompt { if self.mark_prompts { m := PROMPT_MARK + "A" if is_secondary { m += ";k=s" } text = m + ST + text } return Prompt{Text: text, Length: wcswidth.Stringwidth(text)} } func New(loop *loop.Loop, r RlInit) *Readline { hc := r.HistoryCount if hc == 0 { hc = 8192 } ans := &Readline{ mark_prompts: !r.DontMarkPrompts, fmt_ctx: markup.New(true), loop: loop, lines: []string{""}, history: NewHistory(r.HistoryPath, hc), kill_ring: kill_ring{items: list.New().Init()}, } ans.prompt = ans.make_prompt(r.Prompt, false) t := "" if r.ContinuationPrompt != "" || !r.EmptyContinuationPrompt { t = r.ContinuationPrompt if t == "" { t = ans.fmt_ctx.Yellow(">") + " " } } ans.continuation_prompt = ans.make_prompt(t, true) return ans } func (self *Readline) Shutdown() { self.history.Shutdown() } func (self *Readline) AddHistoryItem(hi HistoryItem) { self.history.add_item(hi) } func (self *Readline) ResetText() { self.lines = []string{""} self.cursor = Position{} self.cursor_y = 0 self.last_action = ActionNil self.keyboard_state = KeyboardState{} } func (self *Readline) ChangeLoopAndResetText(lp *loop.Loop) { self.loop = lp self.ResetText() } func (self *Readline) Start() { self.loop.SetCursorShape(loop.BAR_CURSOR, true) self.loop.StartBracketedPaste() self.Redraw() } func (self *Readline) End() { self.loop.SetCursorShape(loop.BLOCK_CURSOR, true) self.loop.EndBracketedPaste() self.loop.QueueWriteString("\r\n") if self.mark_prompts { self.loop.QueueWriteString(PROMPT_MARK + "C" + ST) } } func MarkOutputStart() string { return PROMPT_MARK + "C" + ST } func (self *Readline) Redraw() { self.loop.StartAtomicUpdate() self.RedrawNonAtomic() self.loop.EndAtomicUpdate() } func (self *Readline) RedrawNonAtomic() { self.redraw() } func (self *Readline) OnKeyEvent(event *loop.KeyEvent) error { err := self.handle_key_event(event) if err == ErrCouldNotPerformAction { err = nil self.loop.Beep() } return err } func (self *Readline) OnText(text string, from_key_event bool, in_bracketed_paste bool) error { if in_bracketed_paste { self.bracketed_paste_buffer.WriteString(text) return nil } if self.bracketed_paste_buffer.Len() > 0 { self.bracketed_paste_buffer.WriteString(text) text = self.bracketed_paste_buffer.String() self.bracketed_paste_buffer.Reset() } self.text_to_be_added = text return self.dispatch_key_action(ActionAddText) } func (self *Readline) TextBeforeCursor() string { return self.text_upto_cursor_pos() } func (self *Readline) TextAfterCursor() string { return self.text_after_cursor_pos() } func (self *Readline) AllText() string { return self.all_text() } func (self *Readline) CursorAtEndOfLine() bool { return self.cursor.X >= len(self.lines[self.cursor.Y]) } func (self *Readline) OnResize(old_size loop.ScreenSize, new_size loop.ScreenSize) error { self.screen_width = int(new_size.CellWidth) if self.screen_width < 1 { self.screen_width = 1 } self.Redraw() return nil }