The panes used palette color 0, which color themes such as Catppuccin set to a grey lighter than their background: everything looked washed out and grey text almost disappeared (seen in Ghostty on macOS). The startup probe now also asks for the background color (OSC 11), and with 24-bit color and a dark background that color is used. The probe always runs at startup, so a later switch to real images in the Config view never has to ask the terminal while the screen owns it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
79 lines
2.1 KiB
Go
79 lines
2.1 KiB
Go
//go:build unix
|
|
|
|
package ui
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// probeTerminal asks the terminal two things: whether it understands the Kitty
|
|
// graphics protocol (a tiny query image, which a capable terminal answers with
|
|
// "OK" and does not show) and its background color (OSC 11). A Primary Device
|
|
// Attributes request follows, which every terminal answers, and answers come
|
|
// in order: once the DA answer is in, whatever was not answered before it is
|
|
// not supported. Runs before the screen is set up, on the controlling terminal.
|
|
func probeTerminal(timeout time.Duration) (info terminalInfo) {
|
|
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
|
|
if err != nil {
|
|
return info
|
|
}
|
|
defer tty.Close()
|
|
fd := int(tty.Fd())
|
|
state, err := term.MakeRaw(fd)
|
|
if err != nil {
|
|
return info
|
|
}
|
|
defer term.Restore(fd, state)
|
|
|
|
const query = "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\" + "\x1b]11;?\x1b\\" + "\x1b[c"
|
|
if _, err := tty.WriteString(query); err != nil {
|
|
return info
|
|
}
|
|
// 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 {
|
|
break // no complete answer in time: keep what arrived
|
|
}
|
|
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 {
|
|
break
|
|
}
|
|
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 {
|
|
break
|
|
}
|
|
got = append(got, buf[:n]...)
|
|
if da := bytes.Index(got, []byte("\x1b[?")); da >= 0 && bytes.IndexByte(got[da:], 'c') >= 0 {
|
|
break // device attributes answered: everything else came before
|
|
}
|
|
}
|
|
return parseTerminalAnswers(got)
|
|
}
|