@@ -4,6 +4,7 @@ import (
4 "github.com/charmbracelet/bubbles/key"
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
6 "github.com/charmbracelet/lipgloss"
7+ utilComponents "github.com/opencode-ai/opencode/internal/tui/components/util"
7 "github.com/opencode-ai/opencode/internal/tui/layout"
8 "github.com/opencode-ai/opencode/internal/tui/layout"
8 "github.com/opencode-ai/opencode/internal/tui/styles"
9 "github.com/opencode-ai/opencode/internal/tui/styles"
9 "github.com/opencode-ai/opencode/internal/tui/theme"
10 "github.com/opencode-ai/opencode/internal/tui/theme"
@@ -18,6 +19,33 @@ type Command struct {
18 Handler func(cmd Command) tea.Cmd
19 Handler func(cmd Command) tea.Cmd
22+func (ci Command) Render(selected bool, width int) string {
23+ t := theme.CurrentTheme()
24+ baseStyle := styles.BaseStyle()
26+ descStyle := baseStyle.Width(width).Foreground(t.TextMuted())
27+ itemStyle := baseStyle.Width(width).
28+ Foreground(t.Text()).
29+ Background(t.Background())
32+ itemStyle = itemStyle.
33+ Background(t.Primary()).
34+ Foreground(t.Background()).
36+ descStyle = descStyle.
37+ Background(t.Primary()).
38+ Foreground(t.Background())
41+ title := itemStyle.Padding(0, 1).Render(ci.Title)
42+ if ci.Description != "" {
43+ description := descStyle.Padding(0, 1).Render(ci.Description)
44+ return lipgloss.JoinVertical(lipgloss.Left, title, description)
21// CommandSelectedMsg is sent when a command is selected
49// CommandSelectedMsg is sent when a command is selected
22type CommandSelectedMsg struct {
50type CommandSelectedMsg struct {
23 Command Command
51 Command Command
@@ -31,35 +59,20 @@ type CommandDialog interface {
32 layout.Bindings
60 layout.Bindings
33 SetCommands(commands []Command)
61 SetCommands(commands []Command)
34− SetSelectedCommand(commandID string)
37type commandDialogCmp struct {
64type commandDialogCmp struct {
38− commands []Command
65+ listView utilComponents.SimpleList[Command]
40− width int
66+ width int
41− height int
67+ height int
42− selectedCommandID string
45type commandKeyMap struct {
70type commandKeyMap struct {
48 Enter key.Binding
71 Enter key.Binding
49 Escape key.Binding
72 Escape key.Binding
54var commandKeys = commandKeyMap{
75var commandKeys = commandKeyMap{
57− key.WithHelp("↑", "previous command"),
59− Down: key.NewBinding(
60− key.WithKeys("down"),
61− key.WithHelp("↓", "next command"),
63 Enter: key.NewBinding(
76 Enter: key.NewBinding(
64 key.WithKeys("enter"),
77 key.WithKeys("enter"),
65 key.WithHelp("enter", "select command"),
78 key.WithHelp("enter", "select command"),
@@ -68,38 +81,22 @@ var commandKeys = commandKeyMap{
68 key.WithKeys("esc"),
81 key.WithKeys("esc"),
69 key.WithHelp("esc", "close"),
82 key.WithHelp("esc", "close"),
73− key.WithHelp("j", "next command"),
77− key.WithHelp("k", "previous command"),
81func (c *commandDialogCmp) Init() tea.Cmd {
86func (c *commandDialogCmp) Init() tea.Cmd {
82− return nil
87+ return c.listView.Init()
85func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
90func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
86 switch msg := msg.(type) {
92 switch msg := msg.(type) {
87 case tea.KeyMsg:
93 case tea.KeyMsg:
89− case key.Matches(msg, commandKeys.Up) || key.Matches(msg, commandKeys.K):
90− if c.selectedIdx > 0 {
94− case key.Matches(msg, commandKeys.Down) || key.Matches(msg, commandKeys.J):
95− if c.selectedIdx < len(c.commands)-1 {
99 case key.Matches(msg, commandKeys.Enter):
95 case key.Matches(msg, commandKeys.Enter):
100− if len(c.commands) > 0 {
96+ selectedItem, idx := c.listView.GetSelectedItem()
101 return c, util.CmdHandler(CommandSelectedMsg{
98 return c, util.CmdHandler(CommandSelectedMsg{
102− Command: c.commands[c.selectedIdx],
99+ Command: selectedItem,
105 case key.Matches(msg, commandKeys.Escape):
102 case key.Matches(msg, commandKeys.Escape):
@@ -109,78 +106,35 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
109 c.width = msg.Width
106 c.width = msg.Width
110 c.height = msg.Height
107 c.height = msg.Height
110+ u, cmd := c.listView.Update(msg)
111+ c.listView = u.(utilComponents.SimpleList[Command])
112+ cmds = append(cmds, cmd)
112− return c, nil
114+ return c, tea.Batch(cmds...)
115func (c *commandDialogCmp) View() string {
117func (c *commandDialogCmp) View() string {
116 t := theme.CurrentTheme()
118 t := theme.CurrentTheme()
117 baseStyle := styles.BaseStyle()
119 baseStyle := styles.BaseStyle()
119− if len(c.commands) == 0 {
120− return baseStyle.Padding(1, 2).
121− Border(lipgloss.RoundedBorder()).
122− BorderBackground(t.Background()).
123− BorderForeground(t.TextMuted()).
125− Render("No commands available")
128− // Calculate max width needed for command titles
129− maxWidth := 40 // Minimum width
121+ maxWidth := 40
130− for _, cmd := range c.commands {
131− if len(cmd.Title) > maxWidth-4 { // Account for padding
132− maxWidth = len(cmd.Title) + 4
134− if len(cmd.Description) > maxWidth-4 {
135− maxWidth = len(cmd.Description) + 4
139− // Limit height to avoid taking up too much screen space
123+ commands := c.listView.GetItems()
140− maxVisibleCommands := min(10, len(c.commands))
142− // Build the command list
143− commandItems := make([]string, 0, maxVisibleCommands)
146− // If we have more commands than can be displayed, adjust the start index
147− if len(c.commands) > maxVisibleCommands {
148− // Center the selected item when possible
149− halfVisible := maxVisibleCommands / 2
150− if c.selectedIdx >= halfVisible && c.selectedIdx < len(c.commands)-halfVisible {
151− startIdx = c.selectedIdx - halfVisible
152− } else if c.selectedIdx >= len(c.commands)-halfVisible {
153− startIdx = len(c.commands) - maxVisibleCommands
157− endIdx := min(startIdx+maxVisibleCommands, len(c.commands))
125+ for _, cmd := range commands {
158−
126+ if len(cmd.Title) > maxWidth-4 {
159− for i := startIdx; i < endIdx; i++ {
127+ maxWidth = len(cmd.Title) + 4
160− cmd := c.commands[i]
161− itemStyle := baseStyle.Width(maxWidth)
162− descStyle := baseStyle.Width(maxWidth).Foreground(t.TextMuted())
164− if i == c.selectedIdx {
165− itemStyle = itemStyle.
166− Background(t.Primary()).
167− Foreground(t.Background()).
169− descStyle = descStyle.
170− Background(t.Primary()).
171− Foreground(t.Background())
174− title := itemStyle.Padding(0, 1).Render(cmd.Title)
176 if cmd.Description != "" {
129 if cmd.Description != "" {
177− description = descStyle.Padding(0, 1).Render(cmd.Description)
130+ if len(cmd.Description) > maxWidth-4 {
178− commandItems = append(commandItems, lipgloss.JoinVertical(lipgloss.Left, title, description))
131+ maxWidth = len(cmd.Description) + 4
180− commandItems = append(commandItems, title)
136+ c.listView.SetMaxWidth(maxWidth)
184 title := baseStyle.
138 title := baseStyle.
185 Foreground(t.Primary()).
139 Foreground(t.Primary()).
186 Bold(true).
140 Bold(true).
@@ -192,7 +146,7 @@ func (c *commandDialogCmp) View() string {
192 lipgloss.Left,
146 lipgloss.Left,
194 baseStyle.Width(maxWidth).Render(""),
148 baseStyle.Width(maxWidth).Render(""),
195− baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
149+ baseStyle.Width(maxWidth).Render(c.listView.View()),
196 baseStyle.Width(maxWidth).Render(""),
150 baseStyle.Width(maxWidth).Render(""),
@@ -209,41 +163,18 @@ func (c *commandDialogCmp) BindingKeys() []key.Binding {
211func (c *commandDialogCmp) SetCommands(commands []Command) {
165func (c *commandDialogCmp) SetCommands(commands []Command) {
212− c.commands = commands
214− // If we have a selected command ID, find its index
215− if c.selectedCommandID != "" {
216− for i, cmd := range commands {
217− if cmd.ID == c.selectedCommandID {
224− // Default to first command if selected not found
228−func (c *commandDialogCmp) SetSelectedCommand(commandID string) {
229− c.selectedCommandID = commandID
231− // Update the selected index if commands are already loaded
232− if len(c.commands) > 0 {
166+ c.listView.SetItems(commands)
233− for i, cmd := range c.commands {
234− if cmd.ID == commandID {
242// NewCommandDialogCmp creates a new command selection dialog
169// NewCommandDialogCmp creates a new command selection dialog
243func NewCommandDialogCmp() CommandDialog {
170func NewCommandDialogCmp() CommandDialog {
171+ listView := utilComponents.NewSimpleList[Command](
174+ "No commands available",
244 return &commandDialogCmp{
177 return &commandDialogCmp{
245− commands: []Command{},
178+ listView: listView,
247− selectedCommandID: "",