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