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-keysproperty to turn any keycode into a dead key. - Simple "inline" macro specification to bind behavior sequences.
min-prior-idle-msandmax-prior-idletimeout 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:
remotes:
- name: zmkfirmware
url-base: https://github.com/zmkfirmware
- name: urob
url-base: https://github.com/urob
projects:
- name: zmk
remote: zmkfirmware
revision: v0.3 # Set to desired ZMK release.
import: app/west.yml
- name: zmk-adaptive-key
remote: urob
revision: v0.3 # Should match ZMK release.
self:
path: configAn 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&noneto 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-keybehavior. 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.