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