Adventures in Obscure C Features (2020)

Hi, my names Germ and I write C for making keyboards do keyboard things, Recently I ran into a problem and my only tool was the C Preprocessor. No generation, no external tooling, just good old #define, #undef and #include.

I make very small keyboards, that abuse lesser used features of the QMK project, a open source general purpose keyboard firmware that is extremely powerful. If you like programmable devices, get a programmable keyboard. Once you to ~30 keys or under QMK becomes a bit painful, and this is why I wrote a onboard chording engine that layers onto QMK. Rather then using software like Plover or AHK everything happens on the keyboard itself allowing for a portable and independent setup (Because moving configurations around is a pain!).

The Problem:
For these smaller keyboards, reliance on Chords and Combos is absolutely necessary. Here's a image of GergoPlex's vim-orientated combos that it ships with.

This is a trivial case, Ginny requires over 500 different chords to function!
Now let's look at the code to generate this

To add a combo a enum and a define must be updated, a entry added to an array and a sequence stored in flash memory. Needless to say this can be cleaned up. In it's current state there's 50 lines for a handful of macros, this is only going to get worse as more are added. Let's mock up a macro and see if it can be implemented and maybe we can clean this up. Note: We can't simply embed the PROGMEM'd arrays into here, we need them to be processed into flash storage properly. We're working on an embedded device here with 32K of flash, 2.5K of SRAM and 1K of EEPROM (Who needs memory protection anyway?)!

That's a non starter. What about making up 3 different macros?

Looks OK and might compile but we still have the root problem: A unmaintainable mess of code that's about the same length. What if we could change how macros are evaluated on the fly? Turns out with a bit of #undef and finesse we can! But in order to do this, all the arguments must be the same between invocations. So lets clean that up.

Now for the fun part, we give our macro a name and slam it off into it's own file and #include it multiple times! A caveat to this process is that we need to generate valid identifiers. So Macro-String-Hashing isn't an option. Hold my preprocessor!

Well that's pretty neat. Now the combo defs are slammed into their own little file and we have a mess of C in the keymap.c file. We can tuck that away later into a header. But right now there's a more pressing matter: Sending multiple keystrokes! This is how you do that using QMK Combo features.

...That's not pleasant. We can do the X Macro shenanigans again, but including arbitrary C in a macro doesn't seem ideal. Thankfully there's a good function called SEND_STRING() that handles 99% of what we would want to do in here. So we'll abuse that and add in the SUBS() macro alongside the COMB() macro.

So with the crazyness that is the C preprocessor lines can now be blanked out or modified as needed. In theory this system can be extended as much as we need. And we're going to go a little bit farther. One property we can abuse of the C include system is the fact that it's recursive. So we tuck the gory internals into g/keymap_combo.h and have that call combos.def. In combos.def we include other submodules to compose the combos from here's a light example.

The major benefit to this is that now modules are small one-line includes that are logical as opposed to a mess of keyboard data. More importantly meta-defs are possible that compose other structures. Think of just importing awesome-vim-combos or awesome-emacs-combos and they pull in everything that's deemed 'awesome'. Configuration is also possible with the a PREFIX define that modules/the user can test this or override this if wanted.

Lastly, let's put this all together
So now instead of having a keymap that looks like how we started, there's organization, structure and exportability. Also this runs in a generic way on top of QMK. That's pretty cool eh?

Why not just use a generator?

This hit me multiple times while working on this portion of the project. It's doable but requires external tooling. To even use this feature you're already compiling your own firmware locally (as QMK config doesn't support the extended features), so having a extremely quick turn around time is important. As well using the tooling that is expected to build a firmware in the first place cuts down on external cruft.

This only works on GergoPlex though?

Nope! It's board agnostic, and hooks basic keycodes. In time a virtual keymap will be hooked and easily translated to physical keys (similar to how the chording engine works, but that's for next week!)

Looks cool! How can I help?

Grab a keyboard, clone this repo, and whack any cool dicts you come up with against this organization! And if a GergoPlex or Gergo seems like a neat board, grab one ;)

Next time: Using this methodology to create a composable dictionary for onboard stenography. When a whole language fits on 30 keys, things get fun!