@@ -1,6 +1,7 @@
1package dialog
1package dialog
4 "github.com/charmbracelet/bubbles/key"
5 "github.com/charmbracelet/bubbles/key"
5 "github.com/charmbracelet/bubbles/textinput"
6 "github.com/charmbracelet/bubbles/textinput"
6 tea "github.com/charmbracelet/bubbletea"
7 tea "github.com/charmbracelet/bubbletea"
@@ -11,35 +12,6 @@ import (
11 "github.com/opencode-ai/opencode/internal/tui/util"
12 "github.com/opencode-ai/opencode/internal/tui/util"
14−// ArgumentsDialogCmp is a component that asks the user for command arguments.
15−type ArgumentsDialogCmp struct {
17− textInput textinput.Model
18− keys argumentsDialogKeyMap
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..."
31− ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
32− ti.PromptStyle = ti.PromptStyle.Background(t.Background())
33− ti.TextStyle = ti.TextStyle.Background(t.Background())
35− return ArgumentsDialogCmp{
37− keys: argumentsDialogKeyMap{},
38− commandID: commandID,
43type argumentsDialogKeyMap struct {
15type argumentsDialogKeyMap struct {
44 Enter key.Binding
16 Enter key.Binding
45 Escape key.Binding
17 Escape key.Binding
@@ -64,77 +36,204 @@ func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
64 return [][]key.Binding{k.ShortHelp()}
36 return [][]key.Binding{k.ShortHelp()}
39+// ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
40+type ShowMultiArgumentsDialogMsg struct {
46+// CloseMultiArgumentsDialogMsg is a message that is sent when the multi-arguments dialog is closed.
47+type CloseMultiArgumentsDialogMsg struct {
51+ Args map[string]string
54+// MultiArgumentsDialogCmp is a component that asks the user for multiple command arguments.
55+type MultiArgumentsDialogCmp struct {
57+ inputs []textinput.Model
59+ keys argumentsDialogKeyMap
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))
70+ for i, name := range argNames {
71+ ti := textinput.New()
72+ ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
75+ ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
76+ ti.PromptStyle = ti.PromptStyle.Background(t.Background())
77+ ti.TextStyle = ti.TextStyle.Background(t.Background())
79+ // Only focus the first input initially
82+ ti.PromptStyle = ti.PromptStyle.Foreground(t.Primary())
83+ ti.TextStyle = ti.TextStyle.Foreground(t.Primary())
91+ return MultiArgumentsDialogCmp{
93+ keys: argumentsDialogKeyMap{},
94+ commandID: commandID,
67// Init implements tea.Model.
101// Init implements tea.Model.
68−func (m ArgumentsDialogCmp) Init() tea.Cmd {
102+func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
69− return tea.Batch(
103+ // Make sure only the first input is focused
104+ for i := range m.inputs {
70− textinput.Blink,
112+ return textinput.Blink
75// Update implements tea.Model.
115// Update implements tea.Model.
76−func (m ArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
116+func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
78 var cmds []tea.Cmd
117 var cmds []tea.Cmd
118+ t := theme.CurrentTheme()
80 switch msg := msg.(type) {
120 switch msg := msg.(type) {
81 case tea.KeyMsg:
121 case tea.KeyMsg:
83 case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
123 case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
84− return m, util.CmdHandler(CloseArgumentsDialogMsg{})
124+ return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
85− case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
86− return m, util.CmdHandler(CloseArgumentsDialogMsg{
87− Submit: true,
125+ Submit: false,
88 CommandID: m.commandID,
126 CommandID: m.commandID,
89 Content: m.content,
127 Content: m.content,
90− Arguments: m.textInput.Value(),
128+ Args: nil,
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()
137+ return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
139+ CommandID: m.commandID,
144+ // Otherwise, move to the next input
145+ m.inputs[m.focusIndex].Blur()
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())
93 case tea.WindowSizeMsg:
165 case tea.WindowSizeMsg:
94 m.width = msg.Width
166 m.width = msg.Width
95 m.height = msg.Height
167 m.height = msg.Height
98− m.textInput, cmd = m.textInput.Update(msg)
170+ // Update the focused input
172+ m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
99 cmds = append(cmds, cmd)
173 cmds = append(cmds, cmd)
101 return m, tea.Batch(cmds...)
175 return m, tea.Batch(cmds...)
104// View implements tea.Model.
178// View implements tea.Model.
105−func (m ArgumentsDialogCmp) View() string {
179+func (m MultiArgumentsDialogCmp) View() string {
106 t := theme.CurrentTheme()
180 t := theme.CurrentTheme()
107 baseStyle := styles.BaseStyle()
181 baseStyle := styles.BaseStyle()
109 // Calculate width needed for content
183 // Calculate width needed for content
110 maxWidth := 60 // Width for explanation text
184 maxWidth := 60 // Width for explanation text
112− title := baseStyle.
186+ title := lipgloss.NewStyle().
113 Foreground(t.Primary()).
187 Foreground(t.Primary()).
114 Bold(true).
188 Bold(true).
115 Width(maxWidth).
189 Width(maxWidth).
116 Padding(0, 1).
190 Padding(0, 1).
191+ Background(t.Background()).
117 Render("Command Arguments")
192 Render("Command Arguments")
119− explanation := baseStyle.
194+ explanation := lipgloss.NewStyle().
120 Foreground(t.Text()).
195 Foreground(t.Text()).
121 Width(maxWidth).
196 Width(maxWidth).
122 Padding(0, 1).
197 Padding(0, 1).
198+ Background(t.Background()).
123− Render("This command requires arguments. Please enter the text to replace $ARGUMENTS with:")
199+ Render("This command requires multiple arguments. Please enter values for each:")
125− inputField := baseStyle.
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().
207+ Padding(1, 1, 0, 1).
208+ Background(t.Background())
210+ if i == m.focusIndex {
211+ labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
213+ labelStyle = labelStyle.Foreground(t.TextMuted())
216+ label := labelStyle.Render(m.argNames[i] + ":")
218+ field := lipgloss.NewStyle().
126− Foreground(t.Text()).
219+ Foreground(t.Text()).
127− Width(maxWidth).
220+ Width(maxWidth).
128− Padding(1, 1).
221+ Padding(0, 1).
129− Render(m.textInput.View())
222+ Background(t.Background()).
223+ Render(input.View())
225+ inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
131 maxWidth = min(maxWidth, m.width-10)
228 maxWidth = min(maxWidth, m.width-10)
230+ // Join all elements vertically
231+ elements := []string{title, explanation}
232+ elements = append(elements, inputFields...)
133 content := lipgloss.JoinVertical(
234 content := lipgloss.JoinVertical(
134 lipgloss.Left,
235 lipgloss.Left,
135− title,
236+ elements...,
140 return baseStyle.Padding(1, 2).
239 return baseStyle.Padding(1, 2).
@@ -147,27 +246,12 @@ func (m ArgumentsDialogCmp) View() string {
149// SetSize sets the size of the component.
248// SetSize sets the size of the component.
150−func (m *ArgumentsDialogCmp) SetSize(width, height int) {
249+func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
151 m.width = width
250 m.width = width
152 m.height = height
251 m.height = height
155// Bindings implements layout.Bindings.
254// Bindings implements layout.Bindings.
156−func (m ArgumentsDialogCmp) Bindings() []key.Binding {
255+func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
157 return m.keys.ShortHelp()
256 return m.keys.ShortHelp()
160−// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
161−type CloseArgumentsDialogMsg struct {
168−// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
169−type ShowArgumentsDialogMsg struct {