1package core
2
3import (
4 "image/color"
5 "strings"
6
7 "github.com/alecthomas/chroma/v2"
8 "github.com/charmbracelet/bubbles/v2/help"
9 "github.com/charmbracelet/bubbles/v2/key"
10 "github.com/charmbracelet/crush/internal/tui/exp/diffview"
11 "github.com/charmbracelet/crush/internal/tui/styles"
12 "github.com/charmbracelet/lipgloss/v2"
13 "github.com/charmbracelet/x/ansi"
14)
15
16type KeyMapHelp interface {
17 Help() help.KeyMap
18}
19
20type simpleHelp struct {
21 shortList []key.Binding
22 fullList [][]key.Binding
23}
24
25func NewSimpleHelp(shortList []key.Binding, fullList [][]key.Binding) help.KeyMap {
26 return &simpleHelp{
27 shortList: shortList,
28 fullList: fullList,
29 }
30}
31
32// FullHelp implements help.KeyMap.
33func (s *simpleHelp) FullHelp() [][]key.Binding {
34 return s.fullList
35}
36
37// ShortHelp implements help.KeyMap.
38func (s *simpleHelp) ShortHelp() []key.Binding {
39 return s.shortList
40}
41
42func Section(text string, width int) string {
43 t := styles.CurrentTheme()
44 char := "─"
45 length := lipgloss.Width(text) + 1
46 remainingWidth := width - length
47 lineStyle := t.S().Base.Foreground(t.Border)
48 if remainingWidth > 0 {
49 text = text + " " + lineStyle.Render(strings.Repeat(char, remainingWidth))
50 }
51 return text
52}
53
54func SectionWithInfo(text string, width int, info string) string {
55 t := styles.CurrentTheme()
56 char := "─"
57 length := lipgloss.Width(text) + 1
58 remainingWidth := width - length
59
60 if info != "" {
61 remainingWidth -= lipgloss.Width(info) + 1 // 1 for the space before info
62 }
63 lineStyle := t.S().Base.Foreground(t.Border)
64 if remainingWidth > 0 {
65 text = text + " " + lineStyle.Render(strings.Repeat(char, remainingWidth)) + " " + info
66 }
67 return text
68}
69
70func Title(title string, width int) string {
71 t := styles.CurrentTheme()
72 char := "╱"
73 length := lipgloss.Width(title) + 1
74 remainingWidth := width - length
75 titleStyle := t.S().Base.Foreground(t.Primary)
76 if remainingWidth > 0 {
77 lines := strings.Repeat(char, remainingWidth)
78 lines = styles.ApplyForegroundGrad(lines, t.Primary, t.Secondary)
79 title = titleStyle.Render(title) + " " + lines
80 }
81 return title
82}
83
84type StatusOpts struct {
85 Icon string
86 IconColor color.Color
87 NoIcon bool // If true, no icon will be displayed
88 Title string
89 TitleColor color.Color
90 Description string
91 DescriptionColor color.Color
92 ExtraContent string // Additional content to append after the description
93}
94
95func Status(ops StatusOpts, width int) string {
96 t := styles.CurrentTheme()
97 icon := "●"
98 iconColor := t.Success
99 if ops.Icon != "" {
100 icon = ops.Icon
101 } else if ops.NoIcon {
102 icon = ""
103 }
104 if ops.IconColor != nil {
105 iconColor = ops.IconColor
106 }
107 title := ops.Title
108 titleColor := t.FgMuted
109 if ops.TitleColor != nil {
110 titleColor = ops.TitleColor
111 }
112 description := ops.Description
113 descriptionColor := t.FgSubtle
114 if ops.DescriptionColor != nil {
115 descriptionColor = ops.DescriptionColor
116 }
117 title = t.S().Base.Foreground(titleColor).Render(title)
118 if description != "" {
119 extraContentWidth := lipgloss.Width(ops.ExtraContent)
120 if extraContentWidth > 0 {
121 extraContentWidth += 1
122 }
123 description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContentWidth, "…")
124 }
125 description = t.S().Base.Foreground(descriptionColor).Render(description)
126
127 content := []string{}
128 if icon != "" {
129 content = append(content, t.S().Base.Foreground(iconColor).Render(icon))
130 }
131 content = append(content, title, description)
132 if ops.ExtraContent != "" {
133 content = append(content, ops.ExtraContent)
134 }
135
136 return strings.Join(content, " ")
137}
138
139type ButtonOpts struct {
140 Text string
141 UnderlineIndex int // Index of character to underline (0-based)
142 Selected bool // Whether this button is selected
143}
144
145// SelectableButton creates a button with an underlined character and selection state
146func SelectableButton(opts ButtonOpts) string {
147 t := styles.CurrentTheme()
148
149 // Base style for the button
150 buttonStyle := t.S().Text
151
152 // Apply selection styling
153 if opts.Selected {
154 buttonStyle = buttonStyle.Foreground(t.White).Background(t.Secondary)
155 } else {
156 buttonStyle = buttonStyle.Background(t.BgSubtle)
157 }
158
159 // Create the button text with underlined character
160 text := opts.Text
161 if opts.UnderlineIndex >= 0 && opts.UnderlineIndex < len(text) {
162 before := text[:opts.UnderlineIndex]
163 underlined := text[opts.UnderlineIndex : opts.UnderlineIndex+1]
164 after := text[opts.UnderlineIndex+1:]
165
166 message := buttonStyle.Render(before) +
167 buttonStyle.Underline(true).Render(underlined) +
168 buttonStyle.Render(after)
169
170 return buttonStyle.Padding(0, 2).Render(message)
171 }
172
173 // Fallback if no underline index specified
174 return buttonStyle.Padding(0, 2).Render(text)
175}
176
177// SelectableButtons creates a horizontal row of selectable buttons
178func SelectableButtons(buttons []ButtonOpts, spacing string) string {
179 if spacing == "" {
180 spacing = " "
181 }
182
183 var parts []string
184 for i, button := range buttons {
185 parts = append(parts, SelectableButton(button))
186 if i < len(buttons)-1 {
187 parts = append(parts, spacing)
188 }
189 }
190
191 return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
192}
193
194// SelectableButtonsVertical creates a vertical row of selectable buttons
195func SelectableButtonsVertical(buttons []ButtonOpts, spacing int) string {
196 var parts []string
197 for i, button := range buttons {
198 parts = append(parts, SelectableButton(button))
199 if i < len(buttons)-1 {
200 for j := 0; j < spacing; j++ {
201 parts = append(parts, "")
202 }
203 }
204 }
205
206 return lipgloss.JoinVertical(lipgloss.Center, parts...)
207}
208
209func DiffFormatter() *diffview.DiffView {
210 t := styles.CurrentTheme()
211 formatDiff := diffview.New()
212 style := chroma.MustNewStyle("crush", styles.GetChromaTheme())
213 diff := formatDiff.ChromaStyle(style).Style(t.S().Diff).TabWidth(4)
214 return diff
215}