arguments.go

  1package dialog
  2
  3import (
  4	"fmt"
  5	"github.com/charmbracelet/bubbles/key"
  6	"github.com/charmbracelet/bubbles/textinput"
  7	tea "github.com/charmbracelet/bubbletea"
  8	"github.com/charmbracelet/lipgloss"
  9
 10	"github.com/opencode-ai/opencode/internal/tui/styles"
 11	"github.com/opencode-ai/opencode/internal/tui/theme"
 12	"github.com/opencode-ai/opencode/internal/tui/util"
 13)
 14
 15type argumentsDialogKeyMap struct {
 16	Enter  key.Binding
 17	Escape key.Binding
 18}
 19
 20// ShortHelp implements key.Map.
 21func (k argumentsDialogKeyMap) ShortHelp() []key.Binding {
 22	return []key.Binding{
 23		key.NewBinding(
 24			key.WithKeys("enter"),
 25			key.WithHelp("enter", "confirm"),
 26		),
 27		key.NewBinding(
 28			key.WithKeys("esc"),
 29			key.WithHelp("esc", "cancel"),
 30		),
 31	}
 32}
 33
 34// FullHelp implements key.Map.
 35func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
 36	return [][]key.Binding{k.ShortHelp()}
 37}
 38
 39// ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
 40type 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.
 47type 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.
 55type 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.
 66func 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
101// Init implements tea.Model.
102func (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
113}
114
115// Update implements tea.Model.
116func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
117	var cmds []tea.Cmd
118	t := theme.CurrentTheme()
119
120	switch msg := msg.(type) {
121	case tea.KeyMsg:
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			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())
164		}
165	case tea.WindowSizeMsg:
166		m.width = msg.Width
167		m.height = msg.Height
168	}
169
170	// Update the focused input
171	var cmd tea.Cmd
172	m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
173	cmds = append(cmds, cmd)
174
175	return m, tea.Batch(cmds...)
176}
177
178// View implements tea.Model.
179func (m MultiArgumentsDialogCmp) View() string {
180	t := theme.CurrentTheme()
181	baseStyle := styles.BaseStyle()
182
183	// Calculate width needed for content
184	maxWidth := 60 // Width for explanation text
185
186	title := lipgloss.NewStyle().
187		Foreground(t.Primary()).
188		Bold(true).
189		Width(maxWidth).
190		Padding(0, 1).
191		Background(t.Background()).
192		Render("Command Arguments")
193
194	explanation := lipgloss.NewStyle().
195		Foreground(t.Text()).
196		Width(maxWidth).
197		Padding(0, 1).
198		Background(t.Background()).
199		Render("This command requires multiple arguments. Please enter values for each:")
200
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	}
227
228	maxWidth = min(maxWidth, m.width-10)
229
230	// Join all elements vertically
231	elements := []string{title, explanation}
232	elements = append(elements, inputFields...)
233
234	content := lipgloss.JoinVertical(
235		lipgloss.Left,
236		elements...,
237	)
238
239	return baseStyle.Padding(1, 2).
240		Border(lipgloss.RoundedBorder()).
241		BorderBackground(t.Background()).
242		BorderForeground(t.TextMuted()).
243		Background(t.Background()).
244		Width(lipgloss.Width(content) + 4).
245		Render(content)
246}
247
248// SetSize sets the size of the component.
249func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
250	m.width = width
251	m.height = height
252}
253
254// Bindings implements layout.Bindings.
255func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
256	return m.keys.ShortHelp()
257}