styles.go

  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 }