1package styles
2
3import (
4 "image/color"
5
6 "charm.land/bubbles/v2/filepicker"
7 "charm.land/bubbles/v2/help"
8 "charm.land/bubbles/v2/textarea"
9 "charm.land/bubbles/v2/textinput"
10 tea "charm.land/bubbletea/v2"
11 "charm.land/lipgloss/v2"
12 "github.com/charmbracelet/crush/internal/tui/exp/diffview"
13 "github.com/charmbracelet/glamour/v2/ansi"
14 "github.com/charmbracelet/x/exp/charmtone"
15)
16
17const (
18 CheckIcon string = "✓"
19 ErrorIcon string = "×"
20 WarningIcon string = "⚠"
21 InfoIcon string = "ⓘ"
22 HintIcon string = "∵"
23 SpinnerIcon string = "..."
24 LoadingIcon string = "⟳"
25 DocumentIcon string = "🖼"
26 ModelIcon string = "◇"
27
28 ToolPending string = "●"
29 ToolSuccess string = "✓"
30 ToolError string = "×"
31
32 BorderThin string = "│"
33 BorderThick string = "▌"
34
35 SectionSeparator string = "─"
36)
37
38const (
39 defaultMargin = 2
40 defaultListIndent = 2
41)
42
43type Styles struct {
44 WindowTooSmall lipgloss.Style
45
46 // Reusable text styles
47 Base lipgloss.Style
48 Muted lipgloss.Style
49 Subtle lipgloss.Style
50
51 // Tags
52 TagBase lipgloss.Style
53 TagError lipgloss.Style
54 TagInfo lipgloss.Style
55
56 // Headers
57 HeaderTool lipgloss.Style
58 HeaderToolNested lipgloss.Style
59
60 // Panels
61 PanelMuted lipgloss.Style
62 PanelBase lipgloss.Style
63
64 // Line numbers for code blocks
65 LineNumber lipgloss.Style
66
67 // Message borders
68 FocusedMessageBorder lipgloss.Border
69
70 // Tool calls
71 ToolCallPending lipgloss.Style
72 ToolCallError lipgloss.Style
73 ToolCallSuccess lipgloss.Style
74 ToolCallCancelled lipgloss.Style
75 EarlyStateMessage lipgloss.Style
76
77 // Text selection
78 TextSelection lipgloss.Style
79
80 // LSP and MCP status indicators
81 ItemOfflineIcon lipgloss.Style
82 ItemBusyIcon lipgloss.Style
83 ItemErrorIcon lipgloss.Style
84 ItemOnlineIcon lipgloss.Style
85
86 // Markdown & Chroma
87 Markdown ansi.StyleConfig
88
89 // Inputs
90 TextInput textinput.Styles
91 TextArea textarea.Styles
92
93 // Help
94 Help help.Styles
95
96 // Diff
97 Diff diffview.Style
98
99 // FilePicker
100 FilePicker filepicker.Styles
101
102 // Buttons
103 ButtonFocus lipgloss.Style
104 ButtonBlur lipgloss.Style
105
106 // Borders
107 BorderFocus lipgloss.Style
108 BorderBlur lipgloss.Style
109
110 // Editor
111 EditorPromptNormalFocused lipgloss.Style
112 EditorPromptNormalBlurred lipgloss.Style
113 EditorPromptYoloIconFocused lipgloss.Style
114 EditorPromptYoloIconBlurred lipgloss.Style
115 EditorPromptYoloDotsFocused lipgloss.Style
116 EditorPromptYoloDotsBlurred lipgloss.Style
117
118 // Background
119 Background color.Color
120 // Logo
121 LogoFieldColor color.Color
122 LogoTitleColorA color.Color
123 LogoTitleColorB color.Color
124 LogoCharmColor color.Color
125 LogoVersionColor color.Color
126
127 // Section Title
128 Section struct {
129 Title lipgloss.Style
130 Line lipgloss.Style
131 }
132
133 // Initialize
134 Initialize struct {
135 Header lipgloss.Style
136 Content lipgloss.Style
137 Accent lipgloss.Style
138 }
139
140 // LSP
141 LSP struct {
142 ErrorDiagnostic lipgloss.Style
143 WarningDiagnostic lipgloss.Style
144 HintDiagnostic lipgloss.Style
145 InfoDiagnostic lipgloss.Style
146 }
147
148 // Files
149 Files struct {
150 Path lipgloss.Style
151 Additions lipgloss.Style
152 Deletions lipgloss.Style
153 }
154}
155
156func DefaultStyles() Styles {
157 var (
158 primary = charmtone.Charple
159 secondary = charmtone.Dolly
160 tertiary = charmtone.Bok
161 // accent = charmtone.Zest
162
163 // Backgrounds
164 bgBase = charmtone.Pepper
165 bgBaseLighter = charmtone.BBQ
166 bgSubtle = charmtone.Charcoal
167 bgOverlay = charmtone.Iron
168
169 // Foregrounds
170 fgBase = charmtone.Ash
171 fgMuted = charmtone.Squid
172 fgHalfMuted = charmtone.Smoke
173 fgSubtle = charmtone.Oyster
174 // fgSelected = charmtone.Salt
175
176 // Borders
177 border = charmtone.Charcoal
178 borderFocus = charmtone.Charple
179
180 // Status
181 warning = charmtone.Zest
182 info = charmtone.Malibu
183
184 // Colors
185 white = charmtone.Butter
186
187 blueLight = charmtone.Sardine
188 blue = charmtone.Malibu
189
190 // yellow = charmtone.Mustard
191 // citron = charmtone.Citron
192
193 green = charmtone.Julep
194 greenDark = charmtone.Guac
195 // greenLight = charmtone.Bok
196
197 // red = charmtone.Coral
198 redDark = charmtone.Sriracha
199 // redLight = charmtone.Salmon
200 // cherry = charmtone.Cherry
201 )
202
203 base := lipgloss.NewStyle().Foreground(fgBase)
204
205 s := Styles{}
206
207 s.Background = bgBase
208
209 s.TextInput = textinput.Styles{
210 Focused: textinput.StyleState{
211 Text: base,
212 Placeholder: base.Foreground(fgSubtle),
213 Prompt: base.Foreground(tertiary),
214 Suggestion: base.Foreground(fgSubtle),
215 },
216 Blurred: textinput.StyleState{
217 Text: base.Foreground(fgMuted),
218 Placeholder: base.Foreground(fgSubtle),
219 Prompt: base.Foreground(fgMuted),
220 Suggestion: base.Foreground(fgSubtle),
221 },
222 Cursor: textinput.CursorStyle{
223 Color: secondary,
224 Shape: tea.CursorBar,
225 Blink: true,
226 },
227 }
228
229 s.TextArea = textarea.Styles{
230 Focused: textarea.StyleState{
231 Base: base,
232 Text: base,
233 LineNumber: base.Foreground(fgSubtle),
234 CursorLine: base,
235 CursorLineNumber: base.Foreground(fgSubtle),
236 Placeholder: base.Foreground(fgSubtle),
237 Prompt: base.Foreground(tertiary),
238 },
239 Blurred: textarea.StyleState{
240 Base: base,
241 Text: base.Foreground(fgMuted),
242 LineNumber: base.Foreground(fgMuted),
243 CursorLine: base,
244 CursorLineNumber: base.Foreground(fgMuted),
245 Placeholder: base.Foreground(fgSubtle),
246 Prompt: base.Foreground(fgMuted),
247 },
248 Cursor: textarea.CursorStyle{
249 Color: secondary,
250 Shape: tea.CursorBar,
251 Blink: true,
252 },
253 }
254
255 s.Markdown = ansi.StyleConfig{
256 Document: ansi.StyleBlock{
257 StylePrimitive: ansi.StylePrimitive{
258 // BlockPrefix: "\n",
259 // BlockSuffix: "\n",
260 Color: stringPtr(charmtone.Smoke.Hex()),
261 },
262 // Margin: uintPtr(defaultMargin),
263 },
264 BlockQuote: ansi.StyleBlock{
265 StylePrimitive: ansi.StylePrimitive{},
266 Indent: uintPtr(1),
267 IndentToken: stringPtr("│ "),
268 },
269 List: ansi.StyleList{
270 LevelIndent: defaultListIndent,
271 },
272 Heading: ansi.StyleBlock{
273 StylePrimitive: ansi.StylePrimitive{
274 BlockSuffix: "\n",
275 Color: stringPtr(charmtone.Malibu.Hex()),
276 Bold: boolPtr(true),
277 },
278 },
279 H1: ansi.StyleBlock{
280 StylePrimitive: ansi.StylePrimitive{
281 Prefix: " ",
282 Suffix: " ",
283 Color: stringPtr(charmtone.Zest.Hex()),
284 BackgroundColor: stringPtr(charmtone.Charple.Hex()),
285 Bold: boolPtr(true),
286 },
287 },
288 H2: ansi.StyleBlock{
289 StylePrimitive: ansi.StylePrimitive{
290 Prefix: "## ",
291 },
292 },
293 H3: ansi.StyleBlock{
294 StylePrimitive: ansi.StylePrimitive{
295 Prefix: "### ",
296 },
297 },
298 H4: ansi.StyleBlock{
299 StylePrimitive: ansi.StylePrimitive{
300 Prefix: "#### ",
301 },
302 },
303 H5: ansi.StyleBlock{
304 StylePrimitive: ansi.StylePrimitive{
305 Prefix: "##### ",
306 },
307 },
308 H6: ansi.StyleBlock{
309 StylePrimitive: ansi.StylePrimitive{
310 Prefix: "###### ",
311 Color: stringPtr(charmtone.Guac.Hex()),
312 Bold: boolPtr(false),
313 },
314 },
315 Strikethrough: ansi.StylePrimitive{
316 CrossedOut: boolPtr(true),
317 },
318 Emph: ansi.StylePrimitive{
319 Italic: boolPtr(true),
320 },
321 Strong: ansi.StylePrimitive{
322 Bold: boolPtr(true),
323 },
324 HorizontalRule: ansi.StylePrimitive{
325 Color: stringPtr(charmtone.Charcoal.Hex()),
326 Format: "\n--------\n",
327 },
328 Item: ansi.StylePrimitive{
329 BlockPrefix: "• ",
330 },
331 Enumeration: ansi.StylePrimitive{
332 BlockPrefix: ". ",
333 },
334 Task: ansi.StyleTask{
335 StylePrimitive: ansi.StylePrimitive{},
336 Ticked: "[✓] ",
337 Unticked: "[ ] ",
338 },
339 Link: ansi.StylePrimitive{
340 Color: stringPtr(charmtone.Zinc.Hex()),
341 Underline: boolPtr(true),
342 },
343 LinkText: ansi.StylePrimitive{
344 Color: stringPtr(charmtone.Guac.Hex()),
345 Bold: boolPtr(true),
346 },
347 Image: ansi.StylePrimitive{
348 Color: stringPtr(charmtone.Cheeky.Hex()),
349 Underline: boolPtr(true),
350 },
351 ImageText: ansi.StylePrimitive{
352 Color: stringPtr(charmtone.Squid.Hex()),
353 Format: "Image: {{.text}} →",
354 },
355 Code: ansi.StyleBlock{
356 StylePrimitive: ansi.StylePrimitive{
357 Prefix: " ",
358 Suffix: " ",
359 Color: stringPtr(charmtone.Coral.Hex()),
360 BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
361 },
362 },
363 CodeBlock: ansi.StyleCodeBlock{
364 StyleBlock: ansi.StyleBlock{
365 StylePrimitive: ansi.StylePrimitive{
366 Color: stringPtr(charmtone.Charcoal.Hex()),
367 },
368 Margin: uintPtr(defaultMargin),
369 },
370 Chroma: &ansi.Chroma{
371 Text: ansi.StylePrimitive{
372 Color: stringPtr(charmtone.Smoke.Hex()),
373 },
374 Error: ansi.StylePrimitive{
375 Color: stringPtr(charmtone.Butter.Hex()),
376 BackgroundColor: stringPtr(charmtone.Sriracha.Hex()),
377 },
378 Comment: ansi.StylePrimitive{
379 Color: stringPtr(charmtone.Oyster.Hex()),
380 },
381 CommentPreproc: ansi.StylePrimitive{
382 Color: stringPtr(charmtone.Bengal.Hex()),
383 },
384 Keyword: ansi.StylePrimitive{
385 Color: stringPtr(charmtone.Malibu.Hex()),
386 },
387 KeywordReserved: ansi.StylePrimitive{
388 Color: stringPtr(charmtone.Pony.Hex()),
389 },
390 KeywordNamespace: ansi.StylePrimitive{
391 Color: stringPtr(charmtone.Pony.Hex()),
392 },
393 KeywordType: ansi.StylePrimitive{
394 Color: stringPtr(charmtone.Guppy.Hex()),
395 },
396 Operator: ansi.StylePrimitive{
397 Color: stringPtr(charmtone.Salmon.Hex()),
398 },
399 Punctuation: ansi.StylePrimitive{
400 Color: stringPtr(charmtone.Zest.Hex()),
401 },
402 Name: ansi.StylePrimitive{
403 Color: stringPtr(charmtone.Smoke.Hex()),
404 },
405 NameBuiltin: ansi.StylePrimitive{
406 Color: stringPtr(charmtone.Cheeky.Hex()),
407 },
408 NameTag: ansi.StylePrimitive{
409 Color: stringPtr(charmtone.Mauve.Hex()),
410 },
411 NameAttribute: ansi.StylePrimitive{
412 Color: stringPtr(charmtone.Hazy.Hex()),
413 },
414 NameClass: ansi.StylePrimitive{
415 Color: stringPtr(charmtone.Salt.Hex()),
416 Underline: boolPtr(true),
417 Bold: boolPtr(true),
418 },
419 NameDecorator: ansi.StylePrimitive{
420 Color: stringPtr(charmtone.Citron.Hex()),
421 },
422 NameFunction: ansi.StylePrimitive{
423 Color: stringPtr(charmtone.Guac.Hex()),
424 },
425 LiteralNumber: ansi.StylePrimitive{
426 Color: stringPtr(charmtone.Julep.Hex()),
427 },
428 LiteralString: ansi.StylePrimitive{
429 Color: stringPtr(charmtone.Cumin.Hex()),
430 },
431 LiteralStringEscape: ansi.StylePrimitive{
432 Color: stringPtr(charmtone.Bok.Hex()),
433 },
434 GenericDeleted: ansi.StylePrimitive{
435 Color: stringPtr(charmtone.Coral.Hex()),
436 },
437 GenericEmph: ansi.StylePrimitive{
438 Italic: boolPtr(true),
439 },
440 GenericInserted: ansi.StylePrimitive{
441 Color: stringPtr(charmtone.Guac.Hex()),
442 },
443 GenericStrong: ansi.StylePrimitive{
444 Bold: boolPtr(true),
445 },
446 GenericSubheading: ansi.StylePrimitive{
447 Color: stringPtr(charmtone.Squid.Hex()),
448 },
449 Background: ansi.StylePrimitive{
450 BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
451 },
452 },
453 },
454 Table: ansi.StyleTable{
455 StyleBlock: ansi.StyleBlock{
456 StylePrimitive: ansi.StylePrimitive{},
457 },
458 },
459 DefinitionDescription: ansi.StylePrimitive{
460 BlockPrefix: "\n ",
461 },
462 }
463
464 s.Help = help.Styles{
465 ShortKey: base.Foreground(fgMuted),
466 ShortDesc: base.Foreground(fgSubtle),
467 ShortSeparator: base.Foreground(border),
468 Ellipsis: base.Foreground(border),
469 FullKey: base.Foreground(fgMuted),
470 FullDesc: base.Foreground(fgSubtle),
471 FullSeparator: base.Foreground(border),
472 }
473
474 s.Diff = diffview.Style{
475 DividerLine: diffview.LineStyle{
476 LineNumber: lipgloss.NewStyle().
477 Foreground(fgHalfMuted).
478 Background(bgBaseLighter),
479 Code: lipgloss.NewStyle().
480 Foreground(fgHalfMuted).
481 Background(bgBaseLighter),
482 },
483 MissingLine: diffview.LineStyle{
484 LineNumber: lipgloss.NewStyle().
485 Background(bgBaseLighter),
486 Code: lipgloss.NewStyle().
487 Background(bgBaseLighter),
488 },
489 EqualLine: diffview.LineStyle{
490 LineNumber: lipgloss.NewStyle().
491 Foreground(fgMuted).
492 Background(bgBase),
493 Code: lipgloss.NewStyle().
494 Foreground(fgMuted).
495 Background(bgBase),
496 },
497 InsertLine: diffview.LineStyle{
498 LineNumber: lipgloss.NewStyle().
499 Foreground(lipgloss.Color("#629657")).
500 Background(lipgloss.Color("#2b322a")),
501 Symbol: lipgloss.NewStyle().
502 Foreground(lipgloss.Color("#629657")).
503 Background(lipgloss.Color("#323931")),
504 Code: lipgloss.NewStyle().
505 Background(lipgloss.Color("#323931")),
506 },
507 DeleteLine: diffview.LineStyle{
508 LineNumber: lipgloss.NewStyle().
509 Foreground(lipgloss.Color("#a45c59")).
510 Background(lipgloss.Color("#312929")),
511 Symbol: lipgloss.NewStyle().
512 Foreground(lipgloss.Color("#a45c59")).
513 Background(lipgloss.Color("#383030")),
514 Code: lipgloss.NewStyle().
515 Background(lipgloss.Color("#383030")),
516 },
517 }
518
519 s.FilePicker = filepicker.Styles{
520 DisabledCursor: base.Foreground(fgMuted),
521 Cursor: base.Foreground(fgBase),
522 Symlink: base.Foreground(fgSubtle),
523 Directory: base.Foreground(primary),
524 File: base.Foreground(fgBase),
525 DisabledFile: base.Foreground(fgMuted),
526 DisabledSelected: base.Background(bgOverlay).Foreground(fgMuted),
527 Permission: base.Foreground(fgMuted),
528 Selected: base.Background(primary).Foreground(fgBase),
529 FileSize: base.Foreground(fgMuted),
530 EmptyDirectory: base.Foreground(fgMuted).PaddingLeft(2).SetString("Empty directory"),
531 }
532
533 // borders
534 s.FocusedMessageBorder = lipgloss.Border{Left: BorderThick}
535
536 // text presets
537 s.Base = lipgloss.NewStyle().Foreground(fgBase)
538 s.Muted = lipgloss.NewStyle().Foreground(fgMuted)
539 s.Subtle = lipgloss.NewStyle().Foreground(fgSubtle)
540
541 s.WindowTooSmall = s.Muted
542
543 // tag presets
544 s.TagBase = lipgloss.NewStyle().Padding(0, 1).Foreground(white)
545 s.TagError = s.TagBase.Background(redDark)
546 s.TagInfo = s.TagBase.Background(blueLight)
547
548 // headers
549 s.HeaderTool = lipgloss.NewStyle().Foreground(blue)
550 s.HeaderToolNested = lipgloss.NewStyle().Foreground(fgHalfMuted)
551
552 // panels
553 s.PanelMuted = s.Muted.Background(bgBaseLighter)
554 s.PanelBase = lipgloss.NewStyle().Background(bgBase)
555
556 // code line number
557 s.LineNumber = lipgloss.NewStyle().Foreground(fgMuted).Background(bgBase).PaddingRight(1).PaddingLeft(1)
558
559 // Tool calls
560 s.ToolCallPending = lipgloss.NewStyle().Foreground(greenDark).SetString(ToolPending)
561 s.ToolCallError = lipgloss.NewStyle().Foreground(redDark).SetString(ToolError)
562 s.ToolCallSuccess = lipgloss.NewStyle().Foreground(green).SetString(ToolSuccess)
563 // Cancelled uses muted tone but same glyph as pending
564 s.ToolCallCancelled = s.Muted.SetString(ToolPending)
565 s.EarlyStateMessage = s.Subtle.PaddingLeft(2)
566
567 // Buttons
568 s.ButtonFocus = lipgloss.NewStyle().Foreground(white).Background(secondary)
569 s.ButtonBlur = s.Base.Background(bgSubtle)
570
571 // Borders
572 s.BorderFocus = lipgloss.NewStyle().BorderForeground(borderFocus).Border(lipgloss.RoundedBorder()).Padding(1, 2)
573
574 // Editor
575 s.EditorPromptNormalFocused = lipgloss.NewStyle().Foreground(greenDark).SetString("::: ")
576 s.EditorPromptNormalBlurred = s.EditorPromptNormalFocused.Foreground(fgMuted)
577 s.EditorPromptYoloIconFocused = lipgloss.NewStyle().Foreground(charmtone.Oyster).Background(charmtone.Citron).Bold(true).SetString(" ! ")
578 s.EditorPromptYoloIconBlurred = s.EditorPromptYoloIconFocused.Foreground(charmtone.Pepper).Background(charmtone.Squid)
579 s.EditorPromptYoloDotsFocused = lipgloss.NewStyle().Foreground(charmtone.Zest).SetString(":::")
580 s.EditorPromptYoloDotsBlurred = s.EditorPromptYoloDotsFocused.Foreground(charmtone.Squid)
581
582 // Logo colors
583 s.LogoFieldColor = primary
584 s.LogoTitleColorA = secondary
585 s.LogoTitleColorB = primary
586 s.LogoCharmColor = secondary
587 s.LogoVersionColor = primary
588
589 // Section
590 s.Section.Title = s.Subtle
591 s.Section.Line = s.Base.Foreground(charmtone.Charcoal)
592
593 // Initialize
594 s.Initialize.Header = s.Base
595 s.Initialize.Content = s.Muted
596 s.Initialize.Accent = s.Base.Foreground(greenDark)
597
598 // LSP and MCP status.
599 s.ItemOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Squid).SetString("●")
600 s.ItemBusyIcon = s.ItemOfflineIcon.Foreground(charmtone.Citron)
601 s.ItemErrorIcon = s.ItemOfflineIcon.Foreground(charmtone.Coral)
602 s.ItemOnlineIcon = s.ItemOfflineIcon.Foreground(charmtone.Guac)
603
604 // LSP
605 s.LSP.ErrorDiagnostic = s.Base.Foreground(redDark)
606 s.LSP.WarningDiagnostic = s.Base.Foreground(warning)
607 s.LSP.HintDiagnostic = s.Base.Foreground(fgHalfMuted)
608 s.LSP.InfoDiagnostic = s.Base.Foreground(info)
609
610 s.Files.Path = s.Muted
611 s.Files.Additions = s.Base.Foreground(greenDark)
612 s.Files.Deletions = s.Base.Foreground(redDark)
613 return s
614}
615
616// Helper functions for style pointers
617func boolPtr(b bool) *bool { return &b }
618func stringPtr(s string) *string { return &s }
619func uintPtr(u uint) *uint { return &u }