|
|
@@ -4,6 +4,7 @@ import (
|
|
|
"github.com/charmbracelet/bubbles/key"
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
+ utilComponents "github.com/sst/opencode/internal/tui/components/util"
|
|
|
"github.com/sst/opencode/internal/tui/layout"
|
|
|
"github.com/sst/opencode/internal/tui/styles"
|
|
|
"github.com/sst/opencode/internal/tui/theme"
|
|
|
@@ -18,6 +19,33 @@ type Command struct {
|
|
|
Handler func(cmd Command) tea.Cmd
|
|
|
}
|
|
|
|
|
|
+func (ci Command) Render(selected bool, width int) string {
|
|
|
+ t := theme.CurrentTheme()
|
|
|
+ baseStyle := styles.BaseStyle()
|
|
|
+
|
|
|
+ descStyle := baseStyle.Width(width).Foreground(t.TextMuted())
|
|
|
+ itemStyle := baseStyle.Width(width).
|
|
|
+ Foreground(t.Text()).
|
|
|
+ Background(t.Background())
|
|
|
+
|
|
|
+ if selected {
|
|
|
+ itemStyle = itemStyle.
|
|
|
+ Background(t.Primary()).
|
|
|
+ Foreground(t.Background()).
|
|
|
+ Bold(true)
|
|
|
+ descStyle = descStyle.
|
|
|
+ Background(t.Primary()).
|
|
|
+ Foreground(t.Background())
|
|
|
+ }
|
|
|
+
|
|
|
+ title := itemStyle.Padding(0, 1).Render(ci.Title)
|
|
|
+ if ci.Description != "" {
|
|
|
+ description := descStyle.Padding(0, 1).Render(ci.Description)
|
|
|
+ return lipgloss.JoinVertical(lipgloss.Left, title, description)
|
|
|
+ }
|
|
|
+ return title
|
|
|
+}
|
|
|
+
|
|
|
// CommandSelectedMsg is sent when a command is selected
|
|
|
type CommandSelectedMsg struct {
|
|
|
Command Command
|
|
|
@@ -31,35 +59,20 @@ type CommandDialog interface {
|
|
|
tea.Model
|
|
|
layout.Bindings
|
|
|
SetCommands(commands []Command)
|
|
|
- SetSelectedCommand(commandID string)
|
|
|
}
|
|
|
|
|
|
type commandDialogCmp struct {
|
|
|
- commands []Command
|
|
|
- selectedIdx int
|
|
|
- width int
|
|
|
- height int
|
|
|
- selectedCommandID string
|
|
|
+ listView utilComponents.SimpleList[Command]
|
|
|
+ width int
|
|
|
+ height int
|
|
|
}
|
|
|
|
|
|
type commandKeyMap struct {
|
|
|
- Up key.Binding
|
|
|
- Down key.Binding
|
|
|
Enter key.Binding
|
|
|
Escape key.Binding
|
|
|
- J key.Binding
|
|
|
- K key.Binding
|
|
|
}
|
|
|
|
|
|
var commandKeys = commandKeyMap{
|
|
|
- Up: key.NewBinding(
|
|
|
- key.WithKeys("up"),
|
|
|
- key.WithHelp("↑", "previous command"),
|
|
|
- ),
|
|
|
- Down: key.NewBinding(
|
|
|
- key.WithKeys("down"),
|
|
|
- key.WithHelp("↓", "next command"),
|
|
|
- ),
|
|
|
Enter: key.NewBinding(
|
|
|
key.WithKeys("enter"),
|
|
|
key.WithHelp("enter", "select command"),
|
|
|
@@ -68,38 +81,22 @@ var commandKeys = commandKeyMap{
|
|
|
key.WithKeys("esc"),
|
|
|
key.WithHelp("esc", "close"),
|
|
|
),
|
|
|
- J: key.NewBinding(
|
|
|
- key.WithKeys("j"),
|
|
|
- key.WithHelp("j", "next command"),
|
|
|
- ),
|
|
|
- K: key.NewBinding(
|
|
|
- key.WithKeys("k"),
|
|
|
- key.WithHelp("k", "previous command"),
|
|
|
- ),
|
|
|
}
|
|
|
|
|
|
func (c *commandDialogCmp) Init() tea.Cmd {
|
|
|
- return nil
|
|
|
+ return c.listView.Init()
|
|
|
}
|
|
|
|
|
|
func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
+ var cmds []tea.Cmd
|
|
|
switch msg := msg.(type) {
|
|
|
case tea.KeyMsg:
|
|
|
switch {
|
|
|
- case key.Matches(msg, commandKeys.Up) || key.Matches(msg, commandKeys.K):
|
|
|
- if c.selectedIdx > 0 {
|
|
|
- c.selectedIdx--
|
|
|
- }
|
|
|
- return c, nil
|
|
|
- case key.Matches(msg, commandKeys.Down) || key.Matches(msg, commandKeys.J):
|
|
|
- if c.selectedIdx < len(c.commands)-1 {
|
|
|
- c.selectedIdx++
|
|
|
- }
|
|
|
- return c, nil
|
|
|
case key.Matches(msg, commandKeys.Enter):
|
|
|
- if len(c.commands) > 0 {
|
|
|
+ selectedItem, idx := c.listView.GetSelectedItem()
|
|
|
+ if idx != -1 {
|
|
|
return c, util.CmdHandler(CommandSelectedMsg{
|
|
|
- Command: c.commands[c.selectedIdx],
|
|
|
+ Command: selectedItem,
|
|
|
})
|
|
|
}
|
|
|
case key.Matches(msg, commandKeys.Escape):
|
|
|
@@ -109,78 +106,35 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
c.width = msg.Width
|
|
|
c.height = msg.Height
|
|
|
}
|
|
|
- return c, nil
|
|
|
+
|
|
|
+ u, cmd := c.listView.Update(msg)
|
|
|
+ c.listView = u.(utilComponents.SimpleList[Command])
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+
|
|
|
+ return c, tea.Batch(cmds...)
|
|
|
}
|
|
|
|
|
|
func (c *commandDialogCmp) View() string {
|
|
|
t := theme.CurrentTheme()
|
|
|
baseStyle := styles.BaseStyle()
|
|
|
|
|
|
- if len(c.commands) == 0 {
|
|
|
- return baseStyle.Padding(1, 2).
|
|
|
- Border(lipgloss.RoundedBorder()).
|
|
|
- BorderBackground(t.Background()).
|
|
|
- BorderForeground(t.TextMuted()).
|
|
|
- Width(40).
|
|
|
- Render("No commands available")
|
|
|
- }
|
|
|
+ maxWidth := 40
|
|
|
|
|
|
- // Calculate max width needed for command titles
|
|
|
- maxWidth := 40 // Minimum width
|
|
|
- for _, cmd := range c.commands {
|
|
|
- if len(cmd.Title) > maxWidth-4 { // Account for padding
|
|
|
- maxWidth = len(cmd.Title) + 4
|
|
|
- }
|
|
|
- if len(cmd.Description) > maxWidth-4 {
|
|
|
- maxWidth = len(cmd.Description) + 4
|
|
|
- }
|
|
|
- }
|
|
|
+ commands := c.listView.GetItems()
|
|
|
|
|
|
- // Limit height to avoid taking up too much screen space
|
|
|
- maxVisibleCommands := min(10, len(c.commands))
|
|
|
-
|
|
|
- // Build the command list
|
|
|
- commandItems := make([]string, 0, maxVisibleCommands)
|
|
|
- startIdx := 0
|
|
|
-
|
|
|
- // If we have more commands than can be displayed, adjust the start index
|
|
|
- if len(c.commands) > maxVisibleCommands {
|
|
|
- // Center the selected item when possible
|
|
|
- halfVisible := maxVisibleCommands / 2
|
|
|
- if c.selectedIdx >= halfVisible && c.selectedIdx < len(c.commands)-halfVisible {
|
|
|
- startIdx = c.selectedIdx - halfVisible
|
|
|
- } else if c.selectedIdx >= len(c.commands)-halfVisible {
|
|
|
- startIdx = len(c.commands) - maxVisibleCommands
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- endIdx := min(startIdx+maxVisibleCommands, len(c.commands))
|
|
|
-
|
|
|
- for i := startIdx; i < endIdx; i++ {
|
|
|
- cmd := c.commands[i]
|
|
|
- itemStyle := baseStyle.Width(maxWidth)
|
|
|
- descStyle := baseStyle.Width(maxWidth).Foreground(t.TextMuted())
|
|
|
-
|
|
|
- if i == c.selectedIdx {
|
|
|
- itemStyle = itemStyle.
|
|
|
- Background(t.Primary()).
|
|
|
- Foreground(t.Background()).
|
|
|
- Bold(true)
|
|
|
- descStyle = descStyle.
|
|
|
- Background(t.Primary()).
|
|
|
- Foreground(t.Background())
|
|
|
+ for _, cmd := range commands {
|
|
|
+ if len(cmd.Title) > maxWidth-4 {
|
|
|
+ maxWidth = len(cmd.Title) + 4
|
|
|
}
|
|
|
-
|
|
|
- title := itemStyle.Padding(0, 1).Render(cmd.Title)
|
|
|
- description := ""
|
|
|
if cmd.Description != "" {
|
|
|
- description = descStyle.Padding(0, 1).Render(cmd.Description)
|
|
|
- commandItems = append(commandItems, lipgloss.JoinVertical(lipgloss.Left, title, description))
|
|
|
- } else {
|
|
|
- commandItems = append(commandItems, title)
|
|
|
+ if len(cmd.Description) > maxWidth-4 {
|
|
|
+ maxWidth = len(cmd.Description) + 4
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ c.listView.SetMaxWidth(maxWidth)
|
|
|
+
|
|
|
title := baseStyle.
|
|
|
Foreground(t.Primary()).
|
|
|
Bold(true).
|
|
|
@@ -192,7 +146,7 @@ func (c *commandDialogCmp) View() string {
|
|
|
lipgloss.Left,
|
|
|
title,
|
|
|
baseStyle.Width(maxWidth).Render(""),
|
|
|
- baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
|
|
|
+ baseStyle.Width(maxWidth).Render(c.listView.View()),
|
|
|
baseStyle.Width(maxWidth).Render(""),
|
|
|
)
|
|
|
|
|
|
@@ -209,41 +163,18 @@ func (c *commandDialogCmp) BindingKeys() []key.Binding {
|
|
|
}
|
|
|
|
|
|
func (c *commandDialogCmp) SetCommands(commands []Command) {
|
|
|
- c.commands = commands
|
|
|
-
|
|
|
- // If we have a selected command ID, find its index
|
|
|
- if c.selectedCommandID != "" {
|
|
|
- for i, cmd := range commands {
|
|
|
- if cmd.ID == c.selectedCommandID {
|
|
|
- c.selectedIdx = i
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Default to first command if selected not found
|
|
|
- c.selectedIdx = 0
|
|
|
-}
|
|
|
-
|
|
|
-func (c *commandDialogCmp) SetSelectedCommand(commandID string) {
|
|
|
- c.selectedCommandID = commandID
|
|
|
-
|
|
|
- // Update the selected index if commands are already loaded
|
|
|
- if len(c.commands) > 0 {
|
|
|
- for i, cmd := range c.commands {
|
|
|
- if cmd.ID == commandID {
|
|
|
- c.selectedIdx = i
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ c.listView.SetItems(commands)
|
|
|
}
|
|
|
|
|
|
// NewCommandDialogCmp creates a new command selection dialog
|
|
|
func NewCommandDialogCmp() CommandDialog {
|
|
|
+ listView := utilComponents.NewSimpleList[Command](
|
|
|
+ []Command{},
|
|
|
+ 10,
|
|
|
+ "No commands available",
|
|
|
+ true,
|
|
|
+ )
|
|
|
return &commandDialogCmp{
|
|
|
- commands: []Command{},
|
|
|
- selectedIdx: 0,
|
|
|
- selectedCommandID: "",
|
|
|
+ listView: listView,
|
|
|
}
|
|
|
}
|