commands.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. package commands
  2. import (
  3. "fmt"
  4. "strings"
  5. tea "github.com/charmbracelet/bubbletea/v2"
  6. "github.com/charmbracelet/lipgloss/v2"
  7. "github.com/charmbracelet/lipgloss/v2/compat"
  8. "github.com/sst/opencode/internal/app"
  9. "github.com/sst/opencode/internal/commands"
  10. "github.com/sst/opencode/internal/layout"
  11. "github.com/sst/opencode/internal/styles"
  12. "github.com/sst/opencode/internal/theme"
  13. )
  14. type CommandsComponent interface {
  15. tea.Model
  16. tea.ViewModel
  17. layout.Sizeable
  18. SetBackgroundColor(color compat.AdaptiveColor)
  19. }
  20. type commandsComponent struct {
  21. app *app.App
  22. width, height int
  23. showKeybinds bool
  24. showAll bool
  25. background *compat.AdaptiveColor
  26. limit *int
  27. }
  28. func (c *commandsComponent) SetSize(width, height int) tea.Cmd {
  29. c.width = width
  30. c.height = height
  31. return nil
  32. }
  33. func (c *commandsComponent) GetSize() (int, int) {
  34. return c.width, c.height
  35. }
  36. func (c *commandsComponent) SetBackgroundColor(color compat.AdaptiveColor) {
  37. c.background = &color
  38. }
  39. func (c *commandsComponent) Init() tea.Cmd {
  40. return nil
  41. }
  42. func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  43. switch msg := msg.(type) {
  44. case tea.WindowSizeMsg:
  45. c.width = msg.Width
  46. c.height = msg.Height
  47. }
  48. return c, nil
  49. }
  50. func (c *commandsComponent) View() string {
  51. t := theme.CurrentTheme()
  52. triggerStyle := lipgloss.NewStyle().
  53. Foreground(t.Primary()).
  54. Bold(true)
  55. descriptionStyle := lipgloss.NewStyle().
  56. Foreground(t.Text())
  57. keybindStyle := lipgloss.NewStyle().
  58. Foreground(t.TextMuted())
  59. if c.background != nil {
  60. triggerStyle = triggerStyle.Background(*c.background)
  61. descriptionStyle = descriptionStyle.Background(*c.background)
  62. keybindStyle = keybindStyle.Background(*c.background)
  63. }
  64. var commandsToShow []commands.Command
  65. var triggeredCommands []commands.Command
  66. var untriggeredCommands []commands.Command
  67. for _, cmd := range c.app.Commands.Sorted() {
  68. if c.showAll || cmd.Trigger != "" {
  69. if cmd.Trigger != "" {
  70. triggeredCommands = append(triggeredCommands, cmd)
  71. } else if c.showAll {
  72. untriggeredCommands = append(untriggeredCommands, cmd)
  73. }
  74. }
  75. }
  76. // Combine triggered commands first, then untriggered
  77. commandsToShow = append(commandsToShow, triggeredCommands...)
  78. commandsToShow = append(commandsToShow, untriggeredCommands...)
  79. if c.limit != nil && len(commandsToShow) > *c.limit {
  80. commandsToShow = commandsToShow[:*c.limit]
  81. }
  82. if len(commandsToShow) == 0 {
  83. if c.showAll {
  84. return styles.Muted().Render("No commands available")
  85. }
  86. return styles.Muted().Render("No commands with triggers available")
  87. }
  88. // Calculate column widths
  89. maxTriggerWidth := 0
  90. maxDescriptionWidth := 0
  91. maxKeybindWidth := 0
  92. // Prepare command data
  93. type commandRow struct {
  94. trigger string
  95. description string
  96. keybinds string
  97. }
  98. rows := make([]commandRow, 0, len(commandsToShow))
  99. for _, cmd := range commandsToShow {
  100. trigger := ""
  101. if cmd.Trigger != "" {
  102. trigger = "/" + cmd.Trigger
  103. } else {
  104. trigger = string(cmd.Name)
  105. }
  106. description := cmd.Description
  107. // Format keybindings
  108. var keybindStrs []string
  109. if c.showKeybinds {
  110. for _, kb := range cmd.Keybindings {
  111. if kb.RequiresLeader {
  112. keybindStrs = append(keybindStrs, *c.app.Config.Keybinds.Leader+" "+kb.Key)
  113. } else {
  114. keybindStrs = append(keybindStrs, kb.Key)
  115. }
  116. }
  117. }
  118. keybinds := strings.Join(keybindStrs, ", ")
  119. rows = append(rows, commandRow{
  120. trigger: trigger,
  121. description: description,
  122. keybinds: keybinds,
  123. })
  124. // Update max widths
  125. if len(trigger) > maxTriggerWidth {
  126. maxTriggerWidth = len(trigger)
  127. }
  128. if len(description) > maxDescriptionWidth {
  129. maxDescriptionWidth = len(description)
  130. }
  131. if len(keybinds) > maxKeybindWidth {
  132. maxKeybindWidth = len(keybinds)
  133. }
  134. }
  135. // Add padding between columns
  136. columnPadding := 3
  137. // Build the output
  138. var output strings.Builder
  139. maxWidth := 0
  140. for _, row := range rows {
  141. // Pad each column to align properly
  142. trigger := fmt.Sprintf("%-*s", maxTriggerWidth, row.trigger)
  143. description := fmt.Sprintf("%-*s", maxDescriptionWidth, row.description)
  144. // Apply styles and combine
  145. line := triggerStyle.Render(trigger) +
  146. triggerStyle.Render(strings.Repeat(" ", columnPadding)) +
  147. descriptionStyle.Render(description)
  148. if c.showKeybinds && row.keybinds != "" {
  149. line += keybindStyle.Render(strings.Repeat(" ", columnPadding)) +
  150. keybindStyle.Render(row.keybinds)
  151. }
  152. output.WriteString(line + "\n")
  153. maxWidth = max(maxWidth, lipgloss.Width(line))
  154. }
  155. // Remove trailing newline
  156. result := strings.TrimSuffix(output.String(), "\n")
  157. if c.background != nil {
  158. result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result)
  159. }
  160. return result
  161. }
  162. type Option func(*commandsComponent)
  163. func WithKeybinds(show bool) Option {
  164. return func(c *commandsComponent) {
  165. c.showKeybinds = show
  166. }
  167. }
  168. func WithBackground(background compat.AdaptiveColor) Option {
  169. return func(c *commandsComponent) {
  170. c.background = &background
  171. }
  172. }
  173. func WithLimit(limit int) Option {
  174. return func(c *commandsComponent) {
  175. c.limit = &limit
  176. }
  177. }
  178. func WithShowAll(showAll bool) Option {
  179. return func(c *commandsComponent) {
  180. c.showAll = showAll
  181. }
  182. }
  183. func New(app *app.App, opts ...Option) CommandsComponent {
  184. c := &commandsComponent{
  185. app: app,
  186. background: nil,
  187. showKeybinds: true,
  188. showAll: false,
  189. }
  190. for _, opt := range opts {
  191. opt(c)
  192. }
  193. return c
  194. }