styles.go

  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 }