Poor man’s QMK
QMK is a highly customizable firmware for keyboards. It is used by some mechanical keyboard vendors (e.g., Keychron, Kinesis and Nuphy), primarily on wired ergonomic, premium or DIY keyboards. QMK is also a popular choice for users that want to design and build their keyboard from scratch.
With QMK you can run arbitrary C code (within the microcontroller) upon any key press or release. However, QMK includes features that allow customizing the behavior of keys with a visual editor (QMK configurator) or with minimal C code (i.e., swapping values in a C array and sometimes adding a case
to a switch
). For example:
- Type
Enter
ifAlt
is tapped, but behave normally if held - Type
!
if1
is held for more than 300 milliseconds - Type
Ctrl+A
q
1
ifCaps+1
is held by more than 500 milliseconds
Customizing the keyboard behavior can fix many ergonomic woes of cheap keyboards without spending 300+ USD on an actual ergonomic keyboard. Unfortunately, you cannot simply install QMK on any keyboard, for a democratic reason: most consumers will choose a cheaper keyboard rather than a programmable one if all else is unchanged. Any price difference is then amplified by taxes. For the privilege of paying 100 USD for a keyboard my government wants 98 USD1.
During the holidays, I stumbled upon keyd
and was quickly convinced that configuring it was easier than changing the tax law.
This post explains techniques using personal preferences. If you download my keyd configuration, chances are it will feel worse than the stock keyboard behavior.
My ergonomic issues
This is my keyboard layout:
Despite spending over 12 hours on my desk, I have avoided any Repetitive Strain Injury (RSI). Unfortunately, I have not been a role model for ergonomics These are some personal ergonomic annoyances:
- As a programmer, there is too much strain on my pinky, especially due to shifted symbols, such as
(
- As a vim2 user (nvim, VSCode and IntelliJ), I use the
Esc
key often, bending my left wrist and leaving the home row - Since mice have hurt my feelings mlutiple times, I tend to overuse keyboard shortcuts with
Ctrl
+Shift
, straining my pinky, my wrist and again causing my hand to leave the home row - Some characters are often typed in enclosing pairs:
()
,[]
,<>
,""
and''
. After double tapping, I have to bend my wrist and use my right pinky to move the cursor first into the pair and then out of the pair (in vim mode,Esc
h
requires a bit less bending, but it costs one more tap) - Some keys that are often surrounded by spaces (
&
,=
,!=
) require elbow movement and leaving the home row - Outside of normal mode in vim mode, any navigation requires moving the entire hand out of the home row.
Setting up keyd
Installing keyd
is as simple as pacman -S keyd
(I use Arch BTW). The man page (man keyd
) is fairly complete and helpful, but there is no default starter configuration file on /etc/keyd/default.conf
, which would be nice.
After installation, run:
raido ~$ sudo keyd monitor
[sudo] password for alexis:
device added: 0001:0001:a38e6885 AT Translated Set 2 keyboard (/dev/input/event3)
device added: 258a:002a:75e84400 SINO WEALTH Gaming KB (/dev/input/event5)
device added: 258a:002a:000c7ead SINO WEALTH Gaming KB Consumer Control (/dev/input/event7)
device added: 258a:002a:65e37239 SINO WEALTH Gaming KB Keyboard (/dev/input/event8)
device added: 046d:c542:c48e2d82 Logitech Wireless Receiver Mouse (/dev/input/event10)
device added: 0000:0006:bdb72f48 Video Bus (/dev/input/event12)
device added: 06cb:ce2d:9519f2e5 MSFT0001:00 06CB:CE2D Mouse (/dev/input/event14)
device added: 06cb:ce2d:dcef41b3 MSFT0001:00 06CB:CE2D Touchpad (/dev/input/event15)
SINO WEALTH Gaming KB 258a:002a:75e84400 enter up
SINO WEALTH Gaming KB 258a:002a:75e84400 leftcontrol down
SINO WEALTH Gaming KB 258a:002a:75e84400 leftshift down
SINO WEALTH Gaming KB 258a:002a:75e84400 leftalt down
SINO WEALTH Gaming KB 258a:002a:75e84400 l down
SINO WEALTH Gaming KB 258a:002a:75e84400 l up
SINO WEALTH Gaming KB 258a:002a:75e84400 leftshift up
SINO WEALTH Gaming KB 258a:002a:75e84400 leftalt up
SINO WEALTH Gaming KB 258a:002a:75e84400 leftcontrol up
SINO WEALTH Gaming KB 258a:002a:75e84400 esc down
SINO WEALTH Gaming KB 258a:002a:75e84400 esc up
SINO WEALTH Gaming KB 258a:002a:75e84400 leftcontrol down
SINO WEALTH Gaming KB 258a:002a:75e84400 c down
This utility will listen for keyboard events and will print then. Hit any key to obtain a line such as
SINO WEALTH Gaming KB 258a:002a:75e84400 enter up
Where:
SINO WEALTH Gaming KB
is your keyboard name258a:002a:75e84400
is the unique vendor model and device ID of your keyboard. Copy this IDleftcontrol
is a key namedown
andup
are event types
To start a configuration file, first specify the target keyboard:
[ids]
258a:002a:75e84400
258a:002a:65e37239
This will instruct keyd
to capture all events from the listed keyboard, apply remapping rules in this file to those events, and then re-emit the remapped events as if they originated from keyd virtual keyboard 0fac:0ade:efba1ddf
. At this stage, there are no remaps and events will be forwarded without modification by the virtual keyboard.
The man page mentions a special match-all ID *
and the ability to subsequently exclude IDs by prefixing them with -
. Didn’t work on my machine. Could be a bug.
It is safe to start keyd
with sudo systemctl start keyd
.
Remember to systemctl enable keyd
, else it will not be auto-started on the next boot.
Use journalctl -eu keyd
to inspect the log and verify if your keyboard was matched
Jan 02 23:03:45 raido keyd[780617]: CONFIG: parsing /etc/keyd/default.conf
Jan 02 23:03:45 raido keyd[780617]: Starting keyd v2.5.0 ()
Jan 02 23:03:45 raido keyd[780617]: DEVICE: ignoring 0001:0001:a38e6885 (AT Translated Set 2 keyboard)
Jan 02 23:03:45 raido keyd[780617]: DEVICE: match 258a:002a:75e84400 /etc/keyd/default.conf (SINO WEALTH Gaming KB )
Jan 02 23:03:45 raido keyd[780617]: DEVICE: ignoring 258a:002a:000c7ead (SINO WEALTH Gaming KB Consumer Control)
Jan 02 23:03:45 raido keyd[780617]: DEVICE: match 258a:002a:65e37239 /etc/keyd/default.conf (SINO WEALTH Gaming KB Keyboard)
Jan 02 23:03:45 raido keyd[780617]: DEVICE: ignoring 046d:c542:c48e2d82 (Logitech Wireless Receiver Mouse)
Jan 02 23:03:45 raido keyd[780617]: DEVICE: ignoring 0000:0006:bdb72f48 (Video Bus)
Jan 02 23:03:45 raido keyd[780617]: DEVICE: ignoring 06cb:ce2d:9519f2e5 (MSFT0001:00 06CB:CE2D Mouse)
Jan 02 23:03:45 raido keyd[780617]: DEVICE: ignoring 06cb:ce2d:dcef41b3 (MSFT0001:00 06CB:CE2D Touchpad)
Symbol auto-shift
As a first remap, consider typing 1
when 1
is tapped but !
when 1
is held for more than 200 milliseconds.
Remaps are defined in layers. Every keyboard boots up into a main
layer, which will appear in the configuration file as an .ini
-like header [main]
. Within each layer, add remaps as original_key = action
, with one remap per line.
The updated file should look like:
[ids]
258a:002a:75e84400
258a:002a:65e37239
[main]
# Auto-shift some keys when held
1 = timeout(1, 200, S-1)
timeout
takes the tap action as the first argument, the timeout as the second, and the hold action as the third. In this case, S-1
means hold down Shift
, tap 1
and release Shift
.
You can reload the configuration with sudo keyd reload
.
You can continuously monitor messages from keyd service (watch out for configuration parse errors) using this command:
watch 'journalctl --no-pager -u keyd | tail -n 40'
(adjust 40 to your terminal height in lines)
If there are no errors, try tapping and holding 1
to get a feeling of how long 200 milliseconds is and whether you need to increase the timeout to prevent miss-shifting the key. I do not recommend using this auto-shift strategy as is for letters, see below for a safer method. Furthermore, timeout
introduces a lag when the key is tapped, since keyd
must wait for the timeout to decide whether it should emit a tap for 1
or for S-1
.
As a final note, consider auto-shift for curly braces in the ABNT2 layout:
backslash = timeout(backslash, 200, S-backslash)
There are two important gotchas in this remap:
- While the ABNT2 keyboard has
]}
printed on the keycap,keyd monitor
will print\ down
, not] down
- This happens because the keyboard itself is oblivious to ABNT2. Instead of the keycap symbols, the keyboard sends ISO-defined numbers which the operating system converts to the symbols defined by ABNT2.
- While
keyd monitor
identifies the key as\
,keyd
will not accept\
in the right side of a binding expression. This is fixed by using the alternative namebackslash
.
Use keyd list-keys
to get a list of all key names.
The remaps for other auto-shifted symbols are left as an exercise to the reader.
Safer letter auto-shift
Auto-shift as presented above is somewhat dangerous. Therefore, it should only be applied to symbols and not to letters, else thIngS liKe this could start to cause embarassment in your emails. In my experience, such accidents tend to happen at the end of a word or when the finger responsinble for tapping the next letter is still in movement. Increasing the tap time is not an adequate solution because it would make intentional shifting exceedingly disruptive to the typing flow.
The solution is to intrtoduce another time window: If a key has been tapped in the last 150 milliseconds, auto-shifting is disabled. This is done by wrapping the above remap into a overloadi
action:
1 = overload(1, timeout(1, 200, S-1), 150)
The man page uses the word “struck” as the reference point for the 150ms milliseconds window in the above example. However, the time window specified by overloadi
starts at the last key down event (e.g., last press instead of last release). For example, holding 2
to output @
and immediately holding 1
, will output !
. On the other hand, tapping 2
and holding 1
will very likely output 1
, due to the key down event for 1
happening within 150ms of the key down event for 2
.
Such a change makes it viable to auto-shift all letters, unless you are a vim user that holds, h
, j
, k
or l
. For vim users, auto-shifting letters will require one of the following:
- Do not remap
h
,j
,k
, andl
onkeyd
- Remap
H
,J
,K
, andL
onvim
to their lower letter actions - Self-train to use numbered movements (e.g.,
3j
) or to tap-then-hold instead of directly holdingh
,j
,k
orl
.
Killing Caps Lock
The Caps Lock key is one of the least useful keys, unless you like screaming. As a programmer, I tend to write MAGIC_NUMBER
by holding shift, which only accentuates the uselessness of Caps Lock. Therefore, I remapped Caps Lock to Esc
when tapped and to a custom layer when held:
[main]
capslock = overload(capslayer, esc)
# (more remaps active on the base layer)
[capslayer]
d = M-d
rightalt = macro(space space left)
# (more remaps active only when Caps Lock is held)
The name capslayer
can be anything that makes sense for you. Since capslayer
is not a modifier layer (shift
, meta
, control
, leftalt
or altgr
), it starts with no defined remaps. Which means that any key that is not remapped under [capslayer]
will be evaluated against remaps in the underlying layer, which often is [main]
unless other modifier keys are pressed (e.g., leftshift
activates the shift
layer).
The first example remap, d = M-d
makes CapsLock
+d
be perceived by the operating system as Win
+D
(my shortcut for launching programs).
The second example is a macro, which will execute a sequence of key taps. In this case, it will tap space twice and the left arrow key, effectively wrapping the cursor by spaces. For example, in my config, writing ( )
with the cursor at the center requires only 2 taps instead of 6: tap 9
to insert ()
left
and rightalt
to insert
left
.
Home row arrow keys
Moving the right hand out of the home row (j
k
l
ç
) is also problematic. Vim solves this by using h
, j
k
and l
as replacements of the left, down, up and right arrow keys while in normal mode. With caps lock held down, the same behavior can be achieved anywhere, even in applications without a vim mode, such as Firefox:
h = left
j = down
k = up
l = right
I have found this to be useful also inside vim when doing single-char cursor movements.
However, if these keys are not permanently fused in your muscle memory, maybe this mapping could be more comfortable:
j = left
i = up
k = down
l = right
On the subject of arrow keys, vim and IDEs will often auto-complete wrapping character pairs such as ""
and ()
. When the cursor is just to the left of a )
, moving it to the right requires typing )
(which the editor usually understands as “overwrite )
”) or tapping the right arrow key. For this use case, I have a mapping in the main
layer for rightalt
as right
if tapped, but as moving into the altgr
layer (its original function) if held:
rightalt = overload(altgr, right)
One-shot modifiers
Keyboard shortcuts are more efficient than dragging the mouse through cluttered menus. But The gymnastics involved in pressing Ctrl
+Shift
+Alt
+L
(which triggers my password manager) is not healthy Such modifier combinations are usually followed by a single tap before all modifiers are released. In other words, one does not type ABNT
with Ctrl
or Alt
held. One-shot modifiers will apply only to a single tap, which can happen without any modifier being held down.
My configuration has two one shot modifiers at the main
layer:
[global]
# allow this many milliseconds between each press when detecting a chord
chord_timeout = 300
# after this many ms without a key press, deactivate the oneshot modifier
oneshot_timeout = 1500
[main]
# (other remaps...)
rightalt+leftalt = oneshot(csa)
leftmeta = overloadt2(meta, oneshot(cs), 300)
The previous example can then be achieved as follows:
- Tap
leftalt
andrightalt
“together” (i.e., within 300 milliseconds)- This is known as a combo, denoted with the
+
in the configuration
- This is known as a combo, denoted with the
- After both keys are released, the one-shot modifier is armed
- If a key is pressed within 1500 milliseconds (see the
global
section)- keyd will process it according to remaps defined in the
csa
layer - keyd will deactivate the
csa
layer as well as the layers that correspond to the three modifiers that will be released once the user releases the key (in this example,L
)
- keyd will process it according to remaps defined in the
- Else, the one-shot modifier disarms and subsequent events happen without the three modifiers.
The meta key (a.k.a. the Windows key) is set up as a one-shot modifier for Ctrl
+Shift
shortcuts. However, this key is itself a modifier, leading to a potential ambiguity. Thus, it only arms the one-shot modifier if it is held by 300 milliseconds and no other key is pressed within this time window. If a key is pressed, e.g, 0
, the keyd
virtual keyboard will emit Win
+0
(M-0
in keyd
syntax).
The cs
and csa
layers are custom layers, not built-in. They consist of remaps for all keys using the respective modifiers:
[cs]
` = C-S-`
1 = C-S-1
# ...
[csa]
` = C-S-A-`
1 = C-S-A-1
# ...
Macros
The last trick are macros. As shown above, it is sometimes useful to tap one key to issue multiple taps. Two examples, defined in the capslayer
summarize most of my uses for macros beyond the wrap with space and wrap with parenthesis examples from before:
[capslayer]
1 = timeout(macro(space != space), 300, macro(C-a 50ms q 200ms 1))
, = timeout(macro(space < space), 300, macro(space <= space))
Tapping 1
with CapsLock
held produces !=
(note the spaces surrounding !=
). If instead of a tap, 1
is held, then the output is a not ergonomic sequence of key presses that move the focus in tmux
to the pane second pane (numbering starts at zero). Tmux uses a leader key pattern (Ctrl
+A
) to reduce finger contortion. After activating the leader key, q
is the command to move into a pane by its number and 1
is the destination pane number. The 50ms
within the macro is a cautionary sleep to simulate a human using the keyboard. The 200ms
sleep is functional, it allows number hints, drawn by tmux to appear for 200 milliseconds, which will help use the correct the number in a subsequent move.
The second macro simplifies typing comparison operators. Typing <
is actually comfortable, but I have a bad habit of leaving the home row to type such sequences. By tapping ,
with the middle finger keyd
will output <
, but if ,
is held for 300 milliseconds, then <=
is typed without requring the wrist movement that would be otherwise necessary.
Mouse keys
It would be nice if keyd
allowed moving the mouse. I have previously written a tool – kpmouse to do exactly that, and while using the keyboard as a mouse is clumsy, it is enough to fix the keyboard focus being stuck in a sidebar frame or to click on a button for which I do not know the shortcut neither do care to learn it.
Coincidentally, the same author of keyd
has written warpd
which is more robust than kpmouse
, even though the default key bindings are a bit strange despite having a clear logic. keyd
makes it easy to integrate warpd
mouse control using h
, j
, k
and l
when AltGr
is held:
[altgr]
# mouse movement with warpd
r = A-M-c
h = macro(A-M-g 50ms u 10ms esc 10ms A-M-c)
j = macro(A-M-g 50ms j 10ms esc 10ms A-M-c)
k = macro(A-M-g 50ms i 10ms esc 10ms A-M-c)
l = macro(A-M-g 50ms k 10ms esc 10ms A-M-c)
AltGr
+R
will trigger warpd
normal mode: h
, j
, k
and l
move the cursor around, while m
, ,
, and .
click the left, middle and right mouse buttons. AltGr
+H
and correlated will trigger warpd
grid mode, move into one of the four quadrants and then trigger normal mode where fine-grained movement is more comfortable.
On Arch Linux, warpd
is in the AUR. However, since it works by interacting with Xorg (or Wayland), it cannot be run as a system-wide systemd
unit. For my system, I simply added warpd
to my .xinitrc
. Probably there are more elegant ways to auto launch a process on your desktop environment, but I have been editing the same .xinitrc
since at least 2008, and it just works.
Footnotes
The applicable taxes are: 3.38% tax for making an international credit card payment, 60% import tax, 20.48% goods tax (defined as 17% over the final, already taxed value and which also applies over the due import tax), and lastly a 2.42 USD fixed clearing fee.↩︎
If you never used vim: It is a modal text editor. In the normal mode, hitting keys will cause movements (e.g., move the cursor to the next
s
) or actions (e.g., delete the next 3 words). To insert text, enter the insert mode by tappingi
and then it will behave as an editor used by healthy, well-adjusted people. Hint: type:q!
to exit vim.↩︎