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}