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