helpers.go

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