diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
new file mode 100644
index 0000000000..6004b13d18
--- /dev/null
+++ b/.claude/CLAUDE.md
@@ -0,0 +1,18 @@
+
+
+
+## Voice Output (TTS)
+
+When responding, wrap your natural language prose in `«tts»...«/tts»` markers for text-to-speech.
+
+Rules:
+- ONLY wrap conversational prose meant to be spoken aloud
+- Do NOT wrap: code, file paths, commands, tool output, URLs, lists, errors
+- Keep markers on same line as text (no line breaks inside)
+
+Examples:
+✓ «tts»I'll help you fix that bug.«/tts»
+✓ «tts»The tests are passing.«/tts» Here's what changed:
+✗ «tts»src/Header.tsx«/tts» (file path - don't wrap)
+✗ «tts»npm install«/tts» (command - don't wrap)
+
diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson
index 26b437b513..c6e2404add 100644
--- a/data/mappings/info_config.hjson
+++ b/data/mappings/info_config.hjson
@@ -195,6 +195,7 @@
"PERMISSIVE_HOLD_PER_KEY": {"info_key": "tapping.permissive_hold_per_key", "value_type": "bool"},
"RETRO_TAPPING": {"info_key": "tapping.retro", "value_type": "bool"},
"RETRO_TAPPING_PER_KEY": {"info_key": "tapping.retro_per_key", "value_type": "bool"},
+ "SPECULATIVE_HOLD": {"info_key": "tapping.speculative_hold", "value_type": "bool"},
"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/docs/tap_hold.md b/docs/tap_hold.md
index 18c90c6932..51593aaf5d 100644
--- a/docs/tap_hold.md
+++ b/docs/tap_hold.md
@@ -489,6 +489,39 @@ Examples:
[Auto Shift,](feature_auto_shift.md) has its own version of `retro tapping` called `retro shift`. It is extremely similar to `retro tapping`, but holding the key past `AUTO_SHIFT_TIMEOUT` results in the value it sends being shifted. Other configurations also affect it differently; see [here](feature_auto_shift.md#retro-shift) for more information.
+### Speculative Hold
+
+Speculative Hold makes mod-tap keys more responsive by applying the modifier instantly on keydown, before the tap-hold decision is made. This is especially useful for actions like Shift+Click with a mouse, which can feel laggy with standard mod-taps.
+
+The firmware holds the modifier speculatively. Once the key's behavior is settled:
+
+* If held, the modifier remains active as expected until the key is released.
+* If tapped, the speculative modifier is canceled just before the tapping keycode is sent.
+
+Speculative Hold applies the modifier early but does not change the underlying tap-hold decision logic. Speculative Hold is compatible to use in combination with any other tap-hold options.
+
+To enable Speculative Hold, add the following to your `config.h`:
+
+```c
+#define SPECULATIVE_HOLD
+```
+
+By default, Speculative Hold applies to mod-taps using Shift, Ctrl, or Shift + Ctrl. You can override this behavior by defining the `get_speculative_hold()` callback in your keymap, for instance:
+
+```c
+bool get_speculative_hold(uint16_t keycode, keyrecord_t* record) {
+ switch (keycode) { // These keys may be speculatively held.
+ case LCTL_T(KC_ESC):
+ case LSFT_T(KC_Z):
+ case RSFT_T(KC_SLSH):
+ return true;
+ }
+ return false; // Disable otherwise.
+}
+```
+
+Some operating systems or applications assign actions to tapping a modifier key by itself, e.g., tapping GUI to open a start menu. Because Speculative Hold sends a lone modifier key press in some cases, it can falsely trigger these actions. To prevent this, set `DUMMY_MOD_NEUTRALIZER_KEYCODE` (and optionally `MODS_TO_NEUTRALIZE`) in your `config.h` in the same way as described above for [Retro Tapping](#retro-tapping).
+
## Why do we include the key record for the per key functions?
One thing that you may notice is that we include the key record for all of the "per key" functions, and may be wondering why we do that.
diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h
index e341c556f3..6bdfb9054d 100644
--- a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h
+++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h
@@ -17,7 +17,16 @@
// COMM+DOT+SLSH fallback combo fires regardless of the active layer.
#define COMBO_ONLY_FROM_LAYER 0
+// Pressing the Shift key continues Caps Word and inverts the shift state
+#define CAPS_WORD_INVERT_ON_SHIFT
+
// Default tapping term for mod-tap, layer-tap, and tap-dance keys.
#define TAPPING_TERM 200
// Allow per-key overrides via get_tapping_term() in keymap.c.
#define TAPPING_TERM_PER_KEY
+
+// Use right CTRL key to neutralize modifier taps when cancelled.
+#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_RIGHT_CTRL
+
+// Neutralize left ALT and left GUI (Default value)
+#define MODS_TO_NEUTRALIZE {MOD_BIT(KC_LEFT_ALT), MOD_BIT(KC_LEFT_GUI)}
diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/hid_protocol.h b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/hid_protocol.h
index c4649ef2b6..49e068762d 100644
--- a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/hid_protocol.h
+++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/hid_protocol.h
@@ -31,6 +31,7 @@
#define HID_CMD_VOLUME 0x41u // System volume level (host → keyboard)
#define HID_CMD_BRIGHTNESS 0x42u // Screen brightness level (host → keyboard)
#define HID_CMD_ACTIVE_APP 0x43u // Active window/app name (reserved — future)
+#define HID_CMD_BATTERY 0x44u // Battery level (keyboard → host; wireless mode only)
#define HID_CMD_ACK 0x7Eu // Generic acknowledgement
// ---------------------------------------------------------------------------
@@ -80,6 +81,15 @@
// ---------------------------------------------------------------------------
#define HID_APP_NAME_MAX 28u
+// ---------------------------------------------------------------------------
+// HID_CMD_BATTERY payload
+// [0] Battery percentage 0-100, or HID_BATT_UNAVAILABLE when the keyboard
+// is in USB transport mode (battery reading not meaningful / charging).
+// Keyboard only sends this packet when get_transport() & TRANSPORT_WIRELESS.
+// ---------------------------------------------------------------------------
+#define HID_BATT_OFF_LEVEL 0u // Payload byte 0: percentage (0-100)
+#define HID_BATT_UNAVAILABLE 0xFFu // Sentinel: not in wireless mode
+
// ---------------------------------------------------------------------------
// Packet size
// ---------------------------------------------------------------------------
diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c
index 9da254b35c..fd051663f8 100644
--- a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c
+++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c
@@ -20,6 +20,10 @@
#include "raw_hid.h"
#include "keychron_raw_hid.h"
#include "hid_protocol.h"
+#ifdef LK_WIRELESS_ENABLE
+# include "battery.h"
+# include "transport.h"
+#endif
// Tap Dance declarations
enum {
@@ -37,10 +41,11 @@ enum custom_keycodes {
LCK_FN4, // Lock/unlock FN4
LCK_CTL, // Lock/unlock KEEB_CTL
LCK_BASE, // Clear all locks and return to BASE
- CAPS_MOD, // Tap=ESC, hold=Ctrl, Shift=CapsLock, Alt=CapsWord, GUI=Autocorrect
BSP_DEL, // Tap=Backspace, Shift+Tap=Delete
};
+#define CAPS_MOD MT(MOD_LCTL, KC_ESC)
+
// Declare layers early so the HID functions below can reference KEEB_CTL.
enum layers {
BASE,
@@ -77,6 +82,34 @@ static uint8_t g_last_sent_layer = 0xFF;
static uint8_t g_hid_volume = 0;
static uint8_t g_hid_brightness = 0;
+// Battery reporting state (wireless mode only).
+// Pushes the current percentage every BAT_REPORT_INTERVAL_MS ms.
+// Suppresses redundant sends when the level hasn't changed.
+#ifdef LK_WIRELESS_ENABLE
+# define BAT_REPORT_INTERVAL_MS 300000U // 5 minutes
+static uint32_t g_bat_timer = 0;
+static bool g_bat_sent_once = false;
+static uint8_t g_last_sent_bat = HID_BATT_UNAVAILABLE;
+#endif
+
+// Send the current battery percentage to the host.
+// Only fires when get_transport() & TRANSPORT_WIRELESS; no-op otherwise.
+// Suppresses sends when the level matches the last value sent.
+#ifdef LK_WIRELESS_ENABLE
+static void hid_send_battery(void) {
+ if (!(get_transport() & TRANSPORT_WIRELESS)) return;
+ uint8_t level = battery_get_percentage();
+ if (level == g_last_sent_bat) return;
+ g_last_sent_bat = level;
+ uint8_t data[HID_PACKET_SIZE] = {0};
+ data[HID_OFF_CMD] = HID_CMD_BATTERY;
+ data[HID_OFF_SRC] = HID_DEV_Q5MAX;
+ data[HID_OFF_FLAGS] = 0;
+ data[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = level;
+ raw_hid_send(data, HID_PACKET_SIZE);
+}
+#endif
+
// Send the current layer state to the host / bridge application.
static void hid_send_layer_sync(uint8_t layer, uint8_t locked_mask) {
uint8_t data[HID_PACKET_SIZE] = {0};
@@ -151,6 +184,23 @@ bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length) {
break;
}
+ case HID_CMD_BATTERY: {
+ // Host is querying the current battery level.
+ // Reply with the current percentage when in wireless mode, or
+ // HID_BATT_UNAVAILABLE when wired (USB transport / not meaningful).
+ uint8_t resp[HID_PACKET_SIZE] = {0};
+ resp[HID_OFF_CMD] = HID_CMD_BATTERY;
+ resp[HID_OFF_SRC] = HID_DEV_Q5MAX;
+ resp[HID_OFF_FLAGS] = HID_FLAG_RESPONSE;
+#ifdef LK_WIRELESS_ENABLE
+ resp[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = (get_transport() & TRANSPORT_WIRELESS) ? battery_get_percentage() : HID_BATT_UNAVAILABLE;
+#else
+ resp[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = HID_BATT_UNAVAILABLE;
+#endif
+ raw_hid_send(resp, HID_PACKET_SIZE);
+ break;
+ }
+
default:
break;
}
@@ -159,9 +209,7 @@ bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length) {
}
// CAPS_MOD state: tap=ESC, hold=Ctrl, Shift+tap=CapsLock, Alt+tap=CapsWord, GUI+tap=Autocorrect
-static bool caps_mod_held = false;
-static bool caps_mod_ctrl_registered = false;
-static uint16_t caps_mod_timer = 0;
+// (Refactored to use MT(MOD_LCTL, KC_ESC) with custom tap logic)
// clang-format off
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
@@ -298,38 +346,28 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case CAPS_MOD:
- if (record->event.pressed) {
- caps_mod_held = true;
- caps_mod_timer = timer_read();
- // If a real modifier is held, send a dummy key so the OS sees
- // modifier+key rather than a bare modifier hold/tap. Without
- // this, the OS never receives any keycode while the modifier is
- // down and treats the eventual modifier release as a tap (e.g.
- // GUI opening the app menu). KC_F24 is harmless and universally
- // ignored by applications.
- if (get_mods() & (MOD_MASK_GUI | MOD_MASK_ALT | MOD_MASK_SHIFT)) {
- register_code(KC_F24);
- unregister_code(KC_F24);
- }
- } else {
- if (caps_mod_ctrl_registered) {
- unregister_code(KC_LCTL);
- caps_mod_ctrl_registered = false;
- } else {
- uint8_t mods = get_mods();
+ // Custom tap logic: only intercept if it's a TAP AND a modifier is held.
+ // If it's a pure hold (Ctrl) or a pure tap (Esc), return true to let
+ // the MT() core handle it.
+ if (record->tap.count > 0 && record->event.pressed) {
+ uint8_t mods = get_mods();
+ if (mods & (MOD_MASK_GUI | MOD_MASK_ALT)) {
+ // Neutralize the modifier hold so releasing GUI/Alt doesn't
+ // trigger an OS "tap" action (like opening the Start menu).
+ tap_code(DUMMY_MOD_NEUTRALIZER_KEYCODE);
+
if (mods & MOD_MASK_GUI) {
autocorrect_toggle();
- } else if (mods & MOD_MASK_ALT) {
- caps_word_toggle();
- } else if (mods & MOD_MASK_SHIFT) {
- tap_code(KC_CAPS); // Shift still held → host sees Shift+CapsLock (toggles on most OSes)
} else {
- tap_code(KC_ESC);
+ caps_word_toggle();
}
+ return false; // suppress default Esc tap
+ } else if (mods & MOD_MASK_SHIFT) {
+ tap_code(KC_CAPS);
+ return false; // suppress default Esc tap
}
- caps_mod_held = false; // cleared in both hold and tap paths
}
- return false;
+ return true; // let core handle Esc tap or Ctrl hold
case LCK_FN1:
case LCK_FN2:
@@ -414,15 +452,22 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
}
void matrix_scan_user(void) {
- if (caps_mod_held && !caps_mod_ctrl_registered && timer_elapsed(caps_mod_timer) > TAPPING_TERM) {
- caps_mod_ctrl_registered = true;
- register_code(KC_LCTL);
- }
if (alt_tab_active && timer_elapsed(alt_tab_timer) > ALT_TAB_TIMEOUT) {
unregister_code(KC_LALT);
alt_tab_active = false;
}
chord_scan();
+
+#ifdef LK_WIRELESS_ENABLE
+ // Push battery level to host every BAT_REPORT_INTERVAL_MS when wireless.
+ // First call fires immediately (g_bat_sent_once == false) so the host gets
+ // a reading as soon as the keyboard connects over USB in wireless mode.
+ if (!g_bat_sent_once || timer_elapsed32(g_bat_timer) >= BAT_REPORT_INTERVAL_MS) {
+ g_bat_sent_once = true;
+ g_bat_timer = timer_read32();
+ hid_send_battery();
+ }
+#endif
}
// RGB Matrix Indicators --------------------------------------------------
@@ -430,7 +475,7 @@ void matrix_scan_user(void) {
// BASE stays dark; each FN/control layer gets a distinct colour.
#if defined(RGB_MATRIX_ENABLE)
bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {
-#ifdef DIP_SWITCH_ENABLE
+# ifdef DIP_SWITCH_ENABLE
// Win-side override: paint all LEDs white so the user gets a clean white
// backlight regardless of which RGB effect is active. The effect keeps
// ticking internally and resumes the moment the switch returns to Mac side.
@@ -441,7 +486,7 @@ bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {
rgb_matrix_set_color(i, 255, 255, 255);
}
}
-#endif
+# endif
switch (get_highest_layer(layer_state)) {
case FN1:
diff --git a/keyboards/keychron/q5_max/info.json b/keyboards/keychron/q5_max/info.json
index e589981b7c..68a48dc949 100644
--- a/keyboards/keychron/q5_max/info.json
+++ b/keyboards/keychron/q5_max/info.json
@@ -18,7 +18,8 @@
"nkro" : true,
"rgb_matrix": true,
"raw" : true,
- "send_string": true
+ "send_string": true,
+ "speculative_hold": true
},
"matrix_pins": {
"cols": ["C6", "C7", "C8", "A14", "A15", "C10", "C11", "C13", "C14", "C15", "C0", "C1", "C2", "C3", "A0", "A1", "A2", "A3", "B10"],
diff --git a/quantum/action.c b/quantum/action.c
index 29822c39e9..6f5b49862b 100644
--- a/quantum/action.c
+++ b/quantum/action.c
@@ -270,6 +270,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
if (!process_record_quantum(record)) {
#ifndef NO_ACTION_ONESHOT
diff --git a/quantum/action.h b/quantum/action.h
index d5b15c6f17..e316de8465 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 8f238490f2..fbe6ce63f1 100644
--- a/quantum/action_tapping.c
+++ b/quantum/action_tapping.c
@@ -5,7 +5,10 @@
#include "action_layer.h"
#include "action_tapping.h"
#include "keycode.h"
+#include "keycode_config.h"
+#include "quantum_keycodes.h"
#include "timer.h"
+#include "wait.h"
#ifndef NO_ACTION_TAPPING
@@ -49,6 +52,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
+
# ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY
__attribute__((weak)) bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record) {
return false;
@@ -78,6 +96,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: ");
@@ -94,6 +119,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");
@@ -520,6 +551,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
+
/** \brief Tapping key debug print
*
* FIXME: Needs docs
diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h
index 6b518b8298..7fbba4e2a5 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 DYNAMIC_TAPPING_TERM_ENABLE
extern uint16_t g_tapping_term;
#endif
diff --git a/quantum/action_util.c b/quantum/action_util.c
index b034562515..7fa37f1a91 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
@@ -278,6 +279,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 0000000000..e4bcba13c1
--- /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 0000000000..1765122512
--- /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 0000000000..764b97ddde
--- /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 0000000000..9891296f42
--- /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 0000000000..f5decaeb78
--- /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 0000000000..db65374618
--- /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 0000000000..bfa022be11
--- /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 0000000000..6988484226
--- /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 0000000000..ad8675da9d
--- /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 0000000000..0433a2548d
--- /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 0000000000..5b16a60687
--- /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 0000000000..7a19bcab59
--- /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 0000000000..b81485865b
--- /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 0000000000..b20ddbdcad
--- /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 0000000000..9bc3366e14
--- /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 0000000000..de70bf09fd
--- /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);
+}