zmk/docs/blog/2023-12-17-nodefree-config.md

190 lines
9.4 KiB
Markdown
Raw Permalink Normal View History

---
title: "Community Spotlight Series #2: Node-free Config"
author: Cem Aksoylar
author_title: Documentation maintainer
author_url: https://github.com/caksoylar
author_image_url: https://avatars.githubusercontent.com/u/7876996
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 <behaviors.dtsi>` 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/behaviors/mod-morph) behavior that sends <kbd>.</kbd>
("dot") when pressed by itself and sends <kbd>:</kbd> ("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/features/combos) and [positional
hold-taps](/docs/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 <kbd>ä</kbd>/<kbd>Ä</kbd> 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.