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 styles := ti.Styles()
77 styles.Focused.Placeholder = styles.Focused.Placeholder.Background(t.Background())
78 styles.Blurred.Placeholder = styles.Blurred.Placeholder.Background(t.Background())
79 styles.Focused.Suggestion = styles.Focused.Suggestion.Background(t.Background()).Foreground(t.Primary())
80 styles.Blurred.Suggestion = styles.Blurred.Suggestion.Background(t.Background())
81 styles.Focused.Text = styles.Focused.Text.Background(t.Background()).Foreground(t.Primary())
82 styles.Blurred.Text = styles.Blurred.Text.Background(t.Background())
83
84 // Only focus the first input initially
85 if i == 0 {
86 ti.Focus()
87 } else {
88 ti.Blur()
89 }
90
91 inputs[i] = ti
92 }
93
94 return MultiArgumentsDialogCmp{
95 inputs: inputs,
96 keys: argumentsDialogKeyMap{},
97 commandID: commandID,
98 content: content,
99 argNames: argNames,
100 focusIndex: 0,
101 }
102}
103
104// Init implements tea.Model.
105func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
106 // Make sure only the first input is focused
107 for i := range m.inputs {
108 if i == 0 {
109 m.inputs[i].Focus()
110 } else {
111 m.inputs[i].Blur()
112 }
113 }
114
115 return textinput.Blink
116}
117
118// Update implements tea.Model.
119func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
120 var cmds []tea.Cmd
121 switch msg := msg.(type) {
122 case tea.KeyPressMsg:
123 switch {
124 case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
125 return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
126 Submit: false,
127 CommandID: m.commandID,
128 Content: m.content,
129 Args: nil,
130 })
131 case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
132 // If we're on the last input, submit the form
133 if m.focusIndex == len(m.inputs)-1 {
134 args := make(map[string]string)
135 for i, name := range m.argNames {
136 args[name] = m.inputs[i].Value()
137 }
138 return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
139 Submit: true,
140 CommandID: m.commandID,
141 Content: m.content,
142 Args: args,
143 })
144 }
145 // Otherwise, move to the next input
146 m.inputs[m.focusIndex].Blur()
147 m.focusIndex++
148 m.inputs[m.focusIndex].Focus()
149 case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
150 // Move to the next input
151 m.inputs[m.focusIndex].Blur()
152 m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
153 m.inputs[m.focusIndex].Focus()
154 case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab"))):
155 // Move to the previous input
156 m.inputs[m.focusIndex].Blur()
157 m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs)
158 m.inputs[m.focusIndex].Focus()
159 }
160 case tea.WindowSizeMsg:
161 m.width = msg.Width
162 m.height = msg.Height
163 }
164
165 // Update the focused input
166 var cmd tea.Cmd
167 m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
168 cmds = append(cmds, cmd)
169
170 return m, tea.Batch(cmds...)
171}
172
173// View implements tea.Model.
174func (m MultiArgumentsDialogCmp) View() string {
175 t := theme.CurrentTheme()
176 baseStyle := styles.BaseStyle()
177
178 // Calculate width needed for content
179 maxWidth := 60 // Width for explanation text
180
181 title := lipgloss.NewStyle().
182 Foreground(t.Primary()).
183 Bold(true).
184 Width(maxWidth).
185 Padding(0, 1).
186 Background(t.Background()).
187 Render("Command Arguments")
188
189 explanation := lipgloss.NewStyle().
190 Foreground(t.Text()).
191 Width(maxWidth).
192 Padding(0, 1).
193 Background(t.Background()).
194 Render("This command requires multiple arguments. Please enter values for each:")
195
196 // Create input fields for each argument
197 inputFields := make([]string, len(m.inputs))
198 for i, input := range m.inputs {
199 // Highlight the label of the focused input
200 labelStyle := lipgloss.NewStyle().
201 Width(maxWidth).
202 Padding(1, 1, 0, 1).
203 Background(t.Background())
204
205 if i == m.focusIndex {
206 labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
207 } else {
208 labelStyle = labelStyle.Foreground(t.TextMuted())
209 }
210
211 label := labelStyle.Render(m.argNames[i] + ":")
212
213 field := lipgloss.NewStyle().
214 Foreground(t.Text()).
215 Width(maxWidth).
216 Padding(0, 1).
217 Background(t.Background()).
218 Render(input.View())
219
220 inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
221 }
222
223 maxWidth = min(maxWidth, m.width-10)
224
225 // Join all elements vertically
226 elements := []string{title, explanation}
227 elements = append(elements, inputFields...)
228
229 content := lipgloss.JoinVertical(
230 lipgloss.Left,
231 elements...,
232 )
233
234 return baseStyle.Padding(1, 2).
235 Border(lipgloss.RoundedBorder()).
236 BorderBackground(t.Background()).
237 BorderForeground(t.TextMuted()).
238 Background(t.Background()).
239 Width(lipgloss.Width(content) + 4).
240 Render(content)
241}
242
243// SetSize sets the size of the component.
244func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
245 m.width = width
246 m.height = height
247}
248
249// Bindings implements layout.Bindings.
250func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
251 return m.keys.ShortHelp()
252}