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	Title            string
 42	TitleColor       color.Color
 43	Description      string
 44	DescriptionColor color.Color
 45}
 46
 47func Status(ops StatusOpts, width int) string {
 48	t := styles.CurrentTheme()
 49	icon := "●"
 50	iconColor := t.Success
 51	if ops.Icon != "" {
 52		icon = ops.Icon
 53	}
 54	if ops.IconColor != nil {
 55		iconColor = ops.IconColor
 56	}
 57	title := ops.Title
 58	titleColor := t.FgMuted
 59	if ops.TitleColor != nil {
 60		titleColor = ops.TitleColor
 61	}
 62	description := ops.Description
 63	descriptionColor := t.FgSubtle
 64	if ops.DescriptionColor != nil {
 65		descriptionColor = ops.DescriptionColor
 66	}
 67	icon = t.S().Base.Foreground(iconColor).Render(icon)
 68	title = t.S().Base.Foreground(titleColor).Render(title)
 69	if description != "" {
 70		description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2, "…")
 71	}
 72	description = t.S().Base.Foreground(descriptionColor).Render(description)
 73	return strings.Join([]string{
 74		icon,
 75		title,
 76		description,
 77	}, " ")
 78}
 79
 80type ButtonOpts struct {
 81	Text           string
 82	UnderlineIndex int  // Index of character to underline (0-based)
 83	Selected       bool // Whether this button is selected
 84}
 85
 86// SelectableButton creates a button with an underlined character and selection state
 87func SelectableButton(opts ButtonOpts) string {
 88	t := styles.CurrentTheme()
 89
 90	// Base style for the button
 91	buttonStyle := t.S().Text
 92
 93	// Apply selection styling
 94	if opts.Selected {
 95		buttonStyle = buttonStyle.Foreground(t.White).Background(t.Secondary)
 96	} else {
 97		buttonStyle = buttonStyle.Background(t.BgSubtle)
 98	}
 99
100	// Create the button text with underlined character
101	text := opts.Text
102	if opts.UnderlineIndex >= 0 && opts.UnderlineIndex < len(text) {
103		before := text[:opts.UnderlineIndex]
104		underlined := text[opts.UnderlineIndex : opts.UnderlineIndex+1]
105		after := text[opts.UnderlineIndex+1:]
106
107		message := buttonStyle.Render(before) +
108			buttonStyle.Underline(true).Render(underlined) +
109			buttonStyle.Render(after)
110
111		return buttonStyle.Padding(0, 2).Render(message)
112	}
113
114	// Fallback if no underline index specified
115	return buttonStyle.Padding(0, 2).Render(text)
116}
117
118// SelectableButtons creates a horizontal row of selectable buttons
119func SelectableButtons(buttons []ButtonOpts, spacing string) string {
120	if spacing == "" {
121		spacing = "  "
122	}
123
124	var parts []string
125	for i, button := range buttons {
126		parts = append(parts, SelectableButton(button))
127		if i < len(buttons)-1 {
128			parts = append(parts, spacing)
129		}
130	}
131
132	return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
133}