From fc463aaba5d1b5ec83ba6373ab20f4d6eaec172e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 3 Jan 2025 21:30:05 +0530 Subject: [PATCH] Code to query terminal for capabilities --- tools/tui/loop/api.go | 19 +++++++++++++++++ tools/tui/loop/capabilities.go | 23 +++++++++++++++++++++ tools/tui/loop/read.go | 37 +++++++++++++++++++++++++++++++++- tools/tui/loop/run.go | 36 ++++++++++++++++++++++++++++++++- 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 tools/tui/loop/capabilities.go diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 406d36931ce..fbd6a8cee1e 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -57,6 +57,10 @@ type Loop struct { style_ctx style.Context atomic_update_active bool pointer_shapes []PointerShape + waiting_for_capabilities_response bool + + // Queried capabilities from terminal + TerminalCapabilities TerminalCapabilities // Suspend the loop restoring terminal state, and run the provided function. When it returns terminal state is // put back to what it was before suspending unless the function returns an error or an error occurs saving/restoring state. @@ -111,6 +115,9 @@ type Loop struct { // Called on SIGTERM return true if you wish to handle it yourself OnSIGTERM func() (bool, error) + + // Called when capabilities response is received + OnCapabilitiesReceived func(TerminalCapabilities) } func New(options ...func(self *Loop)) (*Loop, error) { @@ -538,3 +545,15 @@ func (self *Loop) CurrentPointerShape() (ans PointerShape, has_shape bool) { } return } + +// Query the terminal for various capabilities, the OnCapabilitiesReceived +// callback will be called once the query response is received. This +// function should be called as early as possible ideally in OnInitialize. +func (self *Loop) QueryCapabilities() { + if !self.waiting_for_capabilities_response { + self.waiting_for_capabilities_response = true + self.StartAtomicUpdate() + self.QueueWriteString("\x1b[?u\x1b[?996n\x1b[c") + self.EndAtomicUpdate() + } +} diff --git a/tools/tui/loop/capabilities.go b/tools/tui/loop/capabilities.go new file mode 100644 index 00000000000..03145ca07f0 --- /dev/null +++ b/tools/tui/loop/capabilities.go @@ -0,0 +1,23 @@ +package loop + +import ( + "fmt" +) + +var _ = fmt.Print + +type ColorPreference uint8 + +const ( + NO_COLOR_PREFERENCE ColorPreference = iota + DARK_COLOR_PREFERENCE + LIGHT_COLOR_PREFERENCE +) + +type TerminalCapabilities struct { + KeyboardProtocol bool + KeyboardProtocolResponseReceived bool + + ColorPreference ColorPreference + ColorPreferenceResponseReceived bool +} diff --git a/tools/tui/loop/read.go b/tools/tui/loop/read.go index 671864618e6..433c6837000 100644 --- a/tools/tui/loop/read.go +++ b/tools/tui/loop/read.go @@ -6,6 +6,9 @@ import ( "fmt" "io" "os" + "regexp" + "strings" + "time" "golang.org/x/sys/unix" @@ -40,7 +43,7 @@ func read_ignoring_temporary_errors(f *tty.Term, buf []byte) (int, error) { return n, err } -func read_from_tty(pipe_r *os.File, term *tty.Term, results_channel chan<- []byte, err_channel chan<- error, quit_channel <-chan byte) { +func read_from_tty(pipe_r *os.File, term *tty.Term, results_channel chan<- []byte, err_channel chan<- error, quit_channel <-chan byte, leftover_channel chan<- []byte) { keep_going := true pipe_fd := int(pipe_r.Fd()) tty_fd := term.Fd() @@ -94,7 +97,39 @@ func read_from_tty(pipe_r *os.File, term *tty.Term, results_channel chan<- []byt select { case results_channel <- send: case <-quit_channel: + leftover_channel <- send keep_going = false } } } + +func has_da1_response(s string) bool { + pat := regexp.MustCompile("\x1b\\[\\?[0-9:;]+c") + return pat.FindString(s) != "" +} + +func read_until_primary_device_attributes_response(term *tty.Term, initial_bytes []byte, timeout time.Duration) { + s := strings.Builder{} + if initial_bytes != nil { + s.Write(initial_bytes) + } + received := make(chan error) + go func() { + buf := make([]byte, 1024) + n, err := read_ignoring_temporary_errors(term, buf) + if n > 0 { + s.Write(buf[:n]) + if has_da1_response(s.String()) { + received <- nil + return + } + } + if err != nil { + received <- err + } + }() + select { + case <-received: + case <-time.After(timeout): + } +} diff --git a/tools/tui/loop/run.go b/tools/tui/loop/run.go index a12fad51027..601979b32dd 100644 --- a/tools/tui/loop/run.go +++ b/tools/tui/loop/run.go @@ -124,6 +124,25 @@ func (self *Loop) handle_csi(raw []byte) (err error) { return self.handle_mouse_event(me) } } + if self.waiting_for_capabilities_response { + if strings.HasPrefix(csi, "?") && strings.HasSuffix(csi, "c") { + self.waiting_for_capabilities_response = false + if self.OnCapabilitiesReceived != nil { + self.OnCapabilitiesReceived(self.TerminalCapabilities) + } + } else if strings.HasPrefix(csi, "?997;") && strings.HasSuffix(csi, "n") { + switch csi[len(csi)-2] { + case '1': + self.TerminalCapabilities.ColorPreference = DARK_COLOR_PREFERENCE + case '2': + self.TerminalCapabilities.ColorPreference = LIGHT_COLOR_PREFERENCE + } + self.TerminalCapabilities.ColorPreferenceResponseReceived = true + } else if strings.HasPrefix(csi, "?") && strings.HasSuffix(csi, "u") { + self.TerminalCapabilities.KeyboardProtocol = true + self.TerminalCapabilities.KeyboardProtocolResponseReceived = true + } + } if self.OnEscapeCode != nil { return self.OnEscapeCode(CSI, raw) } @@ -368,6 +387,7 @@ func (self *Loop) run() (err error) { var r_r, r_w, w_r, w_w *os.File var tty_reading_done_channel chan byte var tty_read_channel chan []byte + var tty_leftover_read_channel chan []byte start_tty_reader := func() (err error) { r_r, r_w, err = os.Pipe() @@ -376,7 +396,8 @@ func (self *Loop) run() (err error) { } tty_read_channel = make(chan []byte) tty_reading_done_channel = make(chan byte) - go read_from_tty(r_r, controlling_term, tty_read_channel, err_channel, tty_reading_done_channel) + tty_leftover_read_channel = make(chan []byte, 1) + go read_from_tty(r_r, controlling_term, tty_read_channel, err_channel, tty_reading_done_channel, tty_leftover_read_channel) return } err = start_tty_reader() @@ -404,6 +425,19 @@ func (self *Loop) run() (err error) { // wait for tty reader to exit cleanly for range tty_read_channel { } + if !self.waiting_for_capabilities_response { + close(tty_leftover_read_channel) + return + } + var pending_bytes []byte + select { + case msg, ok := <-tty_leftover_read_channel: + if ok { + pending_bytes = msg + } + default: + } + read_until_primary_device_attributes_response(controlling_term, pending_bytes, 2*time.Second) } defer func() {