Home / Notes /

Home-brew define-key

Table of Contents

Or, “Home-brew general.el”. If you don’t know yet, general.el is a package that lets you define keybindings with ease. It lets you define keys like this:

(general-define-key
 :keymaps 'org-mode-map
 :prefix "C-x"
 "C-q" 'counsel-org-tag
 ...)

It has keywords for Evil states and pseudo keymaps like override-map and stuff. Like use-package, it provides an essential feature but is large and external, with a bunch of features I never use. So I decide to write my own, luna-def-key. Unlike luna-load-package, which is identical to use-package for the most part, luna-def-key provides many features of general.el with different approach (and some extra!), and has its distinct characteristics.

1 Introducing luna-key.el

Unlike general-define-key, where keywords affect the whole definition form, keywords in luna-def-key only take effect on the definitions below it. For example, in

(luna-def-key
 "C-a" #'fn1
 :prefix "C-c"
 "C-a" #'fn2)

fn1 is bound to C-a, fn2 is bound to C-c C-a. You can think of luna-def-key as a small stateful machine, where keywords changes the state. Part of the reason why is that I always define all the keybindings together at the beginning of my config file like this:

;;; Keys

(luna-def-key
 "C-/"     #'undo-only
 "C-."     #'undo-redo
 "C-s-i"   #'outline-cycle-buffer
 "C-c C-h" #'hs-toggle-hiding
 "C-="     #'expand-region
 "C--"     #'contract-region
 :keymaps '(c-mode-map c++-mode-map)
 "M-RET"   #'srefactor-refactor-at-point
 :keymaps '(outline-minor-mode-map org-mode-map outline-mode-map)
 "s-i"     #'outline-cycle
 :keymaps 'override
 "C-j"     #'avy-goto-word-1)

;;; Packages

(load-package avy
  :commands avy-goto-word-1)

(load-package ws-butler
  ;; global mode interferes with magit
  :hook (prog-mode . ws-butler-mode))

...

With luna-def-key, I don’t need to write separate forms for each keymap… Weird motivation, I know.

Besides keymaps and prefixes, luna-def-key has some other keywords. Here is all of them:

:keymaps Bind in this keymap
:prefix Bind with this prefix key
:clear Clear all states
:--- Same as :clear
:when Bind conditional command

The :when keyword is fun, I can bind keys that only activates under certain condition, like when the region is active. I used to do that with emulation-mode-map-alists, but that’s not as flexible as :when. By flexible I mean this:

:when (lambda ()
         (and mark-active
              (not (derived-mode-p 'magit-status-mode))))

Cool, huh?

General.el also has a feature called “definer”, basically it’s like macros:

(general-create-definer my-leader-def
  :prefix "C-c")
;; bind "C-c o" to `other-window'
(my-leader-def "o" 'other-window)

luna-key.el does this by “preset keywords”:

(luna-key-def-preset :leader
  :prefix "C-c")
;; bind "C-c o" to `other-window'
(luna-def-key
 :leader
 "o" 'other-window)

You can think of :leader as equivalent to :prefix "C-c" (spoiler alert: they are literally equivalent).

luna-def-key also works for remaps and keyboard macros. IIRC general.el doesn’t allow keyboard macros.

(luna-def-key
 [remap fn1] #'fn2
 "C-d" "woome")

Finally, we have which-key.el support! (Even though I never get to look at which-key panels.)

(luna-def-key
 :leader
 "b" '("Buffer")
 "bm"  '("goto message buffer" .
         (lambda () (interactive) (switch-to-buffer "*Messages*")))
 "bs"  '("goto scratch buffer" .
         (lambda () (interactive) (switch-to-buffer "*scratch*"))))

This syntax is inline with define-key.

2 Implementation details

When I say you can think of luna-def-key as a little stateful machine, I mean it. It is a little stateful machine, consuming arguments one by one. Here is a slightly simplified definition of luna-def-key.

(defun luna-def-key (&rest args)
  (let (arg map-list prefix condition)
    (while args
      (setq arg (pop args))
      (pcase arg
        (:keymaps
         ;; Next argument is either a keymap or a list of them.
         (setq map-list (pop args)))
        (:prefix
         ;; Next argument is a key prefix.
         (setq prefix (pop args)))
        ;; Clear all states.
        ((or :clear :---) (setq prefix nil
                                map-list nil
                                condition nil))
        (:when
         ;; Next argument is a condition predicate.
         (setq condition (pop args)))
        ;; Preset modifiers.
        ((pred keywordp)
         (when-let ((preset (alist-get arg luna-key-preset-alist)))
           (setq args (append preset args))))
        ;; Next two arguments are key and value.
        (_ (let ((key arg)
                 (def (pop args)))
             (luna-key-define key def map-list prefix condition)))))))

Here args is the arguments luna-def-key receives. We have three states: map-list (:keymaps), prefix (:prefix), and condition (:when). If we see these keywords, we pop next arguments out and set the state to it. If we see :clear, we set all states to nil. If we see other keywords, it must be a preset, and we just get its definitions and push them to the beginning of the argument list (so, literally equivalent). If we see anything else, it must be a key followed by a value, we bind them with current states.

There are a bit more hair (and a bit less fun) in luna-key-define. You can have a look if you are interested.

3 Show me the code

As always, local backup and GitHub link.

Written by Yuan Fu

First Published in 2020-09-13 Sun 14:25

Last modified in 2021-02-02 Tue 18:01

Send your comment to [email protected]