From c8296a44eb8c1f635a6a18f80860493df07cda34 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 4 Oct 2022 13:33:13 +0530 Subject: [PATCH] More work on readline --- tools/cmd/at/shell.go | 10 +++- tools/tui/loop/api.go | 34 ++++++++++++++ tools/tui/loop/terminal-state.go | 15 ++++++ tools/tui/readline/api.go | 81 ++++++++++++++++++++++++++++++++ tools/tui/readline/draw.go | 58 +++++++++++++++++++++++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 tools/tui/readline/api.go create mode 100644 tools/tui/readline/draw.go diff --git a/tools/cmd/at/shell.go b/tools/cmd/at/shell.go index a3ff1ccbb..a20f20bab 100644 --- a/tools/cmd/at/shell.go +++ b/tools/cmd/at/shell.go @@ -8,6 +8,7 @@ import ( "kitty/tools/cli" "kitty/tools/cli/markup" "kitty/tools/tui/loop" + "kitty/tools/tui/readline" ) var _ = fmt.Print @@ -21,11 +22,18 @@ func shell_loop(kill_if_signaled bool) (int, error) { if err != nil { return 1, err } + rl := readline.New(lp, readline.RlInit{Prompt: prompt}) + lp.OnInitialize = func() (string, error) { - lp.QueueWriteString(prompt) + rl.Start() return "\r\n", nil } + lp.OnResumeFromStop = func() error { + rl.Start() + return nil + } + err = lp.Run() if err != nil { return 1, err diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 64ab3e2e8..6d1aa5ac3 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -193,6 +193,40 @@ func (self *Loop) Beep() { self.QueueWriteString("\a") } +func (self *Loop) StartAtomicUpdate() { + self.QueueWriteString(PENDING_UPDATE.EscapeCodeToSet()) +} + +func (self *Loop) EndAtomicUpdate() { + self.QueueWriteString(PENDING_UPDATE.EscapeCodeToReset()) +} + +func (self *Loop) SetCursorShape(shape CursorShapes, blink bool) { + self.QueueWriteString(CursorShape(shape, blink)) +} + +func (self *Loop) MoveCursorHorizontally(amt int) { + suffix := "C" + if amt < 0 { + suffix = "D" + amt *= -1 + } + self.QueueWriteString(fmt.Sprintf("\x1b[%d%s", amt, suffix)) +} + +func (self *Loop) MoveCursorVertically(amt int) { + suffix := "B" + if amt < 0 { + suffix = "A" + amt *= -1 + } + self.QueueWriteString(fmt.Sprintf("\x1b[%d%s", amt, suffix)) +} + +func (self *Loop) ClearToEndOfScreen() { + self.QueueWriteString("\x1b[J") +} + func (self *Loop) Quit(exit_code int) { self.exit_code = exit_code self.keep_going = false diff --git a/tools/tui/loop/terminal-state.go b/tools/tui/loop/terminal-state.go index 945144901..5684c7d59 100644 --- a/tools/tui/loop/terminal-state.go +++ b/tools/tui/loop/terminal-state.go @@ -21,6 +21,14 @@ const ( CLEAR_SCREEN = "\033[H\033[2J" ) +type CursorShapes uint + +const ( + BLOCK_CURSOR CursorShapes = 1 + UNDERLINE_CURSOR CursorShapes = 3 + BAR_CURSOR CursorShapes = 5 +) + type Mode uint32 const private Mode = 1 << 31 @@ -147,3 +155,10 @@ func (self *TerminalStateOptions) ResetStateEscapeCodes() string { sb.WriteString(RESTORE_COLORS) return sb.String() } + +func CursorShape(shape CursorShapes, blink bool) string { + if !blink { + shape += 1 + } + return fmt.Sprintf("\x1b[%d q", shape) +} diff --git a/tools/tui/readline/api.go b/tools/tui/readline/api.go new file mode 100644 index 000000000..c8cbb855f --- /dev/null +++ b/tools/tui/readline/api.go @@ -0,0 +1,81 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package readline + +import ( + "fmt" + + "kitty/tools/tui/loop" + "kitty/tools/wcswidth" +) + +var _ = fmt.Print + +const ST = "\x1b\\" +const PROMPT_MARK = "\x1b]133;" + +type RlInit struct { + Prompt string + ContinuationPrompt string + EmptyContinuationPrompt bool + DontMarkPrompts bool +} + +type Readline struct { + prompt string + prompt_len int + continuation_prompt string + continuation_prompt_len int + mark_prompts bool + loop *loop.Loop + + // The number of lines after the initial line + cursor_y int + // Input lines + lines []string + // The line the cursor is at currently + cursor_line int + // The offset into the text of the cursor line + cursor_pos_in_line int +} + +func New(loop *loop.Loop, r RlInit) *Readline { + ans := &Readline{ + prompt: r.Prompt, prompt_len: wcswidth.Stringwidth(r.Prompt), mark_prompts: !r.DontMarkPrompts, + loop: loop, lines: []string{""}, + } + if r.ContinuationPrompt != "" || !r.EmptyContinuationPrompt { + ans.continuation_prompt = r.ContinuationPrompt + if ans.continuation_prompt == "" { + ans.continuation_prompt = "> " + } + } + if ans.mark_prompts { + ans.prompt = PROMPT_MARK + "A" + ST + ans.prompt + ans.continuation_prompt = PROMPT_MARK + "A;k=s" + ST + ans.continuation_prompt + } + return ans +} + +func (self *Readline) Start() { + self.loop.SetCursorShape(loop.BAR_CURSOR, true) + self.Redraw() +} + +func (self *Readline) Redraw() { + self.loop.StartAtomicUpdate() + self.RedrawNonAtomic() + self.loop.EndAtomicUpdate() +} + +func (self *Readline) RedrawNonAtomic() { + self.redraw() +} + +func (self *Readline) End() { + self.loop.QueueWriteString("\r\n") + self.loop.SetCursorShape(loop.BLOCK_CURSOR, true) + if self.mark_prompts { + self.loop.QueueWriteString(PROMPT_MARK + "C" + ST) + } +} diff --git a/tools/tui/readline/draw.go b/tools/tui/readline/draw.go new file mode 100644 index 000000000..efd5bb785 --- /dev/null +++ b/tools/tui/readline/draw.go @@ -0,0 +1,58 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package readline + +import ( + "fmt" + "kitty/tools/wcswidth" +) + +var _ = fmt.Print + +func (self *Readline) write_line_with_prompt(line, prompt string, screen_width int) int { + self.loop.QueueWriteString(prompt) + self.loop.QueueWriteString(line) + w := wcswidth.Stringwidth(prompt) + wcswidth.Stringwidth(line) + return w / screen_width +} + +func (self *Readline) move_cursor_to_text_position(pos, screen_width int) int { + num_of_lines := pos / screen_width + if num_of_lines > 0 { + self.loop.MoveCursorVertically(num_of_lines) + } + self.loop.QueueWriteString("\r") + x := pos % screen_width + self.loop.MoveCursorHorizontally(x) + return num_of_lines +} + +func (self *Readline) redraw() { + if self.cursor_y > 0 { + self.loop.MoveCursorVertically(-self.cursor_y) + } + self.loop.QueueWriteString("\r") + self.loop.ClearToEndOfScreen() + y := 0 + line_with_cursor := 0 + screen_size, err := self.loop.ScreenSize() + if err != nil { + screen_size.WidthCells = 80 + screen_size.HeightCells = 24 + } + for i, line := range self.lines { + p := self.prompt + if i > 0 { + p = self.continuation_prompt + } + num_lines := self.write_line_with_prompt(line, p, int(screen_size.WidthCells)) + if i == self.cursor_line { + line_with_cursor = y + } + y += num_lines + } + self.loop.MoveCursorVertically(-y + line_with_cursor) + line := self.lines[self.cursor_line] + line_with_cursor += self.move_cursor_to_text_position(wcswidth.Stringwidth(line[:self.cursor_pos_in_line]), int(screen_size.WidthCells)) + self.cursor_y = line_with_cursor +}