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