command.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. package commands
  2. import (
  3. "slices"
  4. "strings"
  5. tea "github.com/charmbracelet/bubbletea/v2"
  6. "github.com/sst/opencode/internal/config"
  7. )
  8. type ExecuteCommandMsg Command
  9. type ExecuteCommandsMsg []Command
  10. type CommandExecutedMsg Command
  11. type Keybinding struct {
  12. RequiresLeader bool
  13. Key string
  14. }
  15. func (k Keybinding) Matches(msg tea.KeyPressMsg, leader bool) bool {
  16. key := k.Key
  17. key = strings.TrimSpace(key)
  18. return key == msg.String() && (k.RequiresLeader == leader)
  19. }
  20. type CommandName string
  21. type Command struct {
  22. Name CommandName
  23. Description string
  24. Keybindings []Keybinding
  25. Trigger string
  26. }
  27. func (c Command) Keys() []string {
  28. var keys []string
  29. for _, k := range c.Keybindings {
  30. keys = append(keys, k.Key)
  31. }
  32. return keys
  33. }
  34. type CommandRegistry map[CommandName]Command
  35. func (r CommandRegistry) Sorted() []Command {
  36. var commands []Command
  37. for _, command := range r {
  38. commands = append(commands, command)
  39. }
  40. slices.SortFunc(commands, func(a, b Command) int {
  41. if a.Name == AppExitCommand {
  42. return 1
  43. }
  44. if b.Name == AppExitCommand {
  45. return -1
  46. }
  47. return strings.Compare(string(a.Name), string(b.Name))
  48. })
  49. return commands
  50. }
  51. func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
  52. var matched []Command
  53. for _, command := range r.Sorted() {
  54. if command.Matches(msg, leader) {
  55. matched = append(matched, command)
  56. }
  57. }
  58. return matched
  59. }
  60. const (
  61. AppHelpCommand CommandName = "app_help"
  62. EditorOpenCommand CommandName = "editor_open"
  63. SessionNewCommand CommandName = "session_new"
  64. SessionListCommand CommandName = "session_list"
  65. SessionShareCommand CommandName = "session_share"
  66. SessionInterruptCommand CommandName = "session_interrupt"
  67. SessionCompactCommand CommandName = "session_compact"
  68. ToolDetailsCommand CommandName = "tool_details"
  69. ModelListCommand CommandName = "model_list"
  70. ThemeListCommand CommandName = "theme_list"
  71. ProjectInitCommand CommandName = "project_init"
  72. InputClearCommand CommandName = "input_clear"
  73. InputPasteCommand CommandName = "input_paste"
  74. InputSubmitCommand CommandName = "input_submit"
  75. InputNewlineCommand CommandName = "input_newline"
  76. HistoryPreviousCommand CommandName = "history_previous"
  77. HistoryNextCommand CommandName = "history_next"
  78. MessagesPageUpCommand CommandName = "messages_page_up"
  79. MessagesPageDownCommand CommandName = "messages_page_down"
  80. MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
  81. MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
  82. MessagesPreviousCommand CommandName = "messages_previous"
  83. MessagesNextCommand CommandName = "messages_next"
  84. MessagesFirstCommand CommandName = "messages_first"
  85. MessagesLastCommand CommandName = "messages_last"
  86. AppExitCommand CommandName = "app_exit"
  87. )
  88. func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
  89. for _, binding := range k.Keybindings {
  90. if binding.Matches(msg, leader) {
  91. return true
  92. }
  93. }
  94. return false
  95. }
  96. func (k Command) FromConfig(config *config.Config) Command {
  97. if keybind, ok := config.Keybinds[string(k.Name)]; ok {
  98. k.Keybindings = parseBindings(keybind)
  99. }
  100. return k
  101. }
  102. func parseBindings(bindings ...string) []Keybinding {
  103. var parsedBindings []Keybinding
  104. for _, binding := range bindings {
  105. for p := range strings.SplitSeq(binding, ",") {
  106. requireLeader := strings.HasPrefix(p, "<leader>")
  107. keybinding := strings.ReplaceAll(p, "<leader>", "")
  108. keybinding = strings.TrimSpace(keybinding)
  109. parsedBindings = append(parsedBindings, Keybinding{
  110. RequiresLeader: requireLeader,
  111. Key: keybinding,
  112. })
  113. }
  114. }
  115. return parsedBindings
  116. }
  117. func LoadFromConfig(config *config.Config) CommandRegistry {
  118. defaults := []Command{
  119. {
  120. Name: AppHelpCommand,
  121. Description: "show help",
  122. Keybindings: parseBindings("<leader>h"),
  123. Trigger: "help",
  124. },
  125. {
  126. Name: EditorOpenCommand,
  127. Description: "open editor",
  128. Keybindings: parseBindings("<leader>e"),
  129. Trigger: "editor",
  130. },
  131. {
  132. Name: SessionNewCommand,
  133. Description: "new session",
  134. Keybindings: parseBindings("<leader>n"),
  135. Trigger: "new",
  136. },
  137. {
  138. Name: SessionListCommand,
  139. Description: "list sessions",
  140. Keybindings: parseBindings("<leader>l"),
  141. Trigger: "sessions",
  142. },
  143. {
  144. Name: SessionShareCommand,
  145. Description: "share session",
  146. Keybindings: parseBindings("<leader>s"),
  147. Trigger: "share",
  148. },
  149. {
  150. Name: SessionInterruptCommand,
  151. Description: "interrupt session",
  152. Keybindings: parseBindings("esc"),
  153. },
  154. {
  155. Name: SessionCompactCommand,
  156. Description: "compact the session",
  157. Keybindings: parseBindings("<leader>c"),
  158. Trigger: "compact",
  159. },
  160. {
  161. Name: ToolDetailsCommand,
  162. Description: "toggle tool details",
  163. Keybindings: parseBindings("<leader>d"),
  164. Trigger: "details",
  165. },
  166. {
  167. Name: ModelListCommand,
  168. Description: "list models",
  169. Keybindings: parseBindings("<leader>m"),
  170. Trigger: "models",
  171. },
  172. {
  173. Name: ThemeListCommand,
  174. Description: "list themes",
  175. Keybindings: parseBindings("<leader>t"),
  176. Trigger: "themes",
  177. },
  178. {
  179. Name: ProjectInitCommand,
  180. Description: "create or update AGENTS.md",
  181. Keybindings: parseBindings("<leader>i"),
  182. Trigger: "init",
  183. },
  184. {
  185. Name: InputClearCommand,
  186. Description: "clear input",
  187. Keybindings: parseBindings("ctrl+c"),
  188. },
  189. {
  190. Name: InputPasteCommand,
  191. Description: "paste content",
  192. Keybindings: parseBindings("ctrl+v"),
  193. },
  194. {
  195. Name: InputSubmitCommand,
  196. Description: "submit message",
  197. Keybindings: parseBindings("enter"),
  198. },
  199. {
  200. Name: InputNewlineCommand,
  201. Description: "insert newline",
  202. Keybindings: parseBindings("shift+enter"),
  203. },
  204. {
  205. Name: HistoryPreviousCommand,
  206. Description: "previous prompt",
  207. Keybindings: parseBindings("up"),
  208. },
  209. {
  210. Name: HistoryNextCommand,
  211. Description: "next prompt",
  212. Keybindings: parseBindings("down"),
  213. },
  214. {
  215. Name: MessagesPageUpCommand,
  216. Description: "page up",
  217. Keybindings: parseBindings("pgup"),
  218. },
  219. {
  220. Name: MessagesPageDownCommand,
  221. Description: "page down",
  222. Keybindings: parseBindings("pgdown"),
  223. },
  224. {
  225. Name: MessagesHalfPageUpCommand,
  226. Description: "half page up",
  227. Keybindings: parseBindings("ctrl+alt+u"),
  228. },
  229. {
  230. Name: MessagesHalfPageDownCommand,
  231. Description: "half page down",
  232. Keybindings: parseBindings("ctrl+alt+d"),
  233. },
  234. {
  235. Name: MessagesPreviousCommand,
  236. Description: "previous message",
  237. Keybindings: parseBindings("ctrl+alt+k"),
  238. },
  239. {
  240. Name: MessagesNextCommand,
  241. Description: "next message",
  242. Keybindings: parseBindings("ctrl+alt+j"),
  243. },
  244. {
  245. Name: MessagesFirstCommand,
  246. Description: "first message",
  247. Keybindings: parseBindings("ctrl+g"),
  248. },
  249. {
  250. Name: MessagesLastCommand,
  251. Description: "last message",
  252. Keybindings: parseBindings("ctrl+alt+g"),
  253. },
  254. {
  255. Name: AppExitCommand,
  256. Description: "exit the app",
  257. Keybindings: parseBindings("ctrl+c", "<leader>q"),
  258. Trigger: "exit",
  259. },
  260. }
  261. registry := make(CommandRegistry)
  262. for _, command := range defaults {
  263. registry[command.Name] = command.FromConfig(config)
  264. }
  265. return registry
  266. }