summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPascal Getreuer <50221757+getreuer@users.noreply.github.com>2025-04-14 09:46:24 -0700
committerGitHub <noreply@github.com>2025-04-14 09:46:24 -0700
commit8d8dcb089ed36e7e1a61d77e5a4b6b08c8668869 (patch)
treeb5aa66a32a42e5d1288e8a5751e94cba4ce06350
parenta7bf8e64a584c9a0930f4aa701a09a6e1de7e117 (diff)
[Core] Flow Tap tap-hold option to disable HRMs during fast typing (#25125)
aka Global Quick Tap, Require Prior Idle
-rw-r--r--data/mappings/info_config.hjson1
-rw-r--r--docs/tap_hold.md83
-rw-r--r--quantum/action.c17
-rw-r--r--quantum/action.h6
-rw-r--r--quantum/action_tapping.c138
-rw-r--r--quantum/action_tapping.h57
-rw-r--r--quantum/process_keycode/process_leader.c6
-rw-r--r--tests/tap_hold_configurations/flow_tap/config.h22
-rw-r--r--tests/tap_hold_configurations/flow_tap/test.mk18
-rw-r--r--tests/tap_hold_configurations/flow_tap/test_keymap.c23
-rw-r--r--tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp313
11 files changed, 648 insertions, 36 deletions
diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson
index b643553b52..bab881583a 100644
--- a/data/mappings/info_config.hjson
+++ b/data/mappings/info_config.hjson
@@ -201,6 +201,7 @@
201 201
202 // Tapping 202 // Tapping
203 "CHORDAL_HOLD": {"info_key": "tapping.chordal_hold", "value_type": "flag"}, 203 "CHORDAL_HOLD": {"info_key": "tapping.chordal_hold", "value_type": "flag"},
204 "FLOW_TAP_TERM": {"info_key": "tapping.flow_tap_term", "value_type": "int"},
204 "HOLD_ON_OTHER_KEY_PRESS": {"info_key": "tapping.hold_on_other_key_press", "value_type": "flag"}, 205 "HOLD_ON_OTHER_KEY_PRESS": {"info_key": "tapping.hold_on_other_key_press", "value_type": "flag"},
205 "HOLD_ON_OTHER_KEY_PRESS_PER_KEY": {"info_key": "tapping.hold_on_other_key_press_per_key", "value_type": "flag"}, 206 "HOLD_ON_OTHER_KEY_PRESS_PER_KEY": {"info_key": "tapping.hold_on_other_key_press_per_key", "value_type": "flag"},
206 "PERMISSIVE_HOLD": {"info_key": "tapping.permissive_hold", "value_type": "flag"}, 207 "PERMISSIVE_HOLD": {"info_key": "tapping.permissive_hold", "value_type": "flag"},
diff --git a/docs/tap_hold.md b/docs/tap_hold.md
index 254d5de5ec..f1af753eba 100644
--- a/docs/tap_hold.md
+++ b/docs/tap_hold.md
@@ -425,6 +425,89 @@ uint16_t get_quick_tap_term(uint16_t keycode, keyrecord_t *record) {
425If `QUICK_TAP_TERM` is set higher than `TAPPING_TERM`, it will default to `TAPPING_TERM`. 425If `QUICK_TAP_TERM` is set higher than `TAPPING_TERM`, it will default to `TAPPING_TERM`.
426::: 426:::
427 427
428## Flow Tap
429
430Flow Tap 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 is particularly useful for home row mods to avoid accidental mod triggers. It basically disables the hold behavior during fast typing, creating a "flow of taps." This also helps to reduce the input lag of tap-hold keys during fast typing, since the tapped behavior is sent immediately.
431
432Flow Tap is enabled by defining `FLOW_TAP_TERM` in your `config.h` with the desired timeout in milliseconds. A timeout of 150&nbsp;ms is recommended as a starting point:
433
434```c
435#define FLOW_TAP_TERM 150
436```
437
438By default, Flow Tap is enabled when:
439
440* The tap-hold key is pressed within `FLOW_TAP_TERM` milliseconds of the previous key press.
441
442* The tapping keycodes of the previous key and tap-hold key are *both* among `KC_A`&ndash;`KC_Z`, `KC_COMM`, `KC_DOT`, `KC_SCLN`, `KC_SLSH` (the main alphas area of a conventional QWERTY layout) or `KC_SPC`.
443
444As an exception to the above, Flow Tap is temporarily disabled while a tap-hold key is undecided. This is to allow chording multiple mod-tap keys without having to wait out the Flow Tap term.
445
446
447### is_flow_tap_key()
448
449Optionally, define the `is_flow_tap_key()` callback to specify where Flow Tap is enabled. The callback is called for both the tap-hold key *and* the key press immediately preceding it, and if the callback returns true for both keycodes, Flow Tap is enabled.
450
451The default implementation of this callback is:
452
453```.c
454bool is_flow_tap_key(uint16_t keycode) {
455 if ((get_mods() & (MOD_MASK_CG | MOD_BIT_LALT)) != 0) {
456 return false; // Disable Flow Tap on hotkeys.
457 }
458 switch (get_tap_keycode(keycode)) {
459 case KC_SPC:
460 case KC_A ... KC_Z:
461 case KC_DOT:
462 case KC_COMM:
463 case KC_SCLN:
464 case KC_SLSH:
465 return true;
466 }
467 return false;
468}
469```
470
471Copy the above to your `keymap.c` and edit to customize. For instance, remove the `case KC_SPC` line to disable Flow Tap for the Space key.
472
473### get_flow_tap_term()
474
475Optionally, for further flexibility, define the `get_flow_tap_term()` callback. Flow Tap acts only when key events are closer together than the time returned by the callback. Return a time of 0 to disable filtering. In this way, Flow Tap may be disabled for certain tap-hold keys, or when following certain previous keys.
476
477The default implementation of this callback is
478
479```.c
480uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t* record,
481 uint16_t prev_keycode) {
482 if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
483 return FLOW_TAP_TERM;
484 }
485 return 0;
486}
487```
488
489In this callback, `keycode` and `record` correspond to the current tap-hold key, and `prev_keycode` is the keycode of the previous key. Return the timeout to use. Returning `0` disables Flow Tap. This callback enables setting per-key timeouts. It is also possible to enable or disable Flow Tap for certain tap-hold keys or when following certain previous keys. Example:
490
491```c
492uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t* record,
493 uint16_t prev_keycode) {
494 if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
495 switch (keycode) {
496 case LCTL_T(KC_F):
497 case RCTL_T(KC_H):
498 return FLOW_TAP_TERM - 25; // Short timeout on these keys.
499
500 default:
501 return FLOW_TAP_TERM; // Longer timeout otherwise.
502 }
503 }
504 return 0; // Disable Flow Tap.
505}
506```
507
508::: tip If you define both `is_flow_tap_key()` and `get_flow_tap_term()`, then the latter takes precedence.
509:::
510
428## Chordal Hold 511## Chordal Hold
429 512
430Chordal Hold is intended to be used together with either Permissive Hold or Hold 513Chordal Hold is intended to be used together with either Permissive Hold or Hold
diff --git a/quantum/action.c b/quantum/action.c
index be85192d25..eb0dbc7022 100644
--- a/quantum/action.c
+++ b/quantum/action.c
@@ -1183,6 +1183,23 @@ bool is_tap_action(action_t action) {
1183 return false; 1183 return false;
1184} 1184}
1185 1185
1186uint16_t get_tap_keycode(uint16_t keycode) {
1187 switch (keycode) {
1188 case QK_MOD_TAP ... QK_MOD_TAP_MAX:
1189 return QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
1190 case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
1191 return QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
1192 case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
1193 // IS_SWAP_HANDS_KEYCODE() tests for the special action keycodes
1194 // like SH_TOGG, SH_TT, ..., which overlap the SH_T(kc) range.
1195 if (!IS_SWAP_HANDS_KEYCODE(keycode)) {
1196 return QK_SWAP_HANDS_GET_TAP_KEYCODE(keycode);
1197 }
1198 break;
1199 }
1200 return keycode;
1201}
1202
1186/** \brief Debug print (FIXME: Needs better description) 1203/** \brief Debug print (FIXME: Needs better description)
1187 * 1204 *
1188 * FIXME: Needs documentation. 1205 * FIXME: Needs documentation.
diff --git a/quantum/action.h b/quantum/action.h
index 7596688f31..7616486c6d 100644
--- a/quantum/action.h
+++ b/quantum/action.h
@@ -128,6 +128,12 @@ void layer_switch(uint8_t new_layer);
128bool is_tap_record(keyrecord_t *record); 128bool is_tap_record(keyrecord_t *record);
129bool is_tap_action(action_t action); 129bool is_tap_action(action_t action);
130 130
131/**
132 * Given an MT or LT keycode, returns the tap keycode. Otherwise returns the
133 * original keycode unchanged.
134 */
135uint16_t get_tap_keycode(uint16_t keycode);
136
131#ifndef NO_ACTION_TAPPING 137#ifndef NO_ACTION_TAPPING
132void process_record_tap_hint(keyrecord_t *record); 138void process_record_tap_hint(keyrecord_t *record);
133#endif 139#endif
diff --git a/quantum/action_tapping.c b/quantum/action_tapping.c
index e42a98554d..312c639169 100644
--- a/quantum/action_tapping.c
+++ b/quantum/action_tapping.c
@@ -4,6 +4,7 @@
4#include "action.h" 4#include "action.h"
5#include "action_layer.h" 5#include "action_layer.h"
6#include "action_tapping.h" 6#include "action_tapping.h"
7#include "action_util.h"
7#include "keycode.h" 8#include "keycode.h"
8#include "timer.h" 9#include "timer.h"
9 10
@@ -49,9 +50,7 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re
49} 50}
50# endif 51# endif
51 52
52# if defined(CHORDAL_HOLD) 53# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
53extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
54
55# define REGISTERED_TAPS_SIZE 8 54# define REGISTERED_TAPS_SIZE 8
56// Array of tap-hold keys that have been settled as tapped but not yet released. 55// Array of tap-hold keys that have been settled as tapped but not yet released.
57static keypos_t registered_taps[REGISTERED_TAPS_SIZE] = {}; 56static keypos_t registered_taps[REGISTERED_TAPS_SIZE] = {};
@@ -66,6 +65,14 @@ static void registered_taps_del_index(uint8_t i);
66/** Logs the registered_taps array for debugging. */ 65/** Logs the registered_taps array for debugging. */
67static void debug_registered_taps(void); 66static void debug_registered_taps(void);
68 67
68static bool is_mt_or_lt(uint16_t keycode) {
69 return IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode);
70}
71# endif // defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
72
73# if defined(CHORDAL_HOLD)
74extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
75
69/** \brief Finds which queued events should be held according to Chordal Hold. 76/** \brief Finds which queued events should be held according to Chordal Hold.
70 * 77 *
71 * In a situation with multiple unsettled tap-hold key presses, scan the queue 78 * In a situation with multiple unsettled tap-hold key presses, scan the queue
@@ -82,10 +89,6 @@ static void waiting_buffer_chordal_hold_taps_until(keypos_t key);
82 89
83/** \brief Processes and pops buffered events until the first tap-hold event. */ 90/** \brief Processes and pops buffered events until the first tap-hold event. */
84static void waiting_buffer_process_regular(void); 91static void waiting_buffer_process_regular(void);
85
86static bool is_mt_or_lt(uint16_t keycode) {
87 return IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode);
88}
89# endif // CHORDAL_HOLD 92# endif // CHORDAL_HOLD
90 93
91# ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY 94# ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY
@@ -98,6 +101,13 @@ __attribute__((weak)) bool get_hold_on_other_key_press(uint16_t keycode, keyreco
98# include "process_auto_shift.h" 101# include "process_auto_shift.h"
99# endif 102# endif
100 103
104# if defined(FLOW_TAP_TERM)
105static uint32_t last_input = 0;
106static uint16_t prev_keycode = KC_NO;
107
108uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode);
109# endif // defined(FLOW_TAP_TERM)
110
101static keyrecord_t tapping_key = {}; 111static keyrecord_t tapping_key = {};
102static keyrecord_t waiting_buffer[WAITING_BUFFER_SIZE] = {}; 112static keyrecord_t waiting_buffer[WAITING_BUFFER_SIZE] = {};
103static uint8_t waiting_buffer_head = 0; 113static uint8_t waiting_buffer_head = 0;
@@ -147,6 +157,19 @@ void action_tapping_process(keyrecord_t record) {
147 } 157 }
148 } 158 }
149 if (IS_EVENT(record.event)) { 159 if (IS_EVENT(record.event)) {
160# if defined(FLOW_TAP_TERM)
161 const uint16_t keycode = get_record_keycode(&record, false);
162 // Track the previous key press.
163 if (record.event.pressed) {
164 prev_keycode = keycode;
165 }
166 // If there is no unsettled tap-hold key, update last input time. Ignore
167 // mod keys in this update to allow for chording multiple mods for
168 // hotkeys like "Ctrl+Shift+arrow".
169 if (IS_NOEVENT(tapping_key.event) && !IS_MODIFIER_KEYCODE(keycode)) {
170 last_input = timer_read32();
171 }
172# endif // defined(FLOW_TAP_TERM)
150 ac_dprintf("\n"); 173 ac_dprintf("\n");
151 } 174 }
152} 175}
@@ -205,7 +228,7 @@ void action_tapping_process(keyrecord_t record) {
205bool process_tapping(keyrecord_t *keyp) { 228bool process_tapping(keyrecord_t *keyp) {
206 const keyevent_t event = keyp->event; 229 const keyevent_t event = keyp->event;
207 230
208# if defined(CHORDAL_HOLD) 231# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
209 if (!event.pressed) { 232 if (!event.pressed) {
210 const int8_t i = registered_tap_find(event.key); 233 const int8_t i = registered_tap_find(event.key);
211 if (i != -1) { 234 if (i != -1) {
@@ -217,7 +240,7 @@ bool process_tapping(keyrecord_t *keyp) {
217 debug_registered_taps(); 240 debug_registered_taps();
218 } 241 }
219 } 242 }
220# endif // CHORDAL_HOLD 243# endif // defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
221 244
222 // state machine is in the "reset" state, no tapping key is to be 245 // state machine is in the "reset" state, no tapping key is to be
223 // processed 246 // processed
@@ -227,6 +250,27 @@ bool process_tapping(keyrecord_t *keyp) {
227 } else if (event.pressed && is_tap_record(keyp)) { 250 } else if (event.pressed && is_tap_record(keyp)) {
228 // the currently pressed key is a tapping key, therefore transition 251 // the currently pressed key is a tapping key, therefore transition
229 // into the "pressed" tapping key state 252 // into the "pressed" tapping key state
253
254# if defined(FLOW_TAP_TERM)
255 const uint16_t keycode = get_record_keycode(keyp, false);
256 if (is_mt_or_lt(keycode)) {
257 const uint32_t idle_time = timer_elapsed32(last_input);
258 uint16_t term = get_flow_tap_term(keycode, keyp, prev_keycode);
259 if (term > 500) {
260 term = 500;
261 }
262 if (idle_time < 500 && idle_time < term) {
263 debug_event(keyp->event);
264 ac_dprintf(" within flow tap term (%u < %u) considered a tap\n", (int16_t)idle_time, term);
265 keyp->tap.count = 1;
266 registered_taps_add(keyp->event.key);
267 debug_registered_taps();
268 process_record(keyp);
269 return true;
270 }
271 }
272# endif // defined(FLOW_TAP_TERM)
273
230 ac_dprintf("Tapping: Start(Press tap key).\n"); 274 ac_dprintf("Tapping: Start(Press tap key).\n");
231 tapping_key = *keyp; 275 tapping_key = *keyp;
232 process_record_tap_hint(&tapping_key); 276 process_record_tap_hint(&tapping_key);
@@ -655,28 +699,7 @@ void waiting_buffer_scan_tap(void) {
655 } 699 }
656} 700}
657 701
658# ifdef CHORDAL_HOLD 702# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
659__attribute__((weak)) bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t *tap_hold_record, uint16_t other_keycode, keyrecord_t *other_record) {
660 return get_chordal_hold_default(tap_hold_record, other_record);
661}
662
663bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_record) {
664 if (tap_hold_record->event.type != KEY_EVENT || other_record->event.type != KEY_EVENT) {
665 return true; // Return true on combos or other non-key events.
666 }
667
668 char tap_hold_hand = chordal_hold_handedness(tap_hold_record->event.key);
669 if (tap_hold_hand == '*') {
670 return true;
671 }
672 char other_hand = chordal_hold_handedness(other_record->event.key);
673 return other_hand == '*' || tap_hold_hand != other_hand;
674}
675
676__attribute__((weak)) char chordal_hold_handedness(keypos_t key) {
677 return (char)pgm_read_byte(&chordal_hold_layout[key.row][key.col]);
678}
679
680static void registered_taps_add(keypos_t key) { 703static void registered_taps_add(keypos_t key) {
681 if (num_registered_taps >= REGISTERED_TAPS_SIZE) { 704 if (num_registered_taps >= REGISTERED_TAPS_SIZE) {
682 ac_dprintf("TAPS OVERFLOW: CLEAR ALL STATES\n"); 705 ac_dprintf("TAPS OVERFLOW: CLEAR ALL STATES\n");
@@ -714,6 +737,30 @@ static void debug_registered_taps(void) {
714 ac_dprintf("}\n"); 737 ac_dprintf("}\n");
715} 738}
716 739
740# endif // defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
741
742# ifdef CHORDAL_HOLD
743__attribute__((weak)) bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t *tap_hold_record, uint16_t other_keycode, keyrecord_t *other_record) {
744 return get_chordal_hold_default(tap_hold_record, other_record);
745}
746
747bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_record) {
748 if (tap_hold_record->event.type != KEY_EVENT || other_record->event.type != KEY_EVENT) {
749 return true; // Return true on combos or other non-key events.
750 }
751
752 char tap_hold_hand = chordal_hold_handedness(tap_hold_record->event.key);
753 if (tap_hold_hand == '*') {
754 return true;
755 }
756 char other_hand = chordal_hold_handedness(other_record->event.key);
757 return other_hand == '*' || tap_hold_hand != other_hand;
758}
759
760__attribute__((weak)) char chordal_hold_handedness(keypos_t key) {
761 return (char)pgm_read_byte(&chordal_hold_layout[key.row][key.col]);
762}
763
717static uint8_t waiting_buffer_find_chordal_hold_tap(void) { 764static uint8_t waiting_buffer_find_chordal_hold_tap(void) {
718 keyrecord_t *prev = &tapping_key; 765 keyrecord_t *prev = &tapping_key;
719 uint16_t prev_keycode = get_record_keycode(&tapping_key, false); 766 uint16_t prev_keycode = get_record_keycode(&tapping_key, false);
@@ -761,6 +808,35 @@ static void waiting_buffer_process_regular(void) {
761} 808}
762# endif // CHORDAL_HOLD 809# endif // CHORDAL_HOLD
763 810
811# ifdef FLOW_TAP_TERM
812// By default, enable Flow Tap for the keys in the main alphas area and Space.
813// This should work reasonably even if the layout is remapped on the host to an
814// alt layout or international layout (e.g. Dvorak or AZERTY), where these same
815// key positions are mostly used for typing letters.
816__attribute__((weak)) bool is_flow_tap_key(uint16_t keycode) {
817 if ((get_mods() & (MOD_MASK_CG | MOD_BIT_LALT)) != 0) {
818 return false; // Disable Flow Tap on hotkeys.
819 }
820 switch (get_tap_keycode(keycode)) {
821 case KC_SPC:
822 case KC_A ... KC_Z:
823 case KC_DOT:
824 case KC_COMM:
825 case KC_SCLN:
826 case KC_SLSH:
827 return true;
828 }
829 return false;
830}
831
832__attribute__((weak)) uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode) {
833 if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
834 return FLOW_TAP_TERM;
835 }
836 return 0;
837}
838# endif // FLOW_TAP_TERM
839
764/** \brief Logs tapping key if ACTION_DEBUG is enabled. */ 840/** \brief Logs tapping key if ACTION_DEBUG is enabled. */
765static void debug_tapping_key(void) { 841static void debug_tapping_key(void) {
766 ac_dprintf("TAPPING_KEY="); 842 ac_dprintf("TAPPING_KEY=");
diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h
index c3c7b999ec..2af000ad73 100644
--- a/quantum/action_tapping.h
+++ b/quantum/action_tapping.h
@@ -111,6 +111,63 @@ char chordal_hold_handedness(keypos_t key);
111extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM; 111extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
112#endif 112#endif
113 113
114#ifdef FLOW_TAP_TERM
115/**
116 * Callback to specify the keys where Flow Tap is enabled.
117 *
118 * Flow Tap is constrained to certain keys by the following rule: this callback
119 * is called for both the tap-hold key *and* the key press immediately preceding
120 * it. If the callback returns true for both keycodes, Flow Tap is enabled.
121 *
122 * The default implementation of this callback corresponds to
123 *
124 * bool is_flow_tap_key(uint16_t keycode) {
125 * switch (get_tap_keycode(keycode)) {
126 * case KC_SPC:
127 * case KC_A ... KC_Z:
128 * case KC_DOT:
129 * case KC_COMM:
130 * case KC_SCLN:
131 * case KC_SLSH:
132 * return true;
133 * }
134 * return false;
135 * }
136 *
137 * @param keycode Keycode of the key.
138 * @return Whether to enable Flow Tap for this key.
139 */
140bool is_flow_tap_key(uint16_t keycode);
141
142/**
143 * Callback to customize Flow Tap filtering.
144 *
145 * Flow Tap acts only when key events are closer together than this time.
146 *
147 * Return a time of 0 to disable filtering. In this way, Flow Tap may be
148 * disabled for certain tap-hold keys, or when following certain previous keys.
149 *
150 * The default implementation of this callback is
151 *
152 * uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t* record,
153 * uint16_t prev_keycode) {
154 * if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
155 * return g_flow_tap_term;
156 * }
157 * return 0;
158 * }
159 *
160 * NOTE: If both `is_flow_tap_key()` and `get_flow_tap_term()` are defined, then
161 * `get_flow_tap_term()` takes precedence.
162 *
163 * @param keycode Keycode of the tap-hold key.
164 * @param record keyrecord_t of the tap-hold event.
165 * @param prev_keycode Keycode of the previously pressed key.
166 * @return Time in milliseconds.
167 */
168uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode);
169#endif // FLOW_TAP_TERM
170
114#ifdef DYNAMIC_TAPPING_TERM_ENABLE 171#ifdef DYNAMIC_TAPPING_TERM_ENABLE
115extern uint16_t g_tapping_term; 172extern uint16_t g_tapping_term;
116#endif 173#endif
diff --git a/quantum/process_keycode/process_leader.c b/quantum/process_keycode/process_leader.c
index ca017a577d..a5466c513c 100644
--- a/quantum/process_keycode/process_leader.c
+++ b/quantum/process_keycode/process_leader.c
@@ -22,11 +22,7 @@ bool process_leader(uint16_t keycode, keyrecord_t *record) {
22 if (record->event.pressed) { 22 if (record->event.pressed) {
23 if (leader_sequence_active() && !leader_sequence_timed_out()) { 23 if (leader_sequence_active() && !leader_sequence_timed_out()) {
24#ifndef LEADER_KEY_STRICT_KEY_PROCESSING 24#ifndef LEADER_KEY_STRICT_KEY_PROCESSING
25 if (IS_QK_MOD_TAP(keycode)) { 25 keycode = get_tap_keycode(keycode);
26 keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
27 } else if (IS_QK_LAYER_TAP(keycode)) {
28 keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
29 }
30#endif 26#endif
31 27
32 if (!leader_sequence_add(keycode)) { 28 if (!leader_sequence_add(keycode)) {
diff --git a/tests/tap_hold_configurations/flow_tap/config.h b/tests/tap_hold_configurations/flow_tap/config.h
new file mode 100644
index 0000000000..a17d488214
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/config.h
@@ -0,0 +1,22 @@
1/* Copyright 2022 Vladislav Kucheriavykh
2 * Copyright 2025 Google LLC
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18#pragma once
19
20#include "test_common.h"
21
22#define FLOW_TAP_TERM 150
diff --git a/tests/tap_hold_configurations/flow_tap/test.mk b/tests/tap_hold_configurations/flow_tap/test.mk
new file mode 100644
index 0000000000..81ba8da66d
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/test.mk
@@ -0,0 +1,18 @@
1# Copyright 2022 Vladislav Kucheriavykh
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16COMBO_ENABLE = yes
17
18INTROSPECTION_KEYMAP_C = test_keymap.c
diff --git a/tests/tap_hold_configurations/flow_tap/test_keymap.c b/tests/tap_hold_configurations/flow_tap/test_keymap.c
new file mode 100644
index 0000000000..4dfe5e4cb6
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/test_keymap.c
@@ -0,0 +1,23 @@
1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#include "quantum.h"
16
17uint16_t const mt_lt_combo[] = {SFT_T(KC_X), LT(1, KC_Y), COMBO_END};
18
19// clang-format off
20combo_t key_combos[] = {
21 COMBO(mt_lt_combo, KC_Z),
22};
23// clang-format on
diff --git a/tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp b/tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp
new file mode 100644
index 0000000000..7816fcb6da
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp
@@ -0,0 +1,313 @@
1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#include "keyboard_report_util.hpp"
16#include "keycode.h"
17#include "test_common.hpp"
18#include "action_tapping.h"
19#include "test_fixture.hpp"
20#include "test_keymap_key.hpp"
21
22using testing::_;
23using testing::AnyNumber;
24using testing::InSequence;
25
26class FlowTapTest : public TestFixture {};
27
28TEST_F(FlowTapTest, short_flow_tap_settled_as_tapped) {
29 TestDriver driver;
30 InSequence s;
31 auto regular_key = KeymapKey(0, 0, 0, KC_A);
32 auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_B));
33 auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_C));
34
35 set_keymap({regular_key, mod_tap_key1, mod_tap_key2});
36
37 // Tap regular key.
38 EXPECT_REPORT(driver, (KC_A));
39 EXPECT_EMPTY_REPORT(driver);
40 tap_key(regular_key);
41 VERIFY_AND_CLEAR(driver);
42
43 // Press mod-tap key 1 quickly after regular key. The mod-tap should settle
44 // immediately as tapped, sending `KC_B`.
45 EXPECT_REPORT(driver, (KC_B));
46 mod_tap_key1.press();
47 run_one_scan_loop();
48 VERIFY_AND_CLEAR(driver);
49
50 // Press mod-tap key 2 quickly.
51 EXPECT_REPORT(driver, (KC_B, KC_C));
52 mod_tap_key2.press();
53 run_one_scan_loop();
54 VERIFY_AND_CLEAR(driver);
55
56 // Hold for longer than the tapping term.
57 EXPECT_NO_REPORT(driver);
58 idle_for(TAPPING_TERM + 1);
59 VERIFY_AND_CLEAR(driver);
60
61 // Release mod-tap keys.
62 EXPECT_REPORT(driver, (KC_C));
63 EXPECT_EMPTY_REPORT(driver);
64 mod_tap_key1.release();
65 run_one_scan_loop();
66 mod_tap_key2.release();
67 run_one_scan_loop();
68 VERIFY_AND_CLEAR(driver);
69}
70
71TEST_F(FlowTapTest, long_flow_tap_settled_as_held) {
72 TestDriver driver;
73 InSequence s;
74 auto regular_key = KeymapKey(0, 0, 0, KC_A);
75 auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B));
76
77 set_keymap({regular_key, mod_tap_key});
78
79 // Tap regular key.
80 EXPECT_REPORT(driver, (KC_A));
81 EXPECT_EMPTY_REPORT(driver);
82 tap_key(regular_key);
83 VERIFY_AND_CLEAR(driver);
84
85 EXPECT_NO_REPORT(driver);
86 idle_for(FLOW_TAP_TERM + 1);
87 VERIFY_AND_CLEAR(driver);
88
89 // Press mod-tap key.
90 EXPECT_NO_REPORT(driver);
91 mod_tap_key.press();
92 run_one_scan_loop();
93 VERIFY_AND_CLEAR(driver);
94
95 // Hold for the tapping term.
96 EXPECT_REPORT(driver, (KC_LSFT));
97 idle_for(TAPPING_TERM);
98 VERIFY_AND_CLEAR(driver);
99
100 // Release mod-tap key.
101 EXPECT_EMPTY_REPORT(driver);
102 mod_tap_key.release();
103 run_one_scan_loop();
104 VERIFY_AND_CLEAR(driver);
105}
106
107TEST_F(FlowTapTest, holding_multiple_mod_taps) {
108 TestDriver driver;
109 InSequence s;
110 auto regular_key = KeymapKey(0, 0, 0, KC_A);
111 auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_B));
112 auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_C));
113
114 set_keymap({regular_key, mod_tap_key1, mod_tap_key2});
115
116 // Tap regular key.
117 EXPECT_REPORT(driver, (KC_A));
118 EXPECT_EMPTY_REPORT(driver);
119 tap_key(regular_key);
120 VERIFY_AND_CLEAR(driver);
121
122 EXPECT_NO_REPORT(driver);
123 idle_for(FLOW_TAP_TERM + 1);
124 VERIFY_AND_CLEAR(driver);
125
126 // Press mod-tap keys.
127 EXPECT_NO_REPORT(driver);
128 mod_tap_key1.press();
129 run_one_scan_loop();
130 mod_tap_key2.press();
131 idle_for(TAPPING_TERM - 5); // Hold almost until tapping term.
132 VERIFY_AND_CLEAR(driver);
133
134 // Press regular key.
135 EXPECT_REPORT(driver, (KC_LSFT));
136 EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
137 EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL, KC_A));
138 regular_key.press();
139 idle_for(10);
140 VERIFY_AND_CLEAR(driver);
141
142 // Release keys.
143 EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
144 EXPECT_REPORT(driver, (KC_LCTL));
145 EXPECT_EMPTY_REPORT(driver);
146 regular_key.release();
147 run_one_scan_loop();
148 mod_tap_key1.release();
149 run_one_scan_loop();
150 mod_tap_key2.release();
151 run_one_scan_loop();
152 VERIFY_AND_CLEAR(driver);
153}
154
155TEST_F(FlowTapTest, layer_tap_key) {
156 TestDriver driver;
157 InSequence s;
158 auto regular_key = KeymapKey(0, 0, 0, KC_A);
159 auto layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_B));
160 auto regular_key2 = KeymapKey(1, 0, 0, KC_C);
161
162 set_keymap({regular_key, layer_tap_key, regular_key2});
163
164 // Tap regular key.
165 EXPECT_REPORT(driver, (KC_A));
166 EXPECT_EMPTY_REPORT(driver);
167 tap_key(regular_key);
168 VERIFY_AND_CLEAR(driver);
169
170 // Press layer-tap key, quickly after the regular key.
171 EXPECT_REPORT(driver, (KC_B));
172 layer_tap_key.press();
173 run_one_scan_loop();
174 VERIFY_AND_CLEAR(driver);
175
176 EXPECT_NO_REPORT(driver);
177 idle_for(TAPPING_TERM + 1);
178 VERIFY_AND_CLEAR(driver);
179
180 // Release layer-tap key.
181 EXPECT_EMPTY_REPORT(driver);
182 layer_tap_key.release();
183 run_one_scan_loop();
184
185 // Tap regular key.
186 EXPECT_REPORT(driver, (KC_A));
187 EXPECT_EMPTY_REPORT(driver);
188 tap_key(regular_key);
189 VERIFY_AND_CLEAR(driver);
190
191 EXPECT_NO_REPORT(driver);
192 idle_for(FLOW_TAP_TERM + 1);
193 VERIFY_AND_CLEAR(driver);
194
195 // Press layer-tap key, slowly after the regular key.
196 EXPECT_NO_REPORT(driver);
197 layer_tap_key.press();
198 run_one_scan_loop();
199 VERIFY_AND_CLEAR(driver);
200
201 EXPECT_NO_REPORT(driver);
202 idle_for(TAPPING_TERM + 1);
203 EXPECT_EQ(layer_state, 1 << 1);
204 VERIFY_AND_CLEAR(driver);
205
206 // Tap regular key2.
207 EXPECT_REPORT(driver, (KC_C));
208 EXPECT_EMPTY_REPORT(driver);
209 tap_key(regular_key);
210 VERIFY_AND_CLEAR(driver);
211
212 // Release layer-tap key.
213 EXPECT_NO_REPORT(driver);
214 layer_tap_key.release();
215 run_one_scan_loop();
216 VERIFY_AND_CLEAR(driver);
217}
218
219TEST_F(FlowTapTest, combo_key) {
220 TestDriver driver;
221 InSequence s;
222 auto regular_key = KeymapKey(0, 0, 0, KC_A);
223 auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_X));
224 auto layer_tap_key = KeymapKey(0, 2, 0, LT(1, KC_Y));
225
226 set_keymap({regular_key, mod_tap_key, layer_tap_key});
227
228 // Tap regular key.
229 EXPECT_REPORT(driver, (KC_A));
230 EXPECT_EMPTY_REPORT(driver);
231 tap_key(regular_key);
232 VERIFY_AND_CLEAR(driver);
233
234 // Press combo keys quickly after regular key.
235 EXPECT_REPORT(driver, (KC_Z));
236 EXPECT_EMPTY_REPORT(driver);
237 tap_combo({mod_tap_key, layer_tap_key});
238 VERIFY_AND_CLEAR(driver);
239
240 // Press mod-tap key quickly.
241 EXPECT_REPORT(driver, (KC_X));
242 mod_tap_key.press();
243 idle_for(TAPPING_TERM + 1);
244 VERIFY_AND_CLEAR(driver);
245
246 // Release mod-tap key.
247 EXPECT_EMPTY_REPORT(driver);
248 mod_tap_key.release();
249 run_one_scan_loop();
250 VERIFY_AND_CLEAR(driver);
251}
252
253TEST_F(FlowTapTest, oneshot_mod_key) {
254 TestDriver driver;
255 InSequence s;
256 auto regular_key = KeymapKey(0, 0, 0, KC_A);
257 auto osm_key = KeymapKey(0, 1, 0, OSM(MOD_LSFT));
258
259 set_keymap({regular_key, osm_key});
260
261 // Tap regular key.
262 EXPECT_REPORT(driver, (KC_A));
263 EXPECT_EMPTY_REPORT(driver);
264 tap_key(regular_key);
265 VERIFY_AND_CLEAR(driver);
266
267 // Tap OSM, tap regular key.
268 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
269 EXPECT_REPORT(driver, (KC_LSFT, KC_A));
270 EXPECT_EMPTY_REPORT(driver);
271 tap_key(osm_key);
272 tap_key(regular_key);
273 VERIFY_AND_CLEAR(driver);
274
275 // Nested press of OSM and regular keys.
276 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
277 EXPECT_REPORT(driver, (KC_LSFT, KC_A));
278 EXPECT_EMPTY_REPORT(driver);
279 osm_key.press();
280 run_one_scan_loop();
281 tap_key(regular_key);
282 osm_key.release();
283 run_one_scan_loop();
284 VERIFY_AND_CLEAR(driver);
285}
286
287TEST_F(FlowTapTest, quick_tap) {
288 TestDriver driver;
289 InSequence s;
290 auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_A));
291
292 set_keymap({mod_tap_key});
293
294 EXPECT_REPORT(driver, (KC_A));
295 EXPECT_EMPTY_REPORT(driver);
296 tap_key(mod_tap_key);
297 VERIFY_AND_CLEAR(driver);
298
299 EXPECT_REPORT(driver, (KC_A));
300 mod_tap_key.press();
301 run_one_scan_loop();
302 VERIFY_AND_CLEAR(driver);
303
304 EXPECT_NO_REPORT(driver);
305 idle_for(TAPPING_TERM + 1);
306 VERIFY_AND_CLEAR(driver);
307
308 // Release mod-tap key.
309 EXPECT_EMPTY_REPORT(driver);
310 mod_tap_key.release();
311 run_one_scan_loop();
312 VERIFY_AND_CLEAR(driver);
313}