commands.go 4.1 KB

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