ZMK Behavior Editing #78
Replies: 5 comments
-
Some of the groundwork for this is in place already. If you're using features like mouse emulation which isn't merged into ZMK yet you may see that the behaviors appear in the behavior selector (if they're defined in your keymap file directly) and that even though there isn't a proper picker for its parameters you can view/modify raw values. The "raw" behavior editor was dark-launched recently. A lot had to happen just to get to this point so I haven't been able to test it much yet, but even without others using it directly I want to make sure that nothing else was broken unexpectedly. Once I'm able to get a better look at things I'll make the option to use behavior editing a user-facing option. |
Beta Was this translation helpful? Give feedback.
-
The switch from editing "raw" property values to parsed (in situations where I can confidently parse a property value into reasonable types) was a challenge, but after defining a few value extraction functions based on ZMK's documentation I got to a point where I could have specific editing components for basic types. I spent some time this weekend fleshing this out with some customizations. The I also created a component to process the I think the next thing I'll look at is a bindings picker since I'll get to reuse some UI components from the keybinds. |
Beta Was this translation helpful? Give feedback.
-
It's been a little while since I've touched this, but it hasn't been forgotten. Once I had basic behavior editing functionality in place I had to pause that work to focus on performance. Parsing speedDevicetree searching
I can't rely on that. These groups of nodes can be at any depth in the tree, and because each behavior and macro node specifies its own I've already re-implemented this as a breadth-first search to take advantage of conventions without relying on them. I can further refine this by giving myself a way to rule out certain nodes during traversal. I'm looking for behavior nodes, which can be just about anywhere, but if I encounter Update buffering Edge-cases/unconventional device trees
which ZMK (or Zephyr?) will accept and merge into a single node. As this was technically acceptable I wanted to make sure that the keymap editor would be able to handle it, so the parser will first look for any root nodes in the file, and then search each of them for more specific nodes. You can see how this gets out of hand. Being able to rule out these edge cases would allow for much more targeted searches and improve performance significantly. Somewhat related to this, I've seen errors come up from keymaps that use custom macros to simplify things like homerow mods, or zmk-nodefree to abstract devicetree syntax altogether. I think these are both good ways to simplify code and improve readability and code reuse, but they're problematic for an editor where the code is as much the end product as it is the storage medium. Pre-processing these macros into devicetree syntax that the editor can already parse is one thing, but spitting out keymap code that uses these macros as intended would require building the editor around those concepts specifically. Long story short, I'm recognizing that as nice as it is to be flexible it shouldn't be at the expense of the more typical use cases. I've added some sanity checks to look out for use of the node-free macros and warn the user of the compatibility issues, and I can do the same for C macros to alias actual key bindings or unusual device tree structures. Memory leaksAt the same time, for a little while now, I've been having memory issues with the application causing increasingly frequent crashes. This wasn't a huge issue because the backend is largely stateless and as soon as the process is back up and running the app continues functioning. Still, not ideal. After a lot of digging I found out that, thankfully, the leaks were less of an issue of writing code that can't be garbage collected and more to do with improper use of Tree Sitter. This really just boils down to using |
Beta Was this translation helpful? Give feedback.
-
Since improving performance the other week I've been putting some effort into getting the codebase more organized. The next steps for behavior editing will require utilizing some of the existing functionality in the frontend (so that if you do something like create a new behavior based on hold-tap the app can immediately determine parameters and allow you to bind the appropriate values) and I've started breaking sections of the codebase into chunks with clearer boundaries. Some of this is already being incorporated into the web app behind the scenes. I have a sub-package for the data that gets extracted from the ZMK repo and augmented (behaviors and keycodes, and also the list of supported ZMK keyboards), and the web application bundles this same package so that its no longer necessary to query the API for what is fairly static data. There's also a sub-package with my helper code for parsing/mutating devicetree documents and while I'm not using this in the browser yet I've had to spend a day or two just trying to make sense of that landscape. Right now I'm going back to shore up some of my sorely lacking test coverage because I'm very likely to break things as I try and find better ways to organize them. |
Beta Was this translation helpful? Give feedback.
-
Finally making some real progress. I had to take some time away from the project, but I've been able to slowly separate some of the complex logic to help share code between the server and application. I've also looked at areas of inconsistency in how different parts of the system handle behavior properties. Having taken care of that, it is now possible to create a new behavior in the frontend application, and immediately bind it with the correct parameter types. I have not tested this exhaustively, so here's hoping nothing has broken. There are still limitations to this, however. It is currently possible to create a new behavior such as a hold-tap with This isn't exactly a new problem. Even from the beginning there have been inter-dependencies like binding a layer index as a parameter to |
Beta Was this translation helpful? Give feedback.
-
Regarding the creation and customization of existing and new behavior definitions within the Keymap Editor interface.
TL;DR
Supporting ZMK's pre-defined behaviors has required manually defining the parameters expected for binding cells (e.g., that layer-tap expects a layer index and a keycode), but this isn't feasible for staying on top of new behaviors introduced by ZMK, or customizations defined in user keymaps (like custom hold-taps). Creating custom editors for each behavior is also not ideal, but making something entirely data-driven has its own challenges.
In more detail
Each behavior is described schemas in the ZMK codebase. From this we can see what properties are available and their types, as well as the number of binding cells. This level of detail is, I think, just what's required for Zephyr, and isn't enough on its own explain how to come up with the appropriate values.
As an example, the
mods
property (inzmk,behavior-caps-word
andzmk,behavior-mod-morph
) is described as anint
but the fact that it's a collection of constants describing modifiers from the HID spec joined together as a binary union is something you'd only get by reading ZMK documentation or the actual source code implmenting that behavior. The point being that this requires some amount of human planning, but hopefully this can be done in a way that makes things more manageable.My goal right now is to keep things modular so that custom editors for specific property types are reusable across different behaviors and to ensure that there's always a fallback to editing as a raw value or else ignoring so that even unrecognized behaviors (something new that I haven't had a chance to review yet, or something like mouse emulation that isn't in ZMK's main branch yet) can still be set up outside of the app without causing conflicts.
Roadmap
bindings
propertycompatible
property.mods
selector forzmk,behavior-caps-word
, andzmk,behavior-mod-morph
that accepts 0 or more values from the list of HID modifier keys and serializes them as a union.bindings
selector that accepts only behaviors (for hold tap and others)bindings
selector that accepts behaviors with binding cells (for tap dance and others)&caps_word
'smods
list, or&mt
'stapping-term-ms
.Beta Was this translation helpful? Give feedback.
All reactions