v2.10.3 f7e7684

confirm.go RawHistoryFolder

1package components
2
3import (
4	"fmt"
5	"strings"
6
7	"github.com/bnema/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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close