1package styles
2
3import (
4 "fmt"
5 "image/color"
6
7 "github.com/charmbracelet/bubbles/v2/help"
8 "github.com/charmbracelet/bubbles/v2/textarea"
9 "github.com/charmbracelet/bubbles/v2/textinput"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/glamour/v2/ansi"
12 "github.com/charmbracelet/lipgloss/v2"
13)
14
15const (
16 defaultListIndent = 2
17 defaultListLevelIndent = 4
18 defaultMargin = 2
19)
20
21type Theme struct {
22 Name string
23 IsDark bool
24
25 Primary color.Color
26 Secondary color.Color
27 Tertiary color.Color
28 Accent color.Color
29
30 // Colors
31 Blue color.Color
32 // TODO: add any others needed
33
34 BgBase color.Color
35 BgSubtle color.Color
36 BgOverlay color.Color
37
38 FgBase color.Color
39 FgMuted color.Color
40 FgSubtle color.Color
41 FgSelected color.Color
42
43 Border color.Color
44 BorderFocus color.Color
45
46 Success color.Color
47 Error color.Color
48 Warning color.Color
49 Info color.Color
50
51 // TODO: add more syntax colors, maybe just use a chroma theme here.
52 SyntaxBg color.Color
53 SyntaxKeyword color.Color
54 SyntaxString color.Color
55 SyntaxComment color.Color
56
57 styles *Styles
58}
59
60type Styles struct {
61 Base lipgloss.Style
62 SelectedBase lipgloss.Style
63
64 Title lipgloss.Style
65 Subtitle lipgloss.Style
66 Text lipgloss.Style
67 TextSelected lipgloss.Style
68 Muted lipgloss.Style
69 Subtle lipgloss.Style
70
71 Success lipgloss.Style
72 Error lipgloss.Style
73 Warning lipgloss.Style
74 Info lipgloss.Style
75
76 // Markdown & Chroma
77 Markdown ansi.StyleConfig
78
79 // Inputs
80 TextInput textinput.Styles
81 TextArea textarea.Styles
82
83 // Help
84 Help help.Styles
85}
86
87func (t *Theme) S() *Styles {
88 if t.styles == nil {
89 t.styles = t.buildStyles()
90 }
91 return t.styles
92}
93
94func (t *Theme) buildStyles() *Styles {
95 base := lipgloss.NewStyle().
96 Foreground(t.FgBase)
97 return &Styles{
98 Base: base,
99
100 SelectedBase: base.Background(t.Primary),
101
102 Title: base.
103 Foreground(t.Accent).
104 Bold(true),
105
106 Subtitle: base.
107 Foreground(t.Secondary).
108 Bold(true),
109
110 Text: base,
111 TextSelected: base.Background(t.Primary).Foreground(t.FgSelected),
112
113 Muted: base.Foreground(t.FgMuted),
114
115 Subtle: base.Foreground(t.FgSubtle),
116
117 Success: base.Foreground(t.Success),
118
119 Error: base.Foreground(t.Error),
120
121 Warning: base.Foreground(t.Warning),
122
123 Info: base.Foreground(t.Info),
124
125 TextInput: textinput.Styles{
126 Focused: textinput.StyleState{
127 Text: base,
128 Placeholder: base.Foreground(t.FgMuted),
129 Prompt: base.Foreground(t.Tertiary),
130 Suggestion: base.Foreground(t.FgMuted),
131 },
132 Blurred: textinput.StyleState{
133 Text: base.Foreground(t.FgMuted),
134 Placeholder: base.Foreground(t.FgMuted),
135 Prompt: base.Foreground(t.FgMuted),
136 Suggestion: base.Foreground(t.FgMuted),
137 },
138 Cursor: textinput.CursorStyle{
139 Color: t.Secondary,
140 Shape: tea.CursorBar,
141 Blink: true,
142 },
143 },
144 TextArea: textarea.Styles{
145 Focused: textarea.StyleState{
146 Base: base,
147 Text: base,
148 LineNumber: base.Foreground(t.FgSubtle),
149 CursorLine: base,
150 CursorLineNumber: base.Foreground(t.FgSubtle),
151 Placeholder: base.Foreground(t.FgMuted),
152 Prompt: base.Foreground(t.Tertiary),
153 },
154 Blurred: textarea.StyleState{
155 Base: base,
156 Text: base.Foreground(t.FgMuted),
157 LineNumber: base.Foreground(t.FgMuted),
158 CursorLine: base,
159 CursorLineNumber: base.Foreground(t.FgMuted),
160 Placeholder: base.Foreground(t.FgMuted),
161 Prompt: base.Foreground(t.FgMuted),
162 },
163 Cursor: textarea.CursorStyle{
164 Color: t.Secondary,
165 Shape: tea.CursorBar,
166 Blink: true,
167 },
168 },
169
170 // TODO: update using the colors and add colors if missing
171 Markdown: ansi.StyleConfig{
172 Document: ansi.StyleBlock{
173 StylePrimitive: ansi.StylePrimitive{
174 BlockPrefix: "\n",
175 BlockSuffix: "\n",
176 Color: stringPtr("252"),
177 },
178 Margin: uintPtr(defaultMargin),
179 },
180 BlockQuote: ansi.StyleBlock{
181 StylePrimitive: ansi.StylePrimitive{},
182 Indent: uintPtr(1),
183 IndentToken: stringPtr("│ "),
184 },
185 List: ansi.StyleList{
186 LevelIndent: defaultListIndent,
187 },
188 Heading: ansi.StyleBlock{
189 StylePrimitive: ansi.StylePrimitive{
190 BlockSuffix: "\n",
191 Color: stringPtr("39"),
192 Bold: boolPtr(true),
193 },
194 },
195 H1: ansi.StyleBlock{
196 StylePrimitive: ansi.StylePrimitive{
197 Prefix: " ",
198 Suffix: " ",
199 Color: stringPtr("228"),
200 BackgroundColor: stringPtr("63"),
201 Bold: boolPtr(true),
202 },
203 },
204 H2: ansi.StyleBlock{
205 StylePrimitive: ansi.StylePrimitive{
206 Prefix: "## ",
207 },
208 },
209 H3: ansi.StyleBlock{
210 StylePrimitive: ansi.StylePrimitive{
211 Prefix: "### ",
212 },
213 },
214 H4: ansi.StyleBlock{
215 StylePrimitive: ansi.StylePrimitive{
216 Prefix: "#### ",
217 },
218 },
219 H5: ansi.StyleBlock{
220 StylePrimitive: ansi.StylePrimitive{
221 Prefix: "##### ",
222 },
223 },
224 H6: ansi.StyleBlock{
225 StylePrimitive: ansi.StylePrimitive{
226 Prefix: "###### ",
227 Color: stringPtr("35"),
228 Bold: boolPtr(false),
229 },
230 },
231 Strikethrough: ansi.StylePrimitive{
232 CrossedOut: boolPtr(true),
233 },
234 Emph: ansi.StylePrimitive{
235 Italic: boolPtr(true),
236 },
237 Strong: ansi.StylePrimitive{
238 Bold: boolPtr(true),
239 },
240 HorizontalRule: ansi.StylePrimitive{
241 Color: stringPtr("240"),
242 Format: "\n--------\n",
243 },
244 Item: ansi.StylePrimitive{
245 BlockPrefix: "• ",
246 },
247 Enumeration: ansi.StylePrimitive{
248 BlockPrefix: ". ",
249 },
250 Task: ansi.StyleTask{
251 StylePrimitive: ansi.StylePrimitive{},
252 Ticked: "[✓] ",
253 Unticked: "[ ] ",
254 },
255 Link: ansi.StylePrimitive{
256 Color: stringPtr("30"),
257 Underline: boolPtr(true),
258 },
259 LinkText: ansi.StylePrimitive{
260 Color: stringPtr("35"),
261 Bold: boolPtr(true),
262 },
263 Image: ansi.StylePrimitive{
264 Color: stringPtr("212"),
265 Underline: boolPtr(true),
266 },
267 ImageText: ansi.StylePrimitive{
268 Color: stringPtr("243"),
269 Format: "Image: {{.text}} →",
270 },
271 Code: ansi.StyleBlock{
272 StylePrimitive: ansi.StylePrimitive{
273 Prefix: " ",
274 Suffix: " ",
275 Color: stringPtr("203"),
276 BackgroundColor: stringPtr("236"),
277 },
278 },
279 CodeBlock: ansi.StyleCodeBlock{
280 StyleBlock: ansi.StyleBlock{
281 StylePrimitive: ansi.StylePrimitive{
282 Color: stringPtr("244"),
283 },
284 Margin: uintPtr(defaultMargin),
285 },
286 Chroma: &ansi.Chroma{
287 Text: ansi.StylePrimitive{
288 Color: stringPtr("#C4C4C4"),
289 },
290 Error: ansi.StylePrimitive{
291 Color: stringPtr("#F1F1F1"),
292 BackgroundColor: stringPtr("#F05B5B"),
293 },
294 Comment: ansi.StylePrimitive{
295 Color: stringPtr("#676767"),
296 },
297 CommentPreproc: ansi.StylePrimitive{
298 Color: stringPtr("#FF875F"),
299 },
300 Keyword: ansi.StylePrimitive{
301 Color: stringPtr("#00AAFF"),
302 },
303 KeywordReserved: ansi.StylePrimitive{
304 Color: stringPtr("#FF5FD2"),
305 },
306 KeywordNamespace: ansi.StylePrimitive{
307 Color: stringPtr("#FF5F87"),
308 },
309 KeywordType: ansi.StylePrimitive{
310 Color: stringPtr("#6E6ED8"),
311 },
312 Operator: ansi.StylePrimitive{
313 Color: stringPtr("#EF8080"),
314 },
315 Punctuation: ansi.StylePrimitive{
316 Color: stringPtr("#E8E8A8"),
317 },
318 Name: ansi.StylePrimitive{
319 Color: stringPtr("#C4C4C4"),
320 },
321 NameBuiltin: ansi.StylePrimitive{
322 Color: stringPtr("#FF8EC7"),
323 },
324 NameTag: ansi.StylePrimitive{
325 Color: stringPtr("#B083EA"),
326 },
327 NameAttribute: ansi.StylePrimitive{
328 Color: stringPtr("#7A7AE6"),
329 },
330 NameClass: ansi.StylePrimitive{
331 Color: stringPtr("#F1F1F1"),
332 Underline: boolPtr(true),
333 Bold: boolPtr(true),
334 },
335 NameDecorator: ansi.StylePrimitive{
336 Color: stringPtr("#FFFF87"),
337 },
338 NameFunction: ansi.StylePrimitive{
339 Color: stringPtr("#00D787"),
340 },
341 LiteralNumber: ansi.StylePrimitive{
342 Color: stringPtr("#6EEFC0"),
343 },
344 LiteralString: ansi.StylePrimitive{
345 Color: stringPtr("#C69669"),
346 },
347 LiteralStringEscape: ansi.StylePrimitive{
348 Color: stringPtr("#AFFFD7"),
349 },
350 GenericDeleted: ansi.StylePrimitive{
351 Color: stringPtr("#FD5B5B"),
352 },
353 GenericEmph: ansi.StylePrimitive{
354 Italic: boolPtr(true),
355 },
356 GenericInserted: ansi.StylePrimitive{
357 Color: stringPtr("#00D787"),
358 },
359 GenericStrong: ansi.StylePrimitive{
360 Bold: boolPtr(true),
361 },
362 GenericSubheading: ansi.StylePrimitive{
363 Color: stringPtr("#777777"),
364 },
365 Background: ansi.StylePrimitive{
366 BackgroundColor: stringPtr("#373737"),
367 },
368 },
369 },
370 Table: ansi.StyleTable{
371 StyleBlock: ansi.StyleBlock{
372 StylePrimitive: ansi.StylePrimitive{},
373 },
374 },
375 DefinitionDescription: ansi.StylePrimitive{
376 BlockPrefix: "\n ",
377 },
378 },
379
380 Help: help.Styles{
381 ShortKey: base.Foreground(t.FgMuted),
382 ShortDesc: base.Foreground(t.FgSubtle),
383 ShortSeparator: base.Foreground(t.Border),
384 Ellipsis: base.Foreground(t.Border),
385 FullKey: base.Foreground(t.FgMuted),
386 FullDesc: base.Foreground(t.FgSubtle),
387 FullSeparator: base.Foreground(t.Border),
388 },
389 }
390}
391
392type Manager struct {
393 themes map[string]*Theme
394 current *Theme
395}
396
397var defaultManager *Manager
398
399func SetDefaultManager(m *Manager) {
400 defaultManager = m
401}
402
403func DefaultManager() *Manager {
404 if defaultManager == nil {
405 defaultManager = NewManager("crush")
406 }
407 return defaultManager
408}
409
410func CurrentTheme() *Theme {
411 if defaultManager == nil {
412 defaultManager = NewManager("crush")
413 }
414 return defaultManager.Current()
415}
416
417func NewManager(defaultTheme string) *Manager {
418 m := &Manager{
419 themes: make(map[string]*Theme),
420 }
421
422 m.Register(NewCrushTheme())
423
424 m.current = m.themes[defaultTheme]
425
426 return m
427}
428
429func (m *Manager) Register(theme *Theme) {
430 m.themes[theme.Name] = theme
431}
432
433func (m *Manager) Current() *Theme {
434 return m.current
435}
436
437func (m *Manager) SetTheme(name string) error {
438 if theme, ok := m.themes[name]; ok {
439 m.current = theme
440 return nil
441 }
442 return fmt.Errorf("theme %s not found", name)
443}
444
445func (m *Manager) List() []string {
446 names := make([]string, 0, len(m.themes))
447 for name := range m.themes {
448 names = append(names, name)
449 }
450 return names
451}
452
453// ParseHex converts hex string to color
454func ParseHex(hex string) color.Color {
455 var r, g, b uint8
456 fmt.Sscanf(hex, "#%02x%02x%02x", &r, &g, &b)
457 return color.RGBA{R: r, G: g, B: b, A: 255}
458}
459
460// Alpha returns a color with transparency
461func Alpha(c color.Color, alpha uint8) color.Color {
462 r, g, b, _ := c.RGBA()
463 return color.RGBA{
464 R: uint8(r >> 8),
465 G: uint8(g >> 8),
466 B: uint8(b >> 8),
467 A: alpha,
468 }
469}
470
471// Darken makes a color darker by percentage (0-100)
472func Darken(c color.Color, percent float64) color.Color {
473 r, g, b, a := c.RGBA()
474 factor := 1.0 - percent/100.0
475 return color.RGBA{
476 R: uint8(float64(r>>8) * factor),
477 G: uint8(float64(g>>8) * factor),
478 B: uint8(float64(b>>8) * factor),
479 A: uint8(a >> 8),
480 }
481}
482
483// Lighten makes a color lighter by percentage (0-100)
484func Lighten(c color.Color, percent float64) color.Color {
485 r, g, b, a := c.RGBA()
486 factor := percent / 100.0
487 return color.RGBA{
488 R: uint8(min(255, float64(r>>8)+255*factor)),
489 G: uint8(min(255, float64(g>>8)+255*factor)),
490 B: uint8(min(255, float64(b>>8)+255*factor)),
491 A: uint8(a >> 8),
492 }
493}