Skip to content

Commit

Permalink
Code to query terminal for capabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
kovidgoyal committed Jan 3, 2025
1 parent e1f60fa commit fc463aa
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 2 deletions.
19 changes: 19 additions & 0 deletions tools/tui/loop/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
}
}
23 changes: 23 additions & 0 deletions tools/tui/loop/capabilities.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 36 additions & 1 deletion tools/tui/loop/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"fmt"
"io"
"os"
"regexp"
"strings"
"time"

"golang.org/x/sys/unix"

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
}
}
36 changes: 35 additions & 1 deletion tools/tui/loop/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit fc463aa

Please sign in to comment.