1package commands
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/charmbracelet/bubbles/v2/help"
8 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/textinput"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
13 "github.com/opencode-ai/opencode/internal/tui/styles"
14 "github.com/opencode-ai/opencode/internal/tui/theme"
15 "github.com/opencode-ai/opencode/internal/tui/util"
16)
17
18const (
19 argumentsDialogID dialogs.DialogID = "arguments"
20)
21
22// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
23type ShowArgumentsDialogMsg struct {
24 CommandID string
25 Content string
26 ArgNames []string
27}
28
29// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
30type CloseArgumentsDialogMsg struct {
31 Submit bool
32 CommandID string
33 Content string
34 Args map[string]string
35}
36
37// CommandArgumentsDialog represents the commands dialog.
38type CommandArgumentsDialog interface {
39 dialogs.DialogModel
40}
41
42type commandArgumentsDialogCmp struct {
43 width int
44 wWidth int // Width of the terminal window
45 wHeight int // Height of the terminal window
46
47 inputs []textinput.Model
48 focusIndex int
49 keys ArgumentsDialogKeyMap
50 commandID string
51 content string
52 argNames []string
53 help help.Model
54}
55
56func NewCommandArgumentsDialog(commandID, content string, argNames []string) CommandArgumentsDialog {
57 t := theme.CurrentTheme()
58 inputs := make([]textinput.Model, len(argNames))
59
60 for i, name := range argNames {
61 ti := textinput.New()
62 ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
63 ti.SetWidth(40)
64 ti.SetVirtualCursor(false)
65 ti.Prompt = ""
66 ds := ti.Styles()
67
68 ds.Blurred.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted())
69 ds.Blurred.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.TextMuted())
70 ds.Blurred.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.TextMuted())
71 ds.Focused.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted())
72 ds.Focused.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.Text())
73 ds.Focused.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.Text())
74 ti.SetStyles(ds)
75 // Only focus the first input initially
76 if i == 0 {
77 ti.Focus()
78 } else {
79 ti.Blur()
80 }
81
82 inputs[i] = ti
83 }
84
85 return &commandArgumentsDialogCmp{
86 inputs: inputs,
87 keys: DefaultArgumentsDialogKeyMap(),
88 commandID: commandID,
89 content: content,
90 argNames: argNames,
91 focusIndex: 0,
92 width: 60,
93 help: help.New(),
94 }
95}
96
97// Init implements CommandArgumentsDialog.
98func (c *commandArgumentsDialogCmp) Init() tea.Cmd {
99 return nil
100}
101
102// Update implements CommandArgumentsDialog.
103func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
104 switch msg := msg.(type) {
105 case tea.WindowSizeMsg:
106 c.wWidth = msg.Width
107 c.wHeight = msg.Height
108 case tea.KeyPressMsg:
109 switch {
110 case key.Matches(msg, c.keys.Confirm):
111 if c.focusIndex == len(c.inputs)-1 {
112 content := c.content
113 for i, name := range c.argNames {
114 value := c.inputs[i].Value()
115 placeholder := "$" + name
116 content = strings.ReplaceAll(content, placeholder, value)
117 }
118 return c, tea.Sequence(
119 util.CmdHandler(dialogs.CloseDialogMsg{}),
120 util.CmdHandler(CommandRunCustomMsg{
121 Content: content,
122 }),
123 )
124 }
125 // Otherwise, move to the next input
126 c.inputs[c.focusIndex].Blur()
127 c.focusIndex++
128 c.inputs[c.focusIndex].Focus()
129 case key.Matches(msg, c.keys.Next):
130 // Move to the next input
131 c.inputs[c.focusIndex].Blur()
132 c.focusIndex = (c.focusIndex + 1) % len(c.inputs)
133 c.inputs[c.focusIndex].Focus()
134 case key.Matches(msg, c.keys.Previous):
135 // Move to the previous input
136 c.inputs[c.focusIndex].Blur()
137 c.focusIndex = (c.focusIndex - 1 + len(c.inputs)) % len(c.inputs)
138 c.inputs[c.focusIndex].Focus()
139
140 default:
141 var cmd tea.Cmd
142 c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
143 return c, cmd
144 }
145 }
146 return c, nil
147}
148
149// View implements CommandArgumentsDialog.
150func (c *commandArgumentsDialogCmp) View() tea.View {
151 t := theme.CurrentTheme()
152 baseStyle := styles.BaseStyle()
153
154 title := lipgloss.NewStyle().
155 Foreground(t.Primary()).
156 Bold(true).
157 Padding(0, 1).
158 Background(t.Background()).
159 Render("Command Arguments")
160
161 explanation := lipgloss.NewStyle().
162 Foreground(t.Text()).
163 Padding(0, 1).
164 Background(t.Background()).
165 Render("This command requires arguments.")
166
167 // Create input fields for each argument
168 inputFields := make([]string, len(c.inputs))
169 for i, input := range c.inputs {
170 // Highlight the label of the focused input
171 labelStyle := lipgloss.NewStyle().
172 Padding(1, 1, 0, 1).
173 Background(t.Background())
174
175 if i == c.focusIndex {
176 labelStyle = labelStyle.Foreground(t.Text()).Bold(true)
177 } else {
178 labelStyle = labelStyle.Foreground(t.TextMuted())
179 }
180
181 label := labelStyle.Render(c.argNames[i] + ":")
182
183 field := lipgloss.NewStyle().
184 Foreground(t.Text()).
185 Padding(0, 1).
186 Background(t.Background()).
187 Render(input.View())
188
189 inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
190 }
191
192 // Join all elements vertically
193 elements := []string{title, explanation}
194 elements = append(elements, inputFields...)
195
196 c.help.ShowAll = false
197 helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
198 elements = append(elements, "", helpText)
199
200 content := lipgloss.JoinVertical(
201 lipgloss.Left,
202 elements...,
203 )
204
205 view := tea.NewView(
206 baseStyle.Padding(1, 1, 0, 1).
207 Border(lipgloss.RoundedBorder()).
208 BorderBackground(t.Background()).
209 BorderForeground(t.TextMuted()).
210 Background(t.Background()).
211 Width(c.width).
212 Render(content),
213 )
214 cursor := c.inputs[c.focusIndex].Cursor()
215 if cursor != nil {
216 cursor = c.moveCursor(cursor)
217 }
218 view.SetCursor(cursor)
219 return view
220}
221
222func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
223 offset := 13 + (1+c.focusIndex)*3
224 cursor.Y += offset
225 _, col := c.Position()
226 cursor.X = cursor.X + col + 3
227 return cursor
228}
229
230func (c *commandArgumentsDialogCmp) style() lipgloss.Style {
231 t := theme.CurrentTheme()
232 return styles.BaseStyle().
233 Width(c.width).
234 Padding(1).
235 Border(lipgloss.RoundedBorder()).
236 BorderBackground(t.Background()).
237 BorderForeground(t.TextMuted())
238}
239
240func (q *commandArgumentsDialogCmp) Position() (int, int) {
241 row := 10
242 col := q.wWidth / 2
243 col -= q.width / 2
244 return row, col
245}
246
247// ID implements CommandArgumentsDialog.
248func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
249 return argumentsDialogID
250}