Skip to content

Preprocessor Support

Nick Coutsos edited this page Jul 31, 2023 · 1 revision

What are preprocessor macros?

This refers to statements commonly seen in ZMK keymaps to make repetitive expressions more concise. Some of the simpler examples include:

Avoid repeating parameters for an autoshift behavior

#define AS(x) &as x x

Define identifiers to reference layers by name instead of index

#define DEFAULT 0
#define RAISE 1
#define LOWER 2

Apply homerow modifiers to a number of key positions

#define HRML(a, b, c, d) &kp LGUI a &kp LALT b &kp LSHFT c &kp LCTL d

Simplify definition of ZMK macro behaviors

#define ZMK_MACRO(name,...) \
name: name { \
    label = ZMK_MACRO_STRINGIFY(ZM_ ## name); \
    compatible = "zmk,behavior-macro"; \
    #binding-cells = <0>; \
    __VA_ARGS__ \
};

These are all interpreted by the preprocessor at build-time to generate the actual devicetree keymap code that gets compiled into your firmware. These and others provide a shorthand for writing keymaps without a lot of syntactic sugar and verbosity.

At this time I don't support (and generally discourage) parsing keymaps that use these macros, with some minor exceptions. There are ways to make this work but, as with all software, priorities must be set and value must be balanced with effort.

What are the challenges to supporting preprocessor macros?

Devicetree Grammar

If I decided to shift gears today and start working on support for this, the initial roadblock is purely technical: the devicetree grammar I use to parse keymaps doesn't recognize preprocessor statements at arbitrary locations in the tree.

Actually pre-process

Some ZMK tools will run the keymap file through a preprocessor so that when the time comes to parse that keymap it can be done based on raw values instead of abstractions like in the examples above. This works, and this comes as no surprise because it's one of the first steps to building the firmware.

Should I do the same? There are multiple considerations, the first of which is where to do this preprocessing.

I could do it server side, but that's not without cost (extra computation) and additional data to fetch (extra network or storage use). I'm also not fond of executing arbitrary user code.

That concern could be mitigated by doing the processing client-side with a JS- based preprocessor, but it still means loading up everything needed to make that work.

Can I reasonably expect that all custom macros will be defined within the same keymap file, or do I have to look for any #include statements and fetch those as well? What if a user is building against a custom fork of ZMK, requiring that I parse config/west.yml and fetch a whole ZMK repository?

In addition to these technical challenges there are design constraints, explored below.

Modifying heavily preprocssed keymaps

This is perhaps my biggest concern and can be explained with the most mundane of practices.

Example: aliasing layer indices.

It's extremely common to define identifiers for each layer and reference them in keymap bindings instead of using the corresponding layer index (e.g. #define RAISE 1, and &mo RAISE). You'll see it all over the default keymaps in the ZMK source.

This pattern is handy because you can shift layers around and as long as you're referencing these identifers you don't need to track them down across the entire keymap; there's just this one section near the top of your file where you can update the relevant layer values.

Problem: re-serialization

The problem, however, is that these mappings are one-way. We may understand intuitively that RAISE means 1 and therefore 1 means RAISE in the context of layers but it's not always that simple. What if new layers are being created in the app? Do I auto-generate these identifiers if I can't determine a suitable identifier exists (read: I'm certain that it refers to the new layer and nothing else), or do I bake the idea of these identifiers into the app and prompt the user to pick a name themselves? Not everybody uses this pattern, of course.

It's understandably harder to apply this to the more complicated examples. This isn't something I can just automate, the keymap editor would have to be aware of these macros and capable of producing code that uses them. So assuming that this is something that can be done, should it be used all the time?

ZMK_MACRO is built into ZMK so it's available to everybody but unless I decide that all users should represent their macro behaviors that way there would need to be a way to track whether an indivial behavior was originally defined that way or its "raw" when re-serializing it, and which method the majority of nodes use in order to apply it to new instnces.

Why make this my problem?

If I ignore the "difficult question"-type problems by solving them the way I'd like I could just focus on the technical challenges. Maybe I deal with all the preprocessor stuff by... just preprocessing keymaps, and then I'm only dealing with devicetree code that I'm already able to parse.

Right?

The very first design goal for the keymap editor was to produce readable keymap code. Anything less than that is against the philosophy of the tool. For better or for worse, the current state of ZMK means that you are not using firmware to update a keymap, you are building new firmware. This is necessarily a software development activity and introducing shortcuts like these preprocessor macros makes that more complex, not less. Devicetree code is therefore a first-class entity in this system and it's representation and intention must be preserved.

In my humble opinion, if you want to use this tool to edit keymaps then you don't need preprocessor macros. If you want to use preprocessor macros, then you don't need this tool.

I know that many users are loading their keymaps into this editor without ever making or saving changes, and while that used to confuse me I understand that people like to have graphical representations of their keymaps for reference or printing. This, too, was a design goal, but if that's all you need you may be better off with another tool like caksoylar/keymap-drawer.