Go cannot set a read deadline on a terminal device on macOS (kqueue does not support them), so the probe failed at once and Ghostty and kitty got block posters there. Wait for the answer with select(2) instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
81 lines
2.1 KiB
Go
81 lines
2.1 KiB
Go
//go:build unix
|
|
|
|
package ui
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// probeKittyGraphics asks the terminal whether it understands the Kitty
|
|
// graphics protocol: a tiny query image (which a capable terminal answers with
|
|
// "OK" and does not show) followed by a Primary Device Attributes request,
|
|
// which every terminal answers. If the DA answer arrives without a graphics
|
|
// answer before it, the protocol is not supported. Runs before the screen is
|
|
// set up, on the controlling terminal.
|
|
func probeKittyGraphics(timeout time.Duration) bool {
|
|
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer tty.Close()
|
|
fd := int(tty.Fd())
|
|
state, err := term.MakeRaw(fd)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer term.Restore(fd, state)
|
|
|
|
const query = "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\" + "\x1b[c"
|
|
if _, err := tty.WriteString(query); err != nil {
|
|
return false
|
|
}
|
|
// A terminal that does not know APC sequences may print the query; erase it.
|
|
defer tty.WriteString("\r\x1b[2K")
|
|
|
|
// The wait uses select(2): a read deadline does not work on a terminal
|
|
// device on macOS (kqueue does not support them), which made the probe
|
|
// give up at once there.
|
|
deadline := time.Now().Add(timeout)
|
|
var got []byte
|
|
buf := make([]byte, 256)
|
|
for {
|
|
left := time.Until(deadline)
|
|
if left <= 0 {
|
|
return false // no answer in time: assume no support
|
|
}
|
|
var fds unix.FdSet
|
|
fds.Set(fd)
|
|
tv := unix.NsecToTimeval(left.Nanoseconds())
|
|
n, err := unix.Select(fd+1, &fds, nil, nil, &tv)
|
|
if errors.Is(err, unix.EINTR) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if n == 0 {
|
|
continue // timed out; the loop ends above
|
|
}
|
|
n, err = unix.Read(fd, buf)
|
|
if errors.Is(err, unix.EINTR) || errors.Is(err, unix.EAGAIN) {
|
|
continue
|
|
}
|
|
if err != nil || n <= 0 {
|
|
return false
|
|
}
|
|
got = append(got, buf[:n]...)
|
|
if bytes.Contains(got, []byte("_Gi=31;OK")) {
|
|
return true
|
|
}
|
|
if da := bytes.Index(got, []byte("\x1b[?")); da >= 0 && bytes.IndexByte(got[da:], 'c') >= 0 {
|
|
return false // device attributes answered, graphics query not
|
|
}
|
|
}
|
|
}
|