2022-11-14 15:42:07 +05:30

292 lines
6.1 KiB
Go

// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
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
}