Skip to main content

Configuring Keyboards with Org-mode Babel

This month’s Emacs Carnival features Org Babel. Since I submitted last month about using Org-mode to manage installed Arch packages, it means—I’ve got another chance to show off what Org-mode is capable of! 1

Today, let’s look at a more complex example: how I configure my 60% QMK keyboard with Colemak + APL symbols + Qwerty + other layers with Org-mode, and in particular, Org Babel.

1

Proudly as an Org-mode user, I never run out of Org-mode use cases. ;)

1. Quite a bit of background

1.1. Keyboard layouts

Where do I even start? So, most keyboards today come with a Qwerty layout, which was named so because the first few keys in the top letter row of it are, sequentially, Q W E R T Y.

Laser128a.jpg

A Laser 128 computer with a Qwerty keyboard (from Wikipedia)

Unfortunately, Qwerty is old, with its design coming from typewriters, and can be a bit limited in its ergonomics or typing efficiency: it does not put some of the most frequently pressed keys on the middle row, so you will have to stretch your fingers most of the time.

qwerty-heatmap.png

Key press heatmap for Qwerty, typing a Wikipedia article. (The heatmaps here were generated with Keyboard Heatmap by Patrick Wied.)

And it’s why people tried to come up with alternative keyboard layouts, like Drovak:

drovak-heatmap.png

Or, Colemak:

colemak-heatmap.png

Or Colemak Mod-DH (which I use) or Workman, Norman… you name it.

There are multiple ways to use these layouts on your Qwerty keyboards. One way is through software remapping on your computer, with xkb configs or maybe AutoHotkey—it’s just you need to do this on all your devices. Another way is to flash dedicated firmware onto your keyboard so that the keyboard directly maps the keys for you, portably. And a popular, open-source keyboard firmware (that I use) is QMK.

1.2. QMK configs

QMK configs are plain C files that will be linked with the firmware:

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
[_QWERTY] = LAYOUT_ortho_5x12(
  KC_GRV,  KC_1,    KC_2,    KC_3,    KC_4,    KC_5,    KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_BSPC,
  KC_TAB,  KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,    KC_Y,    KC_U,    KC_I,    KC_O,    KC_P,    KC_DEL,
  KC_ESC,  KC_A,    KC_S,    KC_D,    KC_F,    KC_G,    KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_QUOT,
  KC_LSFT, KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,    KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH, KC_ENT,
  KC_CAPS, KC_LCTL, KC_LALT, KC_LGUI, LOWER,   KC_SPC,  KC_SPC,  RAISE,   KC_LEFT, KC_DOWN, KC_UP,   KC_RGHT
),
// ... other layers
};

QMK also allows you to specify keys (or key combinations) to enter arbitrary Unicode characters by using basic UC(...) keycodes or Unicode maps:

enum unicode_names {
    BANG,
    IRONY,
    SNEK
};
const uint32_t PROGMEM unicode_map[] = {
    [BANG]  = 0x203D,  // UM(BANG)  → ‽
    [IRONY] = 0x2E2E,  // UM(IRONY) → ⸮
    [SNEK]  = 0x1F40D, // UM(SNEK)  → 🐍
};

With QMK, configuring the layout is quite straightforward: just add any keycodes you want, change the keymap matrix array as is shown above, and recompile it. But, things can get quite messy if you want a keyboard layout like this:

colemak-dh-apl.png
Figure 1: My “Colemak-DH + APL symbols” keyboard layout

1.3. When Colemak meets APL

OK, I promise we’re already halfway through the introduction! So, all those Unicode symbols on the keyboard schema above is for APL, a cute array programming language (or a family of them) famous for its terseness. For example, here is one of my learning code snippets in Dyalog APL:

⍝ "⍝" marks line comments (I call it amugus BTW)
⎕IO0     ⍝ use 0-based indices
+/100     ⍝ adding from 0 to 99
(+/÷≢)100 ⍝ averaging (0 1 2 ... 99)
4950
49.5

And this is a Quicksort implementation, which I have yet to fully grasp:

Q{1≥≢⍵:⍵  S{⌿⍨ ⍺⍺ }  ((<S)⍪=S(>S))?≢}

, , , ÷, , … APL uses a lot of non-ASCII symbols as “verbs”, meaning that, to learn APL, you have to first find a keyboard (or, most often, a keyboard mapping) for it.

IDSkeyboard.jpg

Nostalgic greatness! A ‘clicky’ spring beam IBM 3278 keyboard of the 70s” – image courtesy of David Ayre

(Taken from Dyalog Photo Gallery)

There are dedicated key caps and some keyboard stickers sold by Dyalog Ltd. And you can find a few APL mappings for ordinary keyboards online—by “ordinary”, they very much mean “Qwerty”. And, no, the physical key mapping is different when you maps Colemak/Drovak keys into APL symbols, because you don’t want to lose their mnemonics: 2

Letters A E V W J R
Symbols ∘ (Jot) ⍴ (Rho)

So what do we do? Do we actually manually configure our QMK layout for all these symbols, one by one? Or can we take a shortcut to batch-generate them?

2

Also, APL mappings on Qwerty keyboards often put similar symbols on adjacent keys, like ⊥/⊤ (on B/N), ⌈/⌊ (on S/D), ⊂/⊃ (on Z/X), or ↓/↑ (on U/Y). Fortunately, Colemak at least preserves some of them.

1.4. GNU APL mode

course, there’s an emacs command to do that.

The GNU APL mode comes with a gnu-apl-show-keyboard command. It’s in Qwerty layout, of course, but its backing data gnu-apl--symbols actually provide what we need to generate our Colemak + APL mapping.

(defvar gnu-apl--symbols '(;; Top row
                           ;; `
                           ("diamond" "◊" "`")
                           ;; 1
                           ("diaeresis" "¨" "1")
                           ("i-beam" "⌶" "!")
                           ;; 2
                           ("macron" "¯" "2")
                           ("del-tilde" "⍫" "@")
                           ;; ...

                           ;; First row
                           ;; q
                           ("question-mark" "?" "q")
                           ;; w
                           ("omega" "⍵" "w")
                           ("omega-underbar" "⍹" "W")
                           ;; e
                           ("epsilon" "∊" "e")
                           ("epsilon-underbar" "⍷" "E")
                           ;; ...
                           ))

2. Org Babel for keyboards

Now we’re ready for some Org-mode magic. Let’s start with a Qwerty keyboard layout in Org-mode tables:

#+name: qwerty
| KC_ESC  | KC_1    | KC_2    | KC_3   | KC_4    | KC_5    | KC_6    | KC_7    | KC_8    | KC_9   | KC_0    | KC_MINS | KC_EQL  | KC_BSPC |
| KC_TAB  | KC_Q    | KC_W    | KC_E   | KC_R    | KC_T    | KC_Y    | KC_U    | KC_I    | KC_O   | KC_P    | KC_LBRC | KC_RBRC | KC_BSLS |
| KC_CAPS | KC_A    | KC_S    | KC_D   | KC_F    | KC_G    | KC_H    | KC_J    | KC_K    | KC_L   | KC_SCLN | KC_QUOT | KC_ENT  |         |
| KC_LSFT | KC_Z    | KC_X    | KC_C   | KC_V    | KC_B    | KC_N    | KC_M    | KC_COMM | KC_DOT | KC_SLSH | KC_UP   |         |         |
| KC_LCTL | KC_LGUI | KC_LALT | KC_SPC | KC_RALT | KC_LEFT | KC_DOWN | KC_RGHT |         |        |         |         |         |         |
KCESC KC1 KC2 KC3 KC4 KC5 KC6 KC7 KC8 KC9 KC0 KCMINS KCEQL KCBSPC
KCTAB KCQ KCW KCE KCR KCT KCY KCU KCI KCO KCP KCLBRC KCRBRC KCBSLS
KCCAPS KCA KCS KCD KCF KCG KCH KCJ KCK KCL KCSCLN KCQUOT KCENT  
KCLSFT KCZ KCX KCC KCV KCB KCN KCM KCCOMM KCDOT KCSLSH KCUP    
KCLCTL KCLGUI KCLALT KCSPC KCRALT KCLEFT KCDOWN KCRGHT            

You don’t need to type all these yourself. Just copy from your default/keymap.c into Emacs, then M-x replace-regexp <enter> ^\|, <enter> and then <tab>, and you will have a formatted Org table.

Also, ox-html exports strings like KC_ESC as subscripts like KCESC. It’s surprisingly quite pretty in the table above, so I won’t bother replacing them with quoted string literals.

2.1. From Qwerty to Colemak

To map from a Qwerty layout to Colemak(-DH) or your favorite keymap, you either manually replace all the letter keys, or find their textual representation to programatically maps them with Org Babel.

With the :var qwerty=qwerty header args, you can use the table above as a data source and apply any transformation to it, like this:

#+begin_src elisp :var qwerty=qwerty
  (caar qwerty)
#+end_src

#+RESULTS:
: KC_ESC

Let’s now convert the layout from Qwerty to Colemak:

(defun parse-keys (s)
  (mapcar
   (lambda (k) (or (alist-get k '((";" . "SCLN")
                                  ("," . "COMM")
                                  ("." . "DOT")
                                  ("/" . "SLSH"))
                              k nil #'equal)))
   (string-split (upcase s))))
(setq
 ;; https://github.com/ColemakMods/mod-dh/blob/gh-pages/webapp/resources/layout_main/qwerty.keyb
 qwerty-keys (parse-keys "
q  w  e  r  t  y  u  i  o  p
a  s  d  f  g  h  j  k  l  ;
z  x  c  v  b  n  m  ,  .  /
")
 ;; https://github.com/ColemakMods/mod-dh/blob/gh-pages/webapp/resources/layout_main/colemak_dh.keyb
 colemak-dh-keys (parse-keys "
q  w  f  p  b  j  l  u  y  ;
a  r  s  t  g  m  n  e  i  o
z  x  c  d  v  k  h  ,  .  /
"))
(defun table-substitute (new old table)
  (dolist (row table)
    (cl-nsubstitute new old row :test #'equal)))
(seq-mapn (lambda (c q) (table-substitute (concat "→KC_" c) (concat "KC_" q) qwerty))
          colemak-dh-keys qwerty-keys)
(dolist (c colemak-dh-keys)
  (table-substitute (concat "KC_" c) (concat "→KC_" c) qwerty))
qwerty
KCESC KC1 KC2 KC3 KC4 KC5 KC6 KC7 KC8 KC9 KC0 KCMINS KCEQL KCBSPC
KCTAB KCQ KCW KCF KCP KCB KCJ KCL KCU KCY KCSCLN KCLBRC KCRBRC KCBSLS
KCCAPS KCA KCR KCS KCT KCG KCM KCN KCE KCI KCO KCQUOT KCENT  
KCLSFT KCZ KCX KCC KCD KCV KCK KCH KCCOMM KCDOT KCSLSH KCUP    
KCLCTL KCLGUI KCLALT KCSPC KCRALT KCLEFT KCDOWN KCRGHT            

Make some manual adjustments to that, (for example, I usually map Caps Lock to the Escape key) and you now have a layout to map the APL symbols onto.

2.2. From Colemak to APL

Similarly, we can easily create an Org table for APL keyboard layouts. (By adding #+name: colemak attributes onto the source block, you can refer to the Colemak table with :var colemak=colemak.)

Some code are skipped for brevity

The following code deals with upper/lower case, and KC_ prefixes. Just not that fun.

(setq dyalog--symbols gnu-apl--symbols) ; adjust the keymap
(defvar key-to-char-mapping
  '(("GRAVE" "`" "~")
    ("MINS" "-" "_")
    ("EQL" "=" "+")
    ("SCLN" ";" ":")
    ("LBRC" "[" "{")
    ("RBRC" "]" "}")
    ("BSLS" "\\" "|")
    ("QUOT" "'" "\"")
    ("COMM" "," "<")
    ("DOT" "." ">")
    ("SLSH" "/" "?")
    ))
(defun alist-get-nth (n key seq)
  (seq-find (lambda (ele) (equal key (nth n ele))) seq))
(defvar number-to-char-mapping ")!@#$%^&*(")
(defun key-to-char (key)
  (if (string-match "^KC_\\([0-9A-Z]\\)$" key)
      (downcase (match-string 1 key))
    (and (string-prefix-p "KC_" key)
         (cadr (alist-get-nth 0 (substring key 3) key-to-char-mapping)))))
(defun char-to-shifted (char)
  (let ((c (string-to-char char)))
    (cond
     ((<= ?a c ?z)
      (string (upcase c)))
     ((<= ?0 c ?9)
      (string (aref number-to-char-mapping (- c ?0))))
     (t (caddr (alist-get-nth 1 char key-to-char-mapping))))))
(defun to-c (sym)
  (string-replace "-" "_" (upcase sym)))
(defun dyalog-mapping (keys &optional up)
  (mapcar
   (lambda (key)
     (when-let* ((c (key-to-char key))
                 (c (if up (char-to-shifted c) c))
                 (entry (alist-get-nth 2 c dyalog--symbols))
                 (sym (to-c (car entry)))
                 (c (cadr entry)))
       (list sym c key)))
   keys))

The following converts from a table into a keymap array:

(defun keymap-from-table (name table)
  (with-temp-buffer
    (dolist (row table)
      (dolist (key row)
        (insert "| ")
        (insert
         (cond
          ((equal key "___") "_______")
          (key key)
          (t ""))))
      (insert "|\n"))
    (org-table-align)
    ;; Org-table to C args
    (goto-char (point-min)) (replace-string " |" ",")
    (goto-char (point-min)) (replace-regexp "^| " "    ")
    (goto-char (point-min))
    (while (re-search-forward ",\\([ ,]+\\)," nil t)
      (replace-match (concat (make-string (- (match-end 0) (match-beginning 0) 1) ?\ ) ",")))
    ;; No trailing comma
    (search-backward ",")
    (delete-char 1)
    ;; Wrap
    (goto-char (point-min)) (insert "[" name "] = LAYOUT_60_ansi(\n")
    (goto-char (point-max)) (insert "),\n")
    (buffer-string)))
  • APL symbols on Colemak layout (lower-case / with Shift un-pressed):

    (cl-loop for row in colemak
             collect (mapcar (lambda (l) (or (cadr l) "")) (dyalog-mapping row)))
    
      ¨ ¯ < = > × ÷    
      ? _  
             
               
                                 
  • APL symbols on Colemak layout (upper-case / with Shift modifier):

    (cl-loop for row in colemak
             collect (mapcar (lambda (l) (or (cadr l) "")) (dyalog-mapping row t)))
    
      !  
        £   ¥
                   
        χ          
                               

3. Generating source files from Org Babel

With the mappings in place, we now turn all that data into a valid QMK config, that is, a C file. And you can easily do that with org-babel-tangle:

#+begin_src c :tangle keymap.c
  #include QMK_KEYBOARD_H
#+end_src

With the :tangle header argument, all source blocks annotated with it will be collected into the keymap.c file. In combination with the :wrap argument, we can programatically generate all the APL keymaps in C source blocks with :tangle, and then collect them into our QMK keymap.c file:

#+begin_src elisp :wrap "src c :tangle keymap.c"
  (setq en-dash '("DASH"    ; en dash “–”; I have em dashes in my IME.
                  "ZWSP"    ; zero-width space, for Org-mode markup
                  "ESPN_UP" ; Ñ
                  "ESPN_LO" ; ñ
                  ))
  (setq dyalog-enum
        (mapcar (lambda (ele) (to-c (car ele))) dyalog--symbols))
  (concat "enum unicode_names {
      "
          (string-join (append en-dash dyalog-enum) ",\n    ")
          ",\n};\n")
#+end_src

#+RESULTS:
#+begin_src c :tangle keymap.c
  enum unicode_names {
      DASH,
      ZWSP,
      ESPN_UP,
      ESPN_LO,
      DIAMOND,
      DIAERESIS,
      // ...
      QUAD_BACKSLASH,
      INVERTED_QUESTION_MARK,
  };
#+end_src

And we also need to tangle a unicode_map[]:

(setq max-indent
      (apply #'max (mapcar
                    (lambda (ele) (length (car ele)))
                    dyalog--symbols)))

(setq dyalog-unicode-map
      (mapcar
       (lambda (ele)
         (format "[%s]%s = 0x%04X, // %s"
                 (to-c (car ele))
                 (make-string (- max-indent (length (car ele))) ?\ )
                 (string-to-char (cadr ele))
                 (cadr ele)))
       dyalog--symbols))
(concat
 "const uint32_t PROGMEM unicode_map[] = {
    "
 (string-join dyalog-unicode-map "\n    ")
 ",\n};\n")
const uint32_t PROGMEM unicode_map[] = {
    [DIAMOND]                   = 0x25CA, // ◊
    [DIAERESIS]                 = 0x00A8, // ¨
    [I_BEAM]                    = 0x2336, // ⌶
    [MACRON]                    = 0x00AF, // ¯
    // ...
    [QUAD_BACKSLASH]            = 0x2342, // ⍂
    [INVERTED_QUESTION_MARK]    = 0x00BF, // ¿,
};

And finally there’s the keymaps variable:

#+begin_src c :tangle keymap.c
  const uint16_t keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
#+end_src
#+begin_src elisp :var table=base :wrap "src c :tangle keymap.c"
  (keymap-from-table "BASE" table)
#+end_src
#+begin_src elisp :var table=qwerty :wrap "src c :tangle keymap.c"
  (keymap-from-table "QWE" table)
#+end_src
#+begin_src elisp :wrap "src c :tangle keymap.c"
  (concat
   "[DYALOG] = LAYOUT_60_ansi(
      "
   (string-trim (string-join dyalog-layer "\n    ") "" ",")
   "\n),")
#+end_src
#+begin_src elisp :var table=arrows :wrap "src c :tangle keymap.c"
  (keymap-from-table "ARR" table)
#+end_src
#+begin_src elisp :var table=fn1 :wrap "src c :tangle keymap.c"
  (keymap-from-table "FN1" table)
#+end_src
#+begin_src elisp :var table=fn2 :wrap "src c :tangle keymap.c"
  (keymap-from-table "FN2" table)
#+end_src
#+begin_src c :tangle keymap.c
  };
  // clang-format on
#+end_src

Then, org-babel-execute-buffer and org-babel-tangle, and we have our Colemak + Qwerty (toggled layer #1) + APL (tap layer #1) + arrow keys (toggled layer #2) + FN1/2 layer (tap layer #2) keymap ready. Hooray!

Also, with QMK and its Unicode mapping, one can trivially add bindings for em-dashes “—” and en-dashes “–”—the proof is left as an exercise to the reader.

4. “Is It Worth the Time?”

is_it_worth_the_time_2x.png
Figure 2: xkcd 1205: Is It Worth the Time?

As I automate more and more things with Org-mode, this question occasionally pops into my mind: “is is worth the time?” And, the answer is almost always: YES!

Strictly speaking, all the Org-mode Babel tango (pun intended) above might not actually save time—if you count in the time writing this blog post, it might be faster to just manually edit the QMK config myself. Instead, this workflow actually enables me doing the impossible—although it might be quicker that way, I wouldn’t possibly ever have chosen to do so—just think of all those Unicode codepoint magic numbers, the key matrix which must be strictly ordered, how any of these steps can be hard to debug, and how tiring the whole manual editing process is.

As for other simpler tasks, things are not that different. For one, Dyalog’s REPL on Linux is quite difficult to use for me, in that I can’t find much documentation on its weird non-readline shortcuts; and I would have quit learning it if I didn’t build a ob-dyalog wrapper for it. For another, I keep a link blog on this blog, and it was a bit tedious having to enter today’s date and the link, with evil-move-beyond-eol set to nil 3; so I just wrote a 10-line Org Babel snippet to automate that.

3

That means you can’t insert a timestamp at the end of the line. Instead, you need to maybe insert a space first. I vaguely remember I had some reasons to have it set to nil, but probably I should try again sometime.

Being able to gradually eliminate annoyances from your workflow is quite amazing. Quoting Interview with an Emacs Enthusiast, “Emacs takes a lifetime to learn. So the sooner you start, the longer it will take.” It probably also applies to Org-mode. But “is it worth that lifetime?” Then, maybe my answer is, yes.

Comments

This one-pixel image serves as a page view tracker and nothing more. This feedback keeps me motivated, because very few people ever comment.