1package dialog
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
7
8 "github.com/opencode-ai/opencode/internal/tui/styles"
9 "github.com/opencode-ai/opencode/internal/tui/theme"
10 "github.com/opencode-ai/opencode/internal/tui/util"
11)
12
13// InitDialogCmp is a component that asks the user if they want to initialize the project.
14type InitDialogCmp struct {
15 width, height int
16 selected int
17 keys initDialogKeyMap
18}
19
20// NewInitDialogCmp creates a new InitDialogCmp.
21func NewInitDialogCmp() InitDialogCmp {
22 return InitDialogCmp{
23 selected: 0,
24 keys: initDialogKeyMap{},
25 }
26}
27
28type initDialogKeyMap struct {
29 Tab key.Binding
30 Left key.Binding
31 Right key.Binding
32 Enter key.Binding
33 Escape key.Binding
34 Y key.Binding
35 N key.Binding
36}
37
38// ShortHelp implements key.Map.
39func (k initDialogKeyMap) ShortHelp() []key.Binding {
40 return []key.Binding{
41 key.NewBinding(
42 key.WithKeys("tab", "left", "right"),
43 key.WithHelp("tab/←/→", "toggle selection"),
44 ),
45 key.NewBinding(
46 key.WithKeys("enter"),
47 key.WithHelp("enter", "confirm"),
48 ),
49 key.NewBinding(
50 key.WithKeys("esc", "q"),
51 key.WithHelp("esc/q", "cancel"),
52 ),
53 key.NewBinding(
54 key.WithKeys("y", "n"),
55 key.WithHelp("y/n", "yes/no"),
56 ),
57 }
58}
59
60// FullHelp implements key.Map.
61func (k initDialogKeyMap) FullHelp() [][]key.Binding {
62 return [][]key.Binding{k.ShortHelp()}
63}
64
65// Init implements tea.Model.
66func (m InitDialogCmp) Init() tea.Cmd {
67 return nil
68}
69
70// Update implements tea.Model.
71func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
72 switch msg := msg.(type) {
73 case tea.KeyMsg:
74 switch {
75 case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
76 return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
77 case key.Matches(msg, key.NewBinding(key.WithKeys("tab", "left", "right", "h", "l"))):
78 m.selected = (m.selected + 1) % 2
79 return m, nil
80 case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
81 return m, util.CmdHandler(CloseInitDialogMsg{Initialize: m.selected == 0})
82 case key.Matches(msg, key.NewBinding(key.WithKeys("y"))):
83 return m, util.CmdHandler(CloseInitDialogMsg{Initialize: true})
84 case key.Matches(msg, key.NewBinding(key.WithKeys("n"))):
85 return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
86 }
87 case tea.WindowSizeMsg:
88 m.width = msg.Width
89 m.height = msg.Height
90 }
91 return m, nil
92}
93
94// View implements tea.Model.
95func (m InitDialogCmp) View() string {
96 t := theme.CurrentTheme()
97 baseStyle := styles.BaseStyle()
98
99 // Calculate width needed for content
100 maxWidth := 60 // Width for explanation text
101
102 title := baseStyle.
103 Foreground(t.Primary()).
104 Bold(true).
105 Width(maxWidth).
106 Padding(0, 1).
107 Render("Initialize Project")
108
109 explanation := baseStyle.
110 Foreground(t.Text()).
111 Width(maxWidth).
112 Padding(0, 1).
113 Render("Initialization generates a new OpenCode.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
114
115 question := baseStyle.
116 Foreground(t.Text()).
117 Width(maxWidth).
118 Padding(1, 1).
119 Render("Would you like to initialize this project?")
120
121 maxWidth = min(maxWidth, m.width-10)
122 yesStyle := baseStyle
123 noStyle := baseStyle
124
125 if m.selected == 0 {
126 yesStyle = yesStyle.
127 Background(t.Primary()).
128 Foreground(t.Background()).
129 Bold(true)
130 noStyle = noStyle.
131 Background(t.Background()).
132 Foreground(t.Primary())
133 } else {
134 noStyle = noStyle.
135 Background(t.Primary()).
136 Foreground(t.Background()).
137 Bold(true)
138 yesStyle = yesStyle.
139 Background(t.Background()).
140 Foreground(t.Primary())
141 }
142
143 yes := yesStyle.Padding(0, 3).Render("Yes")
144 no := noStyle.Padding(0, 3).Render("No")
145
146 buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no)
147 buttons = baseStyle.
148 Width(maxWidth).
149 Padding(1, 0).
150 Render(buttons)
151
152 content := lipgloss.JoinVertical(
153 lipgloss.Left,
154 title,
155 baseStyle.Width(maxWidth).Render(""),
156 explanation,
157 question,
158 buttons,
159 baseStyle.Width(maxWidth).Render(""),
160 )
161
162 return baseStyle.Padding(1, 2).
163 Border(lipgloss.RoundedBorder()).
164 BorderBackground(t.Background()).
165 BorderForeground(t.TextMuted()).
166 Width(lipgloss.Width(content) + 4).
167 Render(content)
168}
169
170// SetSize sets the size of the component.
171func (m *InitDialogCmp) SetSize(width, height int) {
172 m.width = width
173 m.height = height
174}
175
176// Bindings implements layout.Bindings.
177func (m InitDialogCmp) Bindings() []key.Binding {
178 return m.keys.ShortHelp()
179}
180
181// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
182type CloseInitDialogMsg struct {
183 Initialize bool
184}
185
186// ShowInitDialogMsg is a message that is sent to show the init dialog.
187type ShowInitDialogMsg struct {
188 Show bool
189}