--- title: "Community Spotlight Series #2: Node-free Config" authors: caksoylar tags: [keyboards, firmware, community] --- This blog continues our series of posts where we highlight projects within the ZMK ecosystem that we think are interesting and that the users might benefit from knowing about them. You might be aware that ZMK configurations in the [Devicetree format](/docs/config#devicetree-files) use the [C preprocessor](https://en.wikipedia.org/wiki/C_preprocessor) so that directives like `#define RAISE 2` or `#include ` can be used in them. In this installment we are highlighting the [`zmk-nodefree-config` project](https://github.com/urob/zmk-nodefree-config) by [urob](https://github.com/urob) that contains helper methods that utilizes this fact for users who prefer editing and maintaining their ZMK config directly using the Devicetree syntax format. In the rest of the post we leave it to urob to introduce and explain the motivations of the project, and various ways it can be used to help maintain ZMK keymaps. Stay tuned for future installments in the series! ## Overview Loosely speaking the _nodefree_ repo -- more on the name later -- is a collection of helper functions that simplify configuring keymap files. Unlike the graphical keymap editor covered in the [previous spotlight post](https://zmk.dev/blog/2023/11/09/keymap-editor), it is aimed at users who edit and maintain directly the source code of their keymap files. The provided helpers fall into roughly one of three categories: 1. Helpers that eliminate boilerplate, reduce the complexity of keymaps, and improve readability. 2. Helpers that improve portability of "position-based" properties such as combos. 3. Helpers that define international and other unicode characters. The reminder of this post details each of these three categories. ## Eliminating Boilerplate In ZMK, keymaps are configured using so-called _Devicetree_ files. Devicetree files define a collection of nested _nodes_, whereas each node in turn specifies a variety of _properties_ through which one can customize the keymap. For example, the following snippet sets up a [mod-morph](https://zmk.dev/docs/keymaps/behaviors/mod-morph) behavior that sends . ("dot") when pressed by itself and sends : ("colon") when shifted: ```dts {6-7} showLineNumbers / { behaviors { dot_colon: dot_colon_behavior { compatible = "zmk,behavior-mod-morph"; #binding-cells = <0>; bindings = <&kp DOT>, <&kp COLON>; mods = <(MOD_LSFT|MOD_RSFT)>; }; }; }; ``` Adding this snippet to the keymap will create a new node `dot_colon_behavior` (nested underneath the `behaviors` and root `/` nodes), and assigns it four properties (`compatible`, `#binding-cells`, etc). Here, the crucial properties are `bindings` and `mods`, which spell out the actual functionality of the new behavior. The rest of the snippet (including the nested node-structure) is boilerplate. The idea of the _nodefree_ repo is to use C preprocessor macros to improve readability by eliminating as much boilerplate as possible. Besides hiding redundant behavior properties from the user, it also automatically creates and nests all required behavior nodes, making for a "node-free" and less error-prone user experience (hence the name of the repo). For example, using `ZMK_BEHAVIOR`, one of the repo's helper functions, the above snippet simplifies to: ```dts showLineNumbers ZMK_BEHAVIOR(dot_colon, mod_morph, bindings = <&kp DOT>, <&kp COLON>; mods = <(MOD_LSFT|MOD_RSFT)>; ) ``` For complex keymap files, the gains from eliminating boilerplate can be enormous. To provide a benchmark, consider my [personal config](https://github.com/urob/zmk-config), which uses the _nodefree_ repo to create various behaviors, set up combos, and add layers to the keymap. Without the _nodefree_ helpers, the total size of my keymap would have been 41 kB. Using the helper macros, the actual size is instead reduced to a more sane 12 kB.[^1] [^1]: To compute the impact on file size, I ran `pcpp --passthru-unfound-includes` on the `base.keymap` file, comparing two variants. First, I ran the pre-processor on the actual file. Second, I ran it on a version where I commented out all the _nodefree_ headers, preventing any of the helper functions from getting expanded. The difference isolates precisely the size gains from eliminating boilerplate, which in my ZMK config are especially large due to a vast number of behaviors used to add various Unicode characters to my keymap. ## Simplifying "Position-based" Behaviors In ZMK, there are several features that are position-based. As of today, these are [combos](/docs/keymaps/combos) and [positional hold-taps](/docs/keymaps/behaviors/hold-tap#positional-hold-tap-and-hold-trigger-key-positions), with behaviors like the ["Swapper"](https://github.com/zmkfirmware/zmk/pull/1366) and [Leader key](https://github.com/zmkfirmware/zmk/pull/1380) currently developed by [Nick Conway](https://github.com/nickconway) in pull requests also utilizing them. Configuring these behaviors involves lots of key counting, which can be cumbersome and error-prone, especially on larger keyboards. It also reduces the portability of configuration files across keyboards with different layouts. To facilitate configuring position-based behaviors, the _nodefree_ repo comes with a community-maintained library of "key-position labels" for a variety of popular layouts. The idea is to provide a standardized naming convention that is consistent across different keyboards. For instance, the labels for a 36-key layout are as follows: ``` ╭─────────────────────┬─────────────────────╮ │ LT4 LT3 LT2 LT1 LT0 │ RT0 RT1 RT2 RT3 RT4 │ │ LM4 LM3 LM2 LM1 LM0 │ RM0 RM1 RM2 RM3 RM4 │ │ LB4 LB3 LB2 LB1 LB0 │ RB0 RB1 RB2 RB3 RB4 │ ╰───────╮ LH2 LH1 LH0 │ RH0 RH1 RH2 ╭───────╯ ╰─────────────┴─────────────╯ ``` The labels are all of the following form: - `L/R` for **L**eft/**R**ight side - `T/M/B/H` for **T**op/**M**iddle/**B**ottom and t**H**umb row. - `0/1/2/3/4` for the finger position, counting from the inside to the outside The library currently contains definitions for 17 physical layouts, ranging from the tiny [Osprette](https://github.com/smores56/osprette) to the large-ish [Glove80](https://www.moergo.com/collections/glove80-keyboards). While some of these layouts contain more keys than others, the idea behind the library is that keys that for all practical purposes are in the "same" location share the same label. That is, the 3 rows containing the alpha keys are always labeled `T/M/B` with `LM1` and `RM1` defining the home position of the index fingers. For larger boards, the numbers row is always labeled `N`. For even larger boards, the function key row and the row below `B` are labeled `C` and `F` (mnemonics for **C**eiling and **F**loor), etc. Besides sparing the user from counting keys, the library also makes it easy to port an entire, say, combo configuration from one keyboard to the next by simply switching layout headers. ## Unicode and International Keycodes The final category of helpers is targeted at people who wish to type international characters without switching the input language of their operation system. To do so, the repo comes with helper functions that can be used to define Unicode behaviors. In addition, the repo also ships with a community-maintained library of language-files that define Unicode behaviors for all relevant characters in a given language. For instance, after loading the German language file, one can add `&de_ae` to the keymap, which will send ä/Ä when pressed or shifted. ## About Me My path to ZMK and programmable keyboards started in the early pandemic, when I built a [Katana60](https://geekhack.org/index.php?topic=88719.0) and learned how to touch-type Colemak. Soon after I purchased a Planck, which turned out to be the real gateway drug for me. Committed to making the best out of the Planck's 48 keys, I have since discovered my love for tinkering with tiny layouts and finding new ways of [squeezing out](https://xkcd.com/2583/) a bit more ergonomics. Along the way, I also made the switch from QMK to ZMK, whose "object-oriented" approach to behaviors I found more appealing for complex keymaps.[^2] [^2]: I am using the term object-oriented somewhat loosely here. What I mean by that is the differentiation between abstract behavior classes (such as hold-taps) and specific behavior instances that are added to the keymap. Allowing to set up multiple, reusable instances of each behavior has been a _huge_ time-saver compared to QMK's more limited behavior settings that are either global or key-specific. These days I mostly type on a Corne-ish Zen and are waiting for the day when I will finally put together the [Hypergolic](https://github.com/davidphilipbarr/hypergolic) that's been sitting on my desk for months. My current keymap is designed for 34 keys, making liberal use of combos and [timerless homerow mods](https://github.com/urob/zmk-config#timeless-homerow-mods) to make up for a lack of keys.