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}