Code to query terminal for capabilities

This commit is contained in:
Kovid Goyal
2025-01-03 21:30:05 +05:30
parent e1f60fa71a
commit fc463aaba5
4 changed files with 113 additions and 2 deletions

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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):
}
}

View File

@@ -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() {