commands.go

  1package commands
  2
  3import (
  4	"github.com/charmbracelet/bubbles/v2/key"
  5	tea "github.com/charmbracelet/bubbletea/v2"
  6	"github.com/charmbracelet/lipgloss/v2"
  7
  8	"github.com/opencode-ai/opencode/internal/tui/components/chat"
  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	commandsDialogID dialogs.DialogID = "commands"
 18
 19	defaultWidth int = 60
 20)
 21
 22// Command represents a command that can be executed
 23type Command struct {
 24	ID          string
 25	Title       string
 26	Description string
 27	Handler     func(cmd Command) tea.Cmd
 28}
 29
 30// CommandsDialog represents the commands dialog.
 31type CommandsDialog interface {
 32	dialogs.DialogModel
 33}
 34
 35type commandDialogCmp struct {
 36	width   int
 37	wWidth  int // Width of the terminal window
 38	wHeight int // Height of the terminal window
 39
 40	commandList list.ListModel
 41	commands    []Command
 42	keyMap      CommandsDialogKeyMap
 43}
 44
 45func NewCommandDialog() CommandsDialog {
 46	listKeyMap := list.DefaultKeyMap()
 47	keyMap := DefaultCommandsDialogKeyMap()
 48
 49	listKeyMap.Down.SetEnabled(false)
 50	listKeyMap.Up.SetEnabled(false)
 51	listKeyMap.NDown.SetEnabled(false)
 52	listKeyMap.NUp.SetEnabled(false)
 53	listKeyMap.HalfPageDown.SetEnabled(false)
 54	listKeyMap.HalfPageUp.SetEnabled(false)
 55	listKeyMap.Home.SetEnabled(false)
 56	listKeyMap.End.SetEnabled(false)
 57
 58	listKeyMap.DownOneItem = keyMap.Next
 59	listKeyMap.UpOneItem = keyMap.Previous
 60
 61	commandList := list.New(list.WithFilterable(true), list.WithKeyMap(listKeyMap))
 62	return &commandDialogCmp{
 63		commandList: commandList,
 64		width:       defaultWidth,
 65		keyMap:      DefaultCommandsDialogKeyMap(),
 66	}
 67}
 68
 69func (c *commandDialogCmp) Init() tea.Cmd {
 70	commands, err := LoadCustomCommands()
 71	if err != nil {
 72		return util.ReportError(err)
 73	}
 74	c.commands = commands
 75
 76	commandItems := []util.Model{}
 77	if len(commands) > 0 {
 78		commandItems = append(commandItems, NewItemSection("Custom"))
 79		for _, cmd := range commands {
 80			commandItems = append(commandItems, NewCommandItem(cmd))
 81		}
 82	}
 83
 84	commandItems = append(commandItems, NewItemSection("Default"))
 85
 86	for _, cmd := range c.defaultCommands() {
 87		c.commands = append(c.commands, cmd)
 88		commandItems = append(commandItems, NewCommandItem(cmd))
 89	}
 90
 91	c.commandList.SetItems(commandItems)
 92	return c.commandList.Init()
 93}
 94
 95func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 96	switch msg := msg.(type) {
 97	case tea.WindowSizeMsg:
 98		c.wWidth = msg.Width
 99		c.wHeight = msg.Height
100		return c, c.commandList.SetSize(c.listWidth(), c.listHeight())
101	case tea.KeyPressMsg:
102		switch {
103		case key.Matches(msg, c.keyMap.Select):
104			selectedItemInx := c.commandList.SelectedIndex()
105			if selectedItemInx == list.NoSelection {
106				return c, nil // No item selected, do nothing
107			}
108			items := c.commandList.Items()
109			selectedItem := items[selectedItemInx].(CommandItem).Command()
110			return c, tea.Sequence(
111				util.CmdHandler(dialogs.CloseDialogMsg{}),
112				selectedItem.Handler(selectedItem),
113			)
114		default:
115			u, cmd := c.commandList.Update(msg)
116			c.commandList = u.(list.ListModel)
117			return c, cmd
118		}
119	}
120	return c, nil
121}
122
123func (c *commandDialogCmp) View() tea.View {
124	listView := c.commandList.View()
125	v := tea.NewView(c.style().Render(listView.String()))
126	if listView.Cursor() != nil {
127		c := c.moveCursor(listView.Cursor())
128		v.SetCursor(c)
129	}
130	return v
131}
132
133func (c *commandDialogCmp) listWidth() int {
134	return defaultWidth - 4 // 4 for padding
135}
136
137func (c *commandDialogCmp) listHeight() int {
138	listHeigh := len(c.commandList.Items()) + 2 + 4 // height based on items + 2 for the input + 4 for the sections
139	return min(listHeigh, c.wHeight/2)
140}
141
142func (c *commandDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
143	offset := 10 + 1
144	cursor.Y += offset
145	_, col := c.Position()
146	cursor.X = cursor.X + col + 2
147	return cursor
148}
149
150func (c *commandDialogCmp) style() lipgloss.Style {
151	t := theme.CurrentTheme()
152	return styles.BaseStyle().
153		Width(c.width).
154		Padding(0, 1, 1, 1).
155		Border(lipgloss.RoundedBorder()).
156		BorderBackground(t.Background()).
157		BorderForeground(t.TextMuted())
158}
159
160func (q *commandDialogCmp) Position() (int, int) {
161	row := 10
162	col := q.wWidth / 2
163	col -= q.width / 2
164	return row, col
165}
166
167func (c *commandDialogCmp) defaultCommands() []Command {
168	return []Command{
169		{
170			ID:          "init",
171			Title:       "Initialize Project",
172			Description: "Create/Update the OpenCode.md memory file",
173			Handler: func(cmd Command) tea.Cmd {
174				prompt := `Please analyze this codebase and create a OpenCode.md file containing:
175	1. Build/lint/test commands - especially for running a single test
176	2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
177
178	The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
179	If there's already a opencode.md, improve it.
180	If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
181				return tea.Batch(
182					util.CmdHandler(chat.SendMsg{
183						Text: prompt,
184					}),
185				)
186			},
187		},
188		{
189			ID:          "compact",
190			Title:       "Compact Session",
191			Description: "Summarize the current session and create a new one with the summary",
192			Handler: func(cmd Command) tea.Cmd {
193				return func() tea.Msg {
194					// TODO: implement compact message
195					return ""
196				}
197			},
198		},
199	}
200}
201
202func (c *commandDialogCmp) ID() dialogs.DialogID {
203	return commandsDialogID
204}