1package styles
2
3import (
4 "fmt"
5 "image/color"
6 "strings"
7
8 "github.com/charmbracelet/bubbles/v2/help"
9 "github.com/charmbracelet/bubbles/v2/textarea"
10 "github.com/charmbracelet/bubbles/v2/textinput"
11 tea "github.com/charmbracelet/bubbletea/v2"
12 "github.com/charmbracelet/glamour/v2/ansi"
13 "github.com/charmbracelet/lipgloss/v2"
14 "github.com/lucasb-eyer/go-colorful"
15 "github.com/rivo/uniseg"
16)
17
18const (
19 defaultListIndent = 2
20 defaultListLevelIndent = 4
21 defaultMargin = 2
22)
23
24type Theme struct {
25 Name string
26 IsDark bool
27
28 Primary color.Color
29 Secondary color.Color
30 Tertiary color.Color
31 Accent color.Color
32
33 BgBase color.Color
34 BgSubtle color.Color
35 BgOverlay color.Color
36
37 FgBase color.Color
38 FgMuted color.Color
39 FgHalfMuted 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 // Colors
52 // Blues
53 Blue color.Color
54
55 // Greens
56 Green color.Color
57 GreenDark color.Color
58 GreenLight color.Color
59
60 // Reds
61 Red color.Color
62 RedDark color.Color
63 RedLight color.Color
64
65 // TODO: add any others needed
66
67 styles *Styles
68}
69
70type Diff struct {
71 Added color.Color
72 Removed color.Color
73 Context color.Color
74 HunkHeader color.Color
75 HighlightAdded color.Color
76 HighlightRemoved color.Color
77 AddedBg color.Color
78 RemovedBg color.Color
79 ContextBg color.Color
80 LineNumber color.Color
81 AddedLineNumberBg color.Color
82 RemovedLineNumberBg color.Color
83}
84
85type Styles struct {
86 Base lipgloss.Style
87 SelectedBase lipgloss.Style
88
89 Title lipgloss.Style
90 Subtitle lipgloss.Style
91 Text lipgloss.Style
92 TextSelected lipgloss.Style
93 Muted lipgloss.Style
94 Subtle lipgloss.Style
95
96 Success lipgloss.Style
97 Error lipgloss.Style
98 Warning lipgloss.Style
99 Info lipgloss.Style
100
101 // Markdown & Chroma
102 Markdown ansi.StyleConfig
103
104 // Inputs
105 TextInput textinput.Styles
106 TextArea textarea.Styles
107
108 // Help
109 Help help.Styles
110
111 // Diff
112 Diff Diff
113}
114
115func (t *Theme) S() *Styles {
116 if t.styles == nil {
117 t.styles = t.buildStyles()
118 }
119 return t.styles
120}
121
122func (t *Theme) buildStyles() *Styles {
123 base := lipgloss.NewStyle().
124 Foreground(t.FgBase)
125 return &Styles{
126 Base: base,
127
128 SelectedBase: base.Background(t.Primary),
129
130 Title: base.
131 Foreground(t.Accent).
132 Bold(true),
133
134 Subtitle: base.
135 Foreground(t.Secondary).
136 Bold(true),
137
138 Text: base,
139 TextSelected: base.Background(t.Primary).Foreground(t.FgSelected),
140
141 Muted: base.Foreground(t.FgMuted),
142
143 Subtle: base.Foreground(t.FgSubtle),
144
145 Success: base.Foreground(t.Success),
146
147 Error: base.Foreground(t.Error),
148
149 Warning: base.Foreground(t.Warning),
150
151 Info: base.Foreground(t.Info),
152
153 TextInput: textinput.Styles{
154 Focused: textinput.StyleState{
155 Text: base,
156 Placeholder: base.Foreground(t.FgMuted),
157 Prompt: base.Foreground(t.Tertiary),
158 Suggestion: base.Foreground(t.FgMuted),
159 },
160 Blurred: textinput.StyleState{
161 Text: base.Foreground(t.FgMuted),
162 Placeholder: base.Foreground(t.FgMuted),
163 Prompt: base.Foreground(t.FgMuted),
164 Suggestion: base.Foreground(t.FgMuted),
165 },
166 Cursor: textinput.CursorStyle{
167 Color: t.Secondary,
168 Shape: tea.CursorBar,
169 Blink: true,
170 },
171 },
172 TextArea: textarea.Styles{
173 Focused: textarea.StyleState{
174 Base: base,
175 Text: base,
176 LineNumber: base.Foreground(t.FgSubtle),
177 CursorLine: base,
178 CursorLineNumber: base.Foreground(t.FgSubtle),
179 Placeholder: base.Foreground(t.FgMuted),
180 Prompt: base.Foreground(t.Tertiary),
181 },
182 Blurred: textarea.StyleState{
183 Base: base,
184 Text: base.Foreground(t.FgMuted),
185 LineNumber: base.Foreground(t.FgMuted),
186 CursorLine: base,
187 CursorLineNumber: base.Foreground(t.FgMuted),
188 Placeholder: base.Foreground(t.FgMuted),
189 Prompt: base.Foreground(t.FgMuted),
190 },
191 Cursor: textarea.CursorStyle{
192 Color: t.Secondary,
193 Shape: tea.CursorBar,
194 Blink: true,
195 },
196 },
197
198 // TODO: update using the colors and add colors if missing
199 Markdown: ansi.StyleConfig{
200 Document: ansi.StyleBlock{
201 StylePrimitive: ansi.StylePrimitive{
202 // BlockPrefix: "\n",
203 // BlockSuffix: "\n",
204 Color: stringPtr("252"),
205 },
206 // Margin: uintPtr(defaultMargin),
207 },
208 BlockQuote: ansi.StyleBlock{
209 StylePrimitive: ansi.StylePrimitive{},
210 Indent: uintPtr(1),
211 IndentToken: stringPtr("│ "),
212 },
213 List: ansi.StyleList{
214 LevelIndent: defaultListIndent,
215 },
216 Heading: ansi.StyleBlock{
217 StylePrimitive: ansi.StylePrimitive{
218 BlockSuffix: "\n",
219 Color: stringPtr("39"),
220 Bold: boolPtr(true),
221 },
222 },
223 H1: ansi.StyleBlock{
224 StylePrimitive: ansi.StylePrimitive{
225 Prefix: " ",
226 Suffix: " ",
227 Color: stringPtr("228"),
228 BackgroundColor: stringPtr("63"),
229 Bold: boolPtr(true),
230 },
231 },
232 H2: ansi.StyleBlock{
233 StylePrimitive: ansi.StylePrimitive{
234 Prefix: "## ",
235 },
236 },
237 H3: ansi.StyleBlock{
238 StylePrimitive: ansi.StylePrimitive{
239 Prefix: "### ",
240 },
241 },
242 H4: ansi.StyleBlock{
243 StylePrimitive: ansi.StylePrimitive{
244 Prefix: "#### ",
245 },
246 },
247 H5: ansi.StyleBlock{
248 StylePrimitive: ansi.StylePrimitive{
249 Prefix: "##### ",
250 },
251 },
252 H6: ansi.StyleBlock{
253 StylePrimitive: ansi.StylePrimitive{
254 Prefix: "###### ",
255 Color: stringPtr("35"),
256 Bold: boolPtr(false),
257 },
258 },
259 Strikethrough: ansi.StylePrimitive{
260 CrossedOut: boolPtr(true),
261 },
262 Emph: ansi.StylePrimitive{
263 Italic: boolPtr(true),
264 },
265 Strong: ansi.StylePrimitive{
266 Bold: boolPtr(true),
267 },
268 HorizontalRule: ansi.StylePrimitive{
269 Color: stringPtr("240"),
270 Format: "\n--------\n",
271 },
272 Item: ansi.StylePrimitive{
273 BlockPrefix: "• ",
274 },
275 Enumeration: ansi.StylePrimitive{
276 BlockPrefix: ". ",
277 },
278 Task: ansi.StyleTask{
279 StylePrimitive: ansi.StylePrimitive{},
280 Ticked: "[✓] ",
281 Unticked: "[ ] ",
282 },
283 Link: ansi.StylePrimitive{
284 Color: stringPtr("30"),
285 Underline: boolPtr(true),
286 },
287 LinkText: ansi.StylePrimitive{
288 Color: stringPtr("35"),
289 Bold: boolPtr(true),
290 },
291 Image: ansi.StylePrimitive{
292 Color: stringPtr("212"),
293 Underline: boolPtr(true),
294 },
295 ImageText: ansi.StylePrimitive{
296 Color: stringPtr("243"),
297 Format: "Image: {{.text}} →",
298 },
299 Code: ansi.StyleBlock{
300 StylePrimitive: ansi.StylePrimitive{
301 Prefix: " ",
302 Suffix: " ",
303 Color: stringPtr("203"),
304 BackgroundColor: stringPtr("236"),
305 },
306 },
307 CodeBlock: ansi.StyleCodeBlock{
308 StyleBlock: ansi.StyleBlock{
309 StylePrimitive: ansi.StylePrimitive{
310 Color: stringPtr("244"),
311 },
312 Margin: uintPtr(defaultMargin),
313 },
314 Chroma: &ansi.Chroma{
315 Text: ansi.StylePrimitive{
316 Color: stringPtr("#C4C4C4"),
317 },
318 Error: ansi.StylePrimitive{
319 Color: stringPtr("#F1F1F1"),
320 BackgroundColor: stringPtr("#F05B5B"),
321 },
322 Comment: ansi.StylePrimitive{
323 Color: stringPtr("#676767"),
324 },
325 CommentPreproc: ansi.StylePrimitive{
326 Color: stringPtr("#FF875F"),
327 },
328 Keyword: ansi.StylePrimitive{
329 Color: stringPtr("#00AAFF"),
330 },
331 KeywordReserved: ansi.StylePrimitive{
332 Color: stringPtr("#FF5FD2"),
333 },
334 KeywordNamespace: ansi.StylePrimitive{
335 Color: stringPtr("#FF5F87"),
336 },
337 KeywordType: ansi.StylePrimitive{
338 Color: stringPtr("#6E6ED8"),
339 },
340 Operator: ansi.StylePrimitive{
341 Color: stringPtr("#EF8080"),
342 },
343 Punctuation: ansi.StylePrimitive{
344 Color: stringPtr("#E8E8A8"),
345 },
346 Name: ansi.StylePrimitive{
347 Color: stringPtr("#C4C4C4"),
348 },
349 NameBuiltin: ansi.StylePrimitive{
350 Color: stringPtr("#FF8EC7"),
351 },
352 NameTag: ansi.StylePrimitive{
353 Color: stringPtr("#B083EA"),
354 },
355 NameAttribute: ansi.StylePrimitive{
356 Color: stringPtr("#7A7AE6"),
357 },
358 NameClass: ansi.StylePrimitive{
359 Color: stringPtr("#F1F1F1"),
360 Underline: boolPtr(true),
361 Bold: boolPtr(true),
362 },
363 NameDecorator: ansi.StylePrimitive{
364 Color: stringPtr("#FFFF87"),
365 },
366 NameFunction: ansi.StylePrimitive{
367 Color: stringPtr("#00D787"),
368 },
369 LiteralNumber: ansi.StylePrimitive{
370 Color: stringPtr("#6EEFC0"),
371 },
372 LiteralString: ansi.StylePrimitive{
373 Color: stringPtr("#C69669"),
374 },
375 LiteralStringEscape: ansi.StylePrimitive{
376 Color: stringPtr("#AFFFD7"),
377 },
378 GenericDeleted: ansi.StylePrimitive{
379 Color: stringPtr("#FD5B5B"),
380 },
381 GenericEmph: ansi.StylePrimitive{
382 Italic: boolPtr(true),
383 },
384 GenericInserted: ansi.StylePrimitive{
385 Color: stringPtr("#00D787"),
386 },
387 GenericStrong: ansi.StylePrimitive{
388 Bold: boolPtr(true),
389 },
390 GenericSubheading: ansi.StylePrimitive{
391 Color: stringPtr("#777777"),
392 },
393 Background: ansi.StylePrimitive{
394 BackgroundColor: stringPtr("#373737"),
395 },
396 },
397 },
398 Table: ansi.StyleTable{
399 StyleBlock: ansi.StyleBlock{
400 StylePrimitive: ansi.StylePrimitive{},
401 },
402 },
403 DefinitionDescription: ansi.StylePrimitive{
404 BlockPrefix: "\n ",
405 },
406 },
407
408 Help: help.Styles{
409 ShortKey: base.Foreground(t.FgMuted),
410 ShortDesc: base.Foreground(t.FgSubtle),
411 ShortSeparator: base.Foreground(t.Border),
412 Ellipsis: base.Foreground(t.Border),
413 FullKey: base.Foreground(t.FgMuted),
414 FullDesc: base.Foreground(t.FgSubtle),
415 FullSeparator: base.Foreground(t.Border),
416 },
417
418 // TODO: Fix this this is bad
419 Diff: Diff{
420 Added: t.Green,
421 Removed: t.Red,
422 Context: t.FgSubtle,
423 HunkHeader: t.FgSubtle,
424 HighlightAdded: t.GreenLight,
425 HighlightRemoved: t.RedLight,
426 AddedBg: t.GreenDark,
427 RemovedBg: t.RedDark,
428 ContextBg: t.BgSubtle,
429 LineNumber: t.FgMuted,
430 AddedLineNumberBg: t.GreenDark,
431 RemovedLineNumberBg: t.RedDark,
432 },
433 }
434}
435
436type Manager struct {
437 themes map[string]*Theme
438 current *Theme
439}
440
441var defaultManager *Manager
442
443func SetDefaultManager(m *Manager) {
444 defaultManager = m
445}
446
447func DefaultManager() *Manager {
448 if defaultManager == nil {
449 defaultManager = NewManager("crush")
450 }
451 return defaultManager
452}
453
454func CurrentTheme() *Theme {
455 if defaultManager == nil {
456 defaultManager = NewManager("crush")
457 }
458 return defaultManager.Current()
459}
460
461func NewManager(defaultTheme string) *Manager {
462 m := &Manager{
463 themes: make(map[string]*Theme),
464 }
465
466 m.Register(NewCrushTheme())
467
468 m.current = m.themes[defaultTheme]
469
470 return m
471}
472
473func (m *Manager) Register(theme *Theme) {
474 m.themes[theme.Name] = theme
475}
476
477func (m *Manager) Current() *Theme {
478 return m.current
479}
480
481func (m *Manager) SetTheme(name string) error {
482 if theme, ok := m.themes[name]; ok {
483 m.current = theme
484 return nil
485 }
486 return fmt.Errorf("theme %s not found", name)
487}
488
489func (m *Manager) List() []string {
490 names := make([]string, 0, len(m.themes))
491 for name := range m.themes {
492 names = append(names, name)
493 }
494 return names
495}
496
497// ParseHex converts hex string to color
498func ParseHex(hex string) color.Color {
499 var r, g, b uint8
500 fmt.Sscanf(hex, "#%02x%02x%02x", &r, &g, &b)
501 return color.RGBA{R: r, G: g, B: b, A: 255}
502}
503
504// Alpha returns a color with transparency
505func Alpha(c color.Color, alpha uint8) color.Color {
506 r, g, b, _ := c.RGBA()
507 return color.RGBA{
508 R: uint8(r >> 8),
509 G: uint8(g >> 8),
510 B: uint8(b >> 8),
511 A: alpha,
512 }
513}
514
515// Darken makes a color darker by percentage (0-100)
516func Darken(c color.Color, percent float64) color.Color {
517 r, g, b, a := c.RGBA()
518 factor := 1.0 - percent/100.0
519 return color.RGBA{
520 R: uint8(float64(r>>8) * factor),
521 G: uint8(float64(g>>8) * factor),
522 B: uint8(float64(b>>8) * factor),
523 A: uint8(a >> 8),
524 }
525}
526
527// Lighten makes a color lighter by percentage (0-100)
528func Lighten(c color.Color, percent float64) color.Color {
529 r, g, b, a := c.RGBA()
530 factor := percent / 100.0
531 return color.RGBA{
532 R: uint8(min(255, float64(r>>8)+255*factor)),
533 G: uint8(min(255, float64(g>>8)+255*factor)),
534 B: uint8(min(255, float64(b>>8)+255*factor)),
535 A: uint8(a >> 8),
536 }
537}
538
539// ApplyForegroundGrad renders a given string with a horizontal gradient
540// foreground.
541func ApplyForegroundGrad(input string, color1, color2 color.Color) string {
542 if input == "" {
543 return ""
544 }
545
546 var o strings.Builder
547 if len(input) == 1 {
548 return lipgloss.NewStyle().Foreground(color1).Render(input)
549 }
550
551 var clusters []string
552 gr := uniseg.NewGraphemes(input)
553 for gr.Next() {
554 clusters = append(clusters, string(gr.Runes()))
555 }
556
557 ramp := blendColors(len(clusters), color1, color2)
558 for i, c := range ramp {
559 fmt.Fprint(&o, CurrentTheme().S().Base.Foreground(c).Render(clusters[i]))
560 }
561
562 return o.String()
563}
564
565// blendColors returns a slice of colors blended between the given keys.
566// Blending is done in Hcl to stay in gamut.
567func blendColors(size int, stops ...color.Color) []color.Color {
568 if len(stops) < 2 {
569 return nil
570 }
571
572 stopsPrime := make([]colorful.Color, len(stops))
573 for i, k := range stops {
574 stopsPrime[i], _ = colorful.MakeColor(k)
575 }
576
577 numSegments := len(stopsPrime) - 1
578 blended := make([]color.Color, 0, size)
579
580 // Calculate how many colors each segment should have.
581 segmentSizes := make([]int, numSegments)
582 baseSize := size / numSegments
583 remainder := size % numSegments
584
585 // Distribute the remainder across segments.
586 for i := range numSegments {
587 segmentSizes[i] = baseSize
588 if i < remainder {
589 segmentSizes[i]++
590 }
591 }
592
593 // Generate colors for each segment.
594 for i := range numSegments {
595 c1 := stopsPrime[i]
596 c2 := stopsPrime[i+1]
597 segmentSize := segmentSizes[i]
598
599 for j := range segmentSize {
600 var t float64
601 if segmentSize > 1 {
602 t = float64(j) / float64(segmentSize-1)
603 }
604 c := c1.BlendHcl(c2, t)
605 blended = append(blended, c)
606 }
607 }
608
609 return blended
610}