wip arguments dialog

Kujtim Hoxha created

Change summary

internal/tui/components/core/list/list.go             |   9 
internal/tui/components/dialogs/commands/arguments.go | 174 ++++++++++++
internal/tui/components/dialogs/commands/commands.go  |  46 +++
internal/tui/components/dialogs/commands/item.go      |   6 
internal/tui/components/dialogs/commands/keys.go      |  92 ++++++
internal/tui/tui.go                                   |  10 
6 files changed, 325 insertions(+), 12 deletions(-)

Detailed changes

internal/tui/components/core/list/list.go 🔗

@@ -38,6 +38,7 @@ type ListModel interface {
 	UpdateItem(int, util.Model)     // Replace an item at the specified index
 	ResetView()                     // Clear rendering cache and reset scroll position
 	Items() []util.Model            // Get all items in the list
+	SelectedIndex() int             // Get the index of the currently selected item
 }
 
 // HasAnim interface identifies items that support animation.
@@ -1258,3 +1259,11 @@ func (m *model) filterSection(sect section, search string) *section {
 
 	return nil
 }
+
+// SelectedIndex returns the index of the currently selected item.
+func (m *model) SelectedIndex() int {
+	if m.selectionState.selectedIndex < 0 || m.selectionState.selectedIndex >= len(m.filteredItems) {
+		return NoSelection
+	}
+	return m.selectionState.selectedIndex
+}

internal/tui/components/dialogs/commands/arguments.go 🔗

@@ -1,11 +1,18 @@
 package commands
 
 import (
+	"fmt"
+	"strings"
+
+	"github.com/charmbracelet/bubbles/v2/help"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/textinput"
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
 	"github.com/opencode-ai/opencode/internal/tui/theme"
+	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
 const (
@@ -36,10 +43,55 @@ type commandArgumentsDialogCmp struct {
 	width   int
 	wWidth  int // Width of the terminal window
 	wHeight int // Height of the terminal window
+
+	inputs     []textinput.Model
+	focusIndex int
+	keys       ArgumentsDialogKeyMap
+	commandID  string
+	content    string
+	argNames   []string
+	help       help.Model
 }
 
-func NewCommandArgumentsDialog() CommandArgumentsDialog {
-	return &commandArgumentsDialogCmp{}
+func NewCommandArgumentsDialog(commandID, content string, argNames []string) CommandArgumentsDialog {
+	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.SetWidth(40)
+		ti.SetVirtualCursor(false)
+		ti.Prompt = ""
+		ds := ti.Styles()
+
+		ds.Blurred.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted())
+		ds.Blurred.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.TextMuted())
+		ds.Blurred.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.TextMuted())
+		ds.Focused.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted())
+		ds.Focused.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.Text())
+		ds.Focused.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.Text())
+		ti.SetStyles(ds)
+		// Only focus the first input initially
+		if i == 0 {
+			ti.Focus()
+		} else {
+			ti.Blur()
+		}
+
+		inputs[i] = ti
+	}
+
+	return &commandArgumentsDialogCmp{
+		inputs:     inputs,
+		keys:       DefaultArgumentsDialogKeyMap(),
+		commandID:  commandID,
+		content:    content,
+		argNames:   argNames,
+		focusIndex: 0,
+		width:      60,
+		help:       help.New(),
+	}
 }
 
 // Init implements CommandArgumentsDialog.
@@ -48,20 +100,130 @@ func (c *commandArgumentsDialogCmp) Init() tea.Cmd {
 }
 
 // Update implements CommandArgumentsDialog.
-func (c *commandArgumentsDialogCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
+func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		c.wWidth = msg.Width
+		c.wHeight = msg.Height
+	case tea.KeyPressMsg:
+		switch {
+		case key.Matches(msg, c.keys.Confirm):
+			if c.focusIndex == len(c.inputs)-1 {
+				content := c.content
+				for i, name := range c.argNames {
+					value := c.inputs[i].Value()
+					placeholder := "$" + name
+					content = strings.ReplaceAll(content, placeholder, value)
+				}
+				return c, tea.Sequence(
+					util.CmdHandler(dialogs.CloseDialogMsg{}),
+					util.CmdHandler(CommandRunCustomMsg{
+						Content: content,
+					}),
+				)
+			}
+			// Otherwise, move to the next input
+			c.inputs[c.focusIndex].Blur()
+			c.focusIndex++
+			c.inputs[c.focusIndex].Focus()
+		case key.Matches(msg, c.keys.Next):
+			// Move to the next input
+			c.inputs[c.focusIndex].Blur()
+			c.focusIndex = (c.focusIndex + 1) % len(c.inputs)
+			c.inputs[c.focusIndex].Focus()
+		case key.Matches(msg, c.keys.Previous):
+			// Move to the previous input
+			c.inputs[c.focusIndex].Blur()
+			c.focusIndex = (c.focusIndex - 1 + len(c.inputs)) % len(c.inputs)
+			c.inputs[c.focusIndex].Focus()
+
+		default:
+			var cmd tea.Cmd
+			c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
+			return c, cmd
+		}
+	}
 	return c, nil
 }
 
 // View implements CommandArgumentsDialog.
 func (c *commandArgumentsDialogCmp) View() tea.View {
-	return tea.NewView("")
+	t := theme.CurrentTheme()
+	baseStyle := styles.BaseStyle()
+
+	title := lipgloss.NewStyle().
+		Foreground(t.Primary()).
+		Bold(true).
+		Padding(0, 1).
+		Background(t.Background()).
+		Render("Command Arguments")
+
+	explanation := lipgloss.NewStyle().
+		Foreground(t.Text()).
+		Padding(0, 1).
+		Background(t.Background()).
+		Render("This command requires arguments.")
+
+	// Create input fields for each argument
+	inputFields := make([]string, len(c.inputs))
+	for i, input := range c.inputs {
+		// Highlight the label of the focused input
+		labelStyle := lipgloss.NewStyle().
+			Padding(1, 1, 0, 1).
+			Background(t.Background())
+
+		if i == c.focusIndex {
+			labelStyle = labelStyle.Foreground(t.Text()).Bold(true)
+		} else {
+			labelStyle = labelStyle.Foreground(t.TextMuted())
+		}
+
+		label := labelStyle.Render(c.argNames[i] + ":")
+
+		field := lipgloss.NewStyle().
+			Foreground(t.Text()).
+			Padding(0, 1).
+			Background(t.Background()).
+			Render(input.View())
+
+		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
+	}
+
+	// Join all elements vertically
+	elements := []string{title, explanation}
+	elements = append(elements, inputFields...)
+
+	c.help.ShowAll = false
+	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
+	elements = append(elements, "", helpText)
+
+	content := lipgloss.JoinVertical(
+		lipgloss.Left,
+		elements...,
+	)
+
+	view := tea.NewView(
+		baseStyle.Padding(1, 1, 0, 1).
+			Border(lipgloss.RoundedBorder()).
+			BorderBackground(t.Background()).
+			BorderForeground(t.TextMuted()).
+			Background(t.Background()).
+			Width(c.width).
+			Render(content),
+	)
+	cursor := c.inputs[c.focusIndex].Cursor()
+	if cursor != nil {
+		cursor = c.moveCursor(cursor)
+	}
+	view.SetCursor(cursor)
+	return view
 }
 
 func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
-	offset := 10 + 1
+	offset := 13 + (1+c.focusIndex)*3
 	cursor.Y += offset
 	_, col := c.Position()
-	cursor.X = cursor.X + col + 2
+	cursor.X = cursor.X + col + 3
 	return cursor
 }
 

internal/tui/components/dialogs/commands/commands.go 🔗

@@ -1,6 +1,7 @@
 package commands
 
 import (
+	"github.com/charmbracelet/bubbles/v2/key"
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/lipgloss/v2"
 
@@ -37,14 +38,31 @@ type commandDialogCmp struct {
 	wHeight int // Height of the terminal window
 
 	commandList list.ListModel
+	commands    []Command
+	keyMap      CommandsDialogKeyMap
 }
 
 func NewCommandDialog() CommandsDialog {
-	commandList := list.New(list.WithFilterable(true))
-
+	listKeyMap := list.DefaultKeyMap()
+	keyMap := DefaultCommandsDialogKeyMap()
+
+	listKeyMap.Down.SetEnabled(false)
+	listKeyMap.Up.SetEnabled(false)
+	listKeyMap.NDown.SetEnabled(false)
+	listKeyMap.NUp.SetEnabled(false)
+	listKeyMap.HalfPageDown.SetEnabled(false)
+	listKeyMap.HalfPageUp.SetEnabled(false)
+	listKeyMap.Home.SetEnabled(false)
+	listKeyMap.End.SetEnabled(false)
+
+	listKeyMap.DownOneItem = keyMap.Next
+	listKeyMap.UpOneItem = keyMap.Previous
+
+	commandList := list.New(list.WithFilterable(true), list.WithKeyMap(listKeyMap))
 	return &commandDialogCmp{
 		commandList: commandList,
 		width:       defaultWidth,
+		keyMap:      DefaultCommandsDialogKeyMap(),
 	}
 }
 
@@ -53,6 +71,7 @@ func (c *commandDialogCmp) Init() tea.Cmd {
 	if err != nil {
 		return util.ReportError(err)
 	}
+	c.commands = commands
 
 	commandItems := []util.Model{}
 	if len(commands) > 0 {
@@ -65,6 +84,7 @@ func (c *commandDialogCmp) Init() tea.Cmd {
 	commandItems = append(commandItems, NewItemSection("Default"))
 
 	for _, cmd := range c.defaultCommands() {
+		c.commands = append(c.commands, cmd)
 		commandItems = append(commandItems, NewCommandItem(cmd))
 	}
 
@@ -78,10 +98,26 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		c.wWidth = msg.Width
 		c.wHeight = msg.Height
 		return c, c.commandList.SetSize(c.listWidth(), c.listHeight())
+	case tea.KeyPressMsg:
+		switch {
+		case key.Matches(msg, c.keyMap.Select):
+			selectedItemInx := c.commandList.SelectedIndex()
+			if selectedItemInx == list.NoSelection {
+				return c, nil // No item selected, do nothing
+			}
+			items := c.commandList.Items()
+			selectedItem := items[selectedItemInx].(CommandItem).Command()
+			return c, tea.Sequence(
+				util.CmdHandler(dialogs.CloseDialogMsg{}),
+				selectedItem.Handler(selectedItem),
+			)
+		default:
+			u, cmd := c.commandList.Update(msg)
+			c.commandList = u.(list.ListModel)
+			return c, cmd
+		}
 	}
-	u, cmd := c.commandList.Update(msg)
-	c.commandList = u.(list.ListModel)
-	return c, cmd
+	return c, nil
 }
 
 func (c *commandDialogCmp) View() tea.View {

internal/tui/components/dialogs/commands/item.go 🔗

@@ -18,6 +18,7 @@ type CommandItem interface {
 	util.Model
 	layout.Focusable
 	layout.Sizeable
+	Command() Command
 }
 
 type commandItem struct {
@@ -72,6 +73,11 @@ func (c *commandItem) View() tea.View {
 	return tea.NewView(text)
 }
 
+// Command implements CommandItem.
+func (c *commandItem) Command() Command {
+	return c.command
+}
+
 // Blur implements CommandItem.
 func (c *commandItem) Blur() tea.Cmd {
 	c.focus = false

internal/tui/components/dialogs/commands/keys.go 🔗

@@ -1 +1,93 @@
 package commands
+
+import (
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/opencode-ai/opencode/internal/tui/layout"
+)
+
+type CommandsDialogKeyMap struct {
+	Select   key.Binding
+	Next     key.Binding
+	Previous key.Binding
+}
+
+func DefaultCommandsDialogKeyMap() CommandsDialogKeyMap {
+	return CommandsDialogKeyMap{
+		Select: key.NewBinding(
+			key.WithKeys("enter"),
+			key.WithHelp("enter", "confirm"),
+		),
+		Next: key.NewBinding(
+			key.WithKeys("tab", "down"),
+			key.WithHelp("tab/↓", "next"),
+		),
+		Previous: key.NewBinding(
+			key.WithKeys("shift+tab", "up"),
+			key.WithHelp("shift+tab/↑", "previous"),
+		),
+	}
+}
+
+// FullHelp implements help.KeyMap.
+func (k CommandsDialogKeyMap) FullHelp() [][]key.Binding {
+	m := [][]key.Binding{}
+	slice := layout.KeyMapToSlice(k)
+	for i := 0; i < len(slice); i += 4 {
+		end := min(i+4, len(slice))
+		m = append(m, slice[i:end])
+	}
+	return m
+}
+
+// ShortHelp implements help.KeyMap.
+func (k CommandsDialogKeyMap) ShortHelp() []key.Binding {
+	return []key.Binding{
+		k.Select,
+		k.Next,
+		k.Previous,
+	}
+}
+
+type ArgumentsDialogKeyMap struct {
+	Confirm  key.Binding
+	Next     key.Binding
+	Previous key.Binding
+}
+
+func DefaultArgumentsDialogKeyMap() ArgumentsDialogKeyMap {
+	return ArgumentsDialogKeyMap{
+		Confirm: key.NewBinding(
+			key.WithKeys("enter"),
+			key.WithHelp("enter", "confirm"),
+		),
+
+		Next: key.NewBinding(
+			key.WithKeys("tab", "down"),
+			key.WithHelp("tab/↓", "next"),
+		),
+		Previous: key.NewBinding(
+			key.WithKeys("shift+tab", "up"),
+			key.WithHelp("shift+tab/↑", "previous"),
+		),
+	}
+}
+
+// FullHelp implements help.KeyMap.
+func (k ArgumentsDialogKeyMap) FullHelp() [][]key.Binding {
+	m := [][]key.Binding{}
+	slice := layout.KeyMapToSlice(k)
+	for i := 0; i < len(slice); i += 4 {
+		end := min(i+4, len(slice))
+		m = append(m, slice[i:end])
+	}
+	return m
+}
+
+// ShortHelp implements help.KeyMap.
+func (k ArgumentsDialogKeyMap) ShortHelp() []key.Binding {
+	return []key.Binding{
+		k.Confirm,
+		k.Next,
+		k.Previous,
+	}
+}

internal/tui/tui.go 🔗

@@ -58,7 +58,15 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.dialog = u.(dialogs.DialogCmp)
 		return a, dialogCmd
 	case commands.ShowArgumentsDialogMsg:
-
+		return a, util.CmdHandler(
+			dialogs.OpenDialogMsg{
+				Model: commands.NewCommandArgumentsDialog(
+					msg.CommandID,
+					msg.Content,
+					msg.ArgNames,
+				),
+			},
+		)
 	// Page change messages
 	case page.PageChangeMsg:
 		return a, a.moveToPage(msg.ID)