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/lipgloss/v2"
 10)
 11
 12type Theme struct {
 13	Name   string
 14	IsDark bool
 15
 16	Primary   color.Color
 17	Secondary color.Color
 18	Tertiary  color.Color
 19	Accent    color.Color
 20
 21	BgBase    color.Color
 22	BgSubtle  color.Color
 23	BgOverlay color.Color
 24
 25	FgBase   color.Color
 26	FgMuted  color.Color
 27	FgSubtle color.Color
 28
 29	Border      color.Color
 30	BorderFocus color.Color
 31
 32	Success color.Color
 33	Error   color.Color
 34	Warning color.Color
 35	Info    color.Color
 36
 37	// TODO: add more syntax colors, maybe just use a chroma theme here.
 38	SyntaxBg      color.Color
 39	SyntaxKeyword color.Color
 40	SyntaxString  color.Color
 41	SyntaxComment color.Color
 42
 43	styles *Styles
 44}
 45
 46type Styles struct {
 47	Base lipgloss.Style
 48
 49	Title    lipgloss.Style
 50	Subtitle lipgloss.Style
 51	Text     lipgloss.Style
 52	Muted    lipgloss.Style
 53	Subtle   lipgloss.Style
 54
 55	Success lipgloss.Style
 56	Error   lipgloss.Style
 57	Warning lipgloss.Style
 58	Info    lipgloss.Style
 59
 60	// Inputs
 61	TextArea textarea.Styles
 62}
 63
 64func (t *Theme) S() *Styles {
 65	if t.styles == nil {
 66		t.styles = t.buildStyles()
 67	}
 68	return t.styles
 69}
 70
 71func (t *Theme) buildStyles() *Styles {
 72	base := lipgloss.NewStyle().
 73		Background(t.BgBase).
 74		Foreground(t.FgBase)
 75	return &Styles{
 76		Base: base,
 77
 78		Title: base.
 79			Foreground(t.Accent).
 80			Bold(true),
 81
 82		Subtitle: base.
 83			Foreground(t.Secondary).
 84			Bold(true),
 85
 86		Text: base,
 87
 88		Muted: base.Foreground(t.FgMuted),
 89
 90		Subtle: base.Foreground(t.FgSubtle),
 91
 92		Success: base.Foreground(t.Success),
 93
 94		Error: base.Foreground(t.Error),
 95
 96		Warning: base.Foreground(t.Warning),
 97
 98		Info: base.Foreground(t.Info),
 99
100		TextArea: textarea.Styles{
101			Focused: textarea.StyleState{
102				Base:             base,
103				Text:             base,
104				LineNumber:       base.Foreground(t.FgSubtle),
105				CursorLine:       base,
106				CursorLineNumber: base.Foreground(t.FgSubtle),
107				Placeholder:      base.Foreground(t.FgMuted),
108				Prompt:           base.Foreground(t.Tertiary),
109			},
110			Blurred: textarea.StyleState{
111				Base:             base,
112				Text:             base.Foreground(t.FgMuted),
113				LineNumber:       base.Foreground(t.FgMuted),
114				CursorLine:       base,
115				CursorLineNumber: base.Foreground(t.FgMuted),
116				Placeholder:      base.Foreground(t.FgMuted),
117				Prompt:           base.Foreground(t.FgMuted),
118			},
119			Cursor: textarea.CursorStyle{
120				Color: t.Secondary,
121				Shape: tea.CursorBar,
122				Blink: true,
123			},
124		},
125	}
126}
127
128type Manager struct {
129	themes  map[string]*Theme
130	current *Theme
131}
132
133var defaultManager *Manager
134
135func SetDefaultManager(m *Manager) {
136	defaultManager = m
137}
138
139func DefaultManager() *Manager {
140	if defaultManager == nil {
141		defaultManager = NewManager("crush")
142	}
143	return defaultManager
144}
145
146func CurrentTheme() *Theme {
147	if defaultManager == nil {
148		defaultManager = NewManager("crush")
149	}
150	return defaultManager.Current()
151}
152
153func NewManager(defaultTheme string) *Manager {
154	m := &Manager{
155		themes: make(map[string]*Theme),
156	}
157
158	m.Register(NewCrushTheme())
159
160	m.current = m.themes[defaultTheme]
161
162	return m
163}
164
165func (m *Manager) Register(theme *Theme) {
166	m.themes[theme.Name] = theme
167}
168
169func (m *Manager) Current() *Theme {
170	return m.current
171}
172
173func (m *Manager) SetTheme(name string) error {
174	if theme, ok := m.themes[name]; ok {
175		m.current = theme
176		return nil
177	}
178	return fmt.Errorf("theme %s not found", name)
179}
180
181func (m *Manager) List() []string {
182	names := make([]string, 0, len(m.themes))
183	for name := range m.themes {
184		names = append(names, name)
185	}
186	return names
187}
188
189// ParseHex converts hex string to color
190func ParseHex(hex string) color.Color {
191	var r, g, b uint8
192	fmt.Sscanf(hex, "#%02x%02x%02x", &r, &g, &b)
193	return color.RGBA{R: r, G: g, B: b, A: 255}
194}
195
196// Alpha returns a color with transparency
197func Alpha(c color.Color, alpha uint8) color.Color {
198	r, g, b, _ := c.RGBA()
199	return color.RGBA{
200		R: uint8(r >> 8),
201		G: uint8(g >> 8),
202		B: uint8(b >> 8),
203		A: alpha,
204	}
205}
206
207// Darken makes a color darker by percentage (0-100)
208func Darken(c color.Color, percent float64) color.Color {
209	r, g, b, a := c.RGBA()
210	factor := 1.0 - percent/100.0
211	return color.RGBA{
212		R: uint8(float64(r>>8) * factor),
213		G: uint8(float64(g>>8) * factor),
214		B: uint8(float64(b>>8) * factor),
215		A: uint8(a >> 8),
216	}
217}
218
219// Lighten makes a color lighter by percentage (0-100)
220func Lighten(c color.Color, percent float64) color.Color {
221	r, g, b, a := c.RGBA()
222	factor := percent / 100.0
223	return color.RGBA{
224		R: uint8(min(255, float64(r>>8)+255*factor)),
225		G: uint8(min(255, float64(g>>8)+255*factor)),
226		B: uint8(min(255, float64(b>>8)+255*factor)),
227		A: uint8(a >> 8),
228	}
229}