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