summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/components/dialog/arguments.go
diff options
context:
space:
mode:
authorEd Zynda <[email protected]>2025-05-15 16:52:24 +0300
committeradamdottv <[email protected]>2025-05-15 09:43:33 -0500
commitab150be7c393a91d9fb2347b7012ad2f7d77700b (patch)
treef83cc39853f73d62227121af27739d1c310e02e6 /internal/tui/components/dialog/arguments.go
parenta203fb8cccf45d74fb217bdc579155ee058af248 (diff)
downloadopencode-ab150be7c393a91d9fb2347b7012ad2f7d77700b.tar.gz
opencode-ab150be7c393a91d9fb2347b7012ad2f7d77700b.zip
feat: Support named arguments in custom commands (#158)
* Allow multiple named args * fix: Fix styling in multi-arguments dialog * Remove old unused modal * Focus on only one input at a time
Diffstat (limited to 'internal/tui/components/dialog/arguments.go')
-rw-r--r--internal/tui/components/dialog/arguments.go225
1 files changed, 155 insertions, 70 deletions
diff --git a/internal/tui/components/dialog/arguments.go b/internal/tui/components/dialog/arguments.go
index 7bd9b4056..fed79bce3 100644
--- a/internal/tui/components/dialog/arguments.go
+++ b/internal/tui/components/dialog/arguments.go
@@ -1,6 +1,7 @@
package dialog
import (
+ "fmt"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
@@ -11,35 +12,6 @@ import (
"github.com/sst/opencode/internal/tui/util"
)
-// ArgumentsDialogCmp is a component that asks the user for command arguments.
-type ArgumentsDialogCmp struct {
- width, height int
- textInput textinput.Model
- keys argumentsDialogKeyMap
- commandID string
- content string
-}
-
-// NewArgumentsDialogCmp creates a new ArgumentsDialogCmp.
-func NewArgumentsDialogCmp(commandID, content string) ArgumentsDialogCmp {
- t := theme.CurrentTheme()
- ti := textinput.New()
- ti.Placeholder = "Enter arguments..."
- ti.Focus()
- ti.Width = 40
- ti.Prompt = ""
- ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
- ti.PromptStyle = ti.PromptStyle.Background(t.Background())
- ti.TextStyle = ti.TextStyle.Background(t.Background())
-
- return ArgumentsDialogCmp{
- textInput: ti,
- keys: argumentsDialogKeyMap{},
- commandID: commandID,
- content: content,
- }
-}
-
type argumentsDialogKeyMap struct {
Enter key.Binding
Escape key.Binding
@@ -64,77 +36,204 @@ func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{k.ShortHelp()}
}
+// ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
+type ShowMultiArgumentsDialogMsg struct {
+ CommandID string
+ Content string
+ ArgNames []string
+}
+
+// CloseMultiArgumentsDialogMsg is a message that is sent when the multi-arguments dialog is closed.
+type CloseMultiArgumentsDialogMsg struct {
+ Submit bool
+ CommandID string
+ Content string
+ Args map[string]string
+}
+
+// MultiArgumentsDialogCmp is a component that asks the user for multiple command arguments.
+type MultiArgumentsDialogCmp struct {
+ width, height int
+ inputs []textinput.Model
+ focusIndex int
+ keys argumentsDialogKeyMap
+ commandID string
+ content string
+ argNames []string
+}
+
+// NewMultiArgumentsDialogCmp creates a new MultiArgumentsDialogCmp.
+func NewMultiArgumentsDialogCmp(commandID, content string, argNames []string) MultiArgumentsDialogCmp {
+ t := theme.CurrentTheme()
+ inputs := make([]textinput.Model, len(argNames))
+
+ for i, name := range argNames {
+ ti := textinput.New()
+ ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
+ ti.Width = 40
+ ti.Prompt = ""
+ ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
+ ti.PromptStyle = ti.PromptStyle.Background(t.Background())
+ ti.TextStyle = ti.TextStyle.Background(t.Background())
+
+ // Only focus the first input initially
+ if i == 0 {
+ ti.Focus()
+ ti.PromptStyle = ti.PromptStyle.Foreground(t.Primary())
+ ti.TextStyle = ti.TextStyle.Foreground(t.Primary())
+ } else {
+ ti.Blur()
+ }
+
+ inputs[i] = ti
+ }
+
+ return MultiArgumentsDialogCmp{
+ inputs: inputs,
+ keys: argumentsDialogKeyMap{},
+ commandID: commandID,
+ content: content,
+ argNames: argNames,
+ focusIndex: 0,
+ }
+}
+
// Init implements tea.Model.
-func (m ArgumentsDialogCmp) Init() tea.Cmd {
- return tea.Batch(
- textinput.Blink,
- m.textInput.Focus(),
- )
+func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
+ // Make sure only the first input is focused
+ for i := range m.inputs {
+ if i == 0 {
+ m.inputs[i].Focus()
+ } else {
+ m.inputs[i].Blur()
+ }
+ }
+
+ return textinput.Blink
}
// Update implements tea.Model.
-func (m ArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmd tea.Cmd
+func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
+ t := theme.CurrentTheme()
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
- return m, util.CmdHandler(CloseArgumentsDialogMsg{})
- case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
- return m, util.CmdHandler(CloseArgumentsDialogMsg{
- Submit: true,
+ return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
+ Submit: false,
CommandID: m.commandID,
Content: m.content,
- Arguments: m.textInput.Value(),
+ Args: nil,
})
+ case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
+ // If we're on the last input, submit the form
+ if m.focusIndex == len(m.inputs)-1 {
+ args := make(map[string]string)
+ for i, name := range m.argNames {
+ args[name] = m.inputs[i].Value()
+ }
+ return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
+ Submit: true,
+ CommandID: m.commandID,
+ Content: m.content,
+ Args: args,
+ })
+ }
+ // Otherwise, move to the next input
+ m.inputs[m.focusIndex].Blur()
+ m.focusIndex++
+ m.inputs[m.focusIndex].Focus()
+ m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
+ m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
+ case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
+ // Move to the next input
+ m.inputs[m.focusIndex].Blur()
+ m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
+ m.inputs[m.focusIndex].Focus()
+ m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
+ m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
+ case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab"))):
+ // Move to the previous input
+ m.inputs[m.focusIndex].Blur()
+ m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs)
+ m.inputs[m.focusIndex].Focus()
+ m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
+ m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
- m.textInput, cmd = m.textInput.Update(msg)
+ // Update the focused input
+ var cmd tea.Cmd
+ m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
// View implements tea.Model.
-func (m ArgumentsDialogCmp) View() string {
+func (m MultiArgumentsDialogCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
// Calculate width needed for content
maxWidth := 60 // Width for explanation text
- title := baseStyle.
+ title := lipgloss.NewStyle().
Foreground(t.Primary()).
Bold(true).
Width(maxWidth).
Padding(0, 1).
+ Background(t.Background()).
Render("Command Arguments")
- explanation := baseStyle.
+ explanation := lipgloss.NewStyle().
Foreground(t.Text()).
Width(maxWidth).
Padding(0, 1).
- Render("This command requires arguments. Please enter the text to replace $ARGUMENTS with:")
+ Background(t.Background()).
+ Render("This command requires multiple arguments. Please enter values for each:")
- inputField := baseStyle.
- Foreground(t.Text()).
- Width(maxWidth).
- Padding(1, 1).
- Render(m.textInput.View())
+ // Create input fields for each argument
+ inputFields := make([]string, len(m.inputs))
+ for i, input := range m.inputs {
+ // Highlight the label of the focused input
+ labelStyle := lipgloss.NewStyle().
+ Width(maxWidth).
+ Padding(1, 1, 0, 1).
+ Background(t.Background())
+
+ if i == m.focusIndex {
+ labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
+ } else {
+ labelStyle = labelStyle.Foreground(t.TextMuted())
+ }
+
+ label := labelStyle.Render(m.argNames[i] + ":")
+
+ field := lipgloss.NewStyle().
+ Foreground(t.Text()).
+ Width(maxWidth).
+ Padding(0, 1).
+ Background(t.Background()).
+ Render(input.View())
+
+ inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
+ }
maxWidth = min(maxWidth, m.width-10)
+ // Join all elements vertically
+ elements := []string{title, explanation}
+ elements = append(elements, inputFields...)
+
content := lipgloss.JoinVertical(
lipgloss.Left,
- title,
- explanation,
- inputField,
+ elements...,
)
return baseStyle.Padding(1, 2).
@@ -147,26 +246,12 @@ func (m ArgumentsDialogCmp) View() string {
}
// SetSize sets the size of the component.
-func (m *ArgumentsDialogCmp) SetSize(width, height int) {
+func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
m.width = width
m.height = height
}
// Bindings implements layout.Bindings.
-func (m ArgumentsDialogCmp) Bindings() []key.Binding {
+func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
return m.keys.ShortHelp()
}
-
-// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
-type CloseArgumentsDialogMsg struct {
- Submit bool
- CommandID string
- Content string
- Arguments string
-}
-
-// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
-type ShowArgumentsDialogMsg struct {
- CommandID string
- Content string
-}