diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index e4def1a4d72e..83f5b0b50eee 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -211,6 +211,7 @@ "PERMISSIVE_HOLD_PER_KEY": {"info_key": "tapping.permissive_hold_per_key", "value_type": "flag"}, "RETRO_TAPPING": {"info_key": "tapping.retro", "value_type": "flag"}, "RETRO_TAPPING_PER_KEY": {"info_key": "tapping.retro_per_key", "value_type": "flag"}, + "SPECULATIVE_HOLD": {"info_key": "tapping.speculative_hold", "value_type": "flag"}, "TAP_CODE_DELAY": {"info_key": "qmk.tap_keycode_delay", "value_type": "int"}, "TAP_HOLD_CAPS_DELAY": {"info_key": "qmk.tap_capslock_delay", "value_type": "int"}, "TAPPING_TERM": {"info_key": "tapping.term", "value_type": "int"}, diff --git a/quantum/action.c b/quantum/action.c index dd82c9ec99ff..aacafbe2ffbb 100644 --- a/quantum/action.c +++ b/quantum/action.c @@ -281,6 +281,11 @@ void process_record(keyrecord_t *record) { if (IS_NOEVENT(record->event)) { return; } +#ifdef SPECULATIVE_HOLD + if (record->event.pressed) { + speculative_key_settled(record); + } +#endif // SPECULATIVE_HOLD #ifdef FLOW_TAP_TERM flow_tap_update_last_event(record); #endif // FLOW_TAP_TERM diff --git a/quantum/action.h b/quantum/action.h index 7616486c6d83..a459c438c119 100644 --- a/quantum/action.h +++ b/quantum/action.h @@ -38,7 +38,7 @@ extern "C" { /* tapping count and state */ typedef struct { bool interrupted : 1; - bool reserved2 : 1; + bool speculated : 1; bool reserved1 : 1; bool reserved0 : 1; uint8_t count : 4; diff --git a/quantum/action_tapping.c b/quantum/action_tapping.c index b105cd60a916..2212e319c971 100644 --- a/quantum/action_tapping.c +++ b/quantum/action_tapping.c @@ -6,8 +6,10 @@ #include "action_tapping.h" #include "action_util.h" #include "keycode.h" +#include "keycode_config.h" #include "quantum_keycodes.h" #include "timer.h" +#include "wait.h" #ifndef NO_ACTION_TAPPING @@ -51,6 +53,21 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re } # endif +# ifdef SPECULATIVE_HOLD +typedef struct { + keypos_t key; + uint8_t mods; +} speculative_key_t; +# define SPECULATIVE_KEYS_SIZE 8 +static speculative_key_t speculative_keys[SPECULATIVE_KEYS_SIZE] = {}; +static uint8_t num_speculative_keys = 0; +static uint8_t prev_speculative_mods = 0; +static uint8_t speculative_mods = 0; + +/** Handler to be called on incoming press events. */ +static void speculative_key_press(keyrecord_t *record); +# endif // SPECULATIVE_HOLD + # 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. @@ -129,6 +146,13 @@ static void debug_waiting_buffer(void); * FIXME: Needs doc */ void action_tapping_process(keyrecord_t record) { +# ifdef SPECULATIVE_HOLD + prev_speculative_mods = speculative_mods; + if (record.event.pressed) { + speculative_key_press(&record); + } +# endif // SPECULATIVE_HOLD + if (process_tapping(&record)) { if (IS_EVENT(record.event)) { ac_dprintf("processed: "); @@ -145,6 +169,12 @@ void action_tapping_process(keyrecord_t record) { } } +# ifdef SPECULATIVE_HOLD + if (speculative_mods != prev_speculative_mods) { + send_keyboard_report(); + } +# endif // SPECULATIVE_HOLD + // process waiting_buffer if (IS_EVENT(record.event) && waiting_buffer_head != waiting_buffer_tail) { ac_dprintf("---- action_exec: process waiting_buffer -----\n"); @@ -708,6 +738,147 @@ void waiting_buffer_scan_tap(void) { } } +# ifdef SPECULATIVE_HOLD +static void debug_speculative_keys(void) { + ac_dprintf("mods = { "); + for (int8_t i = 0; i < num_speculative_keys; ++i) { + ac_dprintf("%02X ", speculative_keys[i].mods); + } + ac_dprintf("}, keys = { "); + for (int8_t i = 0; i < num_speculative_keys; ++i) { + ac_dprintf("%02X%02X ", speculative_keys[i].key.row, speculative_keys[i].key.col); + } + ac_dprintf("}\n"); +} + +// Find key in speculative_keys. Returns num_speculative_keys if not found. +static int8_t speculative_keys_find(keypos_t key) { + uint8_t i; + for (i = 0; i < num_speculative_keys; ++i) { + if (KEYEQ(speculative_keys[i].key, key)) { + break; + } + } + return i; +} + +static void speculative_key_press(keyrecord_t *record) { + if (num_speculative_keys >= SPECULATIVE_KEYS_SIZE) { // Overflow! + ac_dprintf("SPECULATIVE KEYS OVERFLOW: IGNORING EVENT\n"); + return; // Don't trigger: speculative_keys is full. + } + if (speculative_keys_find(record->event.key) < num_speculative_keys) { + return; // Don't trigger: key is already in speculative_keys. + } + + const uint16_t keycode = get_record_keycode(record, false); + if (!IS_QK_MOD_TAP(keycode)) { + return; // Don't trigger: not a mod-tap key. + } + + uint8_t mods = mod_config(QK_MOD_TAP_GET_MODS(keycode)); + if ((mods & 0x10) != 0) { // Unpack 5-bit mods to 8-bit representation. + mods <<= 4; + } + if ((~(get_mods() | speculative_mods) & mods) == 0) { + return; // Don't trigger: mods are already active. + } + + // Don't do Speculative Hold when there are non-speculated buffered events, + // since that could result in sending keys out of order. + for (uint8_t i = waiting_buffer_tail; i != waiting_buffer_head; i = (i + 1) % WAITING_BUFFER_SIZE) { + if (!waiting_buffer[i].tap.speculated) { + return; + } + } + + if (get_speculative_hold(keycode, record)) { + record->tap.speculated = true; + speculative_mods |= mods; + // Remember the keypos and mods associated with this key. + speculative_keys[num_speculative_keys] = (speculative_key_t){ + .key = record->event.key, + .mods = mods, + }; + ++num_speculative_keys; + + ac_dprintf("Speculative Hold: "); + debug_speculative_keys(); + } +} + +uint8_t get_speculative_mods(void) { + return speculative_mods; +} + +__attribute__((weak)) bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) { + const uint8_t mods = mod_config(QK_MOD_TAP_GET_MODS(keycode)); + return (mods & (MOD_LCTL | MOD_LSFT)) == (mods & (MOD_HYPR)); +} + +void speculative_key_settled(keyrecord_t *record) { + if (num_speculative_keys == 0) { + return; // Early return when there are no active speculative keys. + } + + uint8_t i = speculative_keys_find(record->event.key); + + const uint16_t keycode = get_record_keycode(record, false); + if (IS_QK_MOD_TAP(keycode) && record->tap.count == 0) { // MT hold press. + if (i < num_speculative_keys) { + --num_speculative_keys; + const uint8_t cleared_mods = speculative_keys[i].mods; + + if (num_speculative_keys) { + speculative_mods &= ~cleared_mods; + // Don't call send_keyboard_report() here; allow default + // handling to reapply the mod before the next report. + + // Remove the ith entry from speculative_keys. + for (uint8_t j = i; j < num_speculative_keys; ++j) { + speculative_keys[j] = speculative_keys[j + 1]; + } + } else { + speculative_mods = 0; + } + + ac_dprintf("Speculative Hold: settled %02x, ", cleared_mods); + debug_speculative_keys(); + } + } else { // Tap press event; cancel speculatively-held mod. + if (i >= num_speculative_keys) { + i = 0; + } + + // Clear mods for the ith key and all keys that follow. + uint8_t cleared_mods = 0; + for (uint8_t j = i; j < num_speculative_keys; ++j) { + cleared_mods |= speculative_keys[j].mods; + } + + num_speculative_keys = i; // Remove ith and following entries. + + if ((prev_speculative_mods & cleared_mods) != 0) { +# ifdef DUMMY_MOD_NEUTRALIZER_KEYCODE + neutralize_flashing_modifiers(get_mods() | prev_speculative_mods); +# endif // DUMMY_MOD_NEUTRALIZER_KEYCODE + } + + if (num_speculative_keys) { + speculative_mods &= ~cleared_mods; + } else { + speculative_mods = 0; + } + + send_keyboard_report(); + wait_ms(TAP_CODE_DELAY); + + ac_dprintf("Speculative Hold: canceled %02x, ", cleared_mods); + debug_speculative_keys(); + } +} +# endif // SPECULATIVE_HOLD + # if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM) static void registered_taps_add(keypos_t key) { if (num_registered_taps >= REGISTERED_TAPS_SIZE) { diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h index 0cf4aa12003d..bcb0eb89ba03 100644 --- a/quantum/action_tapping.h +++ b/quantum/action_tapping.h @@ -46,6 +46,36 @@ bool get_permissive_hold(uint16_t keycode, keyrecord_t *record); bool get_retro_tapping(uint16_t keycode, keyrecord_t *record); bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record); +#ifdef SPECULATIVE_HOLD +/** Gets the currently active speculative mods. */ +uint8_t get_speculative_mods(void); + +/** + * Callback to say if a mod-tap key may be speculatively held. + * + * By default, speculative hold is enabled for mod-tap keys where the mod is + * Ctrl, Shift, and Ctrl+Shift for either hand. + * + * @param keycode Keycode of the mod-tap key. + * @param record Record associated with the mod-tap press event. + * @return True if the mod-tap key may be speculatively held. + */ +bool get_speculative_hold(uint16_t keycode, keyrecord_t *record); + +/** + * Handler to be called on press events after tap-holds are settled. + * + * This function is to be called in process_record() in action.c, that is, just + * after tap-hold events are settled as either tapped or held. When `record` + * corresponds to a speculatively-held key, the speculative mod is cleared. + * + * @param record Record associated with the mod-tap press event. + */ +void speculative_key_settled(keyrecord_t *record); +#else +# define get_speculative_mods() 0 +#endif // SPECULATIVE_HOLD + #ifdef CHORDAL_HOLD /** * Callback to say when a key chord before the tapping term may be held. diff --git a/quantum/action_util.c b/quantum/action_util.c index e821e113ef39..7ee02140786c 100644 --- a/quantum/action_util.c +++ b/quantum/action_util.c @@ -19,6 +19,7 @@ along with this program. If not, see . #include "debug.h" #include "action_util.h" #include "action_layer.h" +#include "action_tapping.h" #include "timer.h" #include "keycode_config.h" #include @@ -273,6 +274,10 @@ static uint8_t get_mods_for_report(void) { } #endif +#ifdef SPECULATIVE_HOLD + mods |= get_speculative_mods(); +#endif + #ifdef KEY_OVERRIDE_ENABLE // These need to be last to be able to properly control key overrides mods &= ~suppressed_mods; diff --git a/tests/tap_hold_configurations/speculative_hold/all_mods/config.h b/tests/tap_hold_configurations/speculative_hold/all_mods/config.h new file mode 100644 index 000000000000..e4bcba13c1d8 --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/all_mods/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 SPECULATIVE_HOLD +#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24 diff --git a/tests/tap_hold_configurations/speculative_hold/all_mods/test.mk b/tests/tap_hold_configurations/speculative_hold/all_mods/test.mk new file mode 100644 index 000000000000..1765122512bb --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/all_mods/test.mk @@ -0,0 +1,18 @@ +# 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 . + +MAGIC_ENABLE = yes + diff --git a/tests/tap_hold_configurations/speculative_hold/all_mods/test_tap_hold.cpp b/tests/tap_hold_configurations/speculative_hold/all_mods/test_tap_hold.cpp new file mode 100644 index 000000000000..764b97ddde8c --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/all_mods/test_tap_hold.cpp @@ -0,0 +1,352 @@ +// 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 + +#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; + +namespace { + +// Gets the unpacked 8-bit mods corresponding to a given mod-tap keycode. +uint8_t unpack_mod_tap_mods(uint16_t keycode) { + const uint8_t mods5 = QK_MOD_TAP_GET_MODS(keycode); + return (mods5 & 0x10) != 0 ? (mods5 << 4) : mods5; +} + +bool get_speculative_hold_all_mods(uint16_t keycode, keyrecord_t *record) { + return true; // Enable Speculative Hold for all mod-tap keys. +} + +// Indirection so that get_speculative_hold() can be +// replaced with other functions in the test cases below. +std::function get_speculative_hold_fun = get_speculative_hold_all_mods; + +extern "C" bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) { + return get_speculative_hold_fun(keycode, record); +} + +class SpeculativeHoldAllMods : public TestFixture { + public: + void SetUp() override { + get_speculative_hold_fun = get_speculative_hold_all_mods; + } +}; + +TEST_F(SpeculativeHoldAllMods, tap_mod_tap_neutralized) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, GUI_T(KC_P)); + + set_keymap({mod_tap_key}); + + // Press mod-tap-hold key. Mod is held speculatively. + EXPECT_REPORT(driver, (KC_LGUI)); + mod_tap_key.press(); + idle_for(10); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. Speculative mod is neutralized and canceled. + EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE)); + EXPECT_REPORT(driver, (KC_LGUI)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Idle for tapping term of mod tap hold key. + idle_for(TAPPING_TERM - 10); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldAllMods, hold_two_mod_taps) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, LCTL_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, RALT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + // Press first mod-tap key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + EXPECT_EQ(get_speculative_mods(), MOD_BIT_LCTRL); + + // Press second mod-tap key. + EXPECT_REPORT(driver, (KC_LCTL, KC_RALT)); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + EXPECT_EQ(get_speculative_mods(), MOD_BIT_LCTRL | MOD_BIT_RALT); + + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), MOD_BIT_LCTRL | MOD_BIT_RALT); + + // Release first mod-tap key. + EXPECT_REPORT(driver, (KC_RALT)); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release second mod-tap key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldAllMods, two_mod_taps_same_mods) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, GUI_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, GUI_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + // Press first mod-tap key. + EXPECT_REPORT(driver, (KC_LGUI)); + mod_tap_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Tap second mod-tap key. + EXPECT_NO_REPORT(driver); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release first mod-tap key. + EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE)); + EXPECT_REPORT(driver, (KC_LGUI)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldAllMods, respects_get_speculative_hold_callback) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 0, 0, LSFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 1, 0, LSFT_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, 2, 0, LCTL_T(KC_C)); + auto mod_tap_key4 = KeymapKey(0, 3, 0, LCTL_T(KC_D)); + auto mod_tap_key5 = KeymapKey(0, 4, 0, RSFT_T(KC_E)); + auto mod_tap_key6 = KeymapKey(0, 5, 0, RSFT_T(KC_F)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3, mod_tap_key4, mod_tap_key5, mod_tap_key6}); + + // Enable Speculative Hold selectively for some of the keys. + get_speculative_hold_fun = [](uint16_t keycode, keyrecord_t *record) { + switch (keycode) { + case LSFT_T(KC_B): + case LCTL_T(KC_D): + case RSFT_T(KC_F): + return true; + } + return false; + }; + + for (KeymapKey *mod_tap_key : {&mod_tap_key2, &mod_tap_key4, &mod_tap_key6}) { + SCOPED_TRACE(std::string("mod_tap_key = ") + mod_tap_key->name); + const uint8_t mods = unpack_mod_tap_mods(mod_tap_key->code); + + // Long press and release mod_tap_key. + // For these keys where Speculative Hold is enabled, then the mod should + // activate immediately on keydown. + EXPECT_REPORT(driver, (KC_LCTL + biton(mods))); + mod_tap_key->press(); + run_one_scan_loop(); + EXPECT_EQ(get_speculative_mods(), mods); + EXPECT_EQ(get_mods(), 0); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM + 1); + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), mods); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key->release(); + idle_for(TAPPING_TERM + 1); + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), 0); + VERIFY_AND_CLEAR(driver); + } + + for (KeymapKey *mod_tap_key : {&mod_tap_key1, &mod_tap_key3, &mod_tap_key5}) { + SCOPED_TRACE(std::string("mod_tap_key = ") + mod_tap_key->name); + const uint8_t mods = unpack_mod_tap_mods(mod_tap_key->code); + + // Long press and release mod_tap_key. + // For these keys where Speculative Hold is disabled, the mod should + // activate when the key has settled after the tapping term. + EXPECT_NO_REPORT(driver); + mod_tap_key->press(); + run_one_scan_loop(); + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), 0); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LCTL + biton(mods))); + idle_for(TAPPING_TERM + 1); + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), mods); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key->release(); + idle_for(TAPPING_TERM + 1); + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), 0); + VERIFY_AND_CLEAR(driver); + } +} + +TEST_F(SpeculativeHoldAllMods, respects_magic_mod_config) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, CTL_T(KC_P)); + + set_keymap({mod_tap_key}); + + keymap_config.swap_lctl_lgui = true; + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LGUI)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE)); + EXPECT_REPORT(driver, (KC_LGUI)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + keymap_config.swap_lctl_lgui = false; + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldAllMods, tap_a_mod_tap_key_while_another_mod_tap_key_is_held) { + TestDriver driver; + InSequence s; + auto first_mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + auto second_mod_tap_key = KeymapKey(0, 2, 0, RSFT_T(KC_A)); + + set_keymap({first_mod_tap_key, second_mod_tap_key}); + + // Press first mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LSFT)); + first_mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press second tap-hold key. + EXPECT_REPORT(driver, (KC_LSFT, KC_RSFT)); + second_mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release second tap-hold key. + EXPECT_NO_REPORT(driver); + second_mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release first mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + first_mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldAllMods, tap_mod_tap_key_two_times) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap-hold key again. + EXPECT_REPORT(driver, (KC_P)); + mod_tap_key.press(); + idle_for(TAPPING_TERM); + 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); +} + +} // namespace diff --git a/tests/tap_hold_configurations/speculative_hold/default/config.h b/tests/tap_hold_configurations/speculative_hold/default/config.h new file mode 100644 index 000000000000..9891296f4276 --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/default/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 . + */ + +#pragma once + +#include "test_common.h" + +#define SPECULATIVE_HOLD diff --git a/tests/tap_hold_configurations/speculative_hold/default/test.mk b/tests/tap_hold_configurations/speculative_hold/default/test.mk new file mode 100644 index 000000000000..f5decaeb7875 --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/default/test.mk @@ -0,0 +1,20 @@ +# 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 . + +KEY_OVERRIDE_ENABLE = yes + +INTROSPECTION_KEYMAP_C = test_keymap.c + diff --git a/tests/tap_hold_configurations/speculative_hold/default/test_keymap.c b/tests/tap_hold_configurations/speculative_hold/default/test_keymap.c new file mode 100644 index 000000000000..db65374618dc --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/default/test_keymap.c @@ -0,0 +1,20 @@ +// 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" + +// Shift + Esc = Home +const key_override_t home_esc_override = ko_make_basic(MOD_MASK_SHIFT, KC_ESC, KC_HOME); + +const key_override_t *key_overrides[] = {&home_esc_override}; diff --git a/tests/tap_hold_configurations/speculative_hold/default/test_tap_hold.cpp b/tests/tap_hold_configurations/speculative_hold/default/test_tap_hold.cpp new file mode 100644 index 000000000000..bfa022be1122 --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/default/test_tap_hold.cpp @@ -0,0 +1,535 @@ +// 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 + +#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; + +namespace { + +bool process_record_user_default(uint16_t keycode, keyrecord_t *record) { + return true; +} + +// Indirection so that process_record_user() can be +// replaced with other functions in the test cases below. +std::function process_record_user_fun = process_record_user_default; + +extern "C" bool process_record_user(uint16_t keycode, keyrecord_t *record) { + return process_record_user_fun(keycode, record); +} + +class SpeculativeHoldDefault : public TestFixture { + public: + void SetUp() override { + process_record_user_fun = process_record_user_default; + } +}; + +TEST_F(SpeculativeHoldDefault, get_speculative_hold) { + keyrecord_t record = {}; + + // With the default definition of get_speculative_hold(), Speculative Hold + // is enabled for Ctrl and Shift. + EXPECT_TRUE(get_speculative_hold(MT(MOD_LCTL, KC_NO), &record)); + EXPECT_TRUE(get_speculative_hold(MT(MOD_LSFT, KC_NO), &record)); + EXPECT_TRUE(get_speculative_hold(MT(MOD_LCTL | MOD_LSFT, KC_NO), &record)); + EXPECT_TRUE(get_speculative_hold(MT(MOD_RCTL, KC_NO), &record)); + EXPECT_TRUE(get_speculative_hold(MT(MOD_RSFT, KC_NO), &record)); + EXPECT_TRUE(get_speculative_hold(MT(MOD_RCTL | MOD_RSFT, KC_NO), &record)); + + EXPECT_FALSE(get_speculative_hold(MT(MOD_LALT, KC_NO), &record)); + EXPECT_FALSE(get_speculative_hold(MT(MOD_LGUI, KC_NO), &record)); + EXPECT_FALSE(get_speculative_hold(MT(MOD_RALT, KC_NO), &record)); + EXPECT_FALSE(get_speculative_hold(MT(MOD_RGUI, KC_NO), &record)); + EXPECT_FALSE(get_speculative_hold(MT(MOD_MEH, KC_NO), &record)); + EXPECT_FALSE(get_speculative_hold(MT(MOD_HYPR, KC_NO), &record)); +} + +TEST_F(SpeculativeHoldDefault, tap_mod_tap) { + TestDriver driver; + InSequence s; + static int process_record_user_calls = 0; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + process_record_user_fun = [](uint16_t keycode, keyrecord_t *record) { + ++process_record_user_calls; + return true; + }; + + // Press mod-tap-hold key. Mod is held speculatively. + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + idle_for(10); + VERIFY_AND_CLEAR(driver); + EXPECT_EQ(get_speculative_mods(), MOD_BIT_LSHIFT); + // Speculative mod holds and releases are made directly, bypassing regular + // event processing. No calls have been made yet to process_record_user(). + EXPECT_EQ(process_record_user_calls, 0); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); // Speculative mod canceled. + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), 0); + // Two calls have now been made, for pressing and releasing KC_P. + EXPECT_EQ(process_record_user_calls, 2); + + // Idle for tapping term of mod tap hold key. + idle_for(TAPPING_TERM - 10); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldDefault, key_overrides) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, LSFT_T(KC_A)); + auto esc_key = KeymapKey(0, 3, 0, KC_ESC); + + set_keymap({mod_tap_key, esc_key}); + + // Press mod-tap Shift key. + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press Esc key. + EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_HOME)); + esc_key.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + // Release Esc key. + EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LSFT)); + esc_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap Shift key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldDefault, tap_regular_key_while_mod_tap_key_is_held) { + TestDriver driver; + InSequence s; + 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_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Tap regular key. + EXPECT_NO_REPORT(driver); + regular_key.press(); + run_one_scan_loop(); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Idle for tapping term of mod tap hold key. + idle_for(TAPPING_TERM - 3); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldDefault, tap_mod_tap_key_two_times) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap-hold key again. + EXPECT_REPORT(driver, (KC_P)); + mod_tap_key.press(); + idle_for(TAPPING_TERM); + 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(SpeculativeHoldDefault, tap_mod_tap_key_twice_and_hold_on_second_time) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap-hold key again. + EXPECT_REPORT(driver, (KC_P)); + mod_tap_key.press(); + idle_for(TAPPING_TERM); + 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(SpeculativeHoldDefault, tap_and_hold_mod_tap_key) { + TestDriver driver; + InSequence s; + static int process_record_user_calls = 0; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + process_record_user_fun = [](uint16_t keycode, keyrecord_t *record) { + ++process_record_user_calls; + return true; + }; + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + idle_for(TAPPING_TERM - 1); + EXPECT_EQ(get_speculative_mods(), MOD_BIT_LSHIFT); + EXPECT_EQ(get_mods(), 0); + // Speculative mod holds and releases are made directly, bypassing regular + // event processing. No calls have been made yet to process_record_user(). + EXPECT_EQ(process_record_user_calls, 0); + idle_for(2); + // Now that the key has settled, one call has been made for the hold event. + EXPECT_EQ(process_record_user_calls, 1); + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), MOD_BIT_LSHIFT); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + EXPECT_EQ(process_record_user_calls, 2); + VERIFY_AND_CLEAR(driver); +} + +// Test with layer tap and speculative mod tap keys on the same layer, +// rolling from LT to MT key: +// "LT down, MT down, (wait out tapping term), LT up, MT up." +TEST_F(SpeculativeHoldDefault, lt_mt_same_layer_roll) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + // Press layer tap key. + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap key, after flow tap term but within tapping term. The + // speculative mod activates. + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Wait for the layer tap key to settle. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with layer tap and speculative mod tap keys on the same layer, trying a +// nested press: +// "LT down, MT down, (wait out tapping term), MT up, LT up." +TEST_F(SpeculativeHoldDefault, lt_mt_same_layer_nested_press) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + run_one_scan_loop(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys: MT first, LT second. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with layer tap and speculative mod tap keys on the same layer, trying a +// nested press with the MT first: +// "MT down, LT down, (wait out tapping term), LT up, MT up." +TEST_F(SpeculativeHoldDefault, mt_lt_same_layer_nested_press) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, rolling from +// LT to MT key: +// "LT down, MT down, (wait out tapping term), LT up, MT up." +TEST_F(SpeculativeHoldDefault, lt_mt_different_layer_roll) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + // Press layer tap key. + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + // Press mod tap key. + mod_tap_key.press(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_LSFT)); + layer_tap_key.release(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, slowly +// rolling from LT to MT key: +// "LT down, (wait), MT down, (wait), LT up, MT up." +TEST_F(SpeculativeHoldDefault, lt_mt_different_layer_slow_roll) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, trying a +// nested press: +// "LT down, MT down, (wait out tapping term), MT up, LT up." +TEST_F(SpeculativeHoldDefault, lt_mt_different_layer_nested_press) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + mod_tap_key.press(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, trying a +// slow nested press: +// "LT down, (wait), MT down, MT up, LT up." +TEST_F(SpeculativeHoldDefault, lt_mt_different_layer_slow_nested_press) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +} // namespace diff --git a/tests/tap_hold_configurations/speculative_hold/flow_tap/config.h b/tests/tap_hold_configurations/speculative_hold/flow_tap/config.h new file mode 100644 index 000000000000..6988484226ad --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/flow_tap/config.h @@ -0,0 +1,25 @@ +/* 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 SPECULATIVE_HOLD +#define FLOW_TAP_TERM 150 +#define PERMISSIVE_HOLD +#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24 diff --git a/tests/tap_hold_configurations/speculative_hold/flow_tap/test.mk b/tests/tap_hold_configurations/speculative_hold/flow_tap/test.mk new file mode 100644 index 000000000000..ad8675da9d91 --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/flow_tap/test.mk @@ -0,0 +1,15 @@ +# 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 . + diff --git a/tests/tap_hold_configurations/speculative_hold/flow_tap/test_tap_hold.cpp b/tests/tap_hold_configurations/speculative_hold/flow_tap/test_tap_hold.cpp new file mode 100644 index 000000000000..0433a2548d3e --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/flow_tap/test_tap_hold.cpp @@ -0,0 +1,1114 @@ +// 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; + +extern "C" bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) { + return true; +} + +class SpeculativeHoldFlowTapTest : public TestFixture {}; + +TEST_F(SpeculativeHoldFlowTapTest, tap_mod_tap) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + // Press mod-tap-hold key. Mod is held speculatively. + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + idle_for(10); + VERIFY_AND_CLEAR(driver); + EXPECT_EQ(get_speculative_mods(), MOD_BIT_LSHIFT); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); // Speculative mod canceled. + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), 0); + + // Idle for tapping term of mod tap hold key. + idle_for(TAPPING_TERM - 10); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldFlowTapTest, hold_two_mod_taps) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, LCTL_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, RALT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + // Press first mod-tap key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key1.press(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + EXPECT_EQ(get_speculative_mods(), MOD_BIT_LCTRL); + + // Press second mod-tap key. + EXPECT_REPORT(driver, (KC_LCTL, KC_RALT)); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + EXPECT_EQ(get_speculative_mods(), MOD_BIT_LCTRL | MOD_BIT_RALT); + + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + EXPECT_EQ(get_speculative_mods(), 0); + EXPECT_EQ(get_mods(), MOD_BIT_LCTRL | MOD_BIT_RALT); + + // Release first mod-tap key. + EXPECT_REPORT(driver, (KC_RALT)); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release second mod-tap key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldFlowTapTest, two_mod_taps_same_mods) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, GUI_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, GUI_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + // Press first mod-tap key. + EXPECT_REPORT(driver, (KC_LGUI)); + mod_tap_key1.press(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + + // Tap second mod-tap key. + EXPECT_NO_REPORT(driver); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LGUI, KC_B)); + EXPECT_REPORT(driver, (KC_LGUI)); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release first mod-tap key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test an input of quick distinct taps. All should be settled as tapped. +TEST_F(SpeculativeHoldFlowTapTest, 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_REPORT(driver, (KC_LSFT)); + mod_tap_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// By default, Flow Tap is disabled when mods other than Shift and AltGr are on. +TEST_F(SpeculativeHoldFlowTapTest, 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test input with two mod-taps in a rolled press quickly after a regular key. +TEST_F(SpeculativeHoldFlowTapTest, 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Hold for the tapping term. + EXPECT_NO_REPORT(driver); + 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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_REPORT(driver, (KC_LSFT)); + mod_tap_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL)); + 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, 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL)); + 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, 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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(); + VERIFY_AND_CLEAR(driver); + + // Tap regular key 2. + EXPECT_REPORT(driver, (KC_RALT)); + EXPECT_EMPTY_REPORT(driver); + 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_REPORT(driver, (KC_LCTL)); + mod_tap_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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_LSFT)); + EXPECT_EMPTY_REPORT(driver); + 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL)); + 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_EMPTY_REPORT(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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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_REPORT(driver, (KC_LCTL)); + 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_EMPTY_REPORT(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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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_REPORT(driver, (KC_LCTL)); + EXPECT_REPORT(driver, (KC_LCTL, KC_LGUI)); + EXPECT_REPORT(driver, (KC_LCTL, KC_LGUI, KC_LALT)); + 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_EMPTY_REPORT(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)); + 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +TEST_F(SpeculativeHoldFlowTapTest, 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_REPORT(driver, (KC_LCTL)); + EXPECT_REPORT(driver, (KC_LCTL, KC_LGUI)); + EXPECT_REPORT(driver, (KC_LCTL, KC_LGUI, KC_LALT)); + 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_EMPTY_REPORT(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); + + // 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); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with layer tap and speculative mod tap keys on the same layer, rolling +// from LT to MT key: +// "LT down, MT down, (wait out tapping term), LT up, MT up." +TEST_F(SpeculativeHoldFlowTapTest, lt_mt_same_layer_roll) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + // Press layer tap key. + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap key, after flow tap term but within tapping term. The + // speculative mod activates. + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Wait for the layer tap key to settle. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + idle_for(TAPPING_TERM - FLOW_TAP_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with layer tap and speculative mod tap keys on the same layer, trying +// a nested press from LT to MT key: +// "LT down, MT down, (wait out tapping term), MT up, LT up." +TEST_F(SpeculativeHoldFlowTapTest, lt_mt_same_layer_nested_press) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + idle_for(TAPPING_TERM - FLOW_TAP_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys: MT first, LT second. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with layer tap and speculative mod tap keys on the same layer, trying +// a nested press with the MT first: +// "MT down, LT down, (wait out tapping term), LT up, MT up." +TEST_F(SpeculativeHoldFlowTapTest, mt_lt_same_layer_nested_press) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + run_one_scan_loop(); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, rolling from +// LT to MT key: +// "LT down, MT down, (wait out tapping term), LT up, MT up." +TEST_F(SpeculativeHoldFlowTapTest, lt_mt_different_layer_roll) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + // Press layer tap key. + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(FLOW_TAP_TERM + 1); + // Press mod tap key. + mod_tap_key.press(); + idle_for(TAPPING_TERM - FLOW_TAP_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_B)); + layer_tap_key.release(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, slowly +// rolling from LT to MT key: +// "LT down, (wait), MT down, (wait), LT up, MT up." +TEST_F(SpeculativeHoldFlowTapTest, lt_mt_different_layer_slow_roll) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, trying a +// nested press: +// "LT down, MT down, (wait out tapping term), MT up, LT up." +TEST_F(SpeculativeHoldFlowTapTest, lt_mt_different_layer_nested_press) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(FLOW_TAP_TERM + 1); + mod_tap_key.press(); + idle_for(TAPPING_TERM - FLOW_TAP_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_C)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, trying a +// slow nested press: +// "LT down, (wait), MT down, MT up, LT up." +TEST_F(SpeculativeHoldFlowTapTest, lt_mt_different_layer_slow_nested_press) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + idle_for(FLOW_TAP_TERM + 1); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} diff --git a/tests/tap_hold_configurations/speculative_hold/retro_shift_permissive_hold/config.h b/tests/tap_hold_configurations/speculative_hold/retro_shift_permissive_hold/config.h new file mode 100644 index 000000000000..5b16a60687ca --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/retro_shift_permissive_hold/config.h @@ -0,0 +1,30 @@ +/* Copyright 2022 Isaac Elenbaas + * 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 SPECULATIVE_HOLD +#define PERMISSIVE_HOLD + +#define RETRO_SHIFT 2 * TAPPING_TERM +// releases between AUTO_SHIFT_TIMEOUT and TAPPING_TERM are not tested +#define AUTO_SHIFT_TIMEOUT TAPPING_TERM +#define AUTO_SHIFT_MODIFIERS + +#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24 diff --git a/tests/tap_hold_configurations/speculative_hold/retro_shift_permissive_hold/test.mk b/tests/tap_hold_configurations/speculative_hold/retro_shift_permissive_hold/test.mk new file mode 100644 index 000000000000..7a19bcab59be --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/retro_shift_permissive_hold/test.mk @@ -0,0 +1,17 @@ +# Copyright 2022 Isaac Elenbaas +# 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 . + +AUTO_SHIFT_ENABLE = yes diff --git a/tests/tap_hold_configurations/speculative_hold/retro_shift_permissive_hold/test_retro_shift.cpp b/tests/tap_hold_configurations/speculative_hold/retro_shift_permissive_hold/test_retro_shift.cpp new file mode 100644 index 000000000000..b81485865bc4 --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/retro_shift_permissive_hold/test_retro_shift.cpp @@ -0,0 +1,689 @@ +// Copyright 2022 Isaac Elenbaas +// 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 . + +#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" + +extern "C" { +bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) { + return true; +} + +bool get_auto_shifted_key(uint16_t keycode, keyrecord_t *record) { + return true; +} +} // extern "C" + +using testing::_; +using testing::AnyNumber; +using testing::AnyOf; +using testing::InSequence; + +class RetroShiftPermissiveHold : public TestFixture {}; + +TEST_F(RetroShiftPermissiveHold, tap_regular_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + + set_keymap({mod_tap_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_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_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LCTL, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL)); + 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(RetroShiftPermissiveHold, tap_mod_tap_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A)); + + set_keymap({mod_tap_key, mod_tap_regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap-regular key. + EXPECT_REPORT(driver, (KC_LCTL, KC_LALT)); + mod_tap_regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-regular key. + EXPECT_REPORT(driver, (KC_LCTL)); + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LCTL, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_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(RetroShiftPermissiveHold, tap_regular_key_while_mod_tap_key_is_held_over_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + + set_keymap({mod_tap_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_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_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LCTL, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL)); + regular_key.release(); + run_one_scan_loop(); + idle_for(TAPPING_TERM); + 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(RetroShiftPermissiveHold, tap_mod_tap_key_while_mod_tap_key_is_held_over_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A)); + + set_keymap({mod_tap_key, mod_tap_regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap-regular key. + EXPECT_REPORT(driver, (KC_LCTL, KC_LALT)); + mod_tap_regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-regular key. + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LCTL, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_regular_key.release(); + run_one_scan_loop(); + idle_for(TAPPING_TERM); + 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(RetroShiftPermissiveHold, hold_regular_key_while_mod_tap_key_is_held_over_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + + set_keymap({mod_tap_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_NO_REPORT(driver); + regular_key.press(); + run_one_scan_loop(); + idle_for(AUTO_SHIFT_TIMEOUT); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(KC_LCTL, KC_LSFT), + KeyboardReport(KC_LSFT), + KeyboardReport(KC_LCTL)))) + .Times(AnyNumber()); + // clang-format on + EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_A)); + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(KC_LCTL, KC_LSFT), + KeyboardReport(KC_LSFT)))) + .Times(AnyNumber()); + // clang-format on + EXPECT_REPORT(driver, (KC_LCTL)); + 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(RetroShiftPermissiveHold, hold_mod_tap_key_while_mod_tap_key_is_held_over_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A)); + + set_keymap({mod_tap_key, mod_tap_regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap-regular key. + EXPECT_REPORT(driver, (KC_LCTL, KC_LALT)); + mod_tap_regular_key.press(); + run_one_scan_loop(); + idle_for(AUTO_SHIFT_TIMEOUT); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-regular key. + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(KC_LCTL, KC_LSFT), + KeyboardReport(KC_LSFT), + KeyboardReport(KC_LCTL)))) + .Times(AnyNumber()); + // clang-format on + EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_A)); + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(KC_LCTL, KC_LSFT), + KeyboardReport(KC_LSFT)))) + .Times(AnyNumber()); + // clang-format on + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_regular_key.release(); + run_one_scan_loop(); + idle_for(TAPPING_TERM); + 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(RetroShiftPermissiveHold, roll_tap_regular_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + + set_keymap({mod_tap_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_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 mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(RetroShiftPermissiveHold, roll_tap_mod_tap_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A)); + + set_keymap({mod_tap_key, mod_tap_regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap-regular key. + EXPECT_REPORT(driver, (KC_LCTL, KC_LALT)); + mod_tap_regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-regular key. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(RetroShiftPermissiveHold, roll_hold_regular_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + + set_keymap({mod_tap_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_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 mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + 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); + mod_tap_key.release(); + run_one_scan_loop(); + idle_for(AUTO_SHIFT_TIMEOUT); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_NO_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(RetroShiftPermissiveHold, roll_hold_mod_tap_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A)); + + set_keymap({mod_tap_key, mod_tap_regular_key}); + + // Press mod-tap-hold key. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap-regular key. + EXPECT_REPORT(driver, (KC_LCTL, KC_LALT)); + mod_tap_regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-regular key. + 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); + idle_for(AUTO_SHIFT_TIMEOUT); + mod_tap_regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +// Test with layer tap and speculative mod tap keys on the same layer, rolling +// from LT to MT key: +// "LT down, MT down, (wait out tapping term), LT up, MT up." +TEST_F(RetroShiftPermissiveHold, lt_mt_same_layer_roll) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + // Press layer tap key. + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap key, after flow tap term but within tapping term. The + // speculative mod activates. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Wait for the layer tap key to settle. + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_LSFT, KC_A)); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with layer tap and speculative mod tap keys on the same layer, trying +// a nested press from LT to MT key: +// "LT down, MT down, (wait out tapping term), MT up, LT up." +TEST_F(RetroShiftPermissiveHold, lt_mt_same_layer_nested_press) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + run_one_scan_loop(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys: MT first, LT second. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_LSFT, KC_C)); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with layer tap and speculative mod tap keys on the same layer, trying +// a nested press with the MT first: +// "MT down, LT down, (wait out tapping term), LT up, MT up." +TEST_F(RetroShiftPermissiveHold, mt_lt_same_layer_nested_press) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT)); + EXPECT_REPORT(driver, (KC_LCTL)); + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, rolling from +// LT to MT key: +// "LT down, MT down, (wait out tapping term), LT up, MT up." +TEST_F(RetroShiftPermissiveHold, lt_mt_different_layer_roll) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + // Press layer tap key. + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + // Press mod tap key. + mod_tap_key.press(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_B)); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, slowly +// rolling from LT to MT key: +// "LT down, (wait), MT down, (wait), LT up, MT up." +TEST_F(RetroShiftPermissiveHold, lt_mt_different_layer_slow_roll) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_A)); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, try a nested +// press: +// "LT down, MT down, (wait out tapping term), MT up, LT up." +TEST_F(RetroShiftPermissiveHold, lt_mt_different_layer_nested_press) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + mod_tap_key.press(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_LSFT, KC_C)); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, try a slow +// nested press: +// "LT down, (wait), MT down, MT up, LT up." +TEST_F(RetroShiftPermissiveHold, lt_mt_different_layer_slow_nested_press) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_C)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} diff --git a/tests/tap_hold_configurations/speculative_hold/retro_tapping/config.h b/tests/tap_hold_configurations/speculative_hold/retro_tapping/config.h new file mode 100644 index 000000000000..b20ddbdcad8d --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/retro_tapping/config.h @@ -0,0 +1,24 @@ +/* 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 SPECULATIVE_HOLD +#define RETRO_TAPPING +#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24 diff --git a/tests/tap_hold_configurations/speculative_hold/retro_tapping/test.mk b/tests/tap_hold_configurations/speculative_hold/retro_tapping/test.mk new file mode 100644 index 000000000000..9bc3366e140a --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/retro_tapping/test.mk @@ -0,0 +1,15 @@ +# 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 . diff --git a/tests/tap_hold_configurations/speculative_hold/retro_tapping/test_tap_hold.cpp b/tests/tap_hold_configurations/speculative_hold/retro_tapping/test_tap_hold.cpp new file mode 100644 index 000000000000..de70bf09fd36 --- /dev/null +++ b/tests/tap_hold_configurations/speculative_hold/retro_tapping/test_tap_hold.cpp @@ -0,0 +1,629 @@ +// 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::InSequence; + +extern "C" bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) { + return true; +} + +class SpeculativeHoldRetroTappingTest : public TestFixture {}; + +TEST_F(SpeculativeHoldRetroTappingTest, roll_regular_to_lgui_mod) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, LGUI_T(KC_P)); + auto regular_key = KeymapKey(0, 2, 0, KC_B); + + set_keymap({mod_tap_key, regular_key}); + + EXPECT_REPORT(driver, (KC_B)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_B, KC_LGUI)); + mod_tap_key.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LGUI)); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Neutralizer invoked by Speculative Hold. + EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE)); + EXPECT_REPORT(driver, (KC_LGUI)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldRetroTappingTest, regular_to_mod_under_tap_term) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, LSFT_T(KC_A)); + auto regular_key = KeymapKey(0, 2, 0, KC_B); + + set_keymap({mod_tap_key, regular_key}); + + EXPECT_REPORT(driver, (KC_B)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_B, KC_LSFT)); + mod_tap_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); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldRetroTappingTest, mod_under_tap_term_to_regular) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, LGUI_T(KC_P)); + auto regular_key = KeymapKey(0, 2, 0, KC_B); + + set_keymap({mod_tap_key, regular_key}); + + EXPECT_REPORT(driver, (KC_LGUI)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Neutralizer invoked by Speculative Hold. + EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE)); + EXPECT_REPORT(driver, (KC_LGUI)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_B, KC_P)); + EXPECT_REPORT(driver, (KC_B)); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldRetroTappingTest, mod_over_tap_term_to_regular) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, LSFT_T(KC_A)); + auto regular_key = KeymapKey(0, 2, 0, KC_B); + + set_keymap({mod_tap_key, regular_key}); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_key.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_B)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_B)); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldRetroTappingTest, mod_under_tap_term_to_mod_under_tap_term) { + TestDriver driver; + InSequence s; + auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P)); + auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A)); + + set_keymap({mod_tap_lgui, mod_tap_lsft}); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_lsft.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI)); + mod_tap_lgui.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_lsft.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_lgui.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldRetroTappingTest, mod_over_tap_term_to_mod_under_tap_term) { + TestDriver driver; + InSequence s; + auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P)); + auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A)); + + set_keymap({mod_tap_lgui, mod_tap_lsft}); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_lsft.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI)); + mod_tap_lgui.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + mod_tap_lsft.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_REPORT(driver, (KC_LSFT, KC_P)); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_lgui.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldRetroTappingTest, mod_under_tap_term_to_mod_over_tap_term) { + TestDriver driver; + InSequence s; + auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P)); + auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A)); + + set_keymap({mod_tap_lgui, mod_tap_lsft}); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_lsft.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI)); + mod_tap_lgui.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LGUI)); + mod_tap_lsft.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Neutralizer invoked by Retro Tapping. + EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE)); + EXPECT_REPORT(driver, (KC_LGUI)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_REPORT(driver, (KC_P, KC_LSFT)); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_lgui.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldRetroTappingTest, mod_under_tap_term_to_mod_over_tap_term_offset) { + TestDriver driver; + InSequence s; + auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P)); + auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A)); + + set_keymap({mod_tap_lgui, mod_tap_lsft}); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_lsft.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI)); + mod_tap_lgui.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_lsft.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LGUI)); + // Neutralizer invoked by Retro Tapping. + EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE)); + EXPECT_REPORT(driver, (KC_LGUI)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + idle_for(TAPPING_TERM); + mod_tap_lgui.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldRetroTappingTest, mod_over_tap_term_to_mod_over_tap_term) { + TestDriver driver; + InSequence s; + auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P)); + auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A)); + + set_keymap({mod_tap_lgui, mod_tap_lsft}); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_lsft.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI)); + mod_tap_lgui.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LGUI)); + mod_tap_lsft.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Neutralizer invoked by Retro Tapping. + EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE)); + EXPECT_REPORT(driver, (KC_LGUI)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_REPORT(driver, (KC_P, KC_LSFT)); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_lgui.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(SpeculativeHoldRetroTappingTest, mod_to_mod_to_mod) { + TestDriver driver; + InSequence s; + auto mod_tap_lalt = KeymapKey(0, 1, 0, LALT_T(KC_R)); + auto mod_tap_lsft = KeymapKey(0, 2, 0, SFT_T(KC_A)); + auto mod_tap_lctl = KeymapKey(0, 3, 0, LCTL_T(KC_C)); + + set_keymap({mod_tap_lalt, mod_tap_lsft, mod_tap_lctl}); + + EXPECT_REPORT(driver, (KC_LALT)); + mod_tap_lalt.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT, KC_LALT)); + mod_tap_lsft.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LSFT)); + mod_tap_lalt.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT)); + mod_tap_lctl.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_lsft.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_REPORT(driver, (KC_C, KC_LSFT)); + EXPECT_REPORT(driver, (KC_LSFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_lctl.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +// Test with layer tap and speculative mod tap keys on the same layer, rolling +// from LT to MT key: +// "LT down, MT down, (wait out tapping term), LT up, MT up." +TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_same_layer_roll) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + // Press layer tap key. + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap key, after flow tap term but within tapping term. The + // speculative mod activates. + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Wait for the layer tap key to settle. + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with layer tap and speculative mod tap keys on the same layer, trying a +// nested press: +// "LT down, MT down, (wait out tapping term), MT up, LT up." +TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_same_layer_nested_press) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys: MT first, LT second. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with layer tap and speculative mod tap keys on the same layer, trying a +// nested press with the MT first: +// "MT down, LT down, (wait out tapping term), LT up, MT up." +TEST_F(SpeculativeHoldRetroTappingTest, mt_lt_same_layer_nested_press) { + TestDriver driver; + InSequence s; + auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A)); + auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B)); + auto regular_key = KeymapKey(1, 1, 0, KC_C); + + set_keymap({layer_tap_key, mod_tap_key, regular_key}); + + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_key.press(); + run_one_scan_loop(); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LCTL, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL)); + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, rolling from +// LT to MT key: +// "LT down, MT down, (wait out tapping term), LT up, MT up." +TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_different_layer_roll) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + // Press layer tap key. + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + // Press mod tap key. + mod_tap_key.press(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_LCTL)); + layer_tap_key.release(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, slowly +// rolling from LT to MT key: +// "LT down, (wait), MT down, (wait), LT up, MT up." +TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_different_layer_slow_roll) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_REPORT(driver, (KC_LCTL)); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + layer_tap_key.release(); + run_one_scan_loop(); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, trying a +// nested press: +// "LT down, MT down, (wait out tapping term), MT up, LT up." +TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_different_layer_nested_press) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_key.press(); + run_one_scan_loop(); + mod_tap_key.press(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_LCTL)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +} + +// Test with a speculative mod tap key reached by a layer tap key, slowly making +// a nested press from LT to MT key: +// "LT down, (wait), MT down, MT up, LT up." +TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_different_layer_slow_nested_press) { + 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 placeholder_key = KeymapKey(1, 0, 0, KC_NO); + auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C)); + + set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key}); + + EXPECT_REPORT(driver, (KC_LCTL)); + layer_tap_key.press(); + idle_for(TAPPING_TERM + 1); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + layer_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + // All mods are released. + EXPECT_EQ(get_mods() | get_speculative_mods(), 0); +}