1package quit
2
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
6 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
8 "github.com/opencode-ai/opencode/internal/tui/layout"
9 "github.com/opencode-ai/opencode/internal/tui/styles"
10 "github.com/opencode-ai/opencode/internal/tui/theme"
11 "github.com/opencode-ai/opencode/internal/tui/util"
12)
13
14const (
15 question = "Are you sure you want to quit?"
16 id dialogs.DialogID = "quit"
17)
18
19// QuitDialog represents a confirmation dialog for quitting the application.
20type QuitDialog interface {
21 dialogs.DialogModel
22 layout.Bindings
23}
24
25type quitDialogCmp struct {
26 wWidth int
27 wHeight int
28
29 selectedNo bool // true if "No" button is selected
30 keymap KeyMap
31}
32
33// NewQuitDialog creates a new quit confirmation dialog.
34func NewQuitDialog() QuitDialog {
35 return &quitDialogCmp{
36 selectedNo: true, // Default to "No" for safety
37 keymap: DefaultKeymap(),
38 }
39}
40
41func (q *quitDialogCmp) Init() tea.Cmd {
42 return nil
43}
44
45// Update handles keyboard input for the quit dialog.
46func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
47 switch msg := msg.(type) {
48 case tea.WindowSizeMsg:
49 q.wWidth = msg.Width
50 q.wHeight = msg.Height
51 case tea.KeyPressMsg:
52 switch {
53 case key.Matches(msg, q.keymap.LeftRight) || key.Matches(msg, q.keymap.Tab):
54 q.selectedNo = !q.selectedNo
55 return q, nil
56 case key.Matches(msg, q.keymap.EnterSpace):
57 if !q.selectedNo {
58 return q, tea.Quit
59 }
60 return q, util.CmdHandler(dialogs.CloseDialogMsg{})
61 case key.Matches(msg, q.keymap.Yes):
62 return q, tea.Quit
63 case key.Matches(msg, q.keymap.No):
64 return q, util.CmdHandler(dialogs.CloseDialogMsg{})
65 }
66 }
67 return q, nil
68}
69
70// View renders the quit dialog with Yes/No buttons.
71func (q *quitDialogCmp) View() tea.View {
72 t := theme.CurrentTheme()
73 baseStyle := styles.BaseStyle()
74
75 yesStyle := baseStyle
76 noStyle := baseStyle
77 spacerStyle := baseStyle.Background(t.Background())
78
79 if q.selectedNo {
80 noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
81 yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
82 } else {
83 yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
84 noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
85 }
86
87 yesButton := yesStyle.Padding(0, 1).Render("Yes")
88 noButton := noStyle.Padding(0, 1).Render("No")
89
90 buttons := baseStyle.Width(lipgloss.Width(question)).Align(lipgloss.Right).Render(
91 lipgloss.JoinHorizontal(lipgloss.Center, yesButton, spacerStyle.Render(" "), noButton),
92 )
93
94 content := baseStyle.Render(
95 lipgloss.JoinVertical(
96 lipgloss.Center,
97 question,
98 "",
99 buttons,
100 ),
101 )
102
103 quitDialogStyle := baseStyle.
104 Padding(1, 2).
105 Border(lipgloss.RoundedBorder()).
106 BorderBackground(t.Background()).
107 BorderForeground(t.TextMuted())
108
109 return tea.NewView(
110 quitDialogStyle.Render(content),
111 )
112}
113
114func (q *quitDialogCmp) BindingKeys() []key.Binding {
115 return layout.KeyMapToSlice(q.keymap)
116}
117
118func (q *quitDialogCmp) Position() (int, int) {
119 row := q.wHeight / 2
120 row -= 7 / 2
121 col := q.wWidth / 2
122 col -= (lipgloss.Width(question) + 4) / 2
123
124 return row, col
125}
126
127func (q *quitDialogCmp) ID() dialogs.DialogID {
128 return id
129}