diff --git a/tools/cmd/at/main.go b/tools/cmd/at/main.go index 6fbd0321b..4c85e72fd 100644 --- a/tools/cmd/at/main.go +++ b/tools/cmd/at/main.go @@ -5,6 +5,7 @@ package at import ( "bytes" "encoding/json" + "errors" "fmt" "io" "os" @@ -128,6 +129,34 @@ type rc_io_data struct { send_keypresses bool string_response_is_err bool timeout time.Duration + + pending_chunks [][]byte +} + +func (self *rc_io_data) next_chunk(limit_size bool) (chunk []byte, err error) { + if len(self.pending_chunks) > 0 { + chunk = self.pending_chunks[0] + copy(self.pending_chunks, self.pending_chunks[1:]) + self.pending_chunks = self.pending_chunks[:len(self.pending_chunks)-1] + return + } + block, err := self.next_block(self.rc, self.serializer) + if err != nil && !errors.Is(err, io.EOF) { + return + } + err = nil + const limit = 2048 + if !limit_size || len(block) < limit { + chunk = block + return + } + chunk = block[:limit] + block = block[limit:] + for len(block) > 0 { + self.pending_chunks = append(self.pending_chunks, block[:limit]) + block = block[limit:] + } + return } func get_response(do_io func(io_data *rc_io_data) ([]byte, error), io_data *rc_io_data) (ans *Response, err error) { @@ -137,6 +166,9 @@ func get_response(do_io func(io_data *rc_io_data) ([]byte, error), io_data *rc_i io_data.rc.Payload = nil io_data.rc.CancelAsync = true io_data.rc.NoResponse = true + io_data.next_block = func(rc *utils.RemoteControlCmd, serializer serializer_func) ([]byte, error) { + return serializer(rc) + } _, err = do_io(io_data) } return diff --git a/tools/cmd/at/template.go b/tools/cmd/at/template.go index 92fcc3417..7a99686d2 100644 --- a/tools/cmd/at/template.go +++ b/tools/cmd/at/template.go @@ -63,7 +63,7 @@ func run_CMD_NAME(cmd *cobra.Command, args []string) (err error) { timeout: time.Duration(timeout * float64(time.Second)), string_response_is_err: STRING_RESPONSE_IS_ERROR, next_block: func(rc *utils.RemoteControlCmd, serializer serializer_func) ([]byte, error) { - return make([]byte, 0), nil + return serializer(rc) }, } err = create_payload_CMD_NAME(&io_data, args) diff --git a/tools/cmd/at/tty_io.go b/tools/cmd/at/tty_io.go index f2d80d1e5..13374b701 100644 --- a/tools/cmd/at/tty_io.go +++ b/tools/cmd/at/tty_io.go @@ -2,6 +2,79 @@ package at -func do_tty_io(io_data *rc_io_data) (serialized_response []byte, err error) { +import ( + "kitty/tools/tui" + "os" + "time" +) + +func do_chunked_io(io_data *rc_io_data) (serialized_response []byte, err error) { + serialized_response = make([]byte, 0) + loop, err := tui.CreateLoop() + loop.NoAlternateScreen() + if err != nil { + return + } + + var last_received_data_at time.Time + + check_for_timeout := func(loop *tui.Loop, timer_id tui.TimerId) error { + if time.Now().Sub(last_received_data_at) > io_data.timeout { + return os.ErrDeadlineExceeded + } + return nil + } + + transition_to_read := func() { + if io_data.rc.NoResponse { + loop.Quit(0) + } + last_received_data_at = time.Now() + loop.AddTimer(10*time.Millisecond, true, check_for_timeout) + } + + loop.OnReceivedData = func(loop *tui.Loop, data []byte) error { + last_received_data_at = time.Now() + return nil + } + + loop.OnInitialize = func(loop *tui.Loop) (string, error) { + chunk, err := io_data.next_chunk(true) + if err != nil { + return "", err + } + if len(chunk) == 0 { + transition_to_read() + } else { + loop.QueueWriteBytes(chunk) + } + return "", nil + } + + loop.OnWriteComplete = func(loop *tui.Loop) error { + chunk, err := io_data.next_chunk(true) + if err != nil { + return err + } + if len(chunk) == 0 { + transition_to_read() + } else { + loop.QueueWriteBytes(chunk) + } + return nil + } + + loop.OnRCResponse = func(loop *tui.Loop, raw []byte) error { + serialized_response = raw + loop.Quit(0) + return nil + } + + err = loop.Run() return + +} + +func do_tty_io(io_data *rc_io_data) (serialized_response []byte, err error) { + return do_chunked_io(io_data) } diff --git a/tools/tui/loop.go b/tools/tui/loop.go index 3cac480b4..6b0cfe449 100644 --- a/tools/tui/loop.go +++ b/tools/tui/loop.go @@ -3,6 +3,7 @@ package tui import ( + "bytes" "fmt" "io" "kitty/tools/tty" @@ -75,7 +76,7 @@ type Loop struct { // Called when the terminal has been fully setup. Any string returned is sent to // the terminal on shutdown - OnInitialize func(loop *Loop) string + OnInitialize func(loop *Loop) (string, error) // Called when a key event happens OnKeyEvent func(loop *Loop, event *KeyEvent) error @@ -88,6 +89,12 @@ type Loop struct { // Called when writing is done OnWriteComplete func(loop *Loop) error + + // Called when a response to an rc command is received + OnRCResponse func(loop *Loop, data []byte) error + + // Called when any input form tty is received + OnReceivedData func(loop *Loop, data []byte) error } func (self *Loop) update_screen_size() error { @@ -146,6 +153,9 @@ func (self *Loop) handle_osc(raw []byte) error { } func (self *Loop) handle_dcs(raw []byte) error { + if self.OnRCResponse != nil && bytes.HasPrefix(raw, []byte("@kitty-cmd")) { + return self.OnRCResponse(self, raw[len("@kitty-cmd"):]) + } return nil } @@ -327,7 +337,10 @@ func (self *Loop) Run() (err error) { self.queue_write_to_tty(self.terminal_options.SetStateEscapeCodes()) finalizer := "" if self.OnInitialize != nil { - finalizer = self.OnInitialize(self) + finalizer, err = self.OnInitialize(self) + if err != nil { + return err + } } defer func() { @@ -393,6 +406,12 @@ func (self *Loop) Run() (err error) { return err } if num_read > 0 { + if self.OnReceivedData != nil { + err = self.OnReceivedData(self, read_buf[:num_read]) + if err != nil { + return err + } + } err = self.escape_code_parser.Parse(read_buf[:num_read]) if err != nil { return err @@ -419,6 +438,10 @@ func (self *Loop) QueueWriteString(data string) { self.queue_write_to_tty([]byte(data)) } +func (self *Loop) QueueWriteBytes(data []byte) { + self.queue_write_to_tty(data) +} + func (self *Loop) ExitCode() int { return self.exit_code } diff --git a/tools/tui/password.go b/tools/tui/password.go index 7c30696cd..9b0bad803 100644 --- a/tools/tui/password.go +++ b/tools/tui/password.go @@ -27,9 +27,9 @@ func ReadPassword(prompt string, kill_if_signaled bool) (password string, err er return } - loop.OnInitialize = func(loop *Loop) string { + loop.OnInitialize = func(loop *Loop) (string, error) { loop.QueueWriteString(prompt) - return "\r\n" + return "\r\n", nil } loop.OnText = func(loop *Loop, text string, from_key_event bool, in_bracketed_paste bool) error {