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