theme.go

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