This is my personal ZMK configuration for 34-key Cradio (Hypergolic). It uses contextual hold-tap configuration to make home row modifiers easier to use, along with simple macros to manage the keymap.
ZMK's interrupt based input detection offers a large number of configuration options for managing hold or tap keys. These are my contextual configuration setup for ease of triggering modifiers while avoiding false positives.
Modifiers should not be triggered when a mod-tap key is pressed together with another key on the same hand. However, they should be triggered when held down and another key is tapped with the opposite hand. This is accomplished using balanced
flavor with the following positional hold-tap behavior (mirrored for the right side):
#define TAPPING_TERM 240
#define SHIFT_TAP_MS 180
#define QUICK_TAP_MS 160
lmt: left_mod_tap {
label = "left_mod_tap";
compatible = "zmk,behavior-hold-tap";
flavor = "balanced";
tapping-term-ms = <TAPPING_TERM>;
quick-tap-ms = <QUICK_TAP_MS>;
#binding-cells = <2>;
bindings = <&kp>, <&kp>;
require-prior-idle-ms = <QUICK_TAP_MS>;
hold-trigger-key-positions = <
5 6 7 8 9
13 15 16 17 18 19
25 26 27 28 29
30 31 32 33
>;
};
rmt: right_mod_tap {
label = "right_mod_tap";
compatible = "zmk,behavior-hold-tap";
flavor = "balanced";
tapping-term-ms = <TAPPING_TERM>;
quick-tap-ms = <QUICK_TAP_MS>;
#binding-cells = <2>;
bindings = <&kp>, <&kp>;
require-prior-idle-ms = <QUICK_TAP_MS>;
hold-trigger-key-positions = <
0 1 2 3 4
10 11 12 13 14 16
17 21 22 23 24
30 31 32 33
>;
};
A mod-tap key will send its tap key code if it overlaps with another key index that is not in the hold-trigger-key-positions
. If it overlaps with a key index listed in the hold-trigger-key-positions
, the mod-tap behavior will be as defined. Two home row mod positions on the same side is included to allow chording with Shift. The require-prior-idle-ms
feature is also enabled to prevent mods activation while typing.
The home row mod-tap Shift key to the other modifiers, with a shorter tapping term and without require-prior-idle-ms` feature. They are defined with the following binding (also mirrored for the right side):
lst: left_shift_tap {
label = "left_shift_tap";
compatible = "zmk,behavior-hold-tap";
flavor = "balanced";
tapping-term-ms = <SHIFT_TAP_MS>;
quick-tap-ms = <QUICK_TAP_MS>;
#binding-cells = <2>;
bindings = <&kp>, <&kp>;
hold-trigger-key-positions = <
5 6 7 8 9
15 16 17 18 19
25 26 27 28 29
30 31 32 33
>;
};
rst: right_shift_tap {
label = "right_shift_tap";
compatible = "zmk,behavior-hold-tap";
flavor = "balanced";
tapping-term-ms = <SHIFT_TAP_MS>;
quick-tap-ms = <QUICK_TAP_MS>;
#binding-cells = <2>;
bindings = <&kp>, <&kp>;
hold-trigger-key-positions = <
0 1 2 3 4
10 11 12 13 14
20 21 22 23 24
30 31 32 33
>;
};
A wrapper macro is used to apply the aforementioned home row mod-taps to the keymap in a way that makes them easier to read:
#define HRML(k1,k2,k3,k4) &lmt LCTRL k1 &lmt LALT k2 &lmt LGUI k3 &lst LSHFT k4
#define HRMR(k1,k2,k3,k4) &rst RSHFT k1 &rmt RGUI k2 &rmt RALT k3 &rmt RCTRL k4
The following hold tap macro reduces the number of lines in the behavior
code block:
#define HOLD_TAP(name, ht_flavor, ht_term, ...) \
name: name##_hold_tap { \
flavor = #ht_flavor; \
compatible = "zmk,behavior-hold-tap"; \
tapping-term-ms = <ht_term>; \
quick-tap-ms = <QUICK_TAP_MS>; \
#binding-cells = <2>; \
bindings = <&kp>, <&kp>; \
__VA_ARGS__ \
};
#define L_SHIFT 13
#define R_SHIFT 16
#define L_KEYS 0 1 2 3 4 10 11 12 13 14 20 21 22 23 24 30 31 32 33
#define R_KEYS 5 6 7 8 9 15 16 17 18 19 25 26 27 28 29 30 31 32 33
/ {
behaviors {
// Positional hold-tap for Shift
HOLD_TAP(lst, balanced, SHIFT_TAP_MS, hold-trigger-key-positions = <R_KEYS>;)
HOLD_TAP(rst, balanced, SHIFT_TAP_MS, hold-trigger-key-positions = <L_KEYS>;)
// Positional hold-tap for non-Shift modifiers
HOLD_TAP(lmt, balanced, TAPPING_TERM, hold-trigger-key-positions = <L_SHIFT R_KEYS>; require-prior-idle-ms = <QUICK_TAP_MS>;)
HOLD_TAP(rmt, balanced, TAPPING_TERM, hold-trigger-key-positions = <R_SHIFT L_KEYS>; require-prior-idle-ms = <QUICK_TAP_MS>;)
};
};
Combos are simplified into one-liners using the following code:
#define COMBO(name, kp, pos) \
combo_##name { \
timeout-ms = <30>; \
require-prior-idle-ms = <TAPPING_TERM>;\
bindings = <kp>; \
key-positions = <pos>; \
layers = <0 1>; \
};
/ {
combos {
compatible = "zmk,combos";
COMBO(caps_w, &caps_word, 13 16)
};
};
Macros are also simplified into one-liners using the following code:
#define MACRO(name, keys) \
name: name##_macro { \
compatible = "zmk,behavior-macro";\
tap-ms = <1>; \
wait-ms = <1>; \
#binding-cells = <0>; \
bindings = <keys>; \
};
/ {
macros {
MACRO(dir_up, &kp DOT &kp DOT &kp FSLH)
MACRO(bt_0, &bt BT_SEL 0 &bt BT_DISC 1)
};
};
- nice!nano v2 Pinout
- Customizing ZMK
- Hypergolic PCBs
- Sockets
- Keymapviz
- Keymap Drawer
- Machined header sockets
- Mill-Max 315-43-164-41-001000 sockets
- Mill-Max connector pins
- 301230 Li-po batteries
- MSK-12C02 SMD Switch
- Silicone bumper feet
- Kailh gchoc v1 switches