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