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}