From 873247739eac1992161db04d8077f6c72ba37388 Mon Sep 17 00:00:00 2001
From: Pascal Getreuer <50221757+getreuer@users.noreply.github.com>
Date: Tue, 29 Apr 2025 02:06:33 -0700
Subject: [PATCH] Add Flow Tap to zsa/qmk_firmware. (#408)
* Add Flow Tap to zsa/qmk_firmware.
* Flow Tap bug fixes and enhancements.
Adds Tap Flow bug fix https://github.com/qmk/qmk_firmware/pull/25175 and
enhancement https://github.com/qmk/qmk_firmware/pull/25200.
---
data/mappings/info_config.hjson | 1 +
quantum/action.c | 20 +
quantum/action.h | 6 +
quantum/action_tapping.c | 212 +++-
quantum/action_tapping.h | 60 ++
quantum/process_keycode/process_leader.c | 6 +-
.../permissive_hold_flow_tap/config.h | 23 +
.../permissive_hold_flow_tap/test.mk | 17 +
.../permissive_hold_flow_tap/test_keymap.c | 22 +
.../test_one_shot_keys.cpp | 174 ++++
.../test_tap_hold.cpp | 911 ++++++++++++++++++
.../tap_hold_configurations/flow_tap/config.h | 23 +
.../tap_hold_configurations/flow_tap/test.mk | 18 +
.../flow_tap/test_keymap.c | 23 +
.../flow_tap/test_tap_hold.cpp | 844 ++++++++++++++++
15 files changed, 2324 insertions(+), 36 deletions(-)
create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/config.h
create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test.mk
create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_keymap.c
create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_one_shot_keys.cpp
create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_tap_hold.cpp
create mode 100644 tests/tap_hold_configurations/flow_tap/config.h
create mode 100644 tests/tap_hold_configurations/flow_tap/test.mk
create mode 100644 tests/tap_hold_configurations/flow_tap/test_keymap.c
create mode 100644 tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp
diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson
index 7233282021..f68016422d 100644
--- a/data/mappings/info_config.hjson
+++ b/data/mappings/info_config.hjson
@@ -196,6 +196,7 @@
// Tapping
"CHORDAL_HOLD": {"info_key": "tapping.chordal_hold", "value_type": "flag"},
+ "FLOW_TAP_TERM": {"info_key": "tapping.flow_tap_term", "value_type": "int"},
"HOLD_ON_OTHER_KEY_PRESS": {"info_key": "tapping.hold_on_other_key_press", "value_type": "flag"},
"HOLD_ON_OTHER_KEY_PRESS_PER_KEY": {"info_key": "tapping.hold_on_other_key_press_per_key", "value_type": "flag"},
"PERMISSIVE_HOLD": {"info_key": "tapping.permissive_hold", "value_type": "flag"},
diff --git a/quantum/action.c b/quantum/action.c
index 74ef55e5eb..5d4b6e75f8 100644
--- a/quantum/action.c
+++ b/quantum/action.c
@@ -270,6 +270,9 @@ void process_record(keyrecord_t *record) {
if (IS_NOEVENT(record->event)) {
return;
}
+#ifdef FLOW_TAP_TERM
+ flow_tap_update_last_event(record);
+#endif // FLOW_TAP_TERM
if (!process_record_quantum(record)) {
#ifndef NO_ACTION_ONESHOT
@@ -1153,6 +1156,23 @@ bool is_tap_action(action_t action) {
return false;
}
+uint16_t get_tap_keycode(uint16_t keycode) {
+ switch (keycode) {
+ case QK_MOD_TAP ... QK_MOD_TAP_MAX:
+ return QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
+ case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
+ return QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
+ case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
+ // IS_SWAP_HANDS_KEYCODE() tests for the special action keycodes
+ // like SH_TOGG, SH_TT, ..., which overlap the SH_T(kc) range.
+ if (!IS_SWAP_HANDS_KEYCODE(keycode)) {
+ return QK_SWAP_HANDS_GET_TAP_KEYCODE(keycode);
+ }
+ break;
+ }
+ return keycode;
+}
+
/** \brief Debug print (FIXME: Needs better description)
*
* FIXME: Needs documentation.
diff --git a/quantum/action.h b/quantum/action.h
index d5b15c6f17..4362a6827b 100644
--- a/quantum/action.h
+++ b/quantum/action.h
@@ -128,6 +128,12 @@ void layer_switch(uint8_t new_layer);
bool is_tap_record(keyrecord_t *record);
bool is_tap_action(action_t action);
+/**
+ * Given an MT or LT keycode, returns the tap keycode. Otherwise returns the
+ * original keycode unchanged.
+ */
+uint16_t get_tap_keycode(uint16_t keycode);
+
#ifndef NO_ACTION_TAPPING
void process_record_tap_hint(keyrecord_t *record);
#endif
diff --git a/quantum/action_tapping.c b/quantum/action_tapping.c
index e42a98554d..b105cd60a9 100644
--- a/quantum/action_tapping.c
+++ b/quantum/action_tapping.c
@@ -4,7 +4,9 @@
#include "action.h"
#include "action_layer.h"
#include "action_tapping.h"
+#include "action_util.h"
#include "keycode.h"
+#include "quantum_keycodes.h"
#include "timer.h"
#ifndef NO_ACTION_TAPPING
@@ -49,9 +51,7 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re
}
# endif
-# if defined(CHORDAL_HOLD)
-extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
-
+# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
# define REGISTERED_TAPS_SIZE 8
// Array of tap-hold keys that have been settled as tapped but not yet released.
static keypos_t registered_taps[REGISTERED_TAPS_SIZE] = {};
@@ -66,6 +66,14 @@ static void registered_taps_del_index(uint8_t i);
/** Logs the registered_taps array for debugging. */
static void debug_registered_taps(void);
+static bool is_mt_or_lt(uint16_t keycode) {
+ return IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode);
+}
+# endif // defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
+
+# if defined(CHORDAL_HOLD)
+extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
+
/** \brief Finds which queued events should be held according to Chordal Hold.
*
* In a situation with multiple unsettled tap-hold key presses, scan the queue
@@ -82,10 +90,6 @@ static void waiting_buffer_chordal_hold_taps_until(keypos_t key);
/** \brief Processes and pops buffered events until the first tap-hold event. */
static void waiting_buffer_process_regular(void);
-
-static bool is_mt_or_lt(uint16_t keycode) {
- return IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode);
-}
# endif // CHORDAL_HOLD
# ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY
@@ -98,6 +102,14 @@ __attribute__((weak)) bool get_hold_on_other_key_press(uint16_t keycode, keyreco
# include "process_auto_shift.h"
# endif
+# if defined(FLOW_TAP_TERM)
+static uint16_t flow_tap_prev_keycode = KC_NO;
+static uint16_t flow_tap_prev_time = 0;
+static bool flow_tap_expired = true;
+
+static bool flow_tap_key_if_within_term(keyrecord_t *record, uint16_t prev_time);
+# endif // defined(FLOW_TAP_TERM)
+
static keyrecord_t tapping_key = {};
static keyrecord_t waiting_buffer[WAITING_BUFFER_SIZE] = {};
static uint8_t waiting_buffer_head = 0;
@@ -148,6 +160,12 @@ void action_tapping_process(keyrecord_t record) {
}
if (IS_EVENT(record.event)) {
ac_dprintf("\n");
+ } else {
+# ifdef FLOW_TAP_TERM
+ if (!flow_tap_expired && TIMER_DIFF_16(record.event.time, flow_tap_prev_time) >= INT16_MAX / 2) {
+ flow_tap_expired = true;
+ }
+# endif // FLOW_TAP_TERM
}
}
@@ -205,7 +223,7 @@ void action_tapping_process(keyrecord_t record) {
bool process_tapping(keyrecord_t *keyp) {
const keyevent_t event = keyp->event;
-# if defined(CHORDAL_HOLD)
+# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
if (!event.pressed) {
const int8_t i = registered_tap_find(event.key);
if (i != -1) {
@@ -217,7 +235,7 @@ bool process_tapping(keyrecord_t *keyp) {
debug_registered_taps();
}
}
-# endif // CHORDAL_HOLD
+# endif // defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
// state machine is in the "reset" state, no tapping key is to be
// processed
@@ -227,6 +245,13 @@ bool process_tapping(keyrecord_t *keyp) {
} else if (event.pressed && is_tap_record(keyp)) {
// the currently pressed key is a tapping key, therefore transition
// into the "pressed" tapping key state
+
+# if defined(FLOW_TAP_TERM)
+ if (flow_tap_key_if_within_term(keyp, flow_tap_prev_time)) {
+ return true;
+ }
+# endif // defined(FLOW_TAP_TERM)
+
ac_dprintf("Tapping: Start(Press tap key).\n");
tapping_key = *keyp;
process_record_tap_hint(&tapping_key);
@@ -263,6 +288,27 @@ bool process_tapping(keyrecord_t *keyp) {
// copy tapping state
keyp->tap = tapping_key.tap;
+
+# if defined(FLOW_TAP_TERM)
+ // Now that tapping_key has settled as tapped, check whether
+ // Flow Tap applies to following yet-unsettled keys.
+ uint16_t prev_time = tapping_key.event.time;
+ for (; waiting_buffer_tail != waiting_buffer_head; waiting_buffer_tail = (waiting_buffer_tail + 1) % WAITING_BUFFER_SIZE) {
+ keyrecord_t *record = &waiting_buffer[waiting_buffer_tail];
+ if (!record->event.pressed) {
+ break;
+ }
+ const int16_t next_time = record->event.time;
+ if (!is_tap_record(record)) {
+ process_record(record);
+ } else if (!flow_tap_key_if_within_term(record, prev_time)) {
+ break;
+ }
+ prev_time = next_time;
+ }
+ debug_waiting_buffer();
+# endif // defined(FLOW_TAP_TERM)
+
// enqueue
return false;
}
@@ -538,6 +584,13 @@ bool process_tapping(keyrecord_t *keyp) {
return true;
} else if (is_tap_record(keyp)) {
// Sequential tap can be interfered with other tap key.
+# if defined(FLOW_TAP_TERM)
+ if (flow_tap_key_if_within_term(keyp, flow_tap_prev_time)) {
+ tapping_key = (keyrecord_t){0};
+ debug_tapping_key();
+ return true;
+ }
+# endif // defined(FLOW_TAP_TERM)
ac_dprintf("Tapping: Start with interfering other tap.\n");
tapping_key = *keyp;
waiting_buffer_scan_tap();
@@ -655,28 +708,7 @@ void waiting_buffer_scan_tap(void) {
}
}
-# ifdef CHORDAL_HOLD
-__attribute__((weak)) bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t *tap_hold_record, uint16_t other_keycode, keyrecord_t *other_record) {
- return get_chordal_hold_default(tap_hold_record, other_record);
-}
-
-bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_record) {
- if (tap_hold_record->event.type != KEY_EVENT || other_record->event.type != KEY_EVENT) {
- return true; // Return true on combos or other non-key events.
- }
-
- char tap_hold_hand = chordal_hold_handedness(tap_hold_record->event.key);
- if (tap_hold_hand == '*') {
- return true;
- }
- char other_hand = chordal_hold_handedness(other_record->event.key);
- return other_hand == '*' || tap_hold_hand != other_hand;
-}
-
-__attribute__((weak)) char chordal_hold_handedness(keypos_t key) {
- return (char)pgm_read_byte(&chordal_hold_layout[key.row][key.col]);
-}
-
+# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
static void registered_taps_add(keypos_t key) {
if (num_registered_taps >= REGISTERED_TAPS_SIZE) {
ac_dprintf("TAPS OVERFLOW: CLEAR ALL STATES\n");
@@ -714,6 +746,30 @@ static void debug_registered_taps(void) {
ac_dprintf("}\n");
}
+# endif // defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
+
+# ifdef CHORDAL_HOLD
+__attribute__((weak)) bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t *tap_hold_record, uint16_t other_keycode, keyrecord_t *other_record) {
+ return get_chordal_hold_default(tap_hold_record, other_record);
+}
+
+bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_record) {
+ if (tap_hold_record->event.type != KEY_EVENT || other_record->event.type != KEY_EVENT) {
+ return true; // Return true on combos or other non-key events.
+ }
+
+ char tap_hold_hand = chordal_hold_handedness(tap_hold_record->event.key);
+ if (tap_hold_hand == '*') {
+ return true;
+ }
+ char other_hand = chordal_hold_handedness(other_record->event.key);
+ return other_hand == '*' || tap_hold_hand != other_hand;
+}
+
+__attribute__((weak)) char chordal_hold_handedness(keypos_t key) {
+ return (char)pgm_read_byte(&chordal_hold_layout[key.row][key.col]);
+}
+
static uint8_t waiting_buffer_find_chordal_hold_tap(void) {
keyrecord_t *prev = &tapping_key;
uint16_t prev_keycode = get_record_keycode(&tapping_key, false);
@@ -761,6 +817,100 @@ static void waiting_buffer_process_regular(void) {
}
# endif // CHORDAL_HOLD
+# ifdef FLOW_TAP_TERM
+void flow_tap_update_last_event(keyrecord_t *record) {
+ const uint16_t keycode = get_record_keycode(record, false);
+ // Don't update while a tap-hold key is unsettled.
+ if (record->tap.count == 0 && (waiting_buffer_tail != waiting_buffer_head || (tapping_key.event.pressed && tapping_key.tap.count == 0))) {
+ return;
+ }
+ // Ignore releases of modifiers and held layer switches.
+ if (!record->event.pressed) {
+ switch (keycode) {
+ case MODIFIER_KEYCODE_RANGE:
+ case QK_MOMENTARY ... QK_MOMENTARY_MAX:
+ case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
+# ifndef NO_ACTION_ONESHOT // Ignore one-shot keys.
+ case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
+ case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
+# endif // NO_ACTION_ONESHOT
+# ifdef TRI_LAYER_ENABLE // Ignore Tri Layer keys.
+ case QK_TRI_LAYER_LOWER:
+ case QK_TRI_LAYER_UPPER:
+# endif // TRI_LAYER_ENABLE
+ return;
+ case QK_MODS ... QK_MODS_MAX:
+ if (QK_MODS_GET_BASIC_KEYCODE(keycode) == KC_NO) {
+ return;
+ }
+ break;
+ case QK_MOD_TAP ... QK_MOD_TAP_MAX:
+ case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
+ if (record->tap.count == 0) {
+ return;
+ }
+ break;
+ }
+ }
+
+ flow_tap_prev_keycode = keycode;
+ flow_tap_prev_time = record->event.time;
+ flow_tap_expired = false;
+}
+
+static bool flow_tap_key_if_within_term(keyrecord_t *record, uint16_t prev_time) {
+ const uint16_t idle_time = TIMER_DIFF_16(record->event.time, prev_time);
+ if (flow_tap_expired || idle_time >= 500) {
+ return false;
+ }
+
+ const uint16_t keycode = get_record_keycode(record, false);
+ if (is_mt_or_lt(keycode)) {
+ uint16_t term = get_flow_tap_term(keycode, record, flow_tap_prev_keycode);
+ if (term > 500) {
+ term = 500;
+ }
+ if (idle_time < term) {
+ debug_event(record->event);
+ ac_dprintf(" within flow tap term (%u < %u) considered a tap\n", idle_time, term);
+ record->tap.count = 1;
+ registered_taps_add(record->event.key);
+ debug_registered_taps();
+ process_record(record);
+ return true;
+ }
+ }
+ return false;
+}
+
+// By default, enable Flow Tap for the keys in the main alphas area and Space.
+// This should work reasonably even if the layout is remapped on the host to an
+// alt layout or international layout (e.g. Dvorak or AZERTY), where these same
+// key positions are mostly used for typing letters.
+__attribute__((weak)) bool is_flow_tap_key(uint16_t keycode) {
+ if ((get_mods() & (MOD_MASK_CG | MOD_BIT_LALT)) != 0) {
+ return false; // Disable Flow Tap on hotkeys.
+ }
+ switch (get_tap_keycode(keycode)) {
+ case KC_SPC:
+ case KC_A ... KC_Z:
+ case KC_DOT:
+ case KC_COMM:
+ case KC_SCLN:
+ case KC_SLSH:
+ return true;
+ }
+ return false;
+}
+
+__attribute__((weak)) uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode) {
+ if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
+ return FLOW_TAP_TERM;
+ }
+ return 0;
+}
+# endif // FLOW_TAP_TERM
+
/** \brief Logs tapping key if ACTION_DEBUG is enabled. */
static void debug_tapping_key(void) {
ac_dprintf("TAPPING_KEY=");
diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h
index c3c7b999ec..0cf4aa1200 100644
--- a/quantum/action_tapping.h
+++ b/quantum/action_tapping.h
@@ -111,6 +111,66 @@ char chordal_hold_handedness(keypos_t key);
extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
#endif
+#ifdef FLOW_TAP_TERM
+/**
+ * Callback to specify the keys where Flow Tap is enabled.
+ *
+ * Flow Tap is constrained to certain keys by the following rule: this callback
+ * is called for both the tap-hold key *and* the key press immediately preceding
+ * it. If the callback returns true for both keycodes, Flow Tap is enabled.
+ *
+ * The default implementation of this callback corresponds to
+ *
+ * bool is_flow_tap_key(uint16_t keycode) {
+ * switch (get_tap_keycode(keycode)) {
+ * case KC_SPC:
+ * case KC_A ... KC_Z:
+ * case KC_DOT:
+ * case KC_COMM:
+ * case KC_SCLN:
+ * case KC_SLSH:
+ * return true;
+ * }
+ * return false;
+ * }
+ *
+ * @param keycode Keycode of the key.
+ * @return Whether to enable Flow Tap for this key.
+ */
+bool is_flow_tap_key(uint16_t keycode);
+
+/**
+ * Callback to customize Flow Tap filtering.
+ *
+ * Flow Tap acts only when key events are closer together than this time.
+ *
+ * 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.
+ *
+ * The default implementation of this callback is
+ *
+ * uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t* record,
+ * uint16_t prev_keycode) {
+ * if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
+ * return g_flow_tap_term;
+ * }
+ * return 0;
+ * }
+ *
+ * NOTE: If both `is_flow_tap_key()` and `get_flow_tap_term()` are defined, then
+ * `get_flow_tap_term()` takes precedence.
+ *
+ * @param keycode Keycode of the tap-hold key.
+ * @param record keyrecord_t of the tap-hold event.
+ * @param prev_keycode Keycode of the previously pressed key.
+ * @return Time in milliseconds.
+ */
+uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode);
+
+/** Updates the Flow Tap last key and timer. */
+void flow_tap_update_last_event(keyrecord_t *record);
+#endif // FLOW_TAP_TERM
+
#ifdef DYNAMIC_TAPPING_TERM_ENABLE
extern uint16_t g_tapping_term;
#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) {
if (record->event.pressed) {
if (leader_sequence_active() && !leader_sequence_timed_out()) {
#ifndef LEADER_KEY_STRICT_KEY_PROCESSING
- if (IS_QK_MOD_TAP(keycode)) {
- keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
- } else if (IS_QK_LAYER_TAP(keycode)) {
- keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
- }
+ keycode = get_tap_keycode(keycode);
#endif
if (!leader_sequence_add(keycode)) {
diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/config.h b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/config.h
new file mode 100644
index 0000000000..9267b94bec
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/config.h
@@ -0,0 +1,23 @@
+/* Copyright 2022 Vladislav Kucheriavykh
+ * Copyright 2024-2025 Google LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include "test_common.h"
+#define CHORDAL_HOLD
+#define PERMISSIVE_HOLD
+#define FLOW_TAP_TERM 150
diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test.mk b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test.mk
new file mode 100644
index 0000000000..2b049cea3b
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test.mk
@@ -0,0 +1,17 @@
+# Copyright 2022 Vladislav Kucheriavykh
+# Copyright 2024 Google LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+INTROSPECTION_KEYMAP_C = test_keymap.c
diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_keymap.c b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_keymap.c
new file mode 100644
index 0000000000..8a6a2c59b0
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_keymap.c
@@ -0,0 +1,22 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "quantum.h"
+
+const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = {
+ {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'},
+ {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'},
+ {'*', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'},
+ {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'},
+};
diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_one_shot_keys.cpp b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_one_shot_keys.cpp
new file mode 100644
index 0000000000..e48cba73e2
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_one_shot_keys.cpp
@@ -0,0 +1,174 @@
+/* Copyright 2021 Stefan Kerkmann
+ * Copyright 2024 Google LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "action_util.h"
+#include "keyboard_report_util.hpp"
+#include "test_common.hpp"
+
+using testing::_;
+using testing::InSequence;
+
+class OneShot : public TestFixture {};
+class OneShotParametrizedTestFixture : public ::testing::WithParamInterface>, public OneShot {};
+
+TEST_P(OneShotParametrizedTestFixture, OSMWithAdditionalKeypress) {
+ TestDriver driver;
+ KeymapKey osm_key = GetParam().first;
+ KeymapKey regular_key = GetParam().second;
+
+ set_keymap({osm_key, regular_key});
+
+ // Press and release OSM.
+ EXPECT_NO_REPORT(driver);
+ tap_key(osm_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press regular key.
+ EXPECT_REPORT(driver, (osm_key.report_code, regular_key.report_code));
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release regular key.
+ EXPECT_EMPTY_REPORT(driver);
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_P(OneShotParametrizedTestFixture, OSMAsRegularModifierWithAdditionalKeypress) {
+ TestDriver driver;
+ KeymapKey osm_key = GetParam().first;
+ KeymapKey regular_key = GetParam().second;
+
+ set_keymap({osm_key, regular_key});
+
+ // Press OSM.
+ EXPECT_NO_REPORT(driver);
+ osm_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press regular key.
+ EXPECT_NO_REPORT(driver);
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release regular key.
+ EXPECT_REPORT(driver, (osm_key.report_code)).Times(2);
+ EXPECT_REPORT(driver, (regular_key.report_code, osm_key.report_code));
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release OSM.
+ EXPECT_EMPTY_REPORT(driver);
+ osm_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+// clang-format off
+
+INSTANTIATE_TEST_CASE_P(
+ OneShotModifierTests,
+ OneShotParametrizedTestFixture,
+ ::testing::Values(
+ // First is osm key, second is regular key.
+ std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LSFT), KC_LSFT}, KeymapKey{0, 1, 1, KC_A}),
+ std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LCTL), KC_LCTL}, KeymapKey{0, 1, 1, KC_A}),
+ std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LALT), KC_LALT}, KeymapKey{0, 1, 1, KC_A}),
+ std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LGUI), KC_LGUI}, KeymapKey{0, 1, 1, KC_A}),
+ std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RCTL), KC_RCTL}, KeymapKey{0, 1, 1, KC_A}),
+ std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RSFT), KC_RSFT}, KeymapKey{0, 1, 1, KC_A}),
+ std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RALT), KC_RALT}, KeymapKey{0, 1, 1, KC_A}),
+ std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RGUI), KC_RGUI}, KeymapKey{0, 1, 1, KC_A})
+ ));
+// clang-format on
+
+TEST_F(OneShot, OSLWithAdditionalKeypress) {
+ TestDriver driver;
+ InSequence s;
+ KeymapKey osl_key = KeymapKey{0, 0, 0, OSL(1)};
+ KeymapKey osl_key1 = KeymapKey{1, 0, 0, KC_X};
+ KeymapKey regular_key0 = KeymapKey{0, 1, 0, KC_Y};
+ KeymapKey regular_key1 = KeymapKey{1, 1, 0, KC_A};
+
+ set_keymap({osl_key, osl_key1, regular_key0, regular_key1});
+
+ // Press OSL key.
+ EXPECT_NO_REPORT(driver);
+ osl_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release OSL key.
+ EXPECT_NO_REPORT(driver);
+ osl_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press regular key.
+ EXPECT_REPORT(driver, (regular_key1.report_code));
+ EXPECT_EMPTY_REPORT(driver);
+ regular_key1.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release regular key.
+ EXPECT_NO_REPORT(driver);
+ regular_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(OneShot, OSLWithOsmAndAdditionalKeypress) {
+ TestDriver driver;
+ InSequence s;
+ KeymapKey osl_key = KeymapKey{0, 0, 0, OSL(1)};
+ KeymapKey osm_key = KeymapKey{1, 1, 0, OSM(MOD_LSFT), KC_LSFT};
+ KeymapKey regular_key = KeymapKey{1, 1, 1, KC_A};
+ KeymapKey blank_key = KeymapKey{1, 0, 0, KC_NO};
+
+ set_keymap({osl_key, osm_key, regular_key, blank_key});
+
+ // Press OSL key.
+ EXPECT_NO_REPORT(driver);
+ osl_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release OSL key.
+ EXPECT_NO_REPORT(driver);
+ osl_key.release();
+ run_one_scan_loop();
+ EXPECT_TRUE(layer_state_is(1));
+ VERIFY_AND_CLEAR(driver);
+
+ // Press and release OSM.
+ EXPECT_NO_REPORT(driver);
+ tap_key(osm_key);
+ EXPECT_TRUE(layer_state_is(1));
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (osm_key.report_code, regular_key.report_code));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+}
diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_tap_hold.cpp
new file mode 100644
index 0000000000..05acb6f416
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold_flow_tap/test_tap_hold.cpp
@@ -0,0 +1,911 @@
+// Copyright 2024-2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "action_tapping.h"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using testing::_;
+using testing::InSequence;
+
+class ChordalHoldPermissiveHoldFlowTap : public TestFixture {};
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, chordal_hold_handedness) {
+ EXPECT_EQ(chordal_hold_handedness({.col = 0, .row = 0}), 'L');
+ EXPECT_EQ(chordal_hold_handedness({.col = MATRIX_COLS - 1, .row = 0}), 'R');
+ EXPECT_EQ(chordal_hold_handedness({.col = 0, .row = 2}), '*');
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, get_chordal_hold_default) {
+ auto make_record = [](uint8_t row, uint8_t col, keyevent_type_t type = KEY_EVENT) {
+ return keyrecord_t{
+ .event =
+ {
+ .key = {.col = col, .row = row},
+ .type = type,
+ .pressed = true,
+ },
+ };
+ };
+ // Create two records on the left hand.
+ keyrecord_t record_l0 = make_record(0, 0);
+ keyrecord_t record_l1 = make_record(1, 0);
+ // Create a record on the right hand.
+ keyrecord_t record_r = make_record(0, MATRIX_COLS - 1);
+
+ // Function should return true when records are on opposite hands.
+ EXPECT_TRUE(get_chordal_hold_default(&record_l0, &record_r));
+ EXPECT_TRUE(get_chordal_hold_default(&record_r, &record_l0));
+ // ... and false when on the same hand.
+ EXPECT_FALSE(get_chordal_hold_default(&record_l0, &record_l1));
+ EXPECT_FALSE(get_chordal_hold_default(&record_l1, &record_l0));
+ // But (2, 0) has handedness '*', for which true is returned for chords
+ // with either hand.
+ keyrecord_t record_l2 = make_record(2, 0);
+ EXPECT_TRUE(get_chordal_hold_default(&record_l2, &record_l0));
+ EXPECT_TRUE(get_chordal_hold_default(&record_l2, &record_r));
+
+ // Create a record resulting from a combo.
+ keyrecord_t record_combo = make_record(0, 0, COMBO_EVENT);
+ // Function returns true in all cases.
+ EXPECT_TRUE(get_chordal_hold_default(&record_l0, &record_combo));
+ EXPECT_TRUE(get_chordal_hold_default(&record_r, &record_combo));
+ EXPECT_TRUE(get_chordal_hold_default(&record_combo, &record_l0));
+ EXPECT_TRUE(get_chordal_hold_default(&record_combo, &record_r));
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, chord_nested_press_settled_as_hold) {
+ TestDriver driver;
+ InSequence s;
+ // Mod-tap key on the left hand.
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+ // Regular key on the right hand.
+ auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
+
+ set_keymap({mod_tap_key, regular_key});
+
+ // Press mod-tap key.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap key.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, chord_rolled_press_settled_as_tap) {
+ TestDriver driver;
+ InSequence s;
+ // Mod-tap key on the left hand.
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+ // Regular key on the right hand.
+ auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
+
+ set_keymap({mod_tap_key, regular_key});
+
+ // Press mod-tap key and regular key.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key.press();
+ run_one_scan_loop();
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap key.
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_REPORT(driver, (KC_P, KC_A));
+ EXPECT_REPORT(driver, (KC_A));
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release regular key.
+ EXPECT_EMPTY_REPORT(driver);
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, non_chord_with_mod_tap_settled_as_tap) {
+ TestDriver driver;
+ InSequence s;
+ // Mod-tap key and regular key both on the left hand.
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+ auto regular_key = KeymapKey(0, 2, 0, KC_A);
+
+ set_keymap({mod_tap_key, regular_key});
+
+ // Press mod-tap-hold key.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press regular key.
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_REPORT(driver, (KC_P, KC_A));
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release regular key.
+ EXPECT_REPORT(driver, (KC_P));
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap-hold key.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, tap_mod_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM - 1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, hold_mod_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, two_mod_taps_same_hand_hold_til_timeout) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, MATRIX_COLS - 2, 0, RCTL_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_B));
+
+ set_keymap({mod_tap_key1, mod_tap_key2});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Continue holding til the tapping term.
+ EXPECT_REPORT(driver, (KC_RIGHT_CTRL));
+ EXPECT_REPORT(driver, (KC_RIGHT_CTRL, KC_RIGHT_SHIFT));
+ idle_for(TAPPING_TERM);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap keys.
+ EXPECT_REPORT(driver, (KC_RIGHT_SHIFT));
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, two_mod_taps_nested_press_opposite_hands) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_B));
+
+ set_keymap({mod_tap_key1, mod_tap_key2});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap keys.
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, two_mod_taps_nested_press_same_hand) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, RSFT_T(KC_B));
+
+ set_keymap({mod_tap_key1, mod_tap_key2});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap keys.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_A));
+ mod_tap_key2.release();
+ run_one_scan_loop();
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, three_mod_taps_same_hand_streak_roll) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B));
+ auto mod_tap_key3 = KeymapKey(0, 3, 0, RSFT_T(KC_C));
+
+ set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys 1, 2, 3.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_A, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, three_mod_taps_same_hand_streak_orders) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B));
+ auto mod_tap_key3 = KeymapKey(0, 3, 0, RSFT_T(KC_C));
+
+ set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys 3, 2, 1.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_A, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_A));
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys 3, 1, 2.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_A, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_B));
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys 2, 3, 1.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_A, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_A, KC_C));
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, three_mod_taps_opposite_hands_roll) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B));
+ auto mod_tap_key3 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_C));
+
+ set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys 1, 2, 3.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_A, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, three_mod_taps_two_left_one_right) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B));
+ auto mod_tap_key3 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_C));
+
+ set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release key 3.
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL, KC_C));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL));
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release key 2, then key 1.
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release key 3.
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL, KC_C));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL));
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release key 1, then key 2.
+ EXPECT_REPORT(driver, (KC_LEFT_CTRL));
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, three_mod_taps_one_held_two_tapped) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 2, 0, CTL_T(KC_B));
+ auto mod_tap_key3 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_C));
+
+ set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys 3, 2, 1.
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B));
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys 3, 1, 2.
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B));
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_B));
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, two_mod_taps_one_regular_key) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 2, 0, CTL_T(KC_B));
+ auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_C);
+
+ set_keymap({mod_tap_key1, mod_tap_key2, regular_key});
+
+ // Press keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys.
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_C));
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys.
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B));
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_B));
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, tap_regular_key_while_layer_tap_key_is_held) {
+ TestDriver driver;
+ InSequence s;
+ auto layer_tap_hold_key = KeymapKey(0, 1, 0, LT(1, KC_P));
+ auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
+ auto no_key = KeymapKey(1, 1, 0, XXXXXXX);
+ auto layer_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_B);
+
+ set_keymap({layer_tap_hold_key, regular_key, no_key, layer_key});
+
+ // Press layer-tap-hold key.
+ EXPECT_NO_REPORT(driver);
+ layer_tap_hold_key.press();
+ run_one_scan_loop();
+ // Press regular key.
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release regular key.
+ EXPECT_REPORT(driver, (KC_B));
+ EXPECT_EMPTY_REPORT(driver);
+ regular_key.release();
+ run_one_scan_loop();
+ EXPECT_EQ(layer_state, 2);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release layer-tap-hold key.
+ EXPECT_NO_REPORT(driver);
+ layer_tap_hold_key.release();
+ run_one_scan_loop();
+ EXPECT_EQ(layer_state, 0);
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, nested_tap_of_layer_0_layer_tap_keys) {
+ TestDriver driver;
+ InSequence s;
+ // The keys are layer-taps on layer 2 but regular keys on layer 1.
+ auto first_layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_A));
+ auto second_layer_tap_key = KeymapKey(0, MATRIX_COLS - 1, 0, LT(1, KC_P));
+ auto first_key_on_layer = KeymapKey(1, 1, 0, KC_B);
+ auto second_key_on_layer = KeymapKey(1, MATRIX_COLS - 1, 0, KC_Q);
+
+ set_keymap({first_layer_tap_key, second_layer_tap_key, first_key_on_layer, second_key_on_layer});
+
+ // Press first layer-tap key.
+ EXPECT_NO_REPORT(driver);
+ first_layer_tap_key.press();
+ run_one_scan_loop();
+ // Press second layer-tap key.
+ second_layer_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release second layer-tap key.
+ EXPECT_REPORT(driver, (KC_Q));
+ EXPECT_EMPTY_REPORT(driver);
+ second_layer_tap_key.release();
+ run_one_scan_loop();
+ EXPECT_EQ(layer_state, 2);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release first layer-tap key.
+ EXPECT_NO_REPORT(driver);
+ first_layer_tap_key.release();
+ run_one_scan_loop();
+ EXPECT_EQ(layer_state, 0);
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, lt_mt_one_regular_key) {
+ TestDriver driver;
+ InSequence s;
+ auto lt_key = KeymapKey(0, 1, 0, LT(1, KC_A));
+ auto mt_key0 = KeymapKey(0, 2, 0, SFT_T(KC_B));
+ auto mt_key1 = KeymapKey(1, 2, 0, CTL_T(KC_C));
+ auto regular_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_X);
+ auto no_key0 = KeymapKey(0, MATRIX_COLS - 1, 0, XXXXXXX);
+ auto no_key1 = KeymapKey(1, 1, 0, XXXXXXX);
+
+ set_keymap({lt_key, mt_key0, mt_key1, regular_key, no_key0, no_key1});
+
+ // Press LT, MT, and regular key.
+ EXPECT_NO_REPORT(driver);
+ lt_key.press();
+ run_one_scan_loop();
+ mt_key1.press();
+ run_one_scan_loop();
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release the regular key.
+ EXPECT_REPORT(driver, (KC_LCTL));
+ EXPECT_REPORT(driver, (KC_LCTL, KC_X));
+ EXPECT_REPORT(driver, (KC_LCTL));
+ regular_key.release();
+ run_one_scan_loop();
+ EXPECT_EQ(get_mods(), MOD_BIT_LCTRL);
+ EXPECT_EQ(layer_state, 2);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release MT key.
+ EXPECT_EMPTY_REPORT(driver);
+ mt_key1.release();
+ run_one_scan_loop();
+ EXPECT_EQ(get_mods(), 0);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release LT key.
+ EXPECT_NO_REPORT(driver);
+ lt_key.release();
+ run_one_scan_loop();
+ EXPECT_EQ(layer_state, 0);
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, nested_tap_of_layer_tap_keys) {
+ TestDriver driver;
+ InSequence s;
+ // The keys are layer-taps on all layers.
+ auto first_key_layer_0 = KeymapKey(0, 1, 0, LT(1, KC_A));
+ auto second_key_layer_0 = KeymapKey(0, MATRIX_COLS - 1, 0, LT(1, KC_P));
+ auto first_key_layer_1 = KeymapKey(1, 1, 0, LT(2, KC_B));
+ auto second_key_layer_1 = KeymapKey(1, MATRIX_COLS - 1, 0, LT(2, KC_Q));
+ auto first_key_layer_2 = KeymapKey(2, 1, 0, KC_TRNS);
+ auto second_key_layer_2 = KeymapKey(2, MATRIX_COLS - 1, 0, KC_TRNS);
+
+ set_keymap({first_key_layer_0, second_key_layer_0, first_key_layer_1, second_key_layer_1, first_key_layer_2, second_key_layer_2});
+
+ // Press first layer-tap key.
+ EXPECT_NO_REPORT(driver);
+ first_key_layer_0.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press second layer-tap key.
+ EXPECT_NO_REPORT(driver);
+ second_key_layer_0.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release second layer-tap key.
+ EXPECT_REPORT(driver, (KC_Q));
+ EXPECT_EMPTY_REPORT(driver);
+ second_key_layer_0.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release first layer-tap key.
+ EXPECT_NO_REPORT(driver);
+ first_key_layer_0.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, roll_layer_tap_key_with_regular_key) {
+ TestDriver driver;
+ InSequence s;
+
+ auto layer_tap_hold_key = KeymapKey(0, 1, 0, LT(1, KC_P));
+ auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
+ auto layer_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_B);
+
+ set_keymap({layer_tap_hold_key, regular_key, layer_key});
+
+ // Press layer-tap-hold key.
+ EXPECT_NO_REPORT(driver);
+ layer_tap_hold_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press regular key.
+ EXPECT_NO_REPORT(driver);
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release layer-tap-hold key.
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_REPORT(driver, (KC_P, KC_A));
+ EXPECT_REPORT(driver, (KC_A));
+ layer_tap_hold_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release regular key.
+ EXPECT_EMPTY_REPORT(driver);
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldPermissiveHoldFlowTap, two_mod_tap_keys_stuttered_press) {
+ TestDriver driver;
+ InSequence s;
+
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, LSFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, LCTL_T(KC_B));
+
+ set_keymap({mod_tap_key1, mod_tap_key2});
+
+ // Hold first mod-tap key until the tapping term.
+ EXPECT_REPORT(driver, (KC_LSFT));
+ mod_tap_key1.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press the second mod-tap key, then quickly release and press the first.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_LSFT, KC_B));
+ EXPECT_REPORT(driver, (KC_B));
+ EXPECT_REPORT(driver, (KC_B, KC_A));
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ EXPECT_EQ(get_mods(), 0); // Verify that Shift was released.
+ VERIFY_AND_CLEAR(driver);
+
+ // Release both keys.
+ EXPECT_REPORT(driver, (KC_A));
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
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..d6f385d8d4
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/config.h
@@ -0,0 +1,23 @@
+/* Copyright 2022 Vladislav Kucheriavykh
+ * Copyright 2025 Google LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include "test_common.h"
+
+#define FLOW_TAP_TERM 150
+#define PERMISSIVE_HOLD
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 @@
+# Copyright 2022 Vladislav Kucheriavykh
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+COMBO_ENABLE = yes
+
+INTROSPECTION_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 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "quantum.h"
+
+uint16_t const mt_lt_combo[] = {SFT_T(KC_X), LT(1, KC_Y), COMBO_END};
+
+// clang-format off
+combo_t key_combos[] = {
+ COMBO(mt_lt_combo, KC_Z),
+};
+// 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..d419a3b313
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp
@@ -0,0 +1,844 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "action_tapping.h"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using testing::_;
+using testing::AnyNumber;
+using testing::InSequence;
+
+class FlowTapTest : public TestFixture {};
+
+// Test an input of quick distinct taps. All should be settled as tapped.
+TEST_F(FlowTapTest, distinct_taps) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_B));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_C));
+ auto mod_tap_key3 = KeymapKey(0, 3, 0, ALT_T(KC_D));
+
+ set_keymap({regular_key, mod_tap_key1, mod_tap_key2, mod_tap_key3});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key, FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap mod-tap 1.
+ EXPECT_REPORT(driver, (KC_B));
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap mod-tap 2.
+ EXPECT_REPORT(driver, (KC_C));
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap mod-tap 3.
+ EXPECT_REPORT(driver, (KC_D));
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ mod_tap_key3.release();
+ idle_for(FLOW_TAP_TERM + 1); // Pause between taps.
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap mod-tap 1.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_B));
+ EXPECT_EMPTY_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap mod-tap 2.
+ EXPECT_REPORT(driver, (KC_C));
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ mod_tap_key2.release();
+ idle_for(FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+}
+
+// By default, Flow Tap is disabled when mods other than Shift and AltGr are on.
+TEST_F(FlowTapTest, hotkey_taps) {
+ TestDriver driver;
+ InSequence s;
+ auto ctrl_key = KeymapKey(0, 0, 0, KC_LCTL);
+ auto shft_key = KeymapKey(0, 1, 0, KC_LSFT);
+ auto alt_key = KeymapKey(0, 2, 0, KC_LALT);
+ auto gui_key = KeymapKey(0, 3, 0, KC_LGUI);
+ auto regular_key = KeymapKey(0, 4, 0, KC_A);
+ auto mod_tap_key = KeymapKey(0, 5, 0, RCTL_T(KC_B));
+
+ set_keymap({ctrl_key, shft_key, alt_key, gui_key, regular_key, mod_tap_key});
+
+ for (KeymapKey* mod_key : {&ctrl_key, &alt_key, &gui_key}) {
+ // Hold mod key.
+ EXPECT_REPORT(driver, (mod_key->code));
+ mod_key->press();
+ run_one_scan_loop();
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (mod_key->code, KC_A));
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (mod_key->code));
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap, where Flow Tap is disabled due to the held mod.
+ EXPECT_REPORT(driver, (mod_key->code, KC_RCTL));
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap.
+ EXPECT_REPORT(driver, (mod_key->code));
+ mod_tap_key.release();
+ run_one_scan_loop();
+
+ // Release mod key.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_key->release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+ }
+
+ // Hold Shift key.
+ EXPECT_REPORT(driver, (KC_LSFT));
+ shft_key.press();
+ run_one_scan_loop();
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_LSFT));
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap, where Flow Tap applies to settle as tapped.
+ EXPECT_REPORT(driver, (KC_LSFT, KC_B));
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap.
+ EXPECT_REPORT(driver, (KC_LSFT));
+ mod_tap_key.release();
+ run_one_scan_loop();
+
+ // Release Shift key.
+ EXPECT_EMPTY_REPORT(driver);
+ shft_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+// Test input with two mod-taps in a rolled press quickly after a regular key.
+TEST_F(FlowTapTest, rolled_press) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_B));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_C));
+
+ set_keymap({regular_key, mod_tap_key1, mod_tap_key2});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap key 1 quickly after regular key. The mod-tap should settle
+ // immediately as tapped, sending `KC_B`.
+ EXPECT_REPORT(driver, (KC_B));
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap key 2 quickly.
+ EXPECT_REPORT(driver, (KC_B, KC_C));
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Hold for longer than the tapping term.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap keys.
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, long_flow_tap_settled_as_held) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B));
+
+ set_keymap({regular_key, mod_tap_key});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap key.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Hold for the tapping term.
+ EXPECT_REPORT(driver, (KC_LSFT));
+ idle_for(TAPPING_TERM);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap key.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, holding_multiple_mod_taps) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_B));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_C));
+
+ set_keymap({regular_key, mod_tap_key1, mod_tap_key2});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ idle_for(TAPPING_TERM - 5); // Hold almost until tapping term.
+ VERIFY_AND_CLEAR(driver);
+
+ // Press regular key.
+ EXPECT_REPORT(driver, (KC_LSFT));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL, KC_A));
+ regular_key.press();
+ idle_for(10);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys.
+ EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
+ EXPECT_REPORT(driver, (KC_LCTL));
+ EXPECT_EMPTY_REPORT(driver);
+ regular_key.release();
+ run_one_scan_loop();
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, holding_mod_tap_with_regular_mod) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_key = KeymapKey(0, 1, 0, KC_LSFT);
+ auto mod_tap_key = KeymapKey(0, 2, 0, CTL_T(KC_C));
+
+ set_keymap({regular_key, mod_key, mod_tap_key});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod and mod-tap keys.
+ EXPECT_REPORT(driver, (KC_LSFT));
+ mod_key.press();
+ run_one_scan_loop();
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM - 5); // Hold almost until tapping term.
+ VERIFY_AND_CLEAR(driver);
+
+ // Press regular key.
+ EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL, KC_A));
+ regular_key.press();
+ idle_for(10);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys.
+ EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
+ EXPECT_REPORT(driver, (KC_LCTL));
+ EXPECT_EMPTY_REPORT(driver);
+ regular_key.release();
+ run_one_scan_loop();
+ mod_key.release();
+ run_one_scan_loop();
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, layer_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_B));
+ auto regular_key2 = KeymapKey(1, 0, 0, KC_C);
+
+ set_keymap({regular_key, layer_tap_key, regular_key2});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press layer-tap key, quickly after the regular key.
+ EXPECT_REPORT(driver, (KC_B));
+ layer_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release layer-tap key.
+ EXPECT_EMPTY_REPORT(driver);
+ layer_tap_key.release();
+ run_one_scan_loop();
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press layer-tap key, slowly after the regular key.
+ EXPECT_NO_REPORT(driver);
+ layer_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ EXPECT_EQ(layer_state, 1 << 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap regular key2.
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release layer-tap key.
+ EXPECT_NO_REPORT(driver);
+ layer_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, layer_tap_ignored_with_disabled_key) {
+ TestDriver driver;
+ InSequence s;
+ auto no_key = KeymapKey(0, 0, 0, KC_NO);
+ auto regular_key = KeymapKey(1, 0, 0, KC_ESC);
+ auto layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_A));
+ auto mod_tap_key = KeymapKey(0, 2, 0, CTL_T(KC_B));
+
+ set_keymap({no_key, regular_key, layer_tap_key, mod_tap_key});
+
+ EXPECT_REPORT(driver, (KC_ESC));
+ EXPECT_EMPTY_REPORT(driver);
+ layer_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ tap_key(regular_key);
+ layer_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_LCTL));
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, layer_tap_ignored_with_disabled_key_complex) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key1 = KeymapKey(0, 0, 0, KC_Q);
+ auto layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_SPC));
+ auto mod_tap_key1 = KeymapKey(0, 2, 0, CTL_T(KC_T));
+ // Place RALT_T(KC_I), where Flow Tap is enabled, in the same position on
+ // layer 0 as KC_RGHT, where Flow Tap is disabled. This tests that Flow Tap
+ // tracks the keycode from the correct layer.
+ auto mod_tap_key2 = KeymapKey(0, 3, 0, RALT_T(KC_I));
+ auto regular_key2 = KeymapKey(1, 3, 0, KC_RGHT);
+
+ set_keymap({regular_key1, layer_tap_key, mod_tap_key1, mod_tap_key2, regular_key2});
+
+ // Tap regular key 1.
+ EXPECT_REPORT(driver, (KC_Q));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key1);
+ idle_for(FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Hold layer-tap key.
+ EXPECT_NO_REPORT(driver);
+ layer_tap_key.press();
+ run_one_scan_loop();
+ // idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap regular key 2.
+ EXPECT_REPORT(driver, (KC_RGHT));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key2);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release layer-tap key.
+ EXPECT_NO_REPORT(driver);
+ layer_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Quickly hold mod-tap key 1.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_LCTL));
+ EXPECT_REPORT(driver, (KC_LCTL, KC_Q));
+ EXPECT_REPORT(driver, (KC_LCTL));
+ tap_key(regular_key1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, layer_tap_ignored_with_enabled_key) {
+ TestDriver driver;
+ InSequence s;
+ auto no_key = KeymapKey(0, 0, 0, KC_NO);
+ auto regular_key = KeymapKey(1, 0, 0, KC_C);
+ auto layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_A));
+ auto mod_tap_key = KeymapKey(0, 2, 0, CTL_T(KC_B));
+
+ set_keymap({no_key, regular_key, layer_tap_key, mod_tap_key});
+
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ layer_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ tap_key(regular_key);
+ layer_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_B));
+ mod_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, combo_key) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_X));
+ auto layer_tap_key = KeymapKey(0, 2, 0, LT(1, KC_Y));
+
+ set_keymap({regular_key, mod_tap_key, layer_tap_key});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press combo keys quickly after regular key.
+ EXPECT_REPORT(driver, (KC_Z));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_combo({mod_tap_key, layer_tap_key});
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap key quickly.
+ EXPECT_REPORT(driver, (KC_X));
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap key.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, oneshot_mod_key) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto osm_key = KeymapKey(0, 1, 0, OSM(MOD_LSFT));
+
+ set_keymap({regular_key, osm_key});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap OSM, tap regular key.
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
+ EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(osm_key);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Nested press of OSM and regular keys.
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
+ EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
+ EXPECT_EMPTY_REPORT(driver);
+ osm_key.press();
+ run_one_scan_loop();
+ tap_key(regular_key);
+ osm_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, quick_tap) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_A));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(mod_tap_key);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_A));
+ mod_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap key.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, rolling_mt_mt) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B));
+
+ set_keymap({mod_tap_key1, mod_tap_key2});
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_B));
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Hold for longer than the tapping term.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap keys.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, rolling_lt_mt_regular) {
+ TestDriver driver;
+ InSequence s;
+ auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
+ auto mod_tap_key = KeymapKey(0, 1, 0, CTL_T(KC_B));
+ auto regular_key = KeymapKey(0, 2, 0, KC_C);
+
+ set_keymap({layer_tap_key, mod_tap_key, regular_key});
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ layer_tap_key.press();
+ run_one_scan_loop();
+ mod_tap_key.press();
+ run_one_scan_loop();
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_A, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_B, KC_C));
+ layer_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Hold for longer than the tapping term.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap keys.
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, rolling_lt_regular_mt) {
+ TestDriver driver;
+ InSequence s;
+ auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
+ auto regular_key = KeymapKey(0, 1, 0, KC_B);
+ auto mod_tap_key = KeymapKey(0, 2, 0, CTL_T(KC_C));
+
+ set_keymap({layer_tap_key, regular_key, mod_tap_key});
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ layer_tap_key.press();
+ run_one_scan_loop();
+ regular_key.press();
+ run_one_scan_loop();
+ mod_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_A, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_B, KC_C));
+ layer_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Hold for longer than the tapping term.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap keys.
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ regular_key.release();
+ run_one_scan_loop();
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, rolling_mt_mt_mt) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 0, 0, CTL_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 1, 0, GUI_T(KC_B));
+ auto mod_tap_key3 = KeymapKey(0, 2, 0, ALT_T(KC_C));
+
+ set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release first mod-tap key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_A, KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_B, KC_C));
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Hold for longer than the tapping term.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release other mod-tap keys.
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, roll_release_132) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 0, 0, CTL_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 1, 0, GUI_T(KC_B));
+ auto mod_tap_key3 = KeymapKey(0, 2, 0, ALT_T(KC_C));
+
+ set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3});
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ idle_for(FLOW_TAP_TERM + 1);
+ mod_tap_key3.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release first mod-tap key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A, KC_B));
+ EXPECT_REPORT(driver, (KC_B));
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Release other mod-tap keys.
+ EXPECT_REPORT(driver, (KC_B, KC_C));
+ EXPECT_REPORT(driver, (KC_B));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key3.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}