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