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	// 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 }