package ui import ( "bytes" "image/color" "strconv" "strings" "sync" "time" ) // terminalInfo is what the terminal said about itself at startup. type terminalInfo struct { graphics bool // the Kitty graphics protocol is supported bg color.RGBA // its background color, if hasBG hasBG bool } var ( terminalOnce sync.Once terminalResult terminalInfo ) // terminalAnswers asks the terminal once (see probeTerminal) and remembers the // answers. The first call must happen before the screen is set up, which owns // the terminal afterwards; New makes it. It is a variable so tests can replace it. var terminalAnswers = func() terminalInfo { terminalOnce.Do(func() { terminalResult = probeTerminal(300 * time.Millisecond) }) return terminalResult } // parseTerminalAnswers reads the answers to probeTerminal's questions. func parseTerminalAnswers(got []byte) (info terminalInfo) { info.graphics = bytes.Contains(got, []byte("_Gi=31;OK")) info.bg, info.hasBG = parseColorAnswer(got, "11") return info } // parseColorAnswer finds the answer to an OSC color query such as 11 // (background): ESC ] 11 ; rgb:RRRR/GGGG/BBBB, ended by ST or BEL. Each // component has 1 to 4 hex digits. func parseColorAnswer(got []byte, osc string) (color.RGBA, bool) { prefix := []byte("\x1b]" + osc + ";rgb:") i := bytes.Index(got, prefix) if i < 0 { return color.RGBA{}, false } rest := got[i+len(prefix):] if end := bytes.IndexAny(rest, "\x1b\a"); end >= 0 { rest = rest[:end] } parts := strings.Split(string(rest), "/") if len(parts) != 3 { return color.RGBA{}, false } var c [3]uint8 for j, p := range parts { v, err := strconv.ParseUint(p, 16, 16) if err != nil || len(p) < 1 || len(p) > 4 { return color.RGBA{}, false } c[j] = uint8(v * 255 / (1<<(4*len(p)) - 1)) } return color.RGBA{c[0], c[1], c[2], 255}, true } // luminance is the relative brightness of c, from 0 (black) to 1 (white). func luminance(c color.RGBA) float64 { return (0.2126*float64(c.R) + 0.7152*float64(c.G) + 0.0722*float64(c.B)) / 255 }