Implement Nick Conways's tri-state behavior for ZMK.
This is proof of concept that shows that without maintaining fork, new behavior can be implemented.
This implementation supports latest Zephyr 3.5 release with ZMK.
Under config/west.yml
add remotes
and projects
, here is an example of
full file. Feel free to visit my config located here.
manifest:
remotes:
- name: zmkfirmware
url-base: https://github.com/zmkfirmware
- name: dhruvinsh
url-base: https://github.com/dhruvinsh
projects:
- name: zmk
remote: zmkfirmware
revision: main
import: app/west.yml
- name: zmk-tri-state
remote: dhruvinsh
revision: main
self:
path: config
NOTE: common use-case hasn't be define for this behavior.
To use,
#include <dt-bindings/zmk/keys.h>
/ {
behaviors {
swapper: swapper {
compatible = "zmk,behavior-tri-state";
#binding-cells = <0>;
bindings = <&kp A>, <&kp B>, <&kt C>;
};
};
};
Once this define, &swapper
can be use wherever it require. See below
documentation for further details. Working example can be find here.
Tri-States are a way to have something persist while other behaviors occur.
The tri-state key will fire the 'start' behavior when the key is pressed for the first time. Subsequent presses of the same key will output the second, 'continue' behavior, and any key position or layer state change that is not specified (see below) will trigger the 'interrupt behavior'.
The following is a basic definition of a tri-state:
/ {
behaviors {
tri-state: tri-state {
compatible = "zmk,behavior-tri-state";
label = "TRI-STATE";
#binding-cells = <0>;
bindings = <&kp A>, <&kp B>, <&kt C>;
};
};
keymap {
compatible = "zmk,keymap";
label ="Default keymap";
default_layer {
bindings = <
&tri-state &kp D
&kp E &kp F>;
};
};
};
Pressing tri-state
will fire the first behavior, and output A
, as well as
the second behavior, outputting B
. Subsequent presses of tri-state
will
output B
. When another key is pressed or a layer change occurs, the third,
'interrupt' behavior will fire.
Setting timeout-ms
will cause the deactivation behavior to fire when the time
has elapsed after releasing the Tri-State or a ignored key.
- Including
ignored-key-positions
in your tri-state definition will let the key positions specified NOT trigger the interrupt behavior when a tri-state is active. - Pressing any key NOT listed in
ignored-key-positions
will cause the interrupt behavior to fire. - Note that
ignored-key-positions
is an array of key position indexes. Key positions are numbered according to your keymap, starting with 0. So if the first key in your keymap is Q, this key is in position 0. The next key (probably W) will be in position 1, et cetera. - See the following example, which is an implementation of the popular Swapper from Callum Oakley:
/ {
behaviors {
swap: swapper {
compatible = "zmk,behavior-tri-state";
label = "SWAPPER";
#binding-cells = <0>;
bindings = <&kt LALT>, <&kp TAB>, <&kt LALT>;
ignored-key-positions = <1>;
};
};
keymap {
compatible = "zmk,keymap";
label ="Default keymap";
default_layer {
bindings = <
&swap &kp LS(TAB)
&kp B &kp C>;
};
};
};
- The sequence
(swap, swap, LS(TAB))
produces(LA(TAB), LA(TAB), LA(LS(TAB)))
. The LS(TAB) behavior does not fire the interrupt behavior, because it is included inignored-key-positions
. - The sequence
(swap, swap, B)
produces(LA(TAB), LA(TAB), B)
. The B behavior does fire the interrupt behavior, because it is not included inignored-key-positions
.
- By default, any layer change will trigger the end behavior.
- Including
ignored-layers
in your tri-state definition will let the specified layers NOT trigger the end behavior when they become active (include the layer the behavior is on to accommodate for layer toggling). - Activating any layer NOT listed in
ignored-layers
will cause the interrupt behavior to fire. - Note that
ignored-layers
is an array of layer indexes. Layers are numbered according to your keymap, starting with 0. The first layer in your keymap is layer 0. The next layer will be layer 1, et cetera. - Looking back at the swapper implementation, we can see how
ignored-layers
can affect things
/ {
behaviors {
swap: swapper {
compatible = "zmk,behavior-tri-state";
label = "SWAPPER";
#binding-cells = <0>;
bindings = <&kt LALT>, <&kp TAB>, <&kt LALT>;
ignored-key-positions = <1 2 3>;
ignored-layers = <1>;
};
};
keymap {
compatible = "zmk,keymap";
label ="Default keymap";
default_layer {
bindings = <
&swap &kp LS(TAB)
&kp B &tog 1>;
};
layer2 {
bindings = <
&kp DOWN &kp B
&tog 2 &trans>;
};
layer3 {
bindings = <
&kp LEFT &kp N2
&trans &kp N3>;
};
};
};
- The sequence
(swap, tog 1, DOWN)
produces(LA(TAB), LA(DOWN))
. The change to layer 1 does not fire the interrupt behavior, because it is included inignored-layers
, and DOWN is in the same position as the tri-state, also not firing the interrupt behavior. - The sequence
(swap, tog 1, tog 2, LEFT)
produces(LA(TAB), LEFT
. The change to layer 2 does fire the interrupt behavior, because it is not included inignored-layers
.
- Pete Johanson
- Nick Conway
- Cem Aksoylar
- ZMK and Zephyr