← More about keyboards

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.

A fast typing sequence of taps has short duration between successive key events

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.

A longer duration “speed bump” is required preceding the use of a hold function.

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:

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:

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:

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:

Debugging

For in-depth troubleshooting, debug logging may be enabled through these steps:

  1. Enable the debug console as described here.
  2. Define TAP_FLOW_DEBUG in config.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:

  1. Within this hook, Tap Flow tracks the keycode of the previously pressed key. It also tracks whether any LT keys may be currently unsettled.

  2. When a mod-tap MT or layer-tap LT key is pressed, the time elapsed since the previous input is compared against the value returned from get_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 an is_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.

← More about keyboards