on main help sign in
wip arguments dialog
commite9a8bda49a97f905a3db47b2c941591da1511244 authorKujtim Hoxha <kujtimii.h@gmail.com> dateThu 29 May 2025, 18:39 (1 year ago) parent2329178
6 files changed+325−12
internal/tui/components/core/list/list.go+9 internal/tui/components/dialogs/commands/arguments.go+168−6 internal/tui/components/dialogs/commands/commands.go+41−5 internal/tui/components/dialogs/commands/item.go+6 internal/tui/components/dialogs/commands/keys.go+92 internal/tui/tui.go+9−1
internal/tui/components/core/list/list.go+9
@@ -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+168−6
@@ -1,11 +1,18 @@
11package commands
22
33import (
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
1118const (
@@ -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.
56150func (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
60222func (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+41−5
@@ -1,6 +1,7 @@
11package commands
22
33import (
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
4245func 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
87123func (c *commandDialogCmp) View() tea.View {
internal/tui/components/dialogs/commands/item.go+6
@@ -18,6 +18,7 @@ type CommandItem interface {
1818 util.Model
1919 layout.Focusable
2020 layout.Sizeable
21+ Command() Command
2122}
2223
2324type 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.
7682func (c *commandItem) Blur() tea.Cmd {
7783 c.focus = false
internal/tui/components/dialogs/commands/keys.go+92
@@ -1 +1,93 @@
11package commands
2+
3+import (
4+ "github.com/charmbracelet/bubbles/v2/key"
5+ "github.com/opencode-ai/opencode/internal/tui/layout"
6+)
7+
8+type CommandsDialogKeyMap struct {
9+ Select key.Binding
10+ Next key.Binding
11+ Previous key.Binding
12+}
13+
14+func DefaultCommandsDialogKeyMap() CommandsDialogKeyMap {
15+ return CommandsDialogKeyMap{
16+ Select: key.NewBinding(
17+ key.WithKeys("enter"),
18+ key.WithHelp("enter", "confirm"),
19+ ),
20+ Next: key.NewBinding(
21+ key.WithKeys("tab", "down"),
22+ key.WithHelp("tab/↓", "next"),
23+ ),
24+ Previous: key.NewBinding(
25+ key.WithKeys("shift+tab", "up"),
26+ key.WithHelp("shift+tab/↑", "previous"),
27+ ),
28+ }
29+}
30+
31+// FullHelp implements help.KeyMap.
32+func (k CommandsDialogKeyMap) FullHelp() [][]key.Binding {
33+ m := [][]key.Binding{}
34+ slice := layout.KeyMapToSlice(k)
35+ for i := 0; i < len(slice); i += 4 {
36+ end := min(i+4, len(slice))
37+ m = append(m, slice[i:end])
38+ }
39+ return m
40+}
41+
42+// ShortHelp implements help.KeyMap.
43+func (k CommandsDialogKeyMap) ShortHelp() []key.Binding {
44+ return []key.Binding{
45+ k.Select,
46+ k.Next,
47+ k.Previous,
48+ }
49+}
50+
51+type ArgumentsDialogKeyMap struct {
52+ Confirm key.Binding
53+ Next key.Binding
54+ Previous key.Binding
55+}
56+
57+func DefaultArgumentsDialogKeyMap() ArgumentsDialogKeyMap {
58+ return ArgumentsDialogKeyMap{
59+ Confirm: key.NewBinding(
60+ key.WithKeys("enter"),
61+ key.WithHelp("enter", "confirm"),
62+ ),
63+
64+ Next: key.NewBinding(
65+ key.WithKeys("tab", "down"),
66+ key.WithHelp("tab/↓", "next"),
67+ ),
68+ Previous: key.NewBinding(
69+ key.WithKeys("shift+tab", "up"),
70+ key.WithHelp("shift+tab/↑", "previous"),
71+ ),
72+ }
73+}
74+
75+// FullHelp implements help.KeyMap.
76+func (k ArgumentsDialogKeyMap) FullHelp() [][]key.Binding {
77+ m := [][]key.Binding{}
78+ slice := layout.KeyMapToSlice(k)
79+ for i := 0; i < len(slice); i += 4 {
80+ end := min(i+4, len(slice))
81+ m = append(m, slice[i:end])
82+ }
83+ return m
84+}
85+
86+// ShortHelp implements help.KeyMap.
87+func (k ArgumentsDialogKeyMap) ShortHelp() []key.Binding {
88+ return []key.Binding{
89+ k.Confirm,
90+ k.Next,
91+ k.Previous,
92+ }
93+}
internal/tui/tui.go+9−1
@@ -58,7 +58,15 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5858 a.dialog = u.(dialogs.DialogCmp)
5959 return a, dialogCmd
6060 case commands.ShowArgumentsDialogMsg:
61−
61+ return a, util.CmdHandler(
62+ dialogs.OpenDialogMsg{
63+ Model: commands.NewCommandArgumentsDialog(
64+ msg.CommandID,
65+ msg.Content,
66+ msg.ArgNames,
67+ ),
68+ },
69+ )
6270 // Page change messages
6371 case page.PageChangeMsg:
6472 return a, a.moveToPage(msg.ID)