This module adds a adaptive-key
behavior to ZMK. Some highlights compared to
existing alternatives:
- Works as a module without the need to patch ZMK.
- Configurable
dead-keys
property to turn any keycode into a dead key. - Simple "inline" macro specification to bind behavior sequences.
min-prior-idle-ms
andmax-prior-idle
timeout properties that can vary by trigger.- Correct handling of explicit modifiers.
To load the module, add the following entries to remotes
and projects
in
config/west.yml
.
manifest:
defaults:
revision: v0.1 # version to use for this module and for ZMK
remotes:
- name: zmkfirmware
url-base: https://github.com/zmkfirmware
- name: urob
url-base: https://github.com/urob
projects:
- name: zmk
remote: urob # or zmkfirmware, see comment below
import: app/west.yml
- name: zmk-adaptive-key
remote: urob
self:
path: config
Important: The zephyr
remote used by upstream ZMK currently contains a bug
that under certain circumstances causes the build to fail. You will need to
patch Zephyr if your build fails with an error message like:
ERROR: /behavior/leader-key POST_KERNEL 31 < /behaviors/foo POST_KERNEL 49
The simplest way to getting the patch is to use my zmk
remote, as configured
in above manifest. This will automatically build against a patched version of
Zephyr. Alternatively, you can use upstream ZMK and directly overwrite Zephyr by
adding
these lines
to your west.yml
manifest. In either case, if you are building using Github
Actions, you may need to clear your cache (in the left sidebar on the Actions
tab) for the changes to take effect.
An adaptive-key
defines "trigger" conditions on the last keycode pressed
prior to pressing the behavior. If any trigger condition matches, a behavior
bound to that trigger is invoked. If no trigger condition matches, a default
behavior is invoked.
Triggers are defined as child nodes of an adapative-key instance and are checked in order of their definition. Triggers have two required properties:
trigger-keys
: A list of keycodes that trigger the bindings.bindings
: Behaviors bound to the trigger. If set to multiple behaviors they are invoked in sequence.
Additional conditions can be configured via optional properties:
min-prior-idle-ms
: Minimum time that must be elapsed since the last key press. Defaults to none.max-prior-idle-ms
: Maximum time that must be elapsed since the last key press. Defaults to none.strict-modifiers
: If true, modifiers must exactly match thetrigger-keys
. Otherwise it suffices to contain thetrigger-keys
(useful for case-sensitive bindings). Defaults to false.
Besides trigger
child nodes, adaptive-key
instances have the following
properties:
bindings
(required): The default behavior to invoke if no trigger condition is met. Can be&none
to do nothing.dead-keys
: A list of key codes that are converted to dead keys. Dead keys don't send a keycode when pressed the first time but are still considered as trigger condition. If pressed again, dead keys send their normal keycode.
/ {
behaviors {
ak_h: ak_h {
compatible = "zmk,behavior-adaptive-key";
#binding-cells = <0>;
bindings = <&kp H>;
akt_ah { trigger-keys = <A>; max-prior-idle-ms = <300>; bindings = <&kp U>; };
akt_uh { trigger-keys = <U>; max-prior-idle-ms = <300>; bindings = <&kp A>; };
akt_eh { trigger-keys = <E>; max-prior-idle-ms = <300>; bindings = <&kp O>; };
};
ak_m: ak_m {
compatible = "zmk,behavior-adaptive-key";
#binding-cells = <0>;
bindings = <&kp M>;
akt_gm { trigger-keys = <G>; max-prior-idle-ms = <300>; bindings = <&kp L>; };
akt_pm { trigger-keys = <P>; max-prior-idle-ms = <300>; bindings = <&kp L>; };
};
// And similarly for VP->VL, PV->LV, BT->BL, TB->LB
ak_g: ak_g {
compatible = "zmk,behavior-adaptive-key";
#binding-cells = <0>;
bindings = <&kp G>;
// Binding two behaviors: JG->JPG
akt_jg { trigger-keys = <J>; max-prior-idle-ms = <300>; bindings = <&kp P &kp G>; };
};
};
};
/ {
behaviors {
ak_e: ak_e {
compatible = "zmk,behavior-adaptive-key";
#binding-cells = <0>;
bindings = <&kp E>;
dead-keys = <GRAVE CARET APOS QUOTE>;
grave { trigger-keys = <GRAVE>; bindings = <&fr_e_grave>; };
acute { trigger-keys = <APOS>; bindings = <&fr_e_acute>; };
circumflex { trigger-keys = <CARET>; bindings = <&fr_e_circumflex>; };
diaeresis { trigger-keys = <QUOTE>; bindings = <&fr_e_diaeresis>; };
};
};
};
Note: the behavior bindings &fr_e_grave
etc must be defined elsewhere (e.g.,
using the French
language header
from the zmk-helpers
module).
Alternatively, "new" dead keycodes can be "created" by cannibalizing unused keycode. For instance:
#define DEAD1 F21
#define DEAD2 F22
#define DEAD3 F23
#define DEAD4 F24
/ {
behaviors {
ak_e: ak_e {
compatible = "zmk,behavior-adaptive-key";
#binding-cells = <0>;
bindings = <&kp E>;
dead-keys = <DEAD1 DEAD2 DEAD3 DEAD4>;
grave { trigger-keys = <DEAD1>; bindings = <&fr_e_grave>; };
acute { trigger-keys = <DEAD2>; bindings = <&fr_e_acute>; };
circumflex { trigger-keys = <DEAD3>; bindings = <&fr_e_circumflex>; };
diaeresis { trigger-keys = <DEAD4>; bindings = <&fr_e_diaeresis>; };
};
};
};
Note: While the keycodes used in this example are typically unused, they are still defined. Making up new undefined keycodes is unsupported as their working hinges on the execution order of this module, which cannot be configured by any supported means.
/ {
behaviors {
shift-repeat: shift-repeat {
compatible = "zmk,behavior-adaptive-key";
#binding-cells = <0>;
bindings = <&sk LSHFT>;
repeat {
trigger-keys = <A B C D E F G H I J K L M N O P Q R S T U V W X Y Z>;
bindings = <&key_repeat>;
max-prior-idle-ms = <350>;
strict-modifiers;
};
};
};
};
This sets up a shift-repeat
behavior that sends &sk LSHFT
unless when
pressed within 0.35 seconds of any alpha key, in which case it sends
&key_repeat
. Great for your homing thumb key!
CONFIG_ZMK_ADAPTIVE_KEY_MAX_TRIGGER_CONDITIONS
: Maximum number of trigger conditions peradaptive-key
behavior. Defaults to 32.CONFIG_ZMK_ADAPTIVE_KEY_MAX_BINDINGS
: Maximum number of behaviors bound to a trigger (i.e., length of macro sequence). Defaults to 4.CONFIG_ZMK_ADAPTIVE_KEY_WAIT_MS
: Wait time in milliseconds between key presses when binding a macro sequence. Defaults to 5ms.CONFIG_ZMK_ADAPTIVE_KEY_TAP_MS
: Hold time per key tap when binding a macro sequence. Defaults to 5ms.
- The behavior idea is inspired by the Hands Down keyboard layout. The original ZMK feature request provides some further discussion.
- PR #2042 provides an alternative implementation.
- My personal zmk-config contains advanced usage examples.