Terminal background: decide 24-bit color like tcell, and log why

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
This commit is contained in:
2026-10-21 11:53:47 +02:00
co-authored by Claude Opus 5.5
parent 0270462fe4
commit d2e868293f
5 changed files with 48 additions and 18 deletions
+2 -4
View File
@@ -102,7 +102,7 @@ type overlay struct {
func New(cfg *config.Config) *App {
// Ask the terminal before the screen takes it over and before any widget
// copies the default background.
ownBackground := useTerminalBackground(terminalAnswers(), os.Getenv)
backgroundNote := useTerminalBackground(terminalAnswers(), os.Getenv)
a := &App{
app: tview.NewApplication(),
tabs: tview.NewTextView().SetDynamicColors(true),
@@ -139,9 +139,7 @@ func New(cfg *config.Config) *App {
if a.covers {
a.logs.add(levelInfo, coverNote)
}
if ownBackground {
a.logs.add(levelInfo, "Background: the terminal's own color")
}
a.logs.add(levelInfo, backgroundNote)
sabClient := sabnzbd.NewClient(cfg.SABnzbd.URL, cfg.SABnzbd.APIKey)
a.sab = sabClient
a.probes = []probe{
+31 -9
View File
@@ -2,6 +2,7 @@ package ui
import (
"bytes"
"fmt"
"image/color"
"strconv"
"strings"
@@ -9,6 +10,7 @@ import (
"time"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/terminfo"
"github.com/rivo/tview"
)
@@ -67,22 +69,42 @@ func parseTerminalAnswers(got []byte) (info terminalInfo) {
// 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
// reports whether it changed anything.
func useTerminalBackground(info terminalInfo, env func(string) string) bool {
if !info.hasBG || !hasTrueColor(env) || luminance(info.bg) > 0.4 {
return false
// 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 true
return "Background: the terminal's own color " + hex
}
// hasTrueColor reports whether the terminal says it draws 24-bit colors.
// 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 strings.ToLower(env("COLORTERM")) {
case "truecolor", "24bit":
switch env("TCELL_TRUECOLOR") {
case "":
case "disable":
return false
default:
return true
}
return strings.HasSuffix(env("TERM"), "-direct")
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).
+12 -3
View File
@@ -2,6 +2,7 @@ package ui
import (
"image/color"
"strings"
"testing"
"github.com/gdamore/tcell/v2"
@@ -31,6 +32,11 @@ func TestParseTerminalAnswers(t *testing.T) {
}
func TestUseTerminalBackground(t *testing.T) {
// tcell's terminal database also reads these from the real environment
// (and keeps what it added to an entry because of them, so the test uses
// a terminal nothing else looks up).
t.Setenv("COLORTERM", "")
t.Setenv("TCELL_TRUECOLOR", "")
defer func(old tcell.Color) { tview.Styles.PrimitiveBackgroundColor = old }(tview.Styles.PrimitiveBackgroundColor)
env := func(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
dark := terminalInfo{bg: color.RGBA{30, 30, 46, 255}, hasBG: true}
@@ -44,13 +50,16 @@ func TestUseTerminalBackground(t *testing.T) {
want bool
}{
{"dark background, true color", dark, truecolor, true},
{"no true color", dark, env(map[string]string{"TERM": "xterm-256color"}), false},
{"no true color", dark, env(map[string]string{"TERM": "vt100"}), false},
{"ghostty without COLORTERM", dark, env(map[string]string{"TERM": "xterm-ghostty"}), true},
{"true color disabled for tcell", dark, env(map[string]string{"TERM": "xterm-ghostty", "TCELL_TRUECOLOR": "disable"}), false},
{"light background keeps the default", light, truecolor, false},
{"no answer", terminalInfo{}, truecolor, false},
} {
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
if got := useTerminalBackground(tc.info, tc.env); got != tc.want {
t.Errorf("%s: %v", tc.name, got)
note := useTerminalBackground(tc.info, tc.env)
if got := strings.Contains(note, "own color"); got != tc.want {
t.Errorf("%s: %s", tc.name, note)
}
want := tcell.ColorBlack
if tc.want {