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
172var Native = Theme{
173	Name:       "Native",
174	Accent:     lipgloss.Color("42"),
175	AccentDark: lipgloss.Color("#25A065"),
176	AccentText: lipgloss.Color("#FFFDF5"),
177	Secondary:  lipgloss.Color("244"),
178	SubtleText: lipgloss.Color("245"),
179	MutedText:  lipgloss.Color("247"),
180	DimText:    lipgloss.Color("250"),
181	Danger:     lipgloss.Color("196"),
182	Warning:    lipgloss.Color("208"),
183	Tip:        lipgloss.Color("214"),
184	Link:       lipgloss.Color("#9BC4FF"),
185	Directory:  lipgloss.Color("34"),
186	Contrast:   lipgloss.Color("#000000"),
187}
188
189// BuiltinThemes lists all built-in themes in display order.
190var BuiltinThemes = []Theme{
191	Matcha,
192	Native,
193	Rose,
194	Lavender,
195	Ocean,
196	Peach,
197	CatppuccinMocha,
198}
199
200// ActiveTheme is the currently active theme used for styling.
201var ActiveTheme = Matcha
202
203// SetTheme sets the active theme by name. Returns true if found.
204// It searches built-in themes first, then custom themes.
205func SetTheme(name string) bool {
206	if name == "" {
207		ActiveTheme = Matcha
208		return true
209	}
210	for _, t := range BuiltinThemes {
211		if strings.EqualFold(t.Name, name) {
212			ActiveTheme = t
213			return true
214		}
215	}
216	// Try custom themes
217	custom := LoadCustomThemes()
218	for _, t := range custom {
219		if strings.EqualFold(t.Name, name) {
220			ActiveTheme = t
221			return true
222		}
223	}
224	return false
225}
226
227// AllThemes returns all available themes (built-in + custom).
228func AllThemes() []Theme {
229	all := make([]Theme, len(BuiltinThemes)) //nolint:prealloc
230	copy(all, BuiltinThemes)
231	all = append(all, LoadCustomThemes()...)
232	return all
233}
234
235// LoadCustomThemes loads custom themes from ~/.config/matcha/themes/*.json.
236func LoadCustomThemes() []Theme {
237	home, err := os.UserHomeDir()
238	if err != nil {
239		return nil
240	}
241	themesDir := filepath.Join(home, ".config", "matcha", "themes")
242	entries, err := os.ReadDir(themesDir)
243	if err != nil {
244		return nil
245	}
246
247	var themes []Theme
248	for _, entry := range entries {
249		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
250			continue
251		}
252		data, err := os.ReadFile(filepath.Join(themesDir, entry.Name()))
253		if err != nil {
254			continue
255		}
256		var j themeJSON
257		if err := json.Unmarshal(data, &j); err != nil {
258			continue
259		}
260		if j.Name == "" {
261			j.Name = strings.TrimSuffix(entry.Name(), ".json")
262		}
263		themes = append(themes, themeFromJSON(j))
264	}
265	return themes
266}