• Stars
    star
    206
  • Rank 190,504 (Top 4 %)
  • Language
    Emacs Lisp
  • License
    MIT License
  • Created about 8 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Roll your own modal mode

RYO modal mode!

ryo-modal is an Emacs minor-mode, providing useful features for creating your own modal editing environment. Unlike evil, boon, xah-fly-keys, god-mode, fingers, and modal-mode, ryo-modal does not provide any default keybindings: roll your own! ryo-modal is similar to (and inspired by) modalka, but provides more features.

The package 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 use-package:

(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")))

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.
:properties
Since ryo-modal might create new symbol for bound command which can be determined after the binding is defined putting symbol properties would have to be done afterwards. If you specify :properties with list of pairs (PROPNAME . VALUE) these properties will be stored for that new symbol. It might be useful minor for modes like repeat-mode where repeat-map property of the symbol specifies whether the command will be supported by this mode. Example:
(defvar my-switch-buffer-repeat-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "[") 'switch-to-prev-buffer)
    (define-key map (kbd "]") 'switch-to-next-buffer)
    map))

(put 'switch-to-prev-buffer 'repeat-map 'my-switch-buffer-repeat-map)
(put 'switch-to-next-buffer 'repeat-map 'my-switch-buffer-repeat-map)

(ryo-modal-keys
 ("A"
  (("["
    switch-to-prev-buffer
    :name "Switch to previous buffer"
    ;; When Repeat mode is enabled due to `repeat-map' property
    ;; and `my-switch-buffer-repeat-map' keymap you can do
    ;; "A [ [ [" instead of "A [ A [ A [" to switch to third
    ;; previous buffer
    :properties ((repeat-map . my-switch-buffer-repeat-map)))
   ("]"
    switch-to-next-buffer
    ;; or alternate with "A [ ] [ ] [ ] [" to switch between
    ;; previous and next buffer
    :name "Switch to next buffer"
    :properties ((repeat-map . my-switch-buffer-repeat-map))))))
    

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:

(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))

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:

(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))

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:

(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))))

Creating and binding hydras to keys

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:

(ryo-modal-key
 "SPC g" :hydra
 '(hydra-git ()
             "A hydra for git!"
             ("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)))

Adding to preexisting hydras

If, for example, you wanted to add the magit-status function to the previously created hydra-git example, you would do the following:

(ryo-modal-key
 "SPC g" :hydra+
 '(hydra-git ()
             "A hydra for git!"
             ("g" magit-status "magit" :color blue)))

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:

(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))))

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 which-key you might be annoyed that ryo prefixes some commands with ryo:<hash>:. In order to remove that from the which-key menus, add this to your init-file:

(push '((nil . "ryo:.*:") . (nil . "")) which-key-replacement-alist)

If you use prefix keys you can name these, making which-key show something useful instead of +prefix:

(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))

If you have an old version of which-key you may need to update it, since which-key-replacement-alist and wasn’t there from the beginning.

Keybindings when region is active

If you want (some) special keybindings when the region is active, you can use selected.el. In order to turn it on/off at the same time as ryo-modal, you could do something like this:

(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)))

Credits

A lot of inspiration and code peeking from modalka, but also from 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).