Use the terminal's own background color

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
This commit is contained in:
2026-10-21 11:47:27 +02:00
co-authored by Claude Opus 5.5
parent 7e77d56879
commit 0270462fe4
9 changed files with 192 additions and 35 deletions
+16 -18
View File
@@ -12,28 +12,28 @@ import (
"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 {
// 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 false
return info
}
defer tty.Close()
fd := int(tty.Fd())
state, err := term.MakeRaw(fd)
if err != nil {
return false
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[c"
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 false
return info
}
// A terminal that does not know APC sequences may print the query; erase it.
defer tty.WriteString("\r\x1b[2K")
@@ -47,7 +47,7 @@ func probeKittyGraphics(timeout time.Duration) bool {
for {
left := time.Until(deadline)
if left <= 0 {
return false // no answer in time: assume no support
break // no complete answer in time: keep what arrived
}
var fds unix.FdSet
fds.Set(fd)
@@ -57,7 +57,7 @@ func probeKittyGraphics(timeout time.Duration) bool {
continue
}
if err != nil {
return false
break
}
if n == 0 {
continue // timed out; the loop ends above
@@ -67,14 +67,12 @@ func probeKittyGraphics(timeout time.Duration) bool {
continue
}
if err != nil || n <= 0 {
return false
break
}
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
break // device attributes answered: everything else came before
}
}
return parseTerminalAnswers(got)
}