command.go 8.5 KB

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