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