theme.go

  1package theme
  2
  3import (
  4	"encoding/json"
  5	"image/color"
  6	"os"
  7	"path/filepath"
  8	"strings"
  9
 10	"charm.land/lipgloss/v2"
 11)
 12
 13// Theme defines the color palette for the application.
 14type Theme struct {
 15	Name       string      `json:"name"`
 16	Accent     color.Color `json:"-"`
 17	AccentDark color.Color `json:"-"`
 18	AccentText color.Color `json:"-"`
 19	Secondary  color.Color `json:"-"`
 20	SubtleText color.Color `json:"-"`
 21	MutedText  color.Color `json:"-"`
 22	DimText    color.Color `json:"-"`
 23	Danger     color.Color `json:"-"`
 24	Warning    color.Color `json:"-"`
 25	Tip        color.Color `json:"-"`
 26	Link       color.Color `json:"-"`
 27	Directory  color.Color `json:"-"`
 28	Contrast   color.Color `json:"-"`
 29}
 30
 31// themeJSON is the JSON-serializable form of Theme using string color values.
 32type themeJSON struct {
 33	Name       string `json:"name"`
 34	Accent     string `json:"accent"`
 35	AccentDark string `json:"accent_dark"`
 36	AccentText string `json:"accent_text"`
 37	Secondary  string `json:"secondary"`
 38	SubtleText string `json:"subtle_text"`
 39	MutedText  string `json:"muted_text"`
 40	DimText    string `json:"dim_text"`
 41	Danger     string `json:"danger"`
 42	Warning    string `json:"warning"`
 43	Tip        string `json:"tip"`
 44	Link       string `json:"link"`
 45	Directory  string `json:"directory"`
 46	Contrast   string `json:"contrast"`
 47}
 48
 49func themeFromJSON(j themeJSON) Theme {
 50	return Theme{
 51		Name:       j.Name,
 52		Accent:     lipgloss.Color(j.Accent),
 53		AccentDark: lipgloss.Color(j.AccentDark),
 54		AccentText: lipgloss.Color(j.AccentText),
 55		Secondary:  lipgloss.Color(j.Secondary),
 56		SubtleText: lipgloss.Color(j.SubtleText),
 57		MutedText:  lipgloss.Color(j.MutedText),
 58		DimText:    lipgloss.Color(j.DimText),
 59		Danger:     lipgloss.Color(j.Danger),
 60		Warning:    lipgloss.Color(j.Warning),
 61		Tip:        lipgloss.Color(j.Tip),
 62		Link:       lipgloss.Color(j.Link),
 63		Directory:  lipgloss.Color(j.Directory),
 64		Contrast:   lipgloss.Color(j.Contrast),
 65	}
 66}
 67
 68// Built-in themes
 69
 70var Matcha = Theme{
 71	Name:       "Matcha",
 72	Accent:     lipgloss.Color("42"),
 73	AccentDark: lipgloss.Color("#25A065"),
 74	AccentText: lipgloss.Color("#FFFDF5"),
 75	Secondary:  lipgloss.Color("244"),
 76	SubtleText: lipgloss.Color("245"),
 77	MutedText:  lipgloss.Color("247"),
 78	DimText:    lipgloss.Color("250"),
 79	Danger:     lipgloss.Color("196"),
 80	Warning:    lipgloss.Color("208"),
 81	Tip:        lipgloss.Color("214"),
 82	Link:       lipgloss.Color("#9BC4FF"),
 83	Directory:  lipgloss.Color("34"),
 84	Contrast:   lipgloss.Color("#000000"),
 85}
 86
 87var Rose = Theme{
 88	Name:       "Rose",
 89	Accent:     lipgloss.Color("#E8729B"),
 90	AccentDark: lipgloss.Color("#B5547A"),
 91	AccentText: lipgloss.Color("#FFFDF5"),
 92	Secondary:  lipgloss.Color("244"),
 93	SubtleText: lipgloss.Color("245"),
 94	MutedText:  lipgloss.Color("247"),
 95	DimText:    lipgloss.Color("250"),
 96	Danger:     lipgloss.Color("196"),
 97	Warning:    lipgloss.Color("208"),
 98	Tip:        lipgloss.Color("214"),
 99	Link:       lipgloss.Color("#9BC4FF"),
100	Directory:  lipgloss.Color("#E8729B"),
101	Contrast:   lipgloss.Color("#000000"),
102}
103
104var Lavender = Theme{
105	Name:       "Lavender",
106	Accent:     lipgloss.Color("#B4A7D6"),
107	AccentDark: lipgloss.Color("#8E7CC3"),
108	AccentText: lipgloss.Color("#FFFDF5"),
109	Secondary:  lipgloss.Color("244"),
110	SubtleText: lipgloss.Color("245"),
111	MutedText:  lipgloss.Color("247"),
112	DimText:    lipgloss.Color("250"),
113	Danger:     lipgloss.Color("196"),
114	Warning:    lipgloss.Color("208"),
115	Tip:        lipgloss.Color("214"),
116	Link:       lipgloss.Color("#9BC4FF"),
117	Directory:  lipgloss.Color("#B4A7D6"),
118	Contrast:   lipgloss.Color("#000000"),
119}
120
121var Ocean = Theme{
122	Name:       "Ocean",
123	Accent:     lipgloss.Color("#5B9BD5"),
124	AccentDark: lipgloss.Color("#3A7BBF"),
125	AccentText: lipgloss.Color("#FFFDF5"),
126	Secondary:  lipgloss.Color("244"),
127	SubtleText: lipgloss.Color("245"),
128	MutedText:  lipgloss.Color("247"),
129	DimText:    lipgloss.Color("250"),
130	Danger:     lipgloss.Color("196"),
131	Warning:    lipgloss.Color("208"),
132	Tip:        lipgloss.Color("214"),
133	Link:       lipgloss.Color("#9BC4FF"),
134	Directory:  lipgloss.Color("#5B9BD5"),
135	Contrast:   lipgloss.Color("#000000"),
136}
137
138var Peach = Theme{
139	Name:       "Peach",
140	Accent:     lipgloss.Color("#FAB387"),
141	AccentDark: lipgloss.Color("#E0956E"),
142	AccentText: lipgloss.Color("#1E1E2E"),
143	Secondary:  lipgloss.Color("244"),
144	SubtleText: lipgloss.Color("245"),
145	MutedText:  lipgloss.Color("247"),
146	DimText:    lipgloss.Color("250"),
147	Danger:     lipgloss.Color("#F38BA8"),
148	Warning:    lipgloss.Color("#F9E2AF"),
149	Tip:        lipgloss.Color("#F9E2AF"),
150	Link:       lipgloss.Color("#89B4FA"),
151	Directory:  lipgloss.Color("#FAB387"),
152	Contrast:   lipgloss.Color("#1E1E2E"),
153}
154
155var CatppuccinMocha = Theme{
156	Name:       "Catppuccin Mocha",
157	Accent:     lipgloss.Color("#89B4FA"),
158	AccentDark: lipgloss.Color("#74C7EC"),
159	AccentText: lipgloss.Color("#1E1E2E"),
160	Secondary:  lipgloss.Color("#6C7086"),
161	SubtleText: lipgloss.Color("#7F849C"),
162	MutedText:  lipgloss.Color("#9399B2"),
163	DimText:    lipgloss.Color("#BAC2DE"),
164	Danger:     lipgloss.Color("#F38BA8"),
165	Warning:    lipgloss.Color("#FAB387"),
166	Tip:        lipgloss.Color("#F9E2AF"),
167	Link:       lipgloss.Color("#89DCEB"),
168	Directory:  lipgloss.Color("#89B4FA"),
169	Contrast:   lipgloss.Color("#1E1E2E"),
170}
171
172// BuiltinThemes lists all built-in themes in display order.
173var BuiltinThemes = []Theme{
174	Matcha,
175	Rose,
176	Lavender,
177	Ocean,
178	Peach,
179	CatppuccinMocha,
180}
181
182// ActiveTheme is the currently active theme used for styling.
183var ActiveTheme = Matcha
184
185// SetTheme sets the active theme by name. Returns true if found.
186// It searches built-in themes first, then custom themes.
187func SetTheme(name string) bool {
188	if name == "" {
189		ActiveTheme = Matcha
190		return true
191	}
192	for _, t := range BuiltinThemes {
193		if strings.EqualFold(t.Name, name) {
194			ActiveTheme = t
195			return true
196		}
197	}
198	// Try custom themes
199	custom := LoadCustomThemes()
200	for _, t := range custom {
201		if strings.EqualFold(t.Name, name) {
202			ActiveTheme = t
203			return true
204		}
205	}
206	return false
207}
208
209// AllThemes returns all available themes (built-in + custom).
210func AllThemes() []Theme {
211	all := make([]Theme, len(BuiltinThemes))
212	copy(all, BuiltinThemes)
213	all = append(all, LoadCustomThemes()...)
214	return all
215}
216
217// LoadCustomThemes loads custom themes from ~/.config/matcha/themes/*.json.
218func LoadCustomThemes() []Theme {
219	home, err := os.UserHomeDir()
220	if err != nil {
221		return nil
222	}
223	themesDir := filepath.Join(home, ".config", "matcha", "themes")
224	entries, err := os.ReadDir(themesDir)
225	if err != nil {
226		return nil
227	}
228
229	var themes []Theme
230	for _, entry := range entries {
231		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
232			continue
233		}
234		data, err := os.ReadFile(filepath.Join(themesDir, entry.Name()))
235		if err != nil {
236			continue
237		}
238		var j themeJSON
239		if err := json.Unmarshal(data, &j); err != nil {
240			continue
241		}
242		if j.Name == "" {
243			j.Name = strings.TrimSuffix(entry.Name(), ".json")
244		}
245		themes = append(themes, themeFromJSON(j))
246	}
247	return themes
248}