// 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 SyntaxHighlightFunction func(text string, x, y int) string type RlInit struct { Prompt string HistoryPath string HistoryCount int ContinuationPrompt string EmptyContinuationPrompt bool DontMarkPrompts bool SyntaxHighlighter SyntaxHighlightFunction } 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) } // Actions {{{ type Action uint const ( ActionNil Action = iota ActionIgnored ActionBackspace ActionDelete ActionMoveToStartOfLine ActionMoveToEndOfLine ActionMoveToStartOfDocument ActionMoveToEndOfDocument ActionMoveToEndOfWord ActionMoveToStartOfWord ActionCursorLeft ActionCursorRight ActionEndInput ActionAcceptInput ActionCursorUp ActionHistoryPreviousOrCursorUp ActionCursorDown ActionHistoryNextOrCursorDown ActionHistoryNext ActionHistoryPrevious ActionHistoryFirst ActionHistoryLast ActionHistoryIncrementalSearchBackwards ActionHistoryIncrementalSearchForwards ActionTerminateHistorySearchAndApply ActionTerminateHistorySearchAndRestore 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 InputState struct { // Input lines lines []string // The cursor position in the text cursor Position } func (self InputState) copy() InputState { ans := self l := make([]string, len(self.lines)) copy(l, self.lines) ans.lines = l return ans } type syntax_highlighted struct { lines []string src_for_last_highlight string highlighter SyntaxHighlightFunction last_highlighter_name string } type Readline struct { prompt, continuation_prompt Prompt mark_prompts bool loop *loop.Loop history *History kill_ring kill_ring input_state InputState // The number of lines after the initial line on the screen cursor_y int screen_width int 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 syntax_highlighted syntax_highlighted } 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, input_state: InputState{lines: []string{""}}, history: NewHistory(r.HistoryPath, hc), syntax_highlighted: syntax_highlighted{highlighter: r.SyntaxHighlighter}, 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.input_state = InputState{lines: []string{""}} self.last_action = ActionNil self.keyboard_state = KeyboardState{} self.history_search = nil self.cursor_y = 0 } 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.input_state.cursor.X >= len(self.input_state.lines[self.input_state.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 }