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		ti.Styles.Focused.Placeholder = ti.Styles.Focused.Placeholder.Background(t.Background())
 77		ti.Styles.Blurred.Placeholder = ti.Styles.Blurred.Placeholder.Background(t.Background())
 78		ti.Styles.Focused.Suggestion = ti.Styles.Focused.Suggestion.Background(t.Background()).Foreground(t.Primary())
 79		ti.Styles.Blurred.Suggestion = ti.Styles.Blurred.Suggestion.Background(t.Background())
 80		ti.Styles.Focused.Text = ti.Styles.Focused.Text.Background(t.Background()).Foreground(t.Primary())
 81		ti.Styles.Blurred.Text = ti.Styles.Blurred.Text.Background(t.Background())
 82
 83		// Only focus the first input initially
 84		if i == 0 {
 85			ti.Focus()
 86		} else {
 87			ti.Blur()
 88		}
 89
 90		inputs[i] = ti
 91	}
 92
 93	return MultiArgumentsDialogCmp{
 94		inputs:     inputs,
 95		keys:       argumentsDialogKeyMap{},
 96		commandID:  commandID,
 97		content:    content,
 98		argNames:   argNames,
 99		focusIndex: 0,
100	}
101}
102
103// Init implements tea.Model.
104func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
105	// Make sure only the first input is focused
106	for i := range m.inputs {
107		if i == 0 {
108			m.inputs[i].Focus()
109		} else {
110			m.inputs[i].Blur()
111		}
112	}
113
114	return textinput.Blink
115}
116
117// Update implements tea.Model.
118func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
119	var cmds []tea.Cmd
120	switch msg := msg.(type) {
121	case tea.KeyPressMsg:
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		case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
149			// Move to the next input
150			m.inputs[m.focusIndex].Blur()
151			m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
152			m.inputs[m.focusIndex].Focus()
153		case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab"))):
154			// Move to the previous input
155			m.inputs[m.focusIndex].Blur()
156			m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs)
157			m.inputs[m.focusIndex].Focus()
158		}
159	case tea.WindowSizeMsg:
160		m.width = msg.Width
161		m.height = msg.Height
162	}
163
164	// Update the focused input
165	var cmd tea.Cmd
166	m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
167	cmds = append(cmds, cmd)
168
169	return m, tea.Batch(cmds...)
170}
171
172// View implements tea.Model.
173func (m MultiArgumentsDialogCmp) View() string {
174	t := theme.CurrentTheme()
175	baseStyle := styles.BaseStyle()
176
177	// Calculate width needed for content
178	maxWidth := 60 // Width for explanation text
179
180	title := lipgloss.NewStyle().
181		Foreground(t.Primary()).
182		Bold(true).
183		Width(maxWidth).
184		Padding(0, 1).
185		Background(t.Background()).
186		Render("Command Arguments")
187
188	explanation := lipgloss.NewStyle().
189		Foreground(t.Text()).
190		Width(maxWidth).
191		Padding(0, 1).
192		Background(t.Background()).
193		Render("This command requires multiple arguments. Please enter values for each:")
194
195	// Create input fields for each argument
196	inputFields := make([]string, len(m.inputs))
197	for i, input := range m.inputs {
198		// Highlight the label of the focused input
199		labelStyle := lipgloss.NewStyle().
200			Width(maxWidth).
201			Padding(1, 1, 0, 1).
202			Background(t.Background())
203
204		if i == m.focusIndex {
205			labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
206		} else {
207			labelStyle = labelStyle.Foreground(t.TextMuted())
208		}
209
210		label := labelStyle.Render(m.argNames[i] + ":")
211
212		field := lipgloss.NewStyle().
213			Foreground(t.Text()).
214			Width(maxWidth).
215			Padding(0, 1).
216			Background(t.Background()).
217			Render(input.View())
218
219		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
220	}
221
222	maxWidth = min(maxWidth, m.width-10)
223
224	// Join all elements vertically
225	elements := []string{title, explanation}
226	elements = append(elements, inputFields...)
227
228	content := lipgloss.JoinVertical(
229		lipgloss.Left,
230		elements...,
231	)
232
233	return baseStyle.Padding(1, 2).
234		Border(lipgloss.RoundedBorder()).
235		BorderBackground(t.Background()).
236		BorderForeground(t.TextMuted()).
237		Background(t.Background()).
238		Width(lipgloss.Width(content) + 4).
239		Render(content)
240}
241
242// SetSize sets the size of the component.
243func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
244	m.width = width
245	m.height = height
246}
247
248// Bindings implements layout.Bindings.
249func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
250	return m.keys.ShortHelp()
251}