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}