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