clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

feat/app-decomposition
puzzletea / theme / theme.go
theme.go 3.5K
1// Package theme provides color theming for PuzzleTea. It loads terminal2// color schemes from an embedded JSON file and exposes a semantic [Palette]3// derived from the active theme's 16-color ANSI palette.4package theme56import (7 "encoding/json"8 "fmt"9 "strings"10 "sync"11)1213// Theme represents a terminal color scheme with the standard 16 ANSI colors14// plus background, foreground, and cursor colors.15type Theme struct {16 Name string `json:"name"`17 Black string `json:"black"`18 Red string `json:"red"`19 Green string `json:"green"`20 Yellow string `json:"yellow"`21 Blue string `json:"blue"`22 Purple string `json:"purple"`23 Cyan string `json:"cyan"`24 White string `json:"white"`25 BrightBlack string `json:"brightBlack"`26 BrightRed string `json:"brightRed"`27 BrightGreen string `json:"brightGreen"`28 BrightYellow string `json:"brightYellow"`29 BrightBlue string `json:"brightBlue"`30 BrightPurple string `json:"brightPurple"`31 BrightCyan string `json:"brightCyan"`32 BrightWhite string `json:"brightWhite"`33 Background string `json:"background"`34 Foreground string `json:"foreground"`35 CursorColor string `json:"cursorColor,omitempty"`36 SelectionBackground string `json:"selectionBackground,omitempty"`37 Meta struct {38 IsDark bool `json:"isDark"`39 } `json:"meta"`40}4142// Palette returns the semantic color palette derived from this theme.43func (t Theme) Palette() Palette {44 return derivePalette(t)45}4647// MaxNameLen is the length of the longest theme name (including the default).48// Useful for sizing UI elements like the theme picker list.49var MaxNameLen int5051var (52 mu sync.RWMutex53 current Palette54 allThemes []Theme55 byName map[string]int // lowercase name -> index in allThemes56)5758func init() {59 allThemes = parseEmbeddedThemes()60 byName = make(map[string]int, len(allThemes)+1)61 MaxNameLen = len(DefaultThemeName)62 for i, t := range allThemes {63 byName[strings.ToLower(t.Name)] = i64 if len(t.Name) > MaxNameLen {65 MaxNameLen = len(t.Name)66 }67 }68 current = defaultPalette()69}7071// Current returns the active palette. Safe for concurrent use.72func Current() Palette {73 mu.RLock()74 defer mu.RUnlock()75 return current76}7778// Apply activates the named theme. An empty name applies the built-in default.79// Returns an error if the theme name is not found.80func Apply(name string) error {81 mu.Lock()82 defer mu.Unlock()8384 if name == "" || strings.EqualFold(name, DefaultThemeName) {85 current = defaultPalette()86 return nil87 }8889 idx, ok := byName[strings.ToLower(name)]90 if !ok {91 return fmt.Errorf("unknown theme %q", name)92 }93 current = allThemes[idx].Palette()94 return nil95}9697// AllThemes returns every available theme.98func AllThemes() []Theme { return allThemes }99100// ThemeNames returns the list of all theme names, with the default first.101func ThemeNames() []string {102 names := make([]string, 0, len(allThemes)+1)103 names = append(names, DefaultThemeName)104 for _, t := range allThemes {105 names = append(names, t.Name)106 }107 return names108}109110// LookupTheme returns the theme with the given name, or nil if not found.111func LookupTheme(name string) *Theme {112 idx, ok := byName[strings.ToLower(name)]113 if !ok {114 return nil115 }116 t := allThemes[idx]117 return &t118}119120func parseEmbeddedThemes() []Theme {121 var themes []Theme122 if err := json.Unmarshal(themesJSON, &themes); err != nil {123 panic(fmt.Sprintf("theme: failed to parse embedded themes.json: %v", err))124 }125 return themes126}