Polymatheia

Teaching DucKey to Quack 🦆

posted

Meet DucKey, my favourite keycap:

A rubber duck keycap on the top left of a keyboard. Quack! 🦆

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:

  1. On Windows, the easiest solution without any popup is likely Alt - + as described in this blog post, which seems to work for most applications.
  2. On Mac, you can alter the Alt key behaviour to support Unicode hex input when holding it down.
  3. 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:

A Download source link under an orange "Download this layout" button

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.

The Oryx configuration with my duck emoji macro spelling out D-U-C-K

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:

Oryx configuration with my emoji layer

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):

Translation diagram for a normal keypress

It would look like this:

Naïve translation diagram for a Unicode-based keyboard press

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:

Translation
   diagram for a Unicode-based keyboard press, taking into account changing
   desired keyboard layout changing

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.

  1. Foone has a Twitter thread explaining the state of this mess. It… uh, leaves a lot to be desired. 

  2. 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. 

  3. 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.