Files
arr-tui/internal/ui/probe_unix.go
T
nilsandClaude Opus 5.5 08d3417b05 Light terminal themes: use the terminal's text color too
On a light background (Catppuccin Latte in Ghostty on macOS) the panes
kept palette color 0, a dark grey slab with white text. The probe now also
asks for the text color (OSC 10). Light backgrounds are used as well, with
text, borders and titles in the terminal's text color, input fields and
buttons in a light shade of it, and the active tab drawn in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
2026-10-21 11:59:44 +02:00

79 lines
2.2 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 whether it understands the Kitty graphics
// protocol (a tiny query image, which a capable terminal answers with "OK" and
// does not show) and for its text and background colors (OSC 10 and 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]10;?\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)
}