quit.go

  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}