I am a proud owner of both the Atreus and Keyboardio Model01 keyboards. They are both excellent pieces of hardware - wooden enclosures, mechanical switches, the works. With QMK, they are infinitely configurable. The creators are also very responsive. In the course of replacing the Matias Quiet Click switches with linear ones on my Atreus, I broke a couple keycaps and shorted part of the PCB. I reached out to Phil Hagelberg, and in addition to guiding me through the process of how to wire the broken connection by hand, he very kindly mailed me some extra keycaps. I encourage everyone to check out the upcoming Keyboardio Atreus, a professionally manufactured Atreus delivered by the Keyboardio team. Despite already owning an Atreus, I'm sorely tempted to get one of these myself, if only for the new hot-swapping capabilities and beautiful palm rest.1
The Problem
Switching between columnar staggering and traditional laptop keyboards is hard. To make matters worse, I have appalling touch typing habits left over from when I was 11. In a way, though, it makes switching between my laptop and a columnar keyboard easier, as I can learn to touch type properly on the columnar keyboard while keeping my weird 3.5-finger typing muscle memory on the typewriter style board. So as not to confuse myself, I've even switched to Colemak Mod-DHm on the Atreus and Model01 while retaining QWERTY everywhere else.
When typing prose, everything is great. My speed with both QWERTY and Colemak is around 65 WPM, although after a few rounds on typeracer, my hands feel much better with Colemak than QWERTY. I'm not quite there yet when typing code; typing.io reports 29 WPM with Colemak vs. 45 WPM with QWERTY. With more practice, though, I'm sure that the gap will narrow. QMK keyboards have phenomenal access to commonly used symbols through layers - I just have to get used to them.
What I cannot seem to overcome to make the switch full-time to Colemak is my HJKL muscle memory. I switched from Vim to Emacs completely circa October 2018, and I use my own implementation of the Kakoune editing paradigm as opposed to evil mode (a topic for another blog post), but the use of HJKL as arrows persists in my text editor, in my window manager, i32, and in my browser through the Vimium extension. It's so strongly ingrained in my right hand that on my laptop, I rest my right hand on "hjkl" rather than "jkl;" as would be proper. If I were to change all my bindings in all of my programs wholesale, I would remap "hjkl" to "neio" as opposed to "mnei." However, this would destroy my ability to do anything on my laptop, which, although in the long term would probably be ideal, in the short term would be a disaster. Ideally, every program which makes heavy use of "hjkl" bindings should have an option to switch modes to accommodate the same hand position on Colemak. The remapping looks like this:
QWERTY | Colemak |
---|---|
h | n |
j | e |
k | i |
l | o |
n | j |
e | k |
i | l |
o | h |
The choice of what to do with QWERTY "neio" is somewhat arbitrary. I might decide to change them around later. How might we go about affecting this big structural change?
i3
i3's configuration file is written in simple plain-text. It doesn't support conditional logic. In fact, i3 doesn't even support specifying which file to load from; it always reads from /etc/i3/config
, ~/.i3/config
, or ~/.config/i3/config
. It does support binding events to keycodes as opposed to keysyms, but this makes more sense for dealing with keyboard layouts changed with xmodmap
, not directly in the firmware. The simplest thing to do, then, is to copy my existing i3 config to ~/.config/i3/config-{qwerty,colemak}
, edit the colemak version, and then symlink the appropriate version to ~/.config/i3/config
as appropriate. This is somewhat annoying, but mostly a one-time cost, as I very rarely edit my i3 configuration. To switch modes, we add bindsym $mod+i exec --no-startup-id "echo -e qwerty\\\ncolemak | dmenu | fish ~/.config/i3/keyboard-layout.fish"
to the i3 config (change $mod+i
to $mod+l
in the Colemak version), which calls this fish script to symlink the appropriate file and reload i3.
read -l mode
switch $mode
case "qwerty"
ln -sf ~/.config/i3/config-qwerty ~/.config/i3/config
i3-msg reload
case "colemak"
ln -sf ~/.config/i3/config-colemak ~/.config/i3/config
i3-msg reload
case '*'
echo "Unknown keyboard layout" $mode
end
Emacs
For Emacs, making an entirely separate configuration and reloading the editor every time I change keyboards simply will not do. My init.el
is over 1200 lines long vs. a mere 220 lines of i3 config, not counting my kakoune.el
package which bakes in many "hjkl" bindings. Moreover, I tweak my setup constantly, and it would be a nightmare to keep two versions of it synchronized. Fortunately, Emacs is Emacs, and is therefore infinitely malleable in the hands of anyone who can write elisp. I'm not the world's foremost elisp expert by any stretch of the imagination, but if anything, this is a good opportunity to write something that's not just a (use-package)
declaration.
We'll start by defining the translation table and the keymaps we want to alter:
(require 'dash)
(defvar colemak-translations
(-tree-map #'kbd '(("h" "n")
("j" "e")
("k" "i")
("l" "o")
("n" "j")
("e" "k")
("i" "l")
("o" "h")
;; ("C-h" "C-n") etc. go here as well
)))
(defvar colemak-translation-maps
'(ryo-modal-mode-map
lispy-mode-map
;; etc...
))
Then, given a keymap, we can apply these translations or remove them, depending on which keyboard we're using. We create a backup copy of the keymap or reuse an existing one, then loop through the translations we defined earlier and execute the re-mappings.
(require 'cl-lib)
(defun apply-or-revert-translations (translations keymap-symbol apply)
"If APPLY then apply TRANSLATIONS to KEYMAP-SYMBOL. Otherwise revert them."
(when (boundp keymap-symbol)
(let* ((keymap (symbol-value keymap-symbol))
(backup-symbol (intern (concat (symbol-name keymap-symbol) "-backup")))
(backup-map (if (boundp backup-symbol)
(symbol-value backup-symbol)
(set backup-symbol (copy-keymap keymap)))))
(cl-loop for (from to) in translations
for command = (lookup-key backup-map from)
when (commandp command)
do (define-key keymap (if apply to from) command)))))
Finally, we wrap it all into a nice minor mode. I use doom-modeline and want it to indicate whether or not the mode is active so I don't go typing all willy-nilly and destroy my buffer.
(define-minor-mode colemak-mode
"Toggle colemak-mode."
:init-value nil
:lighter " Colemak"
:global t
(progn
(setq global-mode-string (when colemak-mode "Colemak"))
(doom-modeline-refresh-frame)
(cl-loop for map in colemak-translation-maps
do (apply-or-revert-translations
colemak-translations map colemak-mode))))
The full code is available within my .emacs.d 3.
Vimium
Vimium, like i3, has a simple configuration that only supports basic remapping. Unlike i3, there's no way (as far as I know) to bind a keypress to reload the configuration. There is a facility to backup and restore configurations, but that has to be done by going to the configuration page, scrolling to the bottom and clicking "Restore." Fortunately, though, Vimium has enough unused real-estate in normal mode that QWERTY and Colemak bindings can exist simultaneously without conflict.
map e scrollDown
map E previousTab
map i scrollUp
map I nextTab
map n scrollLeft
map N goBack
map o scrollRight
map O goForward
map a enterInsertMode
map b Vomnibar.activate " by default searches bookmarks
map B Vomnibar.activateInNewTab
map s performFind " previously unused, s stands for Search
map S performBackwardsFind
End
If you're still reading this, thanks for sticking around. I hope that anyone with a similar situation can get some use out of this!
This article isn't sponsored by Keyboardio or Phil, I just like their hardware.
The default i3 keybindings actually have "jkl;" as the primary directions, which is probably how it should be, but by the time I started using i3, I was used to "hjkl" so I switched the i3 bindings to match.
Currently, this code just ignores unbound keymaps and commands. In practice, that means I'll have to toggle it again after I enter a previously un-loaded keymap. There should be a way to wrap everything in a (eval-after-load)
call, but I just haven't gotten around to figuring that out yet. If anyone has some pointers on how best to accomplish this, I'd be very grateful for the advice.