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