All Projects → Kungsgeten → Ryo Modal

Kungsgeten / Ryo Modal

Licence: mit
Roll your own modal mode

#+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).
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].