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.
166 lines
4.7 KiB
Go
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)
|
|
}
|