//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) }