helpers.go

  1package core
  2
  3import (
  4	"image/color"
  5	"strings"
  6
  7	"github.com/alecthomas/chroma/v2"
  8	"github.com/charmbracelet/crush/internal/tui/exp/diffview"
  9	"github.com/charmbracelet/crush/internal/tui/styles"
 10	"github.com/charmbracelet/lipgloss/v2"
 11	"github.com/charmbracelet/x/ansi"
 12)
 13
 14func Section(text string, width int) string {
 15	t := styles.CurrentTheme()
 16	char := "─"
 17	length := lipgloss.Width(text) + 1
 18	remainingWidth := width - length
 19	lineStyle := t.S().Base.Foreground(t.Border)
 20	if remainingWidth > 0 {
 21		text = text + " " + lineStyle.Render(strings.Repeat(char, remainingWidth))
 22	}
 23	return text
 24}
 25
 26func SectionWithInfo(text string, width int, info string) string {
 27	t := styles.CurrentTheme()
 28	char := "─"
 29	length := lipgloss.Width(text) + 1
 30	remainingWidth := width - length
 31
 32	if info != "" {
 33		remainingWidth -= lipgloss.Width(info) + 1 // 1 for the space before info
 34	}
 35	lineStyle := t.S().Base.Foreground(t.Border)
 36	if remainingWidth > 0 {
 37		text = text + " " + lineStyle.Render(strings.Repeat(char, remainingWidth)) + " " + info
 38	}
 39	return text
 40}
 41
 42func Title(title string, width int) string {
 43	t := styles.CurrentTheme()
 44	char := "╱"
 45	length := lipgloss.Width(title) + 1
 46	remainingWidth := width - length
 47	titleStyle := t.S().Base.Foreground(t.Primary)
 48	if remainingWidth > 0 {
 49		lines := strings.Repeat(char, remainingWidth)
 50		lines = styles.ApplyForegroundGrad(lines, t.Primary, t.Secondary)
 51		title = titleStyle.Render(title) + " " + lines
 52	}
 53	return title
 54}
 55
 56type StatusOpts struct {
 57	Icon             string
 58	IconColor        color.Color
 59	NoIcon           bool // If true, no icon will be displayed
 60	Title            string
 61	TitleColor       color.Color
 62	Description      string
 63	DescriptionColor color.Color
 64	ExtraContent     string // Additional content to append after the description
 65}
 66
 67func Status(ops StatusOpts, width int) string {
 68	t := styles.CurrentTheme()
 69	icon := "●"
 70	iconColor := t.Success
 71	if ops.Icon != "" {
 72		icon = ops.Icon
 73	} else if ops.NoIcon {
 74		icon = ""
 75	}
 76	if ops.IconColor != nil {
 77		iconColor = ops.IconColor
 78	}
 79	title := ops.Title
 80	titleColor := t.FgMuted
 81	if ops.TitleColor != nil {
 82		titleColor = ops.TitleColor
 83	}
 84	description := ops.Description
 85	descriptionColor := t.FgSubtle
 86	if ops.DescriptionColor != nil {
 87		descriptionColor = ops.DescriptionColor
 88	}
 89	title = t.S().Base.Foreground(titleColor).Render(title)
 90	if description != "" {
 91		extraContentWidth := lipgloss.Width(ops.ExtraContent)
 92		if extraContentWidth > 0 {
 93			extraContentWidth += 1
 94		}
 95		description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContentWidth, "…")
 96	}
 97	description = t.S().Base.Foreground(descriptionColor).Render(description)
 98
 99	content := []string{}
100	if icon != "" {
101		content = append(content, t.S().Base.Foreground(iconColor).Render(icon))
102	}
103	content = append(content, title, description)
104	if ops.ExtraContent != "" {
105		content = append(content, ops.ExtraContent)
106	}
107
108	return strings.Join(content, " ")
109}
110
111type ButtonOpts struct {
112	Text           string
113	UnderlineIndex int  // Index of character to underline (0-based)
114	Selected       bool // Whether this button is selected
115}
116
117// SelectableButton creates a button with an underlined character and selection state
118func SelectableButton(opts ButtonOpts) string {
119	t := styles.CurrentTheme()
120
121	// Base style for the button
122	buttonStyle := t.S().Text
123
124	// Apply selection styling
125	if opts.Selected {
126		buttonStyle = buttonStyle.Foreground(t.White).Background(t.Secondary)
127	} else {
128		buttonStyle = buttonStyle.Background(t.BgSubtle)
129	}
130
131	// Create the button text with underlined character
132	text := opts.Text
133	if opts.UnderlineIndex >= 0 && opts.UnderlineIndex < len(text) {
134		before := text[:opts.UnderlineIndex]
135		underlined := text[opts.UnderlineIndex : opts.UnderlineIndex+1]
136		after := text[opts.UnderlineIndex+1:]
137
138		message := buttonStyle.Render(before) +
139			buttonStyle.Underline(true).Render(underlined) +
140			buttonStyle.Render(after)
141
142		return buttonStyle.Padding(0, 2).Render(message)
143	}
144
145	// Fallback if no underline index specified
146	return buttonStyle.Padding(0, 2).Render(text)
147}
148
149// SelectableButtons creates a horizontal row of selectable buttons
150func SelectableButtons(buttons []ButtonOpts, spacing string) string {
151	if spacing == "" {
152		spacing = "  "
153	}
154
155	var parts []string
156	for i, button := range buttons {
157		parts = append(parts, SelectableButton(button))
158		if i < len(buttons)-1 {
159			parts = append(parts, spacing)
160		}
161	}
162
163	return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
164}
165
166func DiffFormatter() *diffview.DiffView {
167	t := styles.CurrentTheme()
168	formatDiff := diffview.New()
169	style := chroma.MustNewStyle("crush", styles.GetChromaTheme())
170	diff := formatDiff.ChromaStyle(style).Style(t.S().Diff).TabWidth(4)
171	return diff
172}