Files
arr-tui/internal/ui/theme.go
T
nilsandClaude Opus 5.5 820b17842b Fixed dark and light themes, ui.theme auto/dark/light
Colors no longer come from the terminal's palette, whose color 0 many
color themes set to a grey that is not their background. A theme sets
tview's default styles and the color names used in texts, all chosen from
entries 16-255 of the 256-color table so terminals without 24-bit color
show them exactly. "auto" picks light or dark from the background color
the terminal reports. Settable in the Config view, applied at the next
start (widgets copy their colors when created). Replaces using the
terminal's own background and text colors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
2026-10-21 12:05:00 +02:00

111 lines
3.9 KiB
Go

package ui
import (
"image/color"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
// theme is a complete set of colors, so the program looks the same whatever
// palette the terminal's color theme has (themes often make "black" a grey
// that is not their background, and light themes put dark grey slabs under
// white text). Every color is one of the fixed entries 16-255 of the
// 256-color table, so terminals without 24-bit color show exactly these too
// (the nearest match of an exact color is itself, and the first 16 entries,
// which themes change, never equal them).
type theme struct {
name string
bg, text, border tcell.Color
field, fieldMuted tcell.Color // input fields and buttons; placeholders on them
list tcell.Color // open dropdown lists (drawn with bg-colored text)
pressed tcell.Color // text of a pressed button (on the text color)
toast tcell.Color // background of the popups
// The accent colors, used by name in the texts ("[yellow]...").
gray, yellow, green, red, blue, purple tcell.Color
}
func c256(i int) tcell.Color { return tcell.PaletteColor(i).TrueColor() }
var (
darkTheme = theme{
name: "dark",
bg: c256(232), text: c256(253), border: c256(246),
field: c256(236), fieldMuted: c256(245),
list: c256(110), pressed: c256(25), toast: c256(236),
gray: c256(245), yellow: c256(221), green: c256(113), red: c256(203), blue: c256(75), purple: c256(141),
}
lightTheme = theme{
name: "light",
bg: c256(255), text: c256(234), border: c256(243),
field: c256(252), fieldMuted: c256(245),
list: c256(67), pressed: c256(255), toast: c256(252),
gray: c256(243), yellow: c256(130), green: c256(28), red: c256(160), blue: c256(26), purple: c256(91),
}
)
// current is the theme in use.
var current = darkTheme
// chooseTheme resolves the ui.theme setting: "dark", "light", or "auto" (the
// default), which follows the background color the terminal reported and is
// dark when it did not say. The second value explains the choice for the log.
func chooseTheme(setting string, info terminalInfo) (theme, string) {
switch setting {
case "dark":
return darkTheme, "Theme: dark"
case "light":
return lightTheme, "Theme: light"
}
if !info.hasBG {
return darkTheme, "Theme: dark (auto: the terminal did not report its background color)"
}
hex := hexColor(info.bg)
if luminance(info.bg) > 0.5 {
return lightTheme, "Theme: light (auto: the terminal's background " + hex + " is light)"
}
return darkTheme, "Theme: dark (auto: the terminal's background " + hex + " is dark)"
}
// apply makes t the colors of everything created afterwards: tview's default
// styles (widgets copy them when they are created, so this must run before any
// is) and the color names used in the texts, which tview looks up in tcell's
// table: "[yellow]" means this theme's yellow, "white" its text color and
// "black" its background (the active tab is "[black:white]").
func (t theme) apply() {
current = t
st := &tview.Styles
st.PrimitiveBackgroundColor = t.bg
st.ContrastBackgroundColor = t.field
st.MoreContrastBackgroundColor = t.list
st.BorderColor, st.TitleColor, st.GraphicsColor = t.border, t.text, t.border
st.PrimaryTextColor = t.text
st.SecondaryTextColor = t.yellow
st.TertiaryTextColor = t.green
st.InverseTextColor = t.pressed
st.ContrastSecondaryTextColor = t.fieldMuted
for name, c := range map[string]tcell.Color{
"white": t.text, "black": t.bg,
"gray": t.gray, "grey": t.gray,
"yellow": t.yellow, "green": t.green, "red": t.red,
"blue": t.blue, "purple": t.purple,
} {
tcell.ColorNames[name] = c
}
}
// named returns the theme's color for a name used in the texts.
func named(name string) tcell.Color { return tcell.GetColor(name) }
func hexColor(c color.RGBA) string {
const digits = "0123456789abcdef"
b := []byte{'#', 0, 0, 0, 0, 0, 0}
for i, v := range []uint8{c.R, c.G, c.B} {
b[1+2*i], b[2+2*i] = digits[v>>4], digits[v&15]
}
return string(b)
}