commands.go 5.0 KB

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