Files
mapsize/internal/tui/theme.go
T
Andy Dixon df8b10c6dd Add Bubble Tea TUI with interactive treemap
Cell canvas renderer with frame cache and a perceptual 256-colour
quantizer; responsive geometry (large/medium/small/too-small);
squarified treemap with nested previews, LOD grouping, category
colouring and adaptive labels; spatial arrow navigation with stable
NodeID selection; zoom and breadcrumbs; modal system (info, help,
confirm, save); list, file-type, top-N, scan-info, duplicate and
comparison views; live search/filter; mouse support; move-to-trash.
2026-10-02 08:25:35 +01:00

166 lines
4.7 KiB
Go

package tui
import (
"sort"
"strings"
"github.com/andydixon/mapsize/internal/config"
"github.com/andydixon/mapsize/internal/inventory"
)
// Theme holds every colour the UI uses. Nothing else in the TUI hard-codes
// colours.
type Theme struct {
Name string
Mono bool // no colour: rely on attributes and glyphs
ASCII bool // no box-drawing glyphs
Bg, Fg, Muted, Faint Color
Accent, Sel Color
HeaderBg, HeaderFg Color
StatusBg, StatusFg Color
PanelBg Color
Warn, Err, OK Color
Group Color
EmptyDir Color
Cat [inventory.NumCategories]Color
DiffGrow, DiffShrink Color
DiffNew, DiffSame Color
Ext map[string]Color
}
var themes = map[string]func() *Theme{
"default": defaultTheme,
"dark": darkTheme,
"high-contrast": highContrastTheme,
"mono": monoTheme,
}
// ThemeNames lists built-in themes.
func ThemeNames() []string {
var out []string
for k := range themes {
out = append(out, k)
}
sort.Strings(out)
return out
}
func defaultTheme() *Theme {
return &Theme{
Name: "default",
Bg: RGB(0x16, 0x18, 0x1d), Fg: RGB(0xdc, 0xdf, 0xe4), Muted: RGB(0x8a, 0x91, 0x9c), Faint: RGB(0x4a, 0x50, 0x5a),
Accent: RGB(0x61, 0xaf, 0xef), Sel: RGB(0xff, 0xd8, 0x66),
HeaderBg: RGB(0x21, 0x25, 0x2b), HeaderFg: RGB(0xdc, 0xdf, 0xe4),
StatusBg: RGB(0x2c, 0x31, 0x3a), StatusFg: RGB(0xc8, 0xcc, 0xd4),
PanelBg: RGB(0x1c, 0x1f, 0x25),
Warn: RGB(0xe5, 0xc0, 0x7b), Err: RGB(0xe0, 0x6c, 0x75), OK: RGB(0x98, 0xc3, 0x79),
Group: RGB(0x5c, 0x63, 0x70), EmptyDir: RGB(0x4b, 0x52, 0x63),
Cat: [inventory.NumCategories]Color{
inventory.CatOther: RGB(0x7f, 0x8c, 0x9d),
inventory.CatImage: RGB(0xe5, 0xc0, 0x7b),
inventory.CatVideo: RGB(0xe0, 0x6c, 0x75),
inventory.CatAudio: RGB(0xd1, 0x9a, 0x66),
inventory.CatArchive: RGB(0xc6, 0x78, 0xdd),
inventory.CatDiskImage: RGB(0x8e, 0x7c, 0xf0),
inventory.CatDatabase: RGB(0x56, 0xb6, 0xc2),
inventory.CatCode: RGB(0x98, 0xc3, 0x79),
inventory.CatDocument: RGB(0x61, 0xaf, 0xef),
inventory.CatExecutable: RGB(0xbe, 0x50, 0x46),
inventory.CatLog: RGB(0xa3, 0xa8, 0x6a),
inventory.CatCache: RGB(0x6b, 0x77, 0x88),
},
DiffGrow: RGB(0xf0, 0x5a, 0x5a), DiffShrink: RGB(0x5a, 0xd0, 0x7a),
DiffNew: RGB(0x4f, 0xa8, 0xff), DiffSame: RGB(0x50, 0x56, 0x60),
}
}
func darkTheme() *Theme {
t := defaultTheme()
t.Name = "dark"
t.Bg, t.PanelBg = RGB(0, 0, 0), RGB(0x0c, 0x0c, 0x0e)
t.HeaderBg, t.StatusBg = RGB(0x12, 0x12, 0x16), RGB(0x1a, 0x1a, 0x20)
for i, c := range t.Cat {
t.Cat[i] = c.Mix(RGB(0, 0, 0), 0.2)
}
return t
}
func highContrastTheme() *Theme {
t := defaultTheme()
t.Name = "high-contrast"
t.Bg, t.Fg, t.Muted = RGB(0, 0, 0), RGB(0xff, 0xff, 0xff), RGB(0xd0, 0xd0, 0xd0)
t.HeaderBg, t.StatusBg, t.PanelBg = RGB(0, 0, 0), RGB(0x20, 0x20, 0x20), RGB(0, 0, 0)
t.Sel, t.Accent = RGB(0xff, 0xff, 0x00), RGB(0x00, 0xff, 0xff)
t.Cat = [inventory.NumCategories]Color{
RGB(0xb0, 0xb0, 0xb0), RGB(0xff, 0xd7, 0x00), RGB(0xff, 0x30, 0x30), RGB(0xff, 0x8c, 0x00),
RGB(0xff, 0x00, 0xff), RGB(0x9b, 0x7b, 0xff), RGB(0x00, 0xe5, 0xff), RGB(0x00, 0xff, 0x00),
RGB(0x40, 0x9c, 0xff), RGB(0xff, 0x55, 0x55), RGB(0xc8, 0xff, 0x00), RGB(0x90, 0x90, 0xa0),
}
return t
}
func monoTheme() *Theme {
return &Theme{Name: "mono", Mono: true}
}
// LoadTheme returns a built-in theme with user overrides applied.
func LoadTheme(name string, s config.Settings) *Theme {
f, ok := themes[strings.ToLower(name)]
if !ok {
f = defaultTheme
}
t := f()
if t.Mono {
return t
}
for k, v := range s.CategoryColors {
if c, ok := inventory.ParseCategory(k); ok {
if col, ok := Hex(v); ok {
t.Cat[c] = col
}
}
}
for k, v := range s.ExtensionColors {
if col, ok := Hex(v); ok {
if t.Ext == nil {
t.Ext = map[string]Color{}
}
t.Ext[strings.ToLower(strings.TrimPrefix(k, "."))] = col
}
}
return t
}
// Styles derived from the theme.
func (t *Theme) base() Style { return Style{FG: t.Fg, BG: t.Bg} }
func (t *Theme) muted() Style { return Style{FG: t.Muted, BG: t.Bg} }
func (t *Theme) header() Style { return Style{FG: t.HeaderFg, BG: t.HeaderBg} }
func (t *Theme) status() Style { return Style{FG: t.StatusFg, BG: t.StatusBg} }
func (t *Theme) panel() Style { return Style{FG: t.Fg, BG: t.PanelBg} }
func (t *Theme) box() BoxChars {
if t.ASCII {
return BoxASCII
}
return BoxLight
}
func (t *Theme) selBox() BoxChars {
if t.ASCII {
return BoxASCIISel
}
return BoxDouble
}
// textOn picks a readable text colour for a background.
func (t *Theme) textOn(bg Color) Color {
if bg == 0 {
return t.Fg
}
if bg.Luma() > 0.55 {
return RGB(0x10, 0x12, 0x16)
}
return RGB(0xf2, 0xf4, 0xf7)
}