commands.go

  1package commands
  2
  3import (
  4	"github.com/charmbracelet/bubbles/v2/textinput"
  5	tea "github.com/charmbracelet/bubbletea/v2"
  6	"github.com/charmbracelet/lipgloss/v2"
  7
  8	"github.com/opencode-ai/opencode/internal/logging"
  9	"github.com/opencode-ai/opencode/internal/tui/components/core/list"
 10	"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
 11	"github.com/opencode-ai/opencode/internal/tui/styles"
 12	"github.com/opencode-ai/opencode/internal/tui/theme"
 13	"github.com/opencode-ai/opencode/internal/tui/util"
 14)
 15
 16const (
 17	id dialogs.DialogID = "commands"
 18)
 19
 20// Command represents a command that can be executed
 21type Command struct {
 22	ID          string
 23	Title       string
 24	Description string
 25	Handler     func(cmd Command) tea.Cmd
 26}
 27
 28// CommandsDialog represents the commands dialog.
 29type CommandsDialog interface {
 30	dialogs.DialogModel
 31}
 32
 33type commandDialogCmp struct {
 34	width   int
 35	wWidth  int // Width of the terminal window
 36	wHeight int // Height of the terminal window
 37
 38	commandList list.ListModel
 39	input       textinput.Model
 40	oldCursor   tea.Cursor
 41}
 42
 43func NewCommandDialog() CommandsDialog {
 44	ti := textinput.New()
 45	ti.Placeholder = "Type a command or search..."
 46	ti.SetVirtualCursor(false)
 47	ti.Focus()
 48	ti.SetWidth(60 - 7)
 49	commandList := list.New()
 50	return &commandDialogCmp{
 51		commandList: commandList,
 52		width:       60,
 53		input:       ti,
 54	}
 55}
 56
 57func (c *commandDialogCmp) Init() tea.Cmd {
 58	logging.Info("Initializing commands dialog")
 59	commands, err := LoadCustomCommands()
 60	if err != nil {
 61		return util.ReportError(err)
 62	}
 63	logging.Info("Commands loaded", "count", len(commands))
 64
 65	commandItems := make([]util.Model, 0, len(commands))
 66
 67	for _, cmd := range commands {
 68		commandItems = append(commandItems, NewCommandItem(cmd))
 69	}
 70	c.commandList.SetItems(commandItems)
 71	return c.commandList.Init()
 72}
 73
 74func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 75	switch msg := msg.(type) {
 76	case tea.WindowSizeMsg:
 77		c.wWidth = msg.Width
 78		c.wHeight = msg.Height
 79		return c, c.commandList.SetSize(60, min(len(c.commandList.Items())*2, c.wHeight/2))
 80	}
 81	u, cmd := c.input.Update(msg)
 82	c.input = u
 83	return c, cmd
 84}
 85
 86func (c *commandDialogCmp) View() tea.View {
 87	content := lipgloss.JoinVertical(
 88		lipgloss.Left,
 89		c.inputStyle().Render(c.input.View()),
 90		c.commandList.View().String(),
 91	)
 92
 93	v := tea.NewView(c.style().Render(content))
 94	v.SetCursor(c.getCursor())
 95	return v
 96}
 97
 98func (c *commandDialogCmp) getCursor() *tea.Cursor {
 99	cursor := c.input.Cursor()
100	offset := 10 + 1
101	cursor.Y += offset
102	_, col := c.Position()
103	cursor.X = c.input.Cursor().X + col + 2
104	return cursor
105}
106
107func (c *commandDialogCmp) inputStyle() lipgloss.Style {
108	t := theme.CurrentTheme()
109	return styles.BaseStyle().
110		BorderStyle(lipgloss.NormalBorder()).
111		BorderForeground(t.TextMuted()).
112		BorderBackground(t.Background()).
113		BorderBottom(true)
114}
115
116func (c *commandDialogCmp) style() lipgloss.Style {
117	t := theme.CurrentTheme()
118	return styles.BaseStyle().
119		Width(c.width).
120		Padding(0, 1, 1, 1).
121		Border(lipgloss.RoundedBorder()).
122		BorderBackground(t.Background()).
123		BorderForeground(t.TextMuted())
124}
125
126func (q *commandDialogCmp) Position() (int, int) {
127	row := 10
128	col := q.wWidth / 2
129	col -= q.width / 2
130	return row, col
131}
132
133func (c *commandDialogCmp) ID() dialogs.DialogID {
134	return id
135}