summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/components/dialog
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui/components/dialog')
-rw-r--r--internal/tui/components/dialog/commands.go191
-rw-r--r--internal/tui/components/dialog/complete.go265
2 files changed, 326 insertions, 130 deletions
diff --git a/internal/tui/components/dialog/commands.go b/internal/tui/components/dialog/commands.go
index 17f430504..b989154c6 100644
--- a/internal/tui/components/dialog/commands.go
+++ b/internal/tui/components/dialog/commands.go
@@ -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,
}
}
diff --git a/internal/tui/components/dialog/complete.go b/internal/tui/components/dialog/complete.go
new file mode 100644
index 000000000..88e2c3c37
--- /dev/null
+++ b/internal/tui/components/dialog/complete.go
@@ -0,0 +1,265 @@
+package dialog
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/textarea"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/logging"
+ "github.com/sst/opencode/internal/status"
+ 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"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+type CompletionItem struct {
+ title string
+ Title string
+ Value string
+}
+
+type CompletionItemI interface {
+ utilComponents.SimpleListItem
+ GetValue() string
+ DisplayValue() string
+}
+
+func (ci *CompletionItem) Render(selected bool, width int) string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ itemStyle := baseStyle.
+ Width(width).
+ Padding(0, 1)
+
+ if selected {
+ itemStyle = itemStyle.
+ Background(t.Background()).
+ Foreground(t.Primary()).
+ Bold(true)
+ }
+
+ title := itemStyle.Render(
+ ci.GetValue(),
+ )
+
+ return title
+}
+
+func (ci *CompletionItem) DisplayValue() string {
+ return ci.Title
+}
+
+func (ci *CompletionItem) GetValue() string {
+ return ci.Value
+}
+
+func NewCompletionItem(completionItem CompletionItem) CompletionItemI {
+ return &completionItem
+}
+
+type CompletionProvider interface {
+ GetId() string
+ GetEntry() CompletionItemI
+ GetChildEntries(query string) ([]CompletionItemI, error)
+}
+
+type CompletionSelectedMsg struct {
+ SearchString string
+ CompletionValue string
+}
+
+type CompletionDialogCompleteItemMsg struct {
+ Value string
+}
+
+type CompletionDialogCloseMsg struct{}
+
+type CompletionDialog interface {
+ tea.Model
+ layout.Bindings
+ SetWidth(width int)
+}
+
+type completionDialogCmp struct {
+ query string
+ completionProvider CompletionProvider
+ width int
+ height int
+ pseudoSearchTextArea textarea.Model
+ listView utilComponents.SimpleList[CompletionItemI]
+}
+
+type completionDialogKeyMap struct {
+ Complete key.Binding
+ Cancel key.Binding
+}
+
+var completionDialogKeys = completionDialogKeyMap{
+ Complete: key.NewBinding(
+ key.WithKeys("tab", "enter"),
+ ),
+ Cancel: key.NewBinding(
+ key.WithKeys(" ", "esc", "backspace"),
+ ),
+}
+
+func (c *completionDialogCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (c *completionDialogCmp) complete(item CompletionItemI) tea.Cmd {
+ value := c.pseudoSearchTextArea.Value()
+
+ if value == "" {
+ return nil
+ }
+
+ return tea.Batch(
+ util.CmdHandler(CompletionSelectedMsg{
+ SearchString: value,
+ CompletionValue: item.GetValue(),
+ }),
+ c.close(),
+ )
+}
+
+func (c *completionDialogCmp) close() tea.Cmd {
+ c.listView.SetItems([]CompletionItemI{})
+ c.pseudoSearchTextArea.Reset()
+ c.pseudoSearchTextArea.Blur()
+
+ return util.CmdHandler(CompletionDialogCloseMsg{})
+}
+
+func (c *completionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ if c.pseudoSearchTextArea.Focused() {
+
+ if !key.Matches(msg, completionDialogKeys.Complete) {
+
+ var cmd tea.Cmd
+ c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
+ cmds = append(cmds, cmd)
+
+ var query string
+ query = c.pseudoSearchTextArea.Value()
+ if query != "" {
+ query = query[1:]
+ }
+
+ if query != c.query {
+ logging.Info("Query", query)
+ items, err := c.completionProvider.GetChildEntries(query)
+ if err != nil {
+ logging.Error("Failed to get child entries", err)
+ }
+
+ c.listView.SetItems(items)
+ c.query = query
+ }
+
+ u, cmd := c.listView.Update(msg)
+ c.listView = u.(utilComponents.SimpleList[CompletionItemI])
+
+ cmds = append(cmds, cmd)
+ }
+
+ switch {
+ case key.Matches(msg, completionDialogKeys.Complete):
+ item, i := c.listView.GetSelectedItem()
+ if i == -1 {
+ return c, nil
+ }
+
+ cmd := c.complete(item)
+
+ return c, cmd
+ case key.Matches(msg, completionDialogKeys.Cancel):
+ // Only close on backspace when there are no characters left
+ if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
+ return c, c.close()
+ }
+ }
+
+ return c, tea.Batch(cmds...)
+ } else {
+ items, err := c.completionProvider.GetChildEntries("")
+ if err != nil {
+ status.Error(err.Error())
+ }
+
+ c.listView.SetItems(items)
+ c.pseudoSearchTextArea.SetValue(msg.String())
+ return c, c.pseudoSearchTextArea.Focus()
+ }
+ case tea.WindowSizeMsg:
+ c.width = msg.Width
+ c.height = msg.Height
+ }
+
+ return c, tea.Batch(cmds...)
+}
+
+func (c *completionDialogCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ maxWidth := 40
+
+ completions := c.listView.GetItems()
+
+ for _, cmd := range completions {
+ title := cmd.DisplayValue()
+ if len(title) > maxWidth-4 {
+ maxWidth = len(title) + 4
+ }
+ }
+
+ c.listView.SetMaxWidth(maxWidth)
+
+ return baseStyle.Padding(0, 0).
+ Border(lipgloss.NormalBorder()).
+ BorderBottom(false).
+ BorderRight(false).
+ BorderLeft(false).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Width(c.width).
+ Render(c.listView.View())
+}
+
+func (c *completionDialogCmp) SetWidth(width int) {
+ c.width = width
+}
+
+func (c *completionDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(completionDialogKeys)
+}
+
+func NewCompletionDialogCmp(completionProvider CompletionProvider) CompletionDialog {
+ ti := textarea.New()
+
+ items, err := completionProvider.GetChildEntries("")
+ if err != nil {
+ status.Error(err.Error())
+ }
+
+ li := utilComponents.NewSimpleList(
+ items,
+ 7,
+ "No file matches found",
+ false,
+ )
+
+ return &completionDialogCmp{
+ query: "",
+ completionProvider: completionProvider,
+ pseudoSearchTextArea: ti,
+ listView: li,
+ }
+}