Skip to content

urob/zmk-adaptive-key

Repository files navigation

ZMK-ADAPTIVE-KEY

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 and max-prior-idle timeout properties that can vary by trigger.
  • Correct handling of explicit modifiers.

Usage

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.

Configuration

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.

trigger properties

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 the trigger-keys. Otherwise it suffices to contain the trigger-keys (useful for case-sensitive bindings). Defaults to false.

adaptive-key properties

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.

Examples

Hands-down adaptive keys

/ {
    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>; };
        };

    };
};

Dead keys

/ {
    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.

Shift-repeat

/ {
    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!

Kconfig settings

  • CONFIG_ZMK_ADAPTIVE_KEY_MAX_TRIGGER_CONDITIONS: Maximum number of trigger conditions per adaptive-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.

References

  • 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.

About

A ZMK module adding a adaptive-key behavior

Topics

Resources

License

Stars

Watchers

Forks