diff --git a/tools/cmd/at/shell.go b/tools/cmd/at/shell.go index a540a2f8d..d02705eda 100644 --- a/tools/cmd/at/shell.go +++ b/tools/cmd/at/shell.go @@ -3,7 +3,13 @@ package at import ( + "errors" "fmt" + "io" + "os" + "strings" + + "github.com/google/shlex" "kitty/tools/cli" "kitty/tools/cli/markup" @@ -17,17 +23,20 @@ var formatter *markup.Context const prompt = "🐱 " -func shell_loop(kill_if_signaled bool) (int, error) { +var ErrExec = errors.New("Execute command") + +func shell_loop(rl *readline.Readline, kill_if_signaled bool) (int, error) { lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors) if err != nil { return 1, err } - rl := readline.New(lp, readline.RlInit{Prompt: prompt}) + rl.ChangeLoopAndResetText(lp) lp.OnInitialize = func() (string, error) { rl.Start() - return "\r\n", nil + return "", nil } + lp.OnFinalize = func() string { rl.End(); return "" } lp.OnResumeFromStop = func() error { rl.Start() @@ -42,6 +51,17 @@ func shell_loop(kill_if_signaled bool) (int, error) { lp.OnKeyEvent = func(event *loop.KeyEvent) error { err := rl.OnKeyEvent(event) if err != nil { + if err == io.EOF { + lp.Quit(0) + return nil + } + if err == readline.ErrAcceptInput { + if strings.HasSuffix(rl.TextBeforeCursor(), "\\") && strings.HasPrefix(rl.TextAfterCursor(), "\n") { + rl.OnText("\n", false, false) + return nil + } + return ErrExec + } return err } if event.Handled { @@ -67,9 +87,39 @@ func shell_loop(kill_if_signaled bool) (int, error) { return 0, nil } +func exec_command(cmdline string) bool { + parsed_cmdline, err := shlex.Split(cmdline) + if err != nil { + fmt.Fprintln(os.Stderr, "Could not parse cmdline:", err) + return true + } + if len(parsed_cmdline) == 0 { + return true + } + switch parsed_cmdline[0] { + case "exit": + return false + } + return true +} + func shell_main(cmd *cli.Command, args []string) (int, error) { formatter = markup.New(true) fmt.Println("Welcome to the kitty shell!") fmt.Println("Use", formatter.Green("help"), "for assistance or", formatter.Green("exit"), "to quit.") - return shell_loop(true) + rl := readline.New(nil, readline.RlInit{Prompt: prompt}) + for { + rc, err := shell_loop(rl, true) + if err != nil { + if err == ErrExec { + cmdline := rl.AllText() + cmdline = strings.ReplaceAll(cmdline, "\\\n", "") + if !exec_command(cmdline) { + return 0, nil + } + continue + } + } + return rc, err + } } diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 4cabb2853..a8c683af7 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -53,6 +53,10 @@ type Loop struct { // the terminal on shutdown OnInitialize func() (string, error) + // Called just before the loop shuts down. Any returned string is written to the terminal before + // shutdown + OnFinalize func() string + // Called when a key event happens OnKeyEvent func(event *KeyEvent) error diff --git a/tools/tui/readline/actions.go b/tools/tui/readline/actions.go index ea0c22a85..3c29d4f90 100644 --- a/tools/tui/readline/actions.go +++ b/tools/tui/readline/actions.go @@ -4,6 +4,7 @@ package readline import ( "fmt" + "io" "strings" "kitty/tools/utils" @@ -259,24 +260,52 @@ func (self *Readline) erase_chars_after_cursor(amt uint, traverse_line_breaks bo return num } -func (self *Readline) perform_action(ac Action, repeat_count uint) bool { +func (self *Readline) next_word_char_pos(traverse_line_breaks bool) int { + return 0 +} + +func (self *Readline) perform_action(ac Action, repeat_count uint) error { switch ac { case ActionBackspace: - return self.erase_chars_before_cursor(repeat_count, true) > 0 + if self.erase_chars_before_cursor(repeat_count, true) > 0 { + return nil + } case ActionDelete: - return self.erase_chars_after_cursor(repeat_count, true) > 0 + if self.erase_chars_after_cursor(repeat_count, true) > 0 { + return nil + } case ActionMoveToStartOfLine: - return self.move_to_start_of_line() + if self.move_to_start_of_line() { + return nil + } case ActionMoveToEndOfLine: - return self.move_to_end_of_line() + if self.move_to_end_of_line() { + return nil + } case ActionMoveToStartOfDocument: - return self.move_to_start() + if self.move_to_start() { + return nil + } case ActionMoveToEndOfDocument: - return self.move_to_end() + if self.move_to_end() { + return nil + } case ActionCursorLeft: - return self.move_cursor_left(repeat_count, true) > 0 + if self.move_cursor_left(repeat_count, true) > 0 { + return nil + } case ActionCursorRight: - return self.move_cursor_right(repeat_count, true) > 0 + if self.move_cursor_right(repeat_count, true) > 0 { + return nil + } + case ActionEndInput: + line := self.lines[self.cursor.Y] + if line == "" { + return io.EOF + } + return self.perform_action(ActionAcceptInput, 1) + case ActionAcceptInput: + return ErrAcceptInput } - return false + return ErrCouldNotPerformAction } diff --git a/tools/tui/readline/api.go b/tools/tui/readline/api.go index 062498765..1da1a76d6 100644 --- a/tools/tui/readline/api.go +++ b/tools/tui/readline/api.go @@ -42,6 +42,8 @@ const ( ActionMoveToEndOfDocument ActionCursorLeft ActionCursorRight + ActionEndInput + ActionAcceptInput ) type Readline struct { @@ -78,12 +80,32 @@ func New(loop *loop.Loop, r RlInit) *Readline { return ans } +func (self *Readline) ChangeLoopAndResetText(lp *loop.Loop) { + self.loop = lp + self.lines = []string{""} + self.cursor = Position{} + self.cursor_y = 0 +} + 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() @@ -94,15 +116,6 @@ func (self *Readline) RedrawNonAtomic() { self.redraw() } -func (self *Readline) End() { - self.loop.EndBracketedPaste() - self.loop.QueueWriteString("\r\n") - self.loop.SetCursorShape(loop.BLOCK_CURSOR, true) - if self.mark_prompts { - self.loop.QueueWriteString(PROMPT_MARK + "C" + ST) - } -} - func (self *Readline) OnKeyEvent(event *loop.KeyEvent) error { err := self.handle_key_event(event) if err == ErrCouldNotPerformAction { @@ -117,6 +130,14 @@ func (self *Readline) OnText(text string, from_key_event bool, in_bracketed_past return nil } -func (self *Readline) PerformAction(ac Action, repeat_count uint) bool { - return self.perform_action(ac, repeat_count) +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() } diff --git a/tools/tui/readline/keys.go b/tools/tui/readline/keys.go index a3ac971fa..b8e16a4d5 100644 --- a/tools/tui/readline/keys.go +++ b/tools/tui/readline/keys.go @@ -15,7 +15,6 @@ var default_shortcuts = map[string]Action{ "backspace": ActionBackspace, "ctrl+h": ActionBackspace, "delete": ActionDelete, - "ctrl+d": ActionDelete, "home": ActionMoveToStartOfLine, "ctrl+a": ActionMoveToStartOfLine, @@ -30,6 +29,9 @@ var default_shortcuts = map[string]Action{ "ctrl+b": ActionCursorLeft, "right": ActionCursorRight, "ctrl+f": ActionCursorRight, + + "ctrl+d": ActionEndInput, + "enter": ActionAcceptInput, } func action_for_key_event(event *loop.KeyEvent, shortcuts map[string]Action) Action { @@ -42,6 +44,7 @@ func action_for_key_event(event *loop.KeyEvent, shortcuts map[string]Action) Act } var ErrCouldNotPerformAction = errors.New("Could not perform the specified action") +var ErrAcceptInput = errors.New("Accept input") func (self *Readline) handle_key_event(event *loop.KeyEvent) error { if event.Text != "" { @@ -50,9 +53,7 @@ func (self *Readline) handle_key_event(event *loop.KeyEvent) error { ac := action_for_key_event(event, default_shortcuts) if ac != ActionNil { event.Handled = true - if !self.perform_action(ac, 1) { - return ErrCouldNotPerformAction - } + return self.perform_action(ac, 1) } return nil }