-
Notifications
You must be signed in to change notification settings - Fork 314
Preprocessor Support
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.
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.
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.
This is perhaps my biggest concern and can be explained with the most mundane of practices.
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.
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.
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.