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