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