Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit e9a8bda

Browse files
committed
wip arguments dialog
1 parent 2329178 commit e9a8bda

6 files changed

Lines changed: 325 additions & 12 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type ListModel interface {
3838
UpdateItem(int, util.Model) // Replace an item at the specified index
3939
ResetView() // Clear rendering cache and reset scroll position
4040
Items() []util.Model // Get all items in the list
41+
SelectedIndex() int // Get the index of the currently selected item
4142
}
4243

4344
// HasAnim interface identifies items that support animation.
@@ -1258,3 +1259,11 @@ func (m *model) filterSection(sect section, search string) *section {
12581259

12591260
return nil
12601261
}
1262+
1263+
// SelectedIndex returns the index of the currently selected item.
1264+
func (m *model) SelectedIndex() int {
1265+
if m.selectionState.selectedIndex < 0 || m.selectionState.selectedIndex >= len(m.filteredItems) {
1266+
return NoSelection
1267+
}
1268+
return m.selectionState.selectedIndex
1269+
}

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

Lines changed: 168 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
package commands
22

33
import (
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"
410
tea "github.com/charmbracelet/bubbletea/v2"
511
"github.com/charmbracelet/lipgloss/v2"
612
"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
713
"github.com/opencode-ai/opencode/internal/tui/styles"
814
"github.com/opencode-ai/opencode/internal/tui/theme"
15+
"github.com/opencode-ai/opencode/internal/tui/util"
916
)
1017

1118
const (
@@ -36,10 +43,55 @@ type commandArgumentsDialogCmp struct {
3643
width int
3744
wWidth int // Width of the terminal window
3845
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
3954
}
4055

41-
func NewCommandArgumentsDialog() CommandArgumentsDialog {
42-
return &commandArgumentsDialogCmp{}
56+
func 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+
}
4395
}
4496

4597
// Init implements CommandArgumentsDialog.
@@ -48,20 +100,130 @@ func (c *commandArgumentsDialogCmp) Init() tea.Cmd {
48100
}
49101

50102
// Update implements CommandArgumentsDialog.
51-
func (c *commandArgumentsDialogCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
103+
func (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+
}
52146
return c, nil
53147
}
54148

55149
// View implements CommandArgumentsDialog.
56150
func (c *commandArgumentsDialogCmp) View() tea.View {
57-
return tea.NewView("")
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
58220
}
59221

60222
func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
61-
offset := 10 + 1
223+
offset := 13 + (1+c.focusIndex)*3
62224
cursor.Y += offset
63225
_, col := c.Position()
64-
cursor.X = cursor.X + col + 2
226+
cursor.X = cursor.X + col + 3
65227
return cursor
66228
}
67229

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package commands
22

33
import (
4+
"github.com/charmbracelet/bubbles/v2/key"
45
tea "github.com/charmbracelet/bubbletea/v2"
56
"github.com/charmbracelet/lipgloss/v2"
67

@@ -37,14 +38,31 @@ type commandDialogCmp struct {
3738
wHeight int // Height of the terminal window
3839

3940
commandList list.ListModel
41+
commands []Command
42+
keyMap CommandsDialogKeyMap
4043
}
4144

4245
func NewCommandDialog() CommandsDialog {
43-
commandList := list.New(list.WithFilterable(true))
44-
46+
listKeyMap := list.DefaultKeyMap()
47+
keyMap := DefaultCommandsDialogKeyMap()
48+
49+
listKeyMap.Down.SetEnabled(false)
50+
listKeyMap.Up.SetEnabled(false)
51+
listKeyMap.NDown.SetEnabled(false)
52+
listKeyMap.NUp.SetEnabled(false)
53+
listKeyMap.HalfPageDown.SetEnabled(false)
54+
listKeyMap.HalfPageUp.SetEnabled(false)
55+
listKeyMap.Home.SetEnabled(false)
56+
listKeyMap.End.SetEnabled(false)
57+
58+
listKeyMap.DownOneItem = keyMap.Next
59+
listKeyMap.UpOneItem = keyMap.Previous
60+
61+
commandList := list.New(list.WithFilterable(true), list.WithKeyMap(listKeyMap))
4562
return &commandDialogCmp{
4663
commandList: commandList,
4764
width: defaultWidth,
65+
keyMap: DefaultCommandsDialogKeyMap(),
4866
}
4967
}
5068

@@ -53,6 +71,7 @@ func (c *commandDialogCmp) Init() tea.Cmd {
5371
if err != nil {
5472
return util.ReportError(err)
5573
}
74+
c.commands = commands
5675

5776
commandItems := []util.Model{}
5877
if len(commands) > 0 {
@@ -65,6 +84,7 @@ func (c *commandDialogCmp) Init() tea.Cmd {
6584
commandItems = append(commandItems, NewItemSection("Default"))
6685

6786
for _, cmd := range c.defaultCommands() {
87+
c.commands = append(c.commands, cmd)
6888
commandItems = append(commandItems, NewCommandItem(cmd))
6989
}
7090

@@ -78,10 +98,26 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7898
c.wWidth = msg.Width
7999
c.wHeight = msg.Height
80100
return c, c.commandList.SetSize(c.listWidth(), c.listHeight())
101+
case tea.KeyPressMsg:
102+
switch {
103+
case key.Matches(msg, c.keyMap.Select):
104+
selectedItemInx := c.commandList.SelectedIndex()
105+
if selectedItemInx == list.NoSelection {
106+
return c, nil // No item selected, do nothing
107+
}
108+
items := c.commandList.Items()
109+
selectedItem := items[selectedItemInx].(CommandItem).Command()
110+
return c, tea.Sequence(
111+
util.CmdHandler(dialogs.CloseDialogMsg{}),
112+
selectedItem.Handler(selectedItem),
113+
)
114+
default:
115+
u, cmd := c.commandList.Update(msg)
116+
c.commandList = u.(list.ListModel)
117+
return c, cmd
118+
}
81119
}
82-
u, cmd := c.commandList.Update(msg)
83-
c.commandList = u.(list.ListModel)
84-
return c, cmd
120+
return c, nil
85121
}
86122

87123
func (c *commandDialogCmp) View() tea.View {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type CommandItem interface {
1818
util.Model
1919
layout.Focusable
2020
layout.Sizeable
21+
Command() Command
2122
}
2223

2324
type commandItem struct {
@@ -72,6 +73,11 @@ func (c *commandItem) View() tea.View {
7273
return tea.NewView(text)
7374
}
7475

76+
// Command implements CommandItem.
77+
func (c *commandItem) Command() Command {
78+
return c.command
79+
}
80+
7581
// Blur implements CommandItem.
7682
func (c *commandItem) Blur() tea.Cmd {
7783
c.focus = false

0 commit comments

Comments
 (0)