theme.go

  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}