styles.go

  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 }