1package dialog
2
3import (
4 "fmt"
5
6 "github.com/charmbracelet/bubbles/v2/key"
7 "github.com/charmbracelet/bubbles/v2/textinput"
8 tea "github.com/charmbracelet/bubbletea/v2"
9 "github.com/charmbracelet/lipgloss/v2"
10
11 "github.com/opencode-ai/opencode/internal/tui/styles"
12 "github.com/opencode-ai/opencode/internal/tui/theme"
13 "github.com/opencode-ai/opencode/internal/tui/util"
14)
15
16type argumentsDialogKeyMap struct {
17 Enter key.Binding
18 Escape key.Binding
19}
20
21// ShortHelp implements key.Map.
22func (k argumentsDialogKeyMap) ShortHelp() []key.Binding {
23 return []key.Binding{
24 key.NewBinding(
25 key.WithKeys("enter"),
26 key.WithHelp("enter", "confirm"),
27 ),
28 key.NewBinding(
29 key.WithKeys("esc"),
30 key.WithHelp("esc", "cancel"),
31 ),
32 }
33}
34
35// FullHelp implements key.Map.
36func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
37 return [][]key.Binding{k.ShortHelp()}
38}
39
40// ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
41type ShowMultiArgumentsDialogMsg struct {
42 CommandID string
43 Content string
44 ArgNames []string
45}
46
47// CloseMultiArgumentsDialogMsg is a message that is sent when the multi-arguments dialog is closed.
48type CloseMultiArgumentsDialogMsg struct {
49 Submit bool
50 CommandID string
51 Content string
52 Args map[string]string
53}
54
55// MultiArgumentsDialogCmp is a component that asks the user for multiple command arguments.
56type MultiArgumentsDialogCmp struct {
57 width, height int
58 inputs []textinput.Model
59 focusIndex int
60 keys argumentsDialogKeyMap
61 commandID string
62 content string
63 argNames []string
64}
65
66// NewMultiArgumentsDialogCmp creates a new MultiArgumentsDialogCmp.
67func NewMultiArgumentsDialogCmp(commandID, content string, argNames []string) MultiArgumentsDialogCmp {
68 t := theme.CurrentTheme()
69 inputs := make([]textinput.Model, len(argNames))
70
71 for i, name := range argNames {
72 ti := textinput.New()
73 ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
74 ti.SetWidth(40)
75 ti.Prompt = ""
76 ti.Styles.Focused.Placeholder = ti.Styles.Focused.Placeholder.Background(t.Background())
77 ti.Styles.Blurred.Placeholder = ti.Styles.Blurred.Placeholder.Background(t.Background())
78 ti.Styles.Focused.Suggestion = ti.Styles.Focused.Suggestion.Background(t.Background()).Foreground(t.Primary())
79 ti.Styles.Blurred.Suggestion = ti.Styles.Blurred.Suggestion.Background(t.Background())
80 ti.Styles.Focused.Text = ti.Styles.Focused.Text.Background(t.Background()).Foreground(t.Primary())
81 ti.Styles.Blurred.Text = ti.Styles.Blurred.Text.Background(t.Background())
82
83 // Only focus the first input initially
84 if i == 0 {
85 ti.Focus()
86 } else {
87 ti.Blur()
88 }
89
90 inputs[i] = ti
91 }
92
93 return MultiArgumentsDialogCmp{
94 inputs: inputs,
95 keys: argumentsDialogKeyMap{},
96 commandID: commandID,
97 content: content,
98 argNames: argNames,
99 focusIndex: 0,
100 }
101}
102
103// Init implements tea.Model.
104func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
105 // Make sure only the first input is focused
106 for i := range m.inputs {
107 if i == 0 {
108 m.inputs[i].Focus()
109 } else {
110 m.inputs[i].Blur()
111 }
112 }
113
114 return textinput.Blink
115}
116
117// Update implements tea.Model.
118func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
119 var cmds []tea.Cmd
120 switch msg := msg.(type) {
121 case tea.KeyMsg:
122 switch {
123 case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
124 return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
125 Submit: false,
126 CommandID: m.commandID,
127 Content: m.content,
128 Args: nil,
129 })
130 case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
131 // If we're on the last input, submit the form
132 if m.focusIndex == len(m.inputs)-1 {
133 args := make(map[string]string)
134 for i, name := range m.argNames {
135 args[name] = m.inputs[i].Value()
136 }
137 return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
138 Submit: true,
139 CommandID: m.commandID,
140 Content: m.content,
141 Args: args,
142 })
143 }
144 // Otherwise, move to the next input
145 m.inputs[m.focusIndex].Blur()
146 m.focusIndex++
147 m.inputs[m.focusIndex].Focus()
148 case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
149 // Move to the next input
150 m.inputs[m.focusIndex].Blur()
151 m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
152 m.inputs[m.focusIndex].Focus()
153 case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab"))):
154 // Move to the previous input
155 m.inputs[m.focusIndex].Blur()
156 m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs)
157 m.inputs[m.focusIndex].Focus()
158 }
159 case tea.WindowSizeMsg:
160 m.width = msg.Width
161 m.height = msg.Height
162 }
163
164 // Update the focused input
165 var cmd tea.Cmd
166 m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
167 cmds = append(cmds, cmd)
168
169 return m, tea.Batch(cmds...)
170}
171
172// View implements tea.Model.
173func (m MultiArgumentsDialogCmp) View() string {
174 t := theme.CurrentTheme()
175 baseStyle := styles.BaseStyle()
176
177 // Calculate width needed for content
178 maxWidth := 60 // Width for explanation text
179
180 title := lipgloss.NewStyle().
181 Foreground(t.Primary()).
182 Bold(true).
183 Width(maxWidth).
184 Padding(0, 1).
185 Background(t.Background()).
186 Render("Command Arguments")
187
188 explanation := lipgloss.NewStyle().
189 Foreground(t.Text()).
190 Width(maxWidth).
191 Padding(0, 1).
192 Background(t.Background()).
193 Render("This command requires multiple arguments. Please enter values for each:")
194
195 // Create input fields for each argument
196 inputFields := make([]string, len(m.inputs))
197 for i, input := range m.inputs {
198 // Highlight the label of the focused input
199 labelStyle := lipgloss.NewStyle().
200 Width(maxWidth).
201 Padding(1, 1, 0, 1).
202 Background(t.Background())
203
204 if i == m.focusIndex {
205 labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
206 } else {
207 labelStyle = labelStyle.Foreground(t.TextMuted())
208 }
209
210 label := labelStyle.Render(m.argNames[i] + ":")
211
212 field := lipgloss.NewStyle().
213 Foreground(t.Text()).
214 Width(maxWidth).
215 Padding(0, 1).
216 Background(t.Background()).
217 Render(input.View())
218
219 inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
220 }
221
222 maxWidth = min(maxWidth, m.width-10)
223
224 // Join all elements vertically
225 elements := []string{title, explanation}
226 elements = append(elements, inputFields...)
227
228 content := lipgloss.JoinVertical(
229 lipgloss.Left,
230 elements...,
231 )
232
233 return baseStyle.Padding(1, 2).
234 Border(lipgloss.RoundedBorder()).
235 BorderBackground(t.Background()).
236 BorderForeground(t.TextMuted()).
237 Background(t.Background()).
238 Width(lipgloss.Width(content) + 4).
239 Render(content)
240}
241
242// SetSize sets the size of the component.
243func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
244 m.width = width
245 m.height = height
246}
247
248// Bindings implements layout.Bindings.
249func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
250 return m.keys.ShortHelp()
251}