quit.go

  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}