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	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}