Kungsgeten / Ryo Modal
#+TITLE:RYO modal mode!
=ryo-modal= is an Emacs minor-mode, providing useful features for creating your own modal editing environment. Unlike [[https://bitbucket.org/lyro/evil/wiki/Home][evil]], [[https://github.com/jyp/boon][boon]], [[http://ergoemacs.org/misc/ergoemacs_vi_mode.html][xah-fly-keys]], [[https://github.com/chrisdone/god-mode][god-mode]], [[https://github.com/fgeller/fingers.el][fingers]], and [[http://retroj.net/modal-mode][modal-mode]], =ryo-modal= does not provide any default keybindings: roll your own! =ryo-modal= is similar to (and inspired by) [[https://github.com/mrkkrp/modalka][modalka]], but provides more features.
The package [[https://github.com/jmorag/kakoune.el][kakoune.el]] uses =ryo-modal-mode= to implement its bindings.
- Usage
You can use =M-x ryo-modal-mode= to activate =ryo-modal=, but without configuration nothing will happen. You need to add keybindings to it first; this can be done by =ryo-modal-key= (bind one key), =ryo-modal-keys= (bind many keys at once) or =ryo-modal-major-mode-keys= (bind several keys at once, but only if in a specific major mode, or a major mode derived from another).
Here's a simple configuration, using [[https://github.com/jwiegley/use-package][use-package]]:
#+BEGIN_SRC emacs-lisp (use-package ryo-modal :commands ryo-modal-mode :bind ("C-c SPC" . ryo-modal-mode) :config (ryo-modal-keys ("," ryo-modal-repeat) ("q" ryo-modal-mode) ("h" backward-char) ("j" next-line) ("k" previous-line) ("l" forward-char))
(ryo-modal-keys
;; First argument to ryo-modal-keys may be a list of keywords.
;; These keywords will be applied to all keybindings.
(:norepeat t)
("0" "M-0")
("1" "M-1")
("2" "M-2")
("3" "M-3")
("4" "M-4")
("5" "M-5")
("6" "M-6")
("7" "M-7")
("8" "M-8")
("9" "M-9")))
#+END_SRC
Now I can start =ryo-modal-mode= by pressing =C-c SPC=, and get vim-like =hjkl=-navigation and use digit arguments by pressing the number keys. Notice that other keys are unmodified, so pressing =r= would insert =r= into the buffer. =ryo= also defines the command =ryo-modal-repeat=, which will repeat the last command executed by =ryo= (but see =:norepeat= below).
When defining keys the first argument of each binding is the key (will be wrapped inside =kbd=) and the second argument is the /target/; usually a command or a string representing a keypress that should be simulated. The rest of the arguments are keyword pairs, providing extra features. The following keywords exist:
- =:name= :: =ryo-modal= creates a new symbol for the command you bind. By default this name will depend on the target of the binding, but by using =:name= and a string you can give it your own name. It is perfectly fine to have whitespace, or any other symbol, in the name.
- =:mode= :: If =:mode= is set to a quoted major or minor mode symbol (for instance =:mode 'org-mode=) the command will only be active in that mode (or in a major mode that derives from it). If you have a lot of major mode specific bindings, you may want to use =ryo-modal-major-mode-keys= instead to reduce clutter.
- =:exit= :: By providing =:exit t= you will exit =ryo-modal-mode= before running the command. This is useful if you have a command and always want to input text after running it.
- =:read= :: If =:read t= you will be prompted to insert a string in the minibuffer after running the command, and this string will be inserted into the buffer. This can be useful if you want to have a command which for instance replaces a word with another word, without exiting =ryo-modal-mode=.
- =:then= :: By providing a quoted list of command symbols, and/or functions to be run with zero arguments (lambdas works too), to =:then= you can specify additional commands that should be run after the "real" command. This way you can easily define command chains, without using =defun= or similar.
- =:first= :: Similar to =:then=, but will be run before the "real" command. Keep in mind that commands run here will consume =universal-argument= etc, before the real command is run.
- =:norepeat= :: If you specify =:norepeat t= then using the binding will /not/ make it overwrite the current command being triggered by =ryo-modal-repeat=.
- :mc-all :: If you're using =multiple-cursors= it can be annoying that it asks you if you want to use the commands generated by =ryo= for all cursors. If =:mc-all= is =t= then the command will be run by all cursors. If it instead is =0= it will only be run once. Note that setting =:mc-all= to =nil= will do nothing.
Here's an example using the keyword arguments (can be used in =ryo-modal-keys= too), and an example of =ryo-modal-major-mode-keys=:
#+BEGIN_SRC emacs-lisp (ryo-modal-key "SPC k" 'org-previous-visible-heading :then '(forward-to-word org-kill-line) :mode 'org-mode :name "org-replace-previous-heading" :read t)
(ryo-modal-major-mode-keys 'python-mode ("J" python-nav-forward-defun) ("K" python-nav-backward-defun)) #+END_SRC
Notice that the target command argument needs to be quoted when using =ryo-modal-key=, but not when using =ryo-modal-keys=!
In order to get an overview of all the bindings you've defined, use =M-x ryo-modal-bindings=. If you want to change the cursor color or cursor type, edit =ryo-modal-cursor-color= and/or =ryo-modal-cursor-type=.
** Prefix keys
Sometimes you want many keys bound under the same prefix key. A convenient way of doing this is to let the /target/ be a list of the keys in the prefix map. Each element of the list will be sent to =ryo-modal-key=, using the key as a prefix. If the key has any arguments, these will be sent too. Prefix examples:
#+BEGIN_SRC emacs-lisp (ryo-modal-key "SPC" '(("s" save-buffer) ("g" magit-status) ("b" ibuffer-list-buffers)))
(ryo-modal-keys ("v" (("w" er/mark-word :name "Mark word") ("d" er/mark-defun :name "Mark defun") ("s" er/mark-sentence :name "Mark sentence"))) ("k" (("w" er/mark-word :name "Kill word") ("d" er/mark-defun :name "Kill defun") ("s" er/mark-sentence :name "Kill sentence")) :then '(kill-region)) ("c" (("w" er/mark-word :name "Change word") ("d" er/mark-defun :name "Change defun") ("s" er/mark-sentence :name "Change sentence")) :then '(kill-region) :exit t)) #+END_SRC
Notice that the target should /not be quoted/ if using =ryo-modal-keys=, but it should if using =ryo-modal-key=.
As can be seen above, prefix keys could be used in a similar way as /verbs/ and /text objects/ in Vim. An easy way of doing this is to let the /text objects/ be commands which marks a region, and then the /verbs/ kan be simulated by =:then=, operating upon the selected region. In order to not repeat yourself (specifying the /text objects/ over and over again, as the example above), you could do something like the following:
#+BEGIN_SRC emacs-lisp (let ((text-objects '(("w" er/mark-word :name "Word") ("d" er/mark-defun :name "Defun") ("s" er/mark-sentence :name "Sentence")))) (eval `(ryo-modal-keys ("v" ,text-objects) ("k" ,text-objects :then '(kill-region)) ("c" ,text-objects :then '(kill-region) :exit t)))) #+END_SRC
** Creating and binding hydras to keys
[[https://github.com/abo-abo/hydra][Hydra]] is a package that allows creation of bindings which are /sort of modal/. =ryo-modal= does not require =hydra=, but if you have it installed you can easily define and bind hydras to keys. This way you can easily create a new "modal state".
In order to create a hydra, bind it to a key using =ryo-modal-key= or =ryo-modal-keys=. The /target/ of the key should be =:hydra= and the third argument should be a (quoted) list; this list will be used as the arguments sent to =defhydra=. An example:
#+BEGIN_SRC emacs-lisp (ryo-modal-key "SPC g" :hydra '(hydra-git () "A hydra for git!" ("g" magit-status "magit" :color blue) ("j" git-gutter:next-hunk "next") ("k" git-gutter:previous-hunk "previous") ("d" git-gutter:popup-hunk "diff") ("s" git-gutter:stage-hunk "stage") ("r" git-gutter:revert-hunk "revert") ("m" git-gutter:mark-hunk "mark") ("q" nil "cancel" :color blue))) #+END_SRC
** Defining "normal mode" keys which enter =ryo-modal=
If you're not in =ryo-modal-mode= you may want a key sequence which first triggers a command, and then enters =ryo-modal-mode=. You can then use =ryo-modal-command-then-ryo=. It takes a keybinding and usually a command to bind it to. You may also specify a keymap in which the command is bound, but global-map is used by default.
** Use-package keyword
Ryo-modal also provides a =use-package= keyword: =:ryo=, which is similar to =:bind= in that it implies =:defer t= and create autoloads for the bound commands. The keyword is followed by one or more key-binding commands, using the same syntax as used by =ryo-modal-keys= as is illustrated by the following example:
#+begin_src emacs-lisp (use-package simple :ensure nil :ryo ("SPC" (("n" next-line :name "my next line") ("p" previous-line))) ;; A list of keywords will be applied to all following keybindings up to the next list of keywords. (:mode 'org-mode :norepeat t) ("0" "M-0") ("G" end-of-buffer :name "insert at buffer end" :read t)
;; This new list of keywords will reset the applied defaults; it applies to all keybindings following. (:norepeat t) ("SPC g" :hydra '(hydra-nav () "A hydra for navigation" ("n" next-line "next line") ("p" previous-line "previous line") ("q" nil "cancel" :color blue)))) #+end_src
Notice that the target should not be quoted if using =:ryo= (although the third argument when using =:hydra= should be.
** =which-key= integration
If you're using [[https://github.com/justbur/emacs-which-key][which-key]] you might be annoyed that =ryo= prefixes some commands with =ryo::=. In order to remove that from the =which-key= menus, add this to your init-file:
#+BEGIN_SRC emacs-lisp (push '((nil . "ryo:.*:") . (nil . "")) which-key-replacement-alist) #+END_SRC
If you use prefix keys you can name these, making =which-key= show something useful instead of =+prefix=. In order to do this you must set =which-key-enable-extended-define-key= to =t= before loading =which-key= (please see the [[https://github.com/justbur/emacs-which-key][which-key readme]] on what this does). Then you could use the normal =:name= argument on your =ryo= prefix keys:
#+BEGIN_SRC emacs-lisp (ryo-modal-keys ("v" (("w" er/mark-word :name "Mark word") ("d" er/mark-defun :name "Mark defun") ("s" er/mark-sentence :name "Mark sentence")) :name "mark") ("k" (("w" er/mark-word :name "Kill word") ("d" er/mark-defun :name "Kill defun") ("s" er/mark-sentence :name "Kill sentence")) :name "kill" :then '(kill-region)) ("c" (("w" er/mark-word :name "Change word") ("d" er/mark-defun :name "Change defun") ("s" er/mark-sentence :name "Change sentence")) :name "change" :then '(kill-region) :exit t)) #+END_SRC
If you have an old version of =which-key= you may need to update it, since =which-key-replacement-alist= and =which-key-enable-extended-define-key= weren't there from the beginning.
- Keybindings when region is active
If you want (some) special keybindings when the region is active, you can use [[https://github.com/Kungsgeten/selected.el][selected.el]]. In order to turn it on/off at the same time as =ryo-modal=, you could do something like this:
#+BEGIN_SRC emacs-lisp (use-package ryo-modal :commands ryo-modal-mode :bind ("C-c SPC" . ryo-modal-mode) :init (add-hook 'ryo-modal-mode-hook (lambda () (if ryo-modal-mode (selected-minor-mode 1) (selected-minor-mode -1)))) :config (ryo-modal-keys ("q" ryo-modal-mode) ("0" "M-0") ("1" "M-1") ("2" "M-2") ("3" "M-3") ("4" "M-4") ("5" "M-5") ("6" "M-6") ("7" "M-7") ("8" "M-8") ("9" "M-9") ("h" backward-char) ("j" next-line) ("k" previous-line) ("l" forward-char))) #+END_SRC
- Credits
A lot of inspiration and code peeking from [[https://github.com/mrkkrp/modalka][modalka]], but also from [[https://github.com/jwiegley/use-package][use-package/bind-key]].
- Changelog
- November 2020 :: =:mc-all= keyword added, to be used by =muliple-cursors=.
- October 2019 :: The =:mode= keyword now works on modes which derive from the specified mode.
- March 2018 :: Support for naming prefix keys with =which-key=.
- February 2018 :: =ryo-modal-key= now defines commands, in order to make it work with =multiple-cursors= and similar. Also added =:first= keyword, and =:then= (and =:first=) can have functions (taking zero arguments) instead of commands (0.4).
- January 2018 :: Added =use-package= keyword =:ryo=. Also added =ryo-modal-set-key= and =ryo-modal-unset-key= (0.3).
- February 2017 :: Added =ryo-modal-major-mode-keys=. Also possible to specify keywords on all keys with a prefix, or all keys in =ryo-modal-keys=. Added =ryo-modal-repeat= (0.2).
- October 2016 :: Initial version (0.1).