1package components
2
3import (
4 "fmt"
5 "strings"
6
7 "gordon/internal/adapters/in/cli/ui/styles"
8
9 tea "github.com/charmbracelet/bubbletea"
10 "github.com/charmbracelet/lipgloss"
11)
12
13// ConfirmResult represents the result of a confirmation dialog.
14type ConfirmResult int
15
16const (
17 ConfirmPending ConfirmResult = iota
18 ConfirmYes
19 ConfirmNo
20 ConfirmCancelled
21)
22
23// ConfirmModel is a Yes/No confirmation dialog.
24type ConfirmModel struct {
25 question string
26 description string
27 yesLabel string
28 noLabel string
29 focused bool // true = Yes is focused, false = No is focused
30 result ConfirmResult
31 width int
32
33 // Styles
34 questionStyle lipgloss.Style
35 descriptionStyle lipgloss.Style
36 buttonStyle lipgloss.Style
37 focusedStyle lipgloss.Style
38}
39
40// ConfirmOption configures a ConfirmModel.
41type ConfirmOption func(*ConfirmModel)
42
43// NewConfirm creates a new confirmation dialog.
44func NewConfirm(question string, opts ...ConfirmOption) ConfirmModel {
45 m := ConfirmModel{
46 question: question,
47 yesLabel: "Yes",
48 noLabel: "No",
49 focused: false, // Default to No
50 result: ConfirmPending,
51 width: 50,
52 questionStyle: styles.Theme.Bold,
53 descriptionStyle: styles.Theme.Muted,
54 buttonStyle: lipgloss.NewStyle().
55 Padding(0, 2).
56 Foreground(styles.ColorText),
57 focusedStyle: lipgloss.NewStyle().
58 Padding(0, 2).
59 Bold(true).
60 Foreground(styles.ColorBg).
61 Background(styles.ColorPrimary),
62 }
63
64 for _, opt := range opts {
65 opt(&m)
66 }
67
68 return m
69}
70
71// WithDescription adds a description to the confirmation.
72func WithDescription(desc string) ConfirmOption {
73 return func(m *ConfirmModel) {
74 m.description = desc
75 }
76}
77
78// WithDefaultYes sets Yes as the default selection.
79func WithDefaultYes() ConfirmOption {
80 return func(m *ConfirmModel) {
81 m.focused = true
82 }
83}
84
85// WithLabels customizes the Yes/No labels.
86func WithLabels(yes, no string) ConfirmOption {
87 return func(m *ConfirmModel) {
88 m.yesLabel = yes
89 m.noLabel = no
90 }
91}
92
93// WithWidth sets the dialog width.
94func WithWidth(w int) ConfirmOption {
95 return func(m *ConfirmModel) {
96 m.width = w
97 }
98}
99
100// Init implements tea.Model.
101func (m ConfirmModel) Init() tea.Cmd {
102 return nil
103}
104
105// Update implements tea.Model.
106func (m ConfirmModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107 switch msg := msg.(type) {
108 case tea.KeyMsg:
109 switch msg.String() {
110 case "left", "h", "tab", "shift+tab":
111 m.focused = !m.focused
112 case "right", "l":
113 m.focused = !m.focused
114 case "y", "Y":
115 m.result = ConfirmYes
116 return m, tea.Quit
117 case "n", "N":
118 m.result = ConfirmNo
119 return m, tea.Quit
120 case "enter":
121 if m.focused {
122 m.result = ConfirmYes
123 } else {
124 m.result = ConfirmNo
125 }
126 return m, tea.Quit
127 case "esc", "ctrl+c", "q":
128 m.result = ConfirmCancelled
129 return m, tea.Quit
130 }
131 }
132 return m, nil
133}
134
135// View implements tea.Model.
136func (m ConfirmModel) View() string {
137 var b strings.Builder
138
139 // Question
140 b.WriteString(m.questionStyle.Render(m.question))
141 b.WriteString("\n")
142
143 // Description
144 if m.description != "" {
145 b.WriteString(m.descriptionStyle.Render(m.description))
146 b.WriteString("\n")
147 }
148 b.WriteString("\n")
149
150 // Buttons
151 var yesButton, noButton string
152 if m.focused {
153 yesButton = m.focusedStyle.Render(m.yesLabel)
154 noButton = m.buttonStyle.Render(m.noLabel)
155 } else {
156 yesButton = m.buttonStyle.Render(m.yesLabel)
157 noButton = m.focusedStyle.Render(m.noLabel)
158 }
159
160 buttons := lipgloss.JoinHorizontal(lipgloss.Center, yesButton, " ", noButton)
161 b.WriteString(buttons)
162 b.WriteString("\n\n")
163
164 // Help
165 help := styles.RenderKeyHelp("y/n", "select") + " " +
166 styles.RenderKeyHelp("enter", "confirm") + " " +
167 styles.RenderKeyHelp("esc", "cancel")
168 b.WriteString(help)
169
170 return b.String()
171}
172
173// Result returns the confirmation result.
174func (m ConfirmModel) Result() ConfirmResult {
175 return m.result
176}
177
178// Confirmed returns true if the user confirmed.
179func (m ConfirmModel) Confirmed() bool {
180 return m.result == ConfirmYes
181}
182
183// Cancelled returns true if the user cancelled.
184func (m ConfirmModel) Cancelled() bool {
185 return m.result == ConfirmCancelled
186}
187
188// RunConfirm runs a confirmation dialog and returns the result.
189// This is a convenience function for simple confirmations.
190func RunConfirm(question string, opts ...ConfirmOption) (bool, error) {
191 m := NewConfirm(question, opts...)
192 p := tea.NewProgram(m)
193 finalModel, err := p.Run()
194 if err != nil {
195 return false, fmt.Errorf("error running confirmation: %w", err)
196 }
197 result := finalModel.(ConfirmModel)
198 if result.Cancelled() {
199 return false, nil
200 }
201 return result.Confirmed(), nil
202}