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}