arguments.go

  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}