@@ -1,6 +1,7 @@
11package dialog
22
33import (
4+ "fmt"
45 "github.com/charmbracelet/bubbles/key"
56 "github.com/charmbracelet/bubbles/textinput"
67 tea "github.com/charmbracelet/bubbletea"
@@ -11,35 +12,6 @@ import (
1112 "github.com/opencode-ai/opencode/internal/tui/util"
1213)
1314
14−// ArgumentsDialogCmp is a component that asks the user for command arguments.
15−type ArgumentsDialogCmp struct {
16− width, height int
17− textInput textinput.Model
18− keys argumentsDialogKeyMap
19− commandID string
20− content string
21−}
22−
23−// NewArgumentsDialogCmp creates a new ArgumentsDialogCmp.
24−func NewArgumentsDialogCmp(commandID, content string) ArgumentsDialogCmp {
25− t := theme.CurrentTheme()
26− ti := textinput.New()
27− ti.Placeholder = "Enter arguments..."
28− ti.Focus()
29− ti.Width = 40
30− ti.Prompt = ""
31− ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
32− ti.PromptStyle = ti.PromptStyle.Background(t.Background())
33− ti.TextStyle = ti.TextStyle.Background(t.Background())
34−
35− return ArgumentsDialogCmp{
36− textInput: ti,
37− keys: argumentsDialogKeyMap{},
38− commandID: commandID,
39− content: content,
40− }
41−}
42−
4315type argumentsDialogKeyMap struct {
4416 Enter key.Binding
4517 Escape key.Binding
@@ -64,77 +36,204 @@ func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
6436 return [][]key.Binding{k.ShortHelp()}
6537}
6638
39+// ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
40+type 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.
47+type 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.
55+type 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.
66+func 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+
67101// Init implements tea.Model.
68−func (m ArgumentsDialogCmp) Init() tea.Cmd {
69− return tea.Batch(
70− textinput.Blink,
71− m.textInput.Focus(),
72− )
102+func (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
73113}
74114
75115// Update implements tea.Model.
76−func (m ArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
77− var cmd tea.Cmd
116+func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
78117 var cmds []tea.Cmd
118+ t := theme.CurrentTheme()
79119
80120 switch msg := msg.(type) {
81121 case tea.KeyMsg:
82122 switch {
83123 case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
84− return m, util.CmdHandler(CloseArgumentsDialogMsg{})
85− case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
86− return m, util.CmdHandler(CloseArgumentsDialogMsg{
87− Submit: true,
124+ return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
125+ Submit: false,
88126 CommandID: m.commandID,
89127 Content: m.content,
90− Arguments: m.textInput.Value(),
128+ Args: nil,
91129 })
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())
92164 }
93165 case tea.WindowSizeMsg:
94166 m.width = msg.Width
95167 m.height = msg.Height
96168 }
97169
98− m.textInput, cmd = m.textInput.Update(msg)
170+ // Update the focused input
171+ var cmd tea.Cmd
172+ m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
99173 cmds = append(cmds, cmd)
100174
101175 return m, tea.Batch(cmds...)
102176}
103177
104178// View implements tea.Model.
105−func (m ArgumentsDialogCmp) View() string {
179+func (m MultiArgumentsDialogCmp) View() string {
106180 t := theme.CurrentTheme()
107181 baseStyle := styles.BaseStyle()
108182
109183 // Calculate width needed for content
110184 maxWidth := 60 // Width for explanation text
111185
112− title := baseStyle.
186+ title := lipgloss.NewStyle().
113187 Foreground(t.Primary()).
114188 Bold(true).
115189 Width(maxWidth).
116190 Padding(0, 1).
191+ Background(t.Background()).
117192 Render("Command Arguments")
118193
119− explanation := baseStyle.
194+ explanation := lipgloss.NewStyle().
120195 Foreground(t.Text()).
121196 Width(maxWidth).
122197 Padding(0, 1).
123− Render("This command requires arguments. Please enter the text to replace $ARGUMENTS with:")
198+ Background(t.Background()).
199+ Render("This command requires multiple arguments. Please enter values for each:")
124200
125− inputField := baseStyle.
126− Foreground(t.Text()).
127− Width(maxWidth).
128− Padding(1, 1).
129− Render(m.textInput.View())
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+ }
130227
131228 maxWidth = min(maxWidth, m.width-10)
132229
230+ // Join all elements vertically
231+ elements := []string{title, explanation}
232+ elements = append(elements, inputFields...)
233+
133234 content := lipgloss.JoinVertical(
134235 lipgloss.Left,
135− title,
136− explanation,
137− inputField,
236+ elements...,
138237 )
139238
140239 return baseStyle.Padding(1, 2).
@@ -147,27 +246,12 @@ func (m ArgumentsDialogCmp) View() string {
147246}
148247
149248// SetSize sets the size of the component.
150−func (m *ArgumentsDialogCmp) SetSize(width, height int) {
249+func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
151250 m.width = width
152251 m.height = height
153252}
154253
155254// Bindings implements layout.Bindings.
156−func (m ArgumentsDialogCmp) Bindings() []key.Binding {
255+func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
157256 return m.keys.ShortHelp()
158−}
159−
160−// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
161−type CloseArgumentsDialogMsg struct {
162− Submit bool
163− CommandID string
164− Content string
165− Arguments string
166−}
167−
168−// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
169−type ShowArgumentsDialogMsg struct {
170− CommandID string
171− Content string
172−}
173−
257+}