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}