The check only looked at COLORTERM, but tcell also draws 24-bit color for terminals whose description has it (xterm-ghostty, xterm-kitty), so in Ghostty on macOS without COLORTERM the background was never used. Use tcell's rules, and always log which background is used and why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
114 lines
3.8 KiB
Go
114 lines
3.8 KiB
Go
package ui
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image/color"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/gdamore/tcell/v2/terminfo"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
// 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"))
|
|
// OSC 11 answer: ESC ] 11 ; rgb:RRRR/GGGG/BBBB, ended by ST or BEL. Each
|
|
// component has 1 to 4 hex digits.
|
|
if i := bytes.Index(got, []byte("\x1b]11;rgb:")); i >= 0 {
|
|
rest := got[i+len("\x1b]11;rgb:"):]
|
|
if end := bytes.IndexAny(rest, "\x1b\a"); end >= 0 {
|
|
rest = rest[:end]
|
|
}
|
|
if parts := strings.Split(string(rest), "/"); len(parts) == 3 {
|
|
var c [3]uint8
|
|
ok := true
|
|
for j, p := range parts {
|
|
v, err := strconv.ParseUint(p, 16, 16)
|
|
if err != nil || len(p) < 1 || len(p) > 4 {
|
|
ok = false
|
|
break
|
|
}
|
|
c[j] = uint8(v * 255 / (1<<(4*len(p)) - 1))
|
|
}
|
|
if ok {
|
|
info.bg, info.hasBG = color.RGBA{c[0], c[1], c[2], 255}, true
|
|
}
|
|
}
|
|
}
|
|
return info
|
|
}
|
|
|
|
// useTerminalBackground makes the terminal's own background color the
|
|
// background of every pane, instead of color 0 of the palette ("black"), which
|
|
// color themes often set to a grey that differs from their background (and
|
|
// on which grey text almost disappears). Only for dark backgrounds: the text
|
|
// colors are made for those. Needs true color, else the exact color cannot be
|
|
// drawn. Must run before any widget is created, they copy the style. It
|
|
// returns a line for the log that says what was decided and why.
|
|
func useTerminalBackground(info terminalInfo, env func(string) string) string {
|
|
if !info.hasBG {
|
|
return "Background: palette color 0 (the terminal did not report its background color)"
|
|
}
|
|
hex := fmt.Sprintf("#%02x%02x%02x", info.bg.R, info.bg.G, info.bg.B)
|
|
switch {
|
|
case !hasTrueColor(env):
|
|
return "Background: palette color 0 (the terminal's " + hex + " needs 24-bit color, which " + env("TERM") + " does not have; COLORTERM=truecolor enables it)"
|
|
case luminance(info.bg) > 0.4:
|
|
return "Background: palette color 0 (the terminal's " + hex + " is light; the text colors are made for dark backgrounds)"
|
|
}
|
|
tview.Styles.PrimitiveBackgroundColor = tcell.NewRGBColor(int32(info.bg.R), int32(info.bg.G), int32(info.bg.B))
|
|
return "Background: the terminal's own color " + hex
|
|
}
|
|
|
|
// hasTrueColor reports whether tcell will draw 24-bit colors, by the same
|
|
// rules: TCELL_TRUECOLOR, COLORTERM, or a terminal description with direct
|
|
// color (e.g. xterm-ghostty and xterm-kitty, also without COLORTERM).
|
|
func hasTrueColor(env func(string) string) bool {
|
|
switch env("TCELL_TRUECOLOR") {
|
|
case "":
|
|
case "disable":
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
switch strings.ToLower(env("COLORTERM")) {
|
|
case "truecolor", "24bit", "24-bit":
|
|
return true
|
|
}
|
|
if strings.HasSuffix(env("TERM"), "-direct") {
|
|
return true
|
|
}
|
|
ti, err := terminfo.LookupTerminfo(env("TERM"))
|
|
return err == nil && (ti.TrueColor || ti.SetFgBgRGB != "")
|
|
}
|
|
|
|
// 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
|
|
}
|