Skip to content

Autoshift using ZMK behaviors

Nick Coutsos edited this page Sep 29, 2023 · 4 revisions

A common feature of user keymaps is to replicate QMK's autoshift feature with a combination of hold-tap and a custom preprocessor macro to reduce code repetition.

See: the Autoshift tab from Hold-Tap Behavior Example Use-Cases.

Alternative to preprocessor macros

I don't have time, just give me the answer

You can replicate an easy-to-use autoshift by composing various ZMK behaviors to combine functionality with simplicity. Copy the behaviors from the following example keymap into your own and use the &as binding in place of AS().

You can also see this implemented through the Keymap Editor by opening the app and switching to the Demo Keyboard source or the below screenshots:

Thumbnail image: Hold-tap autoshift - click to enlarge Thumbnail image: Parameterized macro autoshift - click to enlarge Thumbnail image: Parameterized macro autoshift - click to enlarge
A full example keymap
#include <behaviors.dtsi>
#include <dt-bindings/zmk/keys.h>

/ {
  behaviors {
    as_ht: autoshift_hold_tap {
      compatible = "zmk,behavior-hold-tap";
      label = "AUTOSHIFT_HOLD_TAP";
      #binding-cells = <2>;
      tapping-term-ms = <200>;
      bindings = <&shifted>, <&kp>;
    };
  };

  macros {
    shifted: macro_shifted_kp {
      #binding-cells = <1>;
      label = "MACRO_SHIFTED_KP";
      compatible = "zmk,behavior-macro-one-param";
      bindings =
        <&macro_press &kp LSHFT>,
        <&macro_param_1to1 &macro_tap &kp MACRO_PLACEHOLDER>,
        <&macro_release &kp LSHFT>;
    };

    as: autoshift {
      compatible = "zmk,behavior-macro-one-param";
      #binding-cells = <1>;
      label = "AUTOSHIFT_KP";
      bindings =
        <&macro_press>,
        <&macro_param_1to1>,
        <&macro_param_1to2>,
        <&as_ht MACRO_PLACEHOLDER MACRO_PLACEHOLDER>,
        <&macro_pause_for_release>,
        <&macro_release>,
        <&macro_param_1to1>,
        <&macro_param_1to2>,
        <&as_ht MACRO_PLACEHOLDER MACRO_PLACEHOLDER>;
    };
  };

  keymap {
    compatible = "zmk,keymap";

    default_layer {
      bindings = <
        &as Q  &as W  &as E  &as R  &as T    &as Y  &as U  &as I  &as O  &as P
        &as A  &as S  &as D  &as F  &as G    &as H  &as J  &as K  &as L  &as SEMI
        &as Z  &as X  &as C  &as V  &as B    &as N  &as M  &as COMMA  &as DOT  &as FSLH
      >;
    };
  };
};

Explanation

The approach described in the ZMK documentation boils down to using the hold-tap behavior to trigger a shifted or regular keypress. With the recent addition of parameterized macro behaviors in ZMK it is now possible to effectively create an "autoshift" behavior that a) is only slightly longer to type out, and b) looks more like normal ZMK bindings.

Firstly a parameterized macro:

  shifted: macro_shifted_kp {
    #binding-cells = <1>;
    label = "MACRO_SHIFTED_KP";
    compatible = "zmk,behavior-macro-one-param";
    bindings =
      <&macro_press &kp LSHFT>,
      <&macro_param_1to1 &macro_tap &kp MACRO_PLACEHOLDER>,
      <&macro_release &kp LSHFT>;
  };

This gives us a behavior version of the LS() modifier function. Now instead of writing &kp LS(A) we can write &shifted A.

Next, we define a hold-tap behavior

  as_ht: autoshift_hold_tap {
    compatible = "zmk,behavior-hold-tap";
    label = "AUTOSHIFT_HOLD_TAP";
    #binding-cells = <2>;
    tapping-term-ms = <135>;
    flavor = "tap-preferred";
    bindings = <&shifted>, <&kp>;
  };

This follows the example autoshift behavior from ZMK's documentation, except it replaces the &kp binding for the "hold" action with our &shifted macro behavior. What's the difference? This version wraps our keypress in a sequence of pressing and releasing the LSHFT key. If we were to use this behavior as it is it would look like &as_ht A A.

Again, parameterized macros can simplify this, so we'll create one more.

  as: autoshift {
    compatible = "zmk,behavior-macro-one-param";
    #binding-cells = <1>;
    label = "AUTOSHIFT_KP";
    bindings =
      <&macro_press>,
      <&macro_param_1to1>,
      <&macro_param_1to2>,
      <&as_ht MACRO_PLACEHOLDER MACRO_PLACEHOLDER>,
      <&macro_pause_for_release>,
      <&macro_release>,
      <&macro_param_1to1>,
      <&macro_param_1to2>,
      <&as_ht MACRO_PLACEHOLDER MACRO_PLACEHOLDER>;
  };

This macro accepts a single parameter and triggers a single behavior, &as_ht using that parameter for both binding cells. We use &macro_pause_for_release so that holding a key bound to this macro also holds the &as_ht binding in the ZMK behavior queue.

The end result is the simplifed syntax &as A

Why won't I parse custom macros?

If you're really interested, I go into much more detail in Preprocessor Support