rolling qmk modifiers

sunday, 21 april 2019

the QMK firmware library provides many convenient modifier macros for redefining keys as dual function key values and modifiers, notably the SFT_T, CTL_T, ALT_T and GUI_T toggle macros.

For several BEAKL layout iterations, the GUI, Control, ALT and Shift keys have been assigned to the left and right hand home row positions (versus conventional keyboard layouts) enabling rapid access to modifier chords for workflow and application control – and, of course, elimination of the commonly assigned pinkie finger Shift key reach..

Chimera BEAKL Zi


QMK toggle keystroke sensitivity is defined by the TAPPING_TERM value (milliseconds) to configure the keystroke window within which a modifier key registers as a key value (on release) or as a modifier (on down). For the most part, the macros function as advertised. Finger rolls on the home row are responsive, indistinguishable from the other rows.

Latency and touch typing technique, however, can produce unexpected results. This was born out on the nimble Chimera Ergo keyboard which, combined with the finger roll emphasis of the BEAKL Zi layout, could easily trigger modified keys instead of their key value under rapid finger rolls.

The latency side effects of the macros with opposite hand home row strikes is even more common – which can be missing or unwanted modified keys. This is exacerbated with the emphasis of the BEAKL layout on the index, middle and ring finger key clusters.

In particular, the speed of the index fingers can exceed the responsiveness of the SFT_T (Shift toggle) modifier macro. Shift values, especially the opposite Shift keys A and T of the BEAKL layout, can fail to be registered as intended. These miss-keyed capitalizations are a result of firmware latency and mechanical keyswitch travel.


the keyboard switch stroke length and activation point, combined with fingering technique, affect the resultant keystroke event sequence – the activation point being the mechanical position at which the key is in a “registered” (down) or “unregistered” (up) state.

Due to switch travel time and fingering speed, especially with inward finger rolls, key sequences are seldom distinct from one another i.e. the succeeding keystroke is pressed before the preceding key has been fully released. This poses no problems for regular (non-modifier) keys – everything can be properly processed in their “registered” sequence.

However, modifier toggle keys, such as SFT_T, behave differently. For their registered duration, their modifier state is applied to succeeding key(s), suppressing its own unmodified key value as long as the key is depressed long enough to exceed the TAPPING_TERM duration. Note: SFT_T is more sophisticated than this simplified description.


for a single modifier toggle key followed by a regular (non-modifier) key, the resultant output is consistently predictable – hold down the modifier long enough, the succeeding key is modified.

But in a rolling finger sequence, the key value is the desired result even if the preceding key is still in its modifier (down) state. Worse still, if the modifier is released before its TAPPING_TERM interval, a modified key and the modifier’s key value can be issued.

In particular, rapid shifts can introduce erratic results when the Shift finger completes its keystroke before the shifted (next) character has been released, resulting in two characters being produced instead. This is more an issue of the speed of the index fingers (and typing technique), as opposed to, a failing of the macro itself.

More problematic is when a sequence of modifier toggle keys are typed in rapid succession – not uncommon when your home row contains eight such keys! – with the resultant cascading modifier versus character timing conflict needing to be resolved.

What to do? Improving touch typing mechanics is obviously priority. But that cannot overcome the macro latency and mechanical keyswitch travel length and activation point constraints (of the user’s hardware).

Enter QMK and rolling your own macros..


for each key event (on down or up) identifies the modifier and key value combination, not unlike SFT_T, albeit with more parameters..

bool process_record_user(uint16_t keycode, keyrecord_t *record) { switch (keycode) { case HOME_Q: mod_roll(record, LEFT, NOSHIFT, KC_LGUI, KC_Q, 0); break; case HOME_H: mod_roll(record, LEFT, NOSHIFT, KC_LCTL, KC_H, 1); break; case HOME_E: mod_roll(record, LEFT, NOSHIFT, KC_LALT, KC_E, 2); break; case HOME_A: mod_roll(record, LEFT, SHIFT, KC_LSFT, KC_A, 3); break; case HOME_T: mod_roll(record, RIGHT, SHIFT, KC_RSFT, KC_T, 6); break; case HOME_R: mod_roll(record, RIGHT, NOSHIFT, KC_RALT, KC_R, 7); break; case HOME_S: mod_roll(record, RIGHT, NOSHIFT, KC_RCTL, KC_S, 8); break; case HOME_W: mod_roll(record, RIGHT, NOSHIFT, KC_RGUI, KC_W, 9); break; ...

event stack

the approach taken concerns itself only with the column position of the key strike – finger rolls are lateral motions (even if they also traverse rows). Of the state information kept, the time of the down or registered state controls the sequence in a finger roll..

#define LEFT 1 #define RIGHT 2 static struct column_event { uint16_t key_timer; uint16_t keycode; uint8_t shift; uint8_t side; } e[10]; static uint8_t next_key = 0; static uint8_t prev_key = 0; void clear_events(void) { for (i = 0; i < 10; i++) { e[i].key_timer = 0; } }

There are 10 columns defined in the BEAKL Zi layout which are mapped in the mod_roll calls above as 0 1 2 3 4 (left hand) and 5 6 7 8 9 (right hand). The order of the column number assignments is arbitrary but consistently mapped to each keyboard row.

on key press

save the event time and state information of the key. Update the concurrent modifier state and register the modifier down if necessary..

void mod_roll(keyrecord_t *record, uint8_t side, uint8_t shift, uint16_t modifier, uint16_t keycode, uint8_t column) { if (record->event.pressed) { e[column].key_timer = timer_read(); e[column].keycode = keycode; e[column].shift = shift; e[column].side = side; prev_key = next_key; next_key = column; if (modifier) { register_modifier(modifier); } } ...

on key release

if applicable, release the modifier toggle. If the key is released within the TAPPING_TERM window and a succeeding key is in progress, issue either the shifted key value of the succeeding key (for Shift down and clear its state) or the current key value – otherwise, treat as a normal key press (clearing any prior key state)..

else { if (modifier) { unregister_modifier(modifier); } if (timer_elapsed(e[column].key_timer) < TAPPING_TERM) { if (e[column].key_timer < e[next_key].key_timer) { mod_all(unregister_code); if (e[column].shift && (e[column].side != e[next_key].side)) { tap_shift(e[next_key].keycode); e[next_key].key_timer = 0; } else { tap_key(keycode); } } else { tap_key(keycode); e[prev_key].key_timer = 0; } } e[column].key_timer = 0; } }

A detected finger roll sequence automatically disables any registered modifiers in progress to prevent spurious results, the theory being that modifier chords are more deliberate workflow actions versus raw typing.

Unlike the QMK toggle macros, there is no auto-repeat for the key on second tap (which likely contributes to the latency behavior that mod_roll attempts to address).

modifier library

Manage modifier states..

static uint8_t mods = 0; void register_modifier(uint16_t keycode) { register_code(keycode); mods |= MOD_BIT(keycode); } void unregister_modifier(uint16_t keycode) { unregister_code(keycode); mods &= ~(MOD_BIT(keycode)); } void mod_all(void (*f)(uint8_t)) { if (mods & MOD_BIT(KC_LGUI)) { f(KC_LGUI); } if (mods & MOD_BIT(KC_LCTL)) { f(KC_LCTL); } if (mods & MOD_BIT(KC_LALT)) { f(KC_LALT); } if (mods & MOD_BIT(KC_LSFT)) { f(KC_LSFT); } if (mods & MOD_BIT(KC_RSFT)) { f(KC_RSFT); } if (mods & MOD_BIT(KC_RALT)) { f(KC_RALT); } if (mods & MOD_BIT(KC_RCTL)) { f(KC_RCTL); } if (mods & MOD_BIT(KC_RGUI)) { f(KC_RGUI); } }

Simple key press primitives..

void tap_key(uint16_t keycode) { register_code (keycode); unregister_code(keycode); } void tap_shift(uint16_t keycode) { register_code (KC_LSFT); tap_key (keycode); unregister_code(KC_LSFT); }

On keyboard initialization..

void matrix_init_user(void) { clear_events(); }

non-home row keys

the mod_roll macro works as designed for the home row but its design impacts rapid home row plus upper/lower row bigrams – in particular, bigrams including the middle or index fingers can produce reversed character/modifier sequences – “qu” can produce “uq” because the pinkie-index finger roll easily registers the “u” before the modifier toggle key can register as “q”.

This is remedied by applying the same mod_roll macro to those rows for the deft index and middle fingers, the difference being, those rows do not actually invoke a modifier state, hence, the non-zero modifier test in the mod_roll macro above.

The remainder of the keyboard is added to process_record_user..

bool process_record_user(uint16_t keycode, keyrecord_t *record) { switch (keycode) { ... case KC_Y: mod_roll(record, LEFT, NOSHIFT, 0, KC_Y, 1); return false; case KC_O: mod_roll(record, LEFT, NOSHIFT, 0, KC_O, 2); return false; case KC_U: mod_roll(record, LEFT, NOSHIFT, 0, KC_U, 3); return false; case KC_G: mod_roll(record, RIGHT, NOSHIFT, 0, KC_G, 5); return false; case KC_D: mod_roll(record, RIGHT, NOSHIFT, 0, KC_D, 6); return false; case KC_N: mod_roll(record, RIGHT, NOSHIFT, 0, KC_N, 7); return false; case KC_M: mod_roll(record, RIGHT, NOSHIFT, 0, KC_M, 8); return false; case KC_C: mod_roll(record, RIGHT, NOSHIFT, 0, KC_C, 5); return false; case KC_MINS: mod_roll(record, LEFT, NOSHIFT, 0, KC_MINS, 1); return false; case KC_QUOT: mod_roll(record, LEFT, NOSHIFT, 0, KC_QUOT, 2); return false; case KC_K: mod_roll(record, LEFT, NOSHIFT, 0, KC_K, 3); return false; case KC_B: mod_roll(record, RIGHT, NOSHIFT, 0, KC_B, 5); return false; case KC_P: mod_roll(record, RIGHT, NOSHIFT, 0, KC_P, 6); return false; case KC_L: mod_roll(record, RIGHT, NOSHIFT, 0, KC_L, 7); return false; case KC_F: mod_roll(record, RIGHT, NOSHIFT, 0, KC_F, 8); return false; ...

Note: In this example, the unassigned BEAKL Zi punctuation keys are defined with other macros, such as tap dance rules, which must be left exempt due to their reliance on the TAPPING_TERM duration to function properly.

Bigrams involving the pinkie-corner keys are likely not typed rapidly enough to require defining with mod_roll except for the most adept touch typists.

trade offs

are to be expected with any macro that extends the functionality of a key. mod_roll is no exception..

  • Most obviously, no auto-repeat for any mod_roll defined key.
  • No same hand Shift character shortcuts. The Shift modifier only applies to the opposite hand keys.
  • Shift can only be applied to the leading character of a finger roll with all remaining characters in the roll issued in lower case. This precludes rapid typing of camelCase words for speed typing warriors.
  • Consecutive shifted characters may be typed as long as finger roll speed is not achieved. Of course, CopsLock is not speed constrained.
  • The slight latency of SFT_T versus the more liberal application of shifted characters – choose which side of the fence best fits your workflow.

in use

capitalized trigrams such as the common “The” (which lead with the “at” index finger chord) are immensely improved with the responsive fingering timing and action. Rapidly shifted home row characters are now more accurately and deftly handled.

The mod_roll macro, although addressing the latency issues of the generic QMK toggle macros (to these hands) – not to dismiss the impact of physical keyboard layout and keyswitch action – imparts its own timing and design constraints.

It can produce an unexpected leading shifted character – in particular, the “H” due to “th” bigram finger memory, but a little practice should accommodate this easily enough – but feels overall more predictable and responsive to these hands.


it must be re-emphasized that the mod_roll macro was written to address the timing issues presented by assigning the SFT_T macro to the index fingers. Previous layout designs with a thumb Shift did not exhibit these issues to the same degree – with the thumb’s different timing, tending to bottom out its key and being mechanically less nimble than the index fingers.

This solution is a bit of a toss: to miss the odd capitalization with SFT_T or inadvertently capitalize with mod_roll from rapid key sequences. Keyboard layout, keyswitch design and even keycap profile all impact modifier toggle accuracy. Dedicated Shift and modifier keys, of course, avoid this issue altogether for the ultimate performance.

There is still no substitute for improved touch typing technique – particularly, learning to type without bottoming out the keys to decrease keystroke travel and minimize accidental modifier activation while increasing typing speed (and reducing finger fatigue).

QMK continues to be refined by its developers with a great deal of focus on latency issues – whose problem domain is as broad as its user base – as well as, expansion of keyboard functionality. Software releases continue to be issued with settings to tune the behavior of the keyboard functions – a remarkable feat considering the diverse requirements of its user base.

As it stands, the mod_roll macro presents a nimble alternative to the supplied QMK toggle macros. YMMV..


while there is limited need for auto-repeat on alpha characters, it can be added cleverly with only three lines (not statements) by comparing the current keycode to the event stack’s previous keycode and using it as a state flag (0 == auto-repeat)..

void mod_roll(keyrecord_t *record, uint8_t side, uint8_t shift, uint16_t modifier, uint16_t keycode, uint8_t column) { if (modifier) { mod_bits(record, modifier); } if (record->event.pressed) { if (keycode == e[column].keycode) { register_code(keycode); e[column].keycode = 0; return; } /* double tap auto-repeat */ e[column].key_timer = timer_read(); ... } else { if (!e[column].keycode) { unregister_code(keycode); e[column].key_timer = 0; return; } /* clear auto-repeat */ if (modifier) { unregister_modifier(modifier); } if (timer_elapsed(e[column].key_timer) < TAPPING_TERM) { ... } else { e[column].keycode = 0; } /* is single tap */ e[column].key_timer = 0; } }

Unfortunately, the latency introduced by unregistering the auto-repeat is significant enough to defeat the responsiveness of the mod_roll macro – its whole point – introducing its own set of latency issues and mistyped key values. This was an unexpected result, illustrating the complexity of latency issues.

Perhaps someday on a keyboard with a faster processor clock speed – or a redesigned mod_roll macro..

»»  chimera ergo zi

comment ?