commands.go

  1package dialog
  2
  3import (
  4	"github.com/charmbracelet/bubbles/key"
  5	tea "github.com/charmbracelet/bubbletea"
  6	"github.com/charmbracelet/lipgloss"
  7	utilComponents "github.com/opencode-ai/opencode/internal/tui/components/util"
  8	"github.com/opencode-ai/opencode/internal/tui/layout"
  9	"github.com/opencode-ai/opencode/internal/tui/styles"
 10	"github.com/opencode-ai/opencode/internal/tui/theme"
 11	"github.com/opencode-ai/opencode/internal/tui/util"
 12)
 13
 14// Command represents a command that can be executed
 15type Command struct {
 16	ID          string
 17	Title       string
 18	Description string
 19	Handler     func(cmd Command) tea.Cmd
 20}
 21
 22func (ci Command) Render(selected bool, width int) string {
 23	t := theme.CurrentTheme()
 24	baseStyle := styles.BaseStyle()
 25
 26	descStyle := baseStyle.Width(width).Foreground(t.TextMuted())
 27	itemStyle := baseStyle.Width(width).
 28		Foreground(t.Text()).
 29		Background(t.Background())
 30
 31	if selected {
 32		itemStyle = itemStyle.
 33			Background(t.Primary()).
 34			Foreground(t.Background()).
 35			Bold(true)
 36		descStyle = descStyle.
 37			Background(t.Primary()).
 38			Foreground(t.Background())
 39	}
 40
 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)
 45	}
 46	return title
 47}
 48
 49// CommandSelectedMsg is sent when a command is selected
 50type CommandSelectedMsg struct {
 51	Command Command
 52}
 53
 54// CloseCommandDialogMsg is sent when the command dialog is closed
 55type CloseCommandDialogMsg struct{}
 56
 57// CommandDialog interface for the command selection dialog
 58type CommandDialog interface {
 59	tea.Model
 60	layout.Bindings
 61	SetCommands(commands []Command)
 62}
 63
 64type commandDialogCmp struct {
 65	listView utilComponents.SimpleList[Command]
 66	width    int
 67	height   int
 68}
 69
 70type commandKeyMap struct {
 71	Enter  key.Binding
 72	Escape key.Binding
 73}
 74
 75var commandKeys = commandKeyMap{
 76	Enter: key.NewBinding(
 77		key.WithKeys("enter"),
 78		key.WithHelp("enter", "select command"),
 79	),
 80	Escape: key.NewBinding(
 81		key.WithKeys("esc"),
 82		key.WithHelp("esc", "close"),
 83	),
 84}
 85
 86func (c *commandDialogCmp) Init() tea.Cmd {
 87	return c.listView.Init()
 88}
 89
 90func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 91	var cmds []tea.Cmd
 92	switch msg := msg.(type) {
 93	case tea.KeyMsg:
 94		switch {
 95		case key.Matches(msg, commandKeys.Enter):
 96			selectedItem, idx := c.listView.GetSelectedItem()
 97			if idx != -1 {
 98				return c, util.CmdHandler(CommandSelectedMsg{
 99					Command: selectedItem,
100				})
101			}
102		case key.Matches(msg, commandKeys.Escape):
103			return c, util.CmdHandler(CloseCommandDialogMsg{})
104		}
105	case tea.WindowSizeMsg:
106		c.width = msg.Width
107		c.height = msg.Height
108	}
109
110	u, cmd := c.listView.Update(msg)
111	c.listView = u.(utilComponents.SimpleList[Command])
112	cmds = append(cmds, cmd)
113
114	return c, tea.Batch(cmds...)
115}
116
117func (c *commandDialogCmp) View() string {
118	t := theme.CurrentTheme()
119	baseStyle := styles.BaseStyle()
120
121	maxWidth := 40
122
123	commands := c.listView.GetItems()
124
125	for _, cmd := range commands {
126		if len(cmd.Title) > maxWidth-4 {
127			maxWidth = len(cmd.Title) + 4
128		}
129		if cmd.Description != "" {
130			if len(cmd.Description) > maxWidth-4 {
131				maxWidth = len(cmd.Description) + 4
132			}
133		}
134	}
135
136	c.listView.SetMaxWidth(maxWidth)
137
138	title := baseStyle.
139		Foreground(t.Primary()).
140		Bold(true).
141		Width(maxWidth).
142		Padding(0, 1).
143		Render("Commands")
144
145	content := lipgloss.JoinVertical(
146		lipgloss.Left,
147		title,
148		baseStyle.Width(maxWidth).Render(""),
149		baseStyle.Width(maxWidth).Render(c.listView.View()),
150		baseStyle.Width(maxWidth).Render(""),
151	)
152
153	return baseStyle.Padding(1, 2).
154		Border(lipgloss.RoundedBorder()).
155		BorderBackground(t.Background()).
156		BorderForeground(t.TextMuted()).
157		Width(lipgloss.Width(content) + 4).
158		Render(content)
159}
160
161func (c *commandDialogCmp) BindingKeys() []key.Binding {
162	return layout.KeyMapToSlice(commandKeys)
163}
164
165func (c *commandDialogCmp) SetCommands(commands []Command) {
166	c.listView.SetItems(commands)
167}
168
169// NewCommandDialogCmp creates a new command selection dialog
170func NewCommandDialogCmp() CommandDialog {
171	listView := utilComponents.NewSimpleList(
172		[]Command{},
173		10,
174		"No commands available",
175		true,
176	)
177	return &commandDialogCmp{
178		listView: listView,
179	}
180}