arguments.go

  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}