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}