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:
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
- **The terminal's own background:** the panes used color 0 of the terminal's palette ("black"),
|
- **The terminal's own background:** the panes used color 0 of the terminal's palette ("black"),
|
||||||
which many color themes set to a grey lighter than their background (e.g. Catppuccin), so the
|
which many color themes set to a grey lighter than their background (e.g. Catppuccin), so the
|
||||||
whole program looked washed out and grey text almost disappeared. The terminal is now asked for
|
whole program looked washed out and grey text almost disappeared. The terminal is now asked for
|
||||||
its background color at startup and that is used (with 24-bit color and a dark background).
|
its background color at startup and that is used (with 24-bit color and a dark background). The
|
||||||
|
Log says which background is used and why.
|
||||||
- **Smooth scrolling on huge terminals:** while an arrow key is held, the poster waits until the
|
- **Smooth scrolling on huge terminals:** while an arrow key is held, the poster waits until the
|
||||||
selection rests (120 ms). Every poster rewrites all of its cells, which on a terminal zoomed far
|
selection rests (120 ms). Every poster rewrites all of its cells, which on a terminal zoomed far
|
||||||
out was more output per step than the terminal could draw, so moving through a list lagged.
|
out was more output per step than the terminal could draw, so moving through a list lagged.
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ Layout sketch (Library view; the other views follow the same frame):
|
|||||||
|
|
||||||
Popups for action feedback appear in the bottom right and fade away; the Log view (F7) keeps the history.
|
Popups for action feedback appear in the bottom right and fade away; the Log view (F7) keeps the history.
|
||||||
|
|
||||||
Colors: the panes' background is the terminal's own background color (asked with OSC 11 at startup) when the terminal has 24-bit color and a dark background; otherwise tview's default, palette color 0. Color themes often make color 0 a grey lighter than their background (Catppuccin: `#45475a` on `#1e1e2e`), on which grey text almost disappears. Light backgrounds keep color 0 because the text colors are made for dark ones.
|
Colors: the panes' background is the terminal's own background color (asked with OSC 11 at startup) when the terminal has 24-bit color (by tcell's rules: `TCELL_TRUECOLOR`, `COLORTERM`, or a terminal description with direct color such as `xterm-ghostty`, which Ghostty on macOS uses without setting `COLORTERM`) and a dark background, and the Log says which and why; otherwise tview's default, palette color 0. Color themes often make color 0 a grey lighter than their background (Catppuccin: `#45475a` on `#1e1e2e`), on which grey text almost disappears. Light backgrounds keep color 0 because the text colors are made for dark ones.
|
||||||
|
|
||||||
tview primitives: `Pages` (views and stacked overlays), `Flex` (layout), `InputField` (search), `List`/`Table` (results, queues, library), `TextView` (details, log), `Form` (add dialog), `Modal` (confirmations). Popups are drawn by a wrapper around the root primitive.
|
tview primitives: `Pages` (views and stacked overlays), `Flex` (layout), `InputField` (search), `List`/`Table` (results, queues, library), `TextView` (details, log), `Form` (add dialog), `Modal` (confirmations). Popups are drawn by a wrapper around the root primitive.
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -102,7 +102,7 @@ type overlay struct {
|
|||||||
func New(cfg *config.Config) *App {
|
func New(cfg *config.Config) *App {
|
||||||
// Ask the terminal before the screen takes it over and before any widget
|
// Ask the terminal before the screen takes it over and before any widget
|
||||||
// copies the default background.
|
// copies the default background.
|
||||||
ownBackground := useTerminalBackground(terminalAnswers(), os.Getenv)
|
backgroundNote := useTerminalBackground(terminalAnswers(), os.Getenv)
|
||||||
a := &App{
|
a := &App{
|
||||||
app: tview.NewApplication(),
|
app: tview.NewApplication(),
|
||||||
tabs: tview.NewTextView().SetDynamicColors(true),
|
tabs: tview.NewTextView().SetDynamicColors(true),
|
||||||
@@ -139,9 +139,7 @@ func New(cfg *config.Config) *App {
|
|||||||
if a.covers {
|
if a.covers {
|
||||||
a.logs.add(levelInfo, coverNote)
|
a.logs.add(levelInfo, coverNote)
|
||||||
}
|
}
|
||||||
if ownBackground {
|
a.logs.add(levelInfo, backgroundNote)
|
||||||
a.logs.add(levelInfo, "Background: the terminal's own color")
|
|
||||||
}
|
|
||||||
sabClient := sabnzbd.NewClient(cfg.SABnzbd.URL, cfg.SABnzbd.APIKey)
|
sabClient := sabnzbd.NewClient(cfg.SABnzbd.URL, cfg.SABnzbd.APIKey)
|
||||||
a.sab = sabClient
|
a.sab = sabClient
|
||||||
a.probes = []probe{
|
a.probes = []probe{
|
||||||
|
|||||||
+31
-9
@@ -2,6 +2,7 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/gdamore/tcell/v2/terminfo"
|
||||||
"github.com/rivo/tview"
|
"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
|
// 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
|
// 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
|
// drawn. Must run before any widget is created, they copy the style. It
|
||||||
// reports whether it changed anything.
|
// returns a line for the log that says what was decided and why.
|
||||||
func useTerminalBackground(info terminalInfo, env func(string) string) bool {
|
func useTerminalBackground(info terminalInfo, env func(string) string) string {
|
||||||
if !info.hasBG || !hasTrueColor(env) || luminance(info.bg) > 0.4 {
|
if !info.hasBG {
|
||||||
return false
|
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))
|
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 {
|
func hasTrueColor(env func(string) string) bool {
|
||||||
switch strings.ToLower(env("COLORTERM")) {
|
switch env("TCELL_TRUECOLOR") {
|
||||||
case "truecolor", "24bit":
|
case "":
|
||||||
|
case "disable":
|
||||||
|
return false
|
||||||
|
default:
|
||||||
return true
|
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).
|
// luminance is the relative brightness of c, from 0 (black) to 1 (white).
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
@@ -31,6 +32,11 @@ func TestParseTerminalAnswers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUseTerminalBackground(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)
|
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] } }
|
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}
|
dark := terminalInfo{bg: color.RGBA{30, 30, 46, 255}, hasBG: true}
|
||||||
@@ -44,13 +50,16 @@ func TestUseTerminalBackground(t *testing.T) {
|
|||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{"dark background, true color", dark, truecolor, true},
|
{"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},
|
{"light background keeps the default", light, truecolor, false},
|
||||||
{"no answer", terminalInfo{}, truecolor, false},
|
{"no answer", terminalInfo{}, truecolor, false},
|
||||||
} {
|
} {
|
||||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
|
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
|
||||||
if got := useTerminalBackground(tc.info, tc.env); got != tc.want {
|
note := useTerminalBackground(tc.info, tc.env)
|
||||||
t.Errorf("%s: %v", tc.name, got)
|
if got := strings.Contains(note, "own color"); got != tc.want {
|
||||||
|
t.Errorf("%s: %s", tc.name, note)
|
||||||
}
|
}
|
||||||
want := tcell.ColorBlack
|
want := tcell.ColorBlack
|
||||||
if tc.want {
|
if tc.want {
|
||||||
|
|||||||
Reference in New Issue
Block a user