1package dialog
2
3import (
4 "strings"
5
6 "github.com/charmbracelet/bubbles/key"
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
9 "github.com/opencode-ai/opencode/internal/tui/layout"
10 "github.com/opencode-ai/opencode/internal/tui/styles"
11 "github.com/opencode-ai/opencode/internal/tui/theme"
12 "github.com/opencode-ai/opencode/internal/tui/util"
13)
14
15const question = "Are you sure you want to quit?"
16
17type CloseQuitMsg struct{}
18
19type QuitDialog interface {
20 tea.Model
21 layout.Bindings
22}
23
24type quitDialogCmp struct {
25 selectedNo bool
26}
27
28type helpMapping struct {
29 LeftRight key.Binding
30 EnterSpace key.Binding
31 Yes key.Binding
32 No key.Binding
33 Tab key.Binding
34}
35
36var helpKeys = helpMapping{
37 LeftRight: key.NewBinding(
38 key.WithKeys("left", "right"),
39 key.WithHelp("←/→", "switch options"),
40 ),
41 EnterSpace: key.NewBinding(
42 key.WithKeys("enter", " "),
43 key.WithHelp("enter/space", "confirm"),
44 ),
45 Yes: key.NewBinding(
46 key.WithKeys("y", "Y"),
47 key.WithHelp("y/Y", "yes"),
48 ),
49 No: key.NewBinding(
50 key.WithKeys("n", "N"),
51 key.WithHelp("n/N", "no"),
52 ),
53 Tab: key.NewBinding(
54 key.WithKeys("tab"),
55 key.WithHelp("tab", "switch options"),
56 ),
57}
58
59func (q *quitDialogCmp) Init() tea.Cmd {
60 return nil
61}
62
63func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
64 switch msg := msg.(type) {
65 case tea.KeyMsg:
66 switch {
67 case key.Matches(msg, helpKeys.LeftRight) || key.Matches(msg, helpKeys.Tab):
68 q.selectedNo = !q.selectedNo
69 return q, nil
70 case key.Matches(msg, helpKeys.EnterSpace):
71 if !q.selectedNo {
72 return q, tea.Quit
73 }
74 return q, util.CmdHandler(CloseQuitMsg{})
75 case key.Matches(msg, helpKeys.Yes):
76 return q, tea.Quit
77 case key.Matches(msg, helpKeys.No):
78 return q, util.CmdHandler(CloseQuitMsg{})
79 }
80 }
81 return q, nil
82}
83
84func (q *quitDialogCmp) View() string {
85 t := theme.CurrentTheme()
86 baseStyle := styles.BaseStyle()
87
88 yesStyle := baseStyle
89 noStyle := baseStyle
90 spacerStyle := baseStyle.Background(t.Background())
91
92 if q.selectedNo {
93 noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
94 yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
95 } else {
96 yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
97 noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
98 }
99
100 yesButton := yesStyle.Padding(0, 1).Render("Yes")
101 noButton := noStyle.Padding(0, 1).Render("No")
102
103 buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, spacerStyle.Render(" "), noButton)
104
105 width := lipgloss.Width(question)
106 remainingWidth := width - lipgloss.Width(buttons)
107 if remainingWidth > 0 {
108 buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons
109 }
110
111 content := baseStyle.Render(
112 lipgloss.JoinVertical(
113 lipgloss.Center,
114 question,
115 "",
116 buttons,
117 ),
118 )
119
120 return baseStyle.Padding(1, 2).
121 Border(lipgloss.RoundedBorder()).
122 BorderBackground(t.Background()).
123 BorderForeground(t.TextMuted()).
124 Width(lipgloss.Width(content) + 4).
125 Render(content)
126}
127
128func (q *quitDialogCmp) BindingKeys() []key.Binding {
129 return layout.KeyMapToSlice(helpKeys)
130}
131
132func NewQuitCmp() QuitDialog {
133 return &quitDialogCmp{
134 selectedNo: true,
135 }
136}