diff options
-rw-r--r-- | data/mappings/info_config.hjson | 1 | ||||
-rw-r--r-- | docs/tap_hold.md | 83 | ||||
-rw-r--r-- | quantum/action.c | 17 | ||||
-rw-r--r-- | quantum/action.h | 6 | ||||
-rw-r--r-- | quantum/action_tapping.c | 138 | ||||
-rw-r--r-- | quantum/action_tapping.h | 57 | ||||
-rw-r--r-- | quantum/process_keycode/process_leader.c | 6 | ||||
-rw-r--r-- | tests/tap_hold_configurations/flow_tap/config.h | 22 | ||||
-rw-r--r-- | tests/tap_hold_configurations/flow_tap/test.mk | 18 | ||||
-rw-r--r-- | tests/tap_hold_configurations/flow_tap/test_keymap.c | 23 | ||||
-rw-r--r-- | tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp | 313 |
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 @@ // 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/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) { If `QUICK_TAP_TERM` is set higher than `TAPPING_TERM`, it will default to `TAPPING_TERM`. ::: +## Flow Tap + +Flow 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. + +Flow Tap is enabled by defining `FLOW_TAP_TERM` in your `config.h` with the desired timeout in milliseconds. A timeout of 150 ms is recommended as a starting point: + +```c +#define FLOW_TAP_TERM 150 +``` + +By default, Flow Tap is enabled when: + +* The tap-hold key is pressed within `FLOW_TAP_TERM` milliseconds of the previous key press. + +* The tapping keycodes of the previous key and tap-hold key are *both* among `KC_A`–`KC_Z`, `KC_COMM`, `KC_DOT`, `KC_SCLN`, `KC_SLSH` (the main alphas area of a conventional QWERTY layout) or `KC_SPC`. + +As 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. + + +### is_flow_tap_key() + +Optionally, 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. + +The default implementation of this callback is: + +```.c +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; +} +``` + +Copy 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. + +### get_flow_tap_term() + +Optionally, 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. + +The default implementation of this callback is + +```.c +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; +} +``` + +In 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: + +```c +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)) { + switch (keycode) { + case LCTL_T(KC_F): + case RCTL_T(KC_H): + return FLOW_TAP_TERM - 25; // Short timeout on these keys. + + default: + return FLOW_TAP_TERM; // Longer timeout otherwise. + } + } + return 0; // Disable Flow Tap. +} +``` + +::: tip If you define both `is_flow_tap_key()` and `get_flow_tap_term()`, then the latter takes precedence. +::: + ## Chordal Hold Chordal 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) { 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 7596688f31..7616486c6d 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..312c639169 100644 --- a/quantum/action_tapping.c +++ b/quantum/action_tapping.c @@ -4,6 +4,7 @@ #include "action.h" #include "action_layer.h" #include "action_tapping.h" +#include "action_util.h" #include "keycode.h" #include "timer.h" @@ -49,9 +50,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 +65,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 +89,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 +101,13 @@ __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 uint32_t last_input = 0; +static uint16_t prev_keycode = KC_NO; + +uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode); +# 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; @@ -147,6 +157,19 @@ void action_tapping_process(keyrecord_t record) { } } if (IS_EVENT(record.event)) { +# if defined(FLOW_TAP_TERM) + const uint16_t keycode = get_record_keycode(&record, false); + // Track the previous key press. + if (record.event.pressed) { + prev_keycode = keycode; + } + // If there is no unsettled tap-hold key, update last input time. Ignore + // mod keys in this update to allow for chording multiple mods for + // hotkeys like "Ctrl+Shift+arrow". + if (IS_NOEVENT(tapping_key.event) && !IS_MODIFIER_KEYCODE(keycode)) { + last_input = timer_read32(); + } +# endif // defined(FLOW_TAP_TERM) ac_dprintf("\n"); } } @@ -205,7 +228,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 +240,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 +250,27 @@ 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) + const uint16_t keycode = get_record_keycode(keyp, false); + if (is_mt_or_lt(keycode)) { + const uint32_t idle_time = timer_elapsed32(last_input); + uint16_t term = get_flow_tap_term(keycode, keyp, prev_keycode); + if (term > 500) { + term = 500; + } + if (idle_time < 500 && idle_time < term) { + debug_event(keyp->event); + ac_dprintf(" within flow tap term (%u < %u) considered a tap\n", (int16_t)idle_time, term); + keyp->tap.count = 1; + registered_taps_add(keyp->event.key); + debug_registered_taps(); + process_record(keyp); + return true; + } + } +# endif // defined(FLOW_TAP_TERM) + ac_dprintf("Tapping: Start(Press tap key).\n"); tapping_key = *keyp; process_record_tap_hint(&tapping_key); @@ -655,28 +699,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 +737,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 +808,35 @@ static void waiting_buffer_process_regular(void) { } # endif // CHORDAL_HOLD +# ifdef FLOW_TAP_TERM +// 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..2af000ad73 100644 --- a/quantum/action_tapping.h +++ b/quantum/action_tapping.h @@ -111,6 +111,63 @@ 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); +#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/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 @@ +/* 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "test_common.h" + +#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 @@ +# 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 <http://www.gnu.org/licenses/>. + +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..7816fcb6da --- /dev/null +++ b/tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp @@ -0,0 +1,313 @@ +// 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_F(FlowTapTest, short_flow_tap_settled_as_tapped) { + 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, 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, 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_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); +} |