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 Title string
44 TitleColor color.Color
45 Description string
46 DescriptionColor color.Color
47}
48
49func Status(ops StatusOpts, width int) string {
50 t := styles.CurrentTheme()
51 icon := "●"
52 iconColor := t.Success
53 if ops.Icon != "" {
54 icon = ops.Icon
55 }
56 if ops.IconColor != nil {
57 iconColor = ops.IconColor
58 }
59 title := ops.Title
60 titleColor := t.FgMuted
61 if ops.TitleColor != nil {
62 titleColor = ops.TitleColor
63 }
64 description := ops.Description
65 descriptionColor := t.FgSubtle
66 if ops.DescriptionColor != nil {
67 descriptionColor = ops.DescriptionColor
68 }
69 icon = t.S().Base.Foreground(iconColor).Render(icon)
70 title = t.S().Base.Foreground(titleColor).Render(title)
71 if description != "" {
72 description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2, "…")
73 }
74 description = t.S().Base.Foreground(descriptionColor).Render(description)
75 return strings.Join([]string{
76 icon,
77 title,
78 description,
79 }, " ")
80}
81
82type ButtonOpts struct {
83 Text string
84 UnderlineIndex int // Index of character to underline (0-based)
85 Selected bool // Whether this button is selected
86}
87
88// SelectableButton creates a button with an underlined character and selection state
89func SelectableButton(opts ButtonOpts) string {
90 t := styles.CurrentTheme()
91
92 // Base style for the button
93 buttonStyle := t.S().Text
94
95 // Apply selection styling
96 if opts.Selected {
97 buttonStyle = buttonStyle.Foreground(t.White).Background(t.Secondary)
98 } else {
99 buttonStyle = buttonStyle.Background(t.BgSubtle)
100 }
101
102 // Create the button text with underlined character
103 text := opts.Text
104 if opts.UnderlineIndex >= 0 && opts.UnderlineIndex < len(text) {
105 before := text[:opts.UnderlineIndex]
106 underlined := text[opts.UnderlineIndex : opts.UnderlineIndex+1]
107 after := text[opts.UnderlineIndex+1:]
108
109 message := buttonStyle.Render(before) +
110 buttonStyle.Underline(true).Render(underlined) +
111 buttonStyle.Render(after)
112
113 return buttonStyle.Padding(0, 2).Render(message)
114 }
115
116 // Fallback if no underline index specified
117 return buttonStyle.Padding(0, 2).Render(text)
118}
119
120// SelectableButtons creates a horizontal row of selectable buttons
121func SelectableButtons(buttons []ButtonOpts, spacing string) string {
122 if spacing == "" {
123 spacing = " "
124 }
125
126 var parts []string
127 for i, button := range buttons {
128 parts = append(parts, SelectableButton(button))
129 if i < len(buttons)-1 {
130 parts = append(parts, spacing)
131 }
132 }
133
134 return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
135}
136
137func DiffFormatter() *diffview.DiffView {
138 formatDiff := diffview.New()
139 style := chroma.MustNewStyle("crush", styles.GetChromaTheme())
140 diff := formatDiff.
141 SyntaxHightlight(true).
142 ChromaStyle(style)
143 return diff
144}