commands.go 5.8 KB

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