Teaching DucKey to Quack đŠ
posted
Meet DucKey, my favourite keycap:
Ever since Iâve gotten a Moonlander and learned how to configure my layouts, I wanted to tune my setup so that DucKey printed out the duck emoji (đŠ) when they were pushed down on a specific layer.
With enough patience and key presses you can do that in all the main operating systems you operate with a keyboard:
- On Windows, the easiest solution without any popup is likely Alt - + as described in this blog post, which seems to work for most applications.
- On Mac, you can alter the Alt key behaviour to support Unicode hex input when holding it down.
- On Linux, you can usually use Ctrl - Shift - U followed by the codepoint of the character you want to print, followed by either Enter or Space.
This technique is also the one youâll probably end up using if you want to program your keyboard to type emojis: There doesnât seem to be an easier alternative out there1.
The ZSA keyboards (of which Moonlander is one) have a tool called Oryx for handling your keyboard layouts, and it supports macros. Those macros can simulate typing up to 5 keys in a row, with an optional Enter at the end.
But five characters plus enter at the end isnât sufficient for a generic Unicode symbol. Unfortunately that applies to the đŠ emoji: I have to type Ctrl - Shift - U, followed by the keys 1F986, followed by Enter. Which is⊠6 keys, one more than the limit of 5 would support.
ZSA funnily enough expanded that length to 5 characters to support Unicode input for Linux, as explained in this blog post. Now, I kinda understand their rationale for keeping it that way, but it also makes it impossible to print any emoji with Linux â and the other OSes too, I suspect.
QMK + Source = Easy
Some quick Googling for how to bypass that limit lead me to QMK, which is â from what I gather â what Oryx uses under the hood. And from the looks of it, I should be able to change and write some C code, then build the firmware from source. From experience, C code with microcontrollers isnât just something you can flesh out in a couple of hours.
So I left the macro idea for a bit as I had more than enough to do, such as learning how to use a Moonlander with an English keyboard layout. Before that, I just used a regular Scandinavian keyboard layout with a numpad, so not knowing where all the non-alphanumeric symbols were was quite the experience.
But when that was all done, I went back to inspect how much effort it would actually take to configure the macros myself. Turns out I was completely wrong!
Getting QMK Up and Running with Moonlander
If youâve made a layout in Oryx before, you have probably downloaded the layout, and youâve probably seen the âDownload Sourceâ link right below it:
This will download a zip file with the compiled firmware, a readme, the C code
under a folder named keyboard_<layoutname>_source
, and a compilation log for
the compiled firmware. The steps werenât difficult, but I felt the readme didnât
make it clear where in the process you had to do the ZSA-fork stuff, which gave
me one false start2. In the end, here are the exact steps on how
I installed it on my Debian-based system:
# first install python3-pip
$ sudo apt install python3-pip
# install qmk via pip
$ python3 -m pip install --user qmk
# NB: this sets up qmk with ZSA's fork
$ qmk setup --home ~/code/qmk_firmware \
--branch firmware21 \
zsa/qmk_firmware
# copy your Oryx source code into a folder
# in the Moonlander keyboard subfolder:
$ mv ~/Downloads/moonlander_duckey_source \
~/code/qmk_firmware/keyboards/moonlander/keymaps/duckey
# then finally compile it:
$ cd ~/code/qmk_firmware
$ make moonlander:duckey
When you have compiled the firmware, you can use ZSAâs flash tool Wally to flash your keyboard.
I am not sure if I was lucky when it comes to dependencies I had already installed or not, as my usual experience is that I need to install additional source packages to get low-level things up and running. It looks to me like QMK has their setup well-designed though.
Modifying the Macro
Alright: I have code, and I have the tools to build our firmware and flash our Moonlander. Now I need to find out where my macro is located.
To make things easier for myself, I made my DucKey macro just send the keys DUCK in sequence. The hope was that I could easily find the sequence somewhere in the source code.
Fortunately there wasnât much code to look at, so the search was quite fast. In
the file keymap.c
, thereâs a function called process_record_user
which seems
to be responsible for handling all the different macros you have created. My
DucKey macro looks like this:
if (record->event.pressed) {
SEND_STRING(SS_TAP(X_D) SS_DELAY(100) SS_TAP(X_U)
SS_DELAY(100) SS_TAP(X_C) SS_DELAY(100) SS_TAP(X_K));
}
This looks surprisingly straightforward to change. Recall that I want to
simulate the call Ctrl - Shift - U, followed by pressing
1F986, followed by
Enter. I donât know how youâd add Ctrl and
Shift yet, but I bet I couldâve looked up how to do that in the QMK
documentation. However, it seemed easier to change the macro in Oryx to hold
down both Ctrl and Shift while pressing down D,
then see how the source would change. I also added an Enter at the
end just in case it is something else than X_ENTER
, but it turns out it
wasnât.
I found out that you use the modifier like a âfunctionâ call in this macro
magic. For example, if you want to simulate pressing down Ctrl while
pressing x, you change the macro from SS_TAP(X_X)
to
SS_LCTRL(SS_TAP(X_X))
.
All in all, I replaced the contents of the macro with this:
SEND_STRING(
// Ctrl - Shift - U
SS_LCTL(SS_LSFT(SS_TAP(X_U))) SS_DELAY(100)
// The codepoints
SS_TAP(X_1) SS_DELAY(100) SS_TAP(X_F) SS_DELAY(100)
SS_TAP(X_9) SS_DELAY(100) SS_TAP(X_A) SS_DELAY(100)
SS_TAP(X_2) SS_DELAY(100)
// and enter to finish off the command:
SS_TAP(X_ENTER));
And after rebuilding the firmware, it worked! Now I had DucKey quacking đŠ
However, as you can see in the video, it is terribly slow, and I bet those
SS_DELAY(100)
macros were the reason. I was tempted to remove them entirely,
but ended up turning them down from 100 to 1 in case they actually have a
purpose:
More than Just Ducks
I use a bit of emojis now and then, and my preferred tool on Linux to do that is the x11-emoji-picker. In practice I donât really need to have emojis on a layer, but it does speed up typing a lot when I have my commonly used emojis there. So I did just that, and made an entire layer with emojis for the ones I type the most:
Automating the Work
I could repeat the manual process as I did with DucKey, but it was more than enough effort for one key. Besides, I would have to reapply all the manual work every time I updated my layout from Oryx! Thatâs too much effort.
So instead, I made a small Python program to convert sequences like DUCK
into
the desired Unicode codepoint. It first reconstructs the macro as Oryx would
define it:
def tap_key(c):
return 'SS_TAP(X_{})'.format(c.upper())
def old_macro(macro_name):
mid = ' SS_DELAY(100) '.join(map(tap_key, macro_name))
return 'SEND_STRING(' + mid + ');'
Then it defines the new macro:
def new_macro(ucode):
mid = ' SS_DELAY(1) '.join(map(tap_key, ucode))
prefix = 'SEND_STRING(SS_LCTL(SS_LSFT(SS_TAP(X_U))) SS_DELAY(1) '
return prefix + mid + ' SS_DELAY(1) SS_TAP(X_ENTER));'
and replaces all occurrences in the source code of the old with the new:
from collections import namedtuple
Macro = namedtuple('Macro', ['name', 'codepoint'])
def replace_macro(src, macro):
return src.replace(old_macro(macro.name),
new_macro(macro.codepoint))
and then we can iterate over all the macros we want to replace!
Capital Duck
Since this turned out to be easier than anticipated, I decided to up the ante a tiny bit: I wanted my emoji keys to act like actual letters and return capital versions of themselves when shift was held down.
Of course, that would mean I need to know what the capital version of an emoji is. There are a couple of obvious ones on my layout:
- đŠ -> đŠą3
- đ° -> đ
- đ -> đ
- đą -> đ
- đŠ” -> đŠż
- đȘ -> đŠŸ
I added some less obvious ones for convenience, but skipped most of the remaining ones.
How to detect whether shiftâs pressed down isnât as clear: There doesnât seem to be a standard recommended approach. Some Google results suggest using
(report->mods & MOD_MASK_SHIFT)
to check whether any shift key is pressed, but others say
(get_mods() & MOD_MASK_SHIFT)
is better as get_mods()
is more up-to-date. Iâm not entirely sure whether this
actually matters for my use case, but using get_mods()
seems to work for me.
Mind you, I still havenât read any of the documentation for QMK. Thatâs mostly because I suspect this is as far as Iâll dig into custom keyboard setups for some time.
Adding Weird Letters
I mentioned earlier in this post that I postponed this because I wanted to learn how to use the Moonlander with an English keyboard layout. However, I am Norwegian, and at times, I have to use the Norwegian characters ĂŠ, Ăž and Ă„.
Now, Oryx does âsupportâ Norwegian keycodes, but it only adds the special characters and doesnât remap the other keys. And it doesnât work with an English input locale, so it is useless to me if I donât want to use the Norwegian locale. So instead, I resigned and mapped my left alt key as my compose key and use Composeae for ĂŠ, Compose/o for Ăž, and Composeaa for Ă„.
Since I donât use them often, Iâve placed the compose key at the far end of my keyboard. Itâs fine for ĂŠ and Ă„, but Ăž is very kludgy to type. I considered them for Oryxâ macros before I got DucKey, but since the Oryx macros donât automatically make Shift-<macro> turn Ă„ into uppercase Ă , I discarded that idea. However, when I found out that I could tune the macros to do different things based on whether shift is held down, I added them back in on my secondary layer.
A Protocol With Need for an Update
Now with my macros being able to type ĂŠ and friends, can I discard my compose button? Perhaps, but not with the software stack I am currently running.
See, hereâs what happens when I press down DucKey while being in an Emacs window:
Apparently, Emacs intercepts Ctrl - Shift - U and treats it as the keybinding for C-u. On default Emacs installations, that defaults to the universal-argument command.
However, Emacs doesnât intercept the compose key, nor does it manage to intercept the application finderâs shortcut for emoji-picker. I rarely use emojis or any Norwegian letters while using Emacs, but if I ever need to, I can use those.
I could probably find a way to make my macros work for all the software I use on my computer, but it feels unnecessary right now. Technically, I could already do that with ĂŠ, Ăž and Ă„ by making them use the compose key sequence instead of the Unicode input method. But it doesnât happen often enough that it breaks, and my compose key is in a wonky spot on my keyboard that I wonât use very often, so itâs not like itâs wasting precious space on my keyboard.
The fact that this is a problem I have is bothering me though: All input keys are, in a sense, defined by the OS, and not by the keyboard itself. To me, itâs slightly bonkers that a Greek keyboard doesnât know that it is a Greek keyboard.
One alternative would be a protocol where the keyboard knows its identity and sends out Unicode code points. So instead of the original protocolâs design (vastly simplified for the discussion at hand):
It would look like this:
This would work for most people, but some people may want or need to use a different keyboard layout from time to time. For that reason, the keyboard does not only need to know what Unicode code point to send, but it also needs to send out some sort of layout identifier to the OS. In that way, the OS can intercept and change the actual key sent to programs if the user wants to:
So for example, a Greek keyboard could send out its identifier (perhaps
el_GR
) whenever it connects to the computer. And whenever it sends out ÎŁ
it
will be translated to S
when you want to type Latin characters.
What would happen in edge cases isnât clear to me, but as long as the default behaviour is âjust print whatever the keyboard says was pressedâ, it doesnât really matter for customisable keyboards.
Getting all the OSes to support a new keyboard input protocol seems unlikely for a lot of reasons. But itâs not entirely outrageous that it will happen for Linux as a patch to the kernel. It just takes an angry enough developer to make a driver and a keyboard to communicate with that driver.
But right now, there doesnât seem to be enough anger contained inside one developer to make that happen: The scancodes wonât have to worry just yet.
-
Foone has a Twitter thread explaining the state of this mess. It⊠uh, leaves a lot to be desired. ↩
-
Itâs a little easier if you read the documentation in their GitHub repo, but itâs weird to me that they donât link directly to that from the README. ↩
-
Well.. This is the most obvious candidate at the moment, but could be superseded by the goose emoji. Since support for the goose emoji is sparse, Iâm postponing that choice until adoption has grown. ↩