Tap Flow: disable HRMs during fast typing
Pascal Getreuer, 2025-03-15
Tap Flow is for your keyboard, not your plumbing.
Overview
This post describes a community module implementation of global quick tap, aka require prior idle, for tap-hold keys in QMK. It is particularly useful for home row mods (HRMs) to avoid accidental mod triggers in fast typing.
Tap Flow modifies mod-tap MT
and layer-tap
LT
keys such that when pressed within a short timeout of
the preceding key, the tapping behavior is triggered. This basically
disables HRMs during fast typing.
The assumption is that only the tap function of tap-hold keys is
desired for regular typing (though perhaps with an exception for Shift
or AltGr, explained below), whereas the hold functions (an
MT
’s mod or LT
’s layer) are used in isolation.
Said another way, Tap Flow puts hold functions behind a speed bump. To
invoke a hold function, you must pause at least briefly before pressing
the tap-hold key.
The implementation is based on and inspired by @filterpaper’s elegant Contextual Mod-Taps.
Limitations
Tap Flow runs before QMK’s core combos and tap-hold logic, which creates some limitations:
Tap Flow does not act on tap-hold keys used in or resulting from a combo.
Tap Flow does not act while a layer-tap
LT
key is unsettled.Changing the tap function of a tap-hold key in
process_record_user()
does not work with Tap Flow. (Changing the hold function, however, does work.)
Add Tap Flow to your keyboard
Install my community
modules. Then enable module getreuer/tap_flow
in your keymap.json
file. Or if keymap.json
does not exist, create it with the following content:
{
"modules": ["getreuer/tap_flow"]
}
The Combos feature or Repeat Key (or both) need to be enabled in your
rules.mk
:
COMBOS_ENABLE = yes
REPEAT_KEY_ENABLE = yes
Either of these features enables the .keycode
field in
keyrecord_t
, which the implementation relies on.
Once installed, Tap Flow’s default behavior is:
Filtering is done only when a tap-hold press is within
TAP_FLOW_TERM
of the previous key event, which defaults to 150 ms.Filtering is done only when both the tap-hold key and the previous key are among Space, letters A–Z, and punctuations , . ; /.
Beyond Tap Flow, you can find further tips for home row mods in Home row mods are hard to use.
Customization
TAP_FLOW_TERM
TAP_FLOW_TERM
is the filtering time in units of
milliseconds. It defaults to 150 if not specified. To set something
else, define it in your config.h
file like:
#define TAP_FLOW_TERM 120
A larger value implies a greater tendency to settle tap-hold keys as tapped. I suggest that a useful value is between 75 and 200. For tuning, you can use these three keys to change Tap Flow’s term on the fly (similar to QMK’s dynamic tapping term feature):
Keycode | Alias | Description |
---|---|---|
TAP_FLOW_PRINT |
TFLOW_P |
Type the current value. |
TAP_FLOW_UP |
TFLOW_U |
Increase by 5 ms. |
TAP_FLOW_DOWN |
TFLOW_D |
Decrease by 5 ms. |
Tuning:
- If there are frequent accidental mod or layer triggers, increase the
Tap Flow term with
TFLOW_U
. - Conversely if extra taps are produced when the hold action was
intended, decrease the value with
TFLOW_D
.
Once a good setting is found, press TFLOW_P
to print the
current value. Then set TAP_FLOW_TERM
in
config.h
to that value:
#define TAP_FLOW_TERM <value from TFLOW_P>
get_tap_flow()
Optionally, filtering can be customized by defining the
get_tap_flow()
callback in your keymap.c
. This
way exceptions may be made for Shift and AltGr (or whatever you wish) to
use a shorter time or to disable filtering for those keys entirely.
An example:
uint16_t get_tap_flow(
uint16_t keycode, keyrecord_t* record, uint16_t prev_keycode) {
if (prev_keycode == KC_BSPC) {
return 0; // Disable filter when immediately following backspace.
}
switch (keycode) {
case LSFT_T(KC_D):
case RSFT_T(KC_K):
return 0; // Disable filter for these keys.
case LCTL_T(KC_F):
case RCTL_T(KC_H):
return g_tap_flow_term - 25; // Shorter timeout for index fingers.
default:
return g_tap_flow_term; // Longer timeout otherwise.
}
}
Notes:
Returning a time of zero has the effect of disabling filtering for those keys.
Use “
g_tap_flow_term
” to refer to Tap Flow’s dynamic term. Its value is controllable on the fly with theTFLOW_
keycodes described above.Note that full keycodes are expected, e.g.
LSFT_T(KC_D)
not justKC_D
.
Debugging
For in-depth troubleshooting, debug logging may be enabled through these steps:
- Enable the debug console as described here.
- Define
TAP_FLOW_DEBUG
inconfig.h
.
Tap Flow then produces console messages like the following:
tap_flow: 0805d within term (135 < 150) converted to tap.
tap_flow: 0805u tap release.
tap_flow: 0802d unchanged (combo key).
The “0805d
” syntax is a compact representation of key
events. For instance 0805d
is read as a press event
(d
for “down”) on matrix position row 8, column 5.
Explanation
Tap Flow hooks into pre_process_record
, which runs
before QMK’s core tap-hold logic:
Within this hook, Tap Flow tracks the keycode of the previously pressed key. It also tracks whether any
LT
keys may be currently unsettled.When a mod-tap
MT
or layer-tapLT
key is pressed, the time elapsed since the previous input is compared against the value returned fromget_tap_flow()
.If the time is less and supposing no
LT
keys are currently unsettled, the event is rewritten as the tapping keycode. This is done by setting the.keycode
field. Additionally, the tap press event is recorded in anis_tapped
array so that the release event can be handled correspondingly.Otherwise, the event continues unchanged. The time when the key will settle according to its tapping term is tracked.
The reason that unsettled tap-hold keys are considered is because
when an LT
settles as held, the layer state will change and
buffered events following the LT
will be reconsidered as
keys on that layer. That may change the buffered keys from tap-hold keys
to non-tap-hold keys, or vice versa, or other such complications. Of
course, we don’t know in advance how the LT
will
settle.
Acknowledgements
Huge thanks to @filterpaper for Contextual Mod-Taps, which inspired this work.