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 // if empty no icon will be shown
86 Title string
87 TitleColor color.Color
88 Description string
89 DescriptionColor color.Color
90 ExtraContent string // additional content to append after the description
91}
92
93func Status(opts StatusOpts, width int) string {
94 t := styles.CurrentTheme()
95 icon := opts.Icon
96 title := opts.Title
97 titleColor := t.FgMuted
98 if opts.TitleColor != nil {
99 titleColor = opts.TitleColor
100 }
101 description := opts.Description
102 descriptionColor := t.FgSubtle
103 if opts.DescriptionColor != nil {
104 descriptionColor = opts.DescriptionColor
105 }
106 title = t.S().Base.Foreground(titleColor).Render(title)
107 if description != "" {
108 extraContentWidth := lipgloss.Width(opts.ExtraContent)
109 if extraContentWidth > 0 {
110 extraContentWidth += 1
111 }
112 description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContentWidth, "…")
113 description = t.S().Base.Foreground(descriptionColor).Render(description)
114 }
115
116 content := []string{}
117 if icon != "" {
118 content = append(content, icon)
119 }
120 content = append(content, title)
121 if description != "" {
122 content = append(content, description)
123 }
124 if opts.ExtraContent != "" {
125 content = append(content, opts.ExtraContent)
126 }
127
128 return strings.Join(content, " ")
129}
130
131type ButtonOpts struct {
132 Text string
133 UnderlineIndex int // Index of character to underline (0-based)
134 Selected bool // Whether this button is selected
135}
136
137// SelectableButton creates a button with an underlined character and selection state
138func SelectableButton(opts ButtonOpts) string {
139 t := styles.CurrentTheme()
140
141 // Base style for the button
142 buttonStyle := t.S().Text
143
144 // Apply selection styling
145 if opts.Selected {
146 buttonStyle = buttonStyle.Foreground(t.White).Background(t.Secondary)
147 } else {
148 buttonStyle = buttonStyle.Background(t.BgSubtle)
149 }
150
151 // Create the button text with underlined character
152 text := opts.Text
153 if opts.UnderlineIndex >= 0 && opts.UnderlineIndex < len(text) {
154 before := text[:opts.UnderlineIndex]
155 underlined := text[opts.UnderlineIndex : opts.UnderlineIndex+1]
156 after := text[opts.UnderlineIndex+1:]
157
158 message := buttonStyle.Render(before) +
159 buttonStyle.Underline(true).Render(underlined) +
160 buttonStyle.Render(after)
161
162 return buttonStyle.Padding(0, 2).Render(message)
163 }
164
165 // Fallback if no underline index specified
166 return buttonStyle.Padding(0, 2).Render(text)
167}
168
169// SelectableButtons creates a horizontal row of selectable buttons
170func SelectableButtons(buttons []ButtonOpts, spacing string) string {
171 if spacing == "" {
172 spacing = " "
173 }
174
175 var parts []string
176 for i, button := range buttons {
177 parts = append(parts, SelectableButton(button))
178 if i < len(buttons)-1 {
179 parts = append(parts, spacing)
180 }
181 }
182
183 return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
184}
185
186// SelectableButtonsVertical creates a vertical row of selectable buttons
187func SelectableButtonsVertical(buttons []ButtonOpts, spacing int) string {
188 var parts []string
189 for i, button := range buttons {
190 parts = append(parts, SelectableButton(button))
191 if i < len(buttons)-1 {
192 for range spacing {
193 parts = append(parts, "")
194 }
195 }
196 }
197
198 return lipgloss.JoinVertical(lipgloss.Center, parts...)
199}
200
201func DiffFormatter() *diffview.DiffView {
202 t := styles.CurrentTheme()
203 formatDiff := diffview.New()
204 style := chroma.MustNewStyle("crush", styles.GetChromaTheme())
205 diff := formatDiff.ChromaStyle(style).Style(t.S().Diff).TabWidth(4)
206 return diff
207}