mirror of
https://github.com/qmk/qmk_firmware.git
synced 2026-06-03 14:13:46 -03:00
[Core] Options to constrain Speculative Hold: SPECULATIVE_HOLD_ONE_KEY and SPECULATIVE_HOLD_FLOW_TERM. (#26099)
This commit is contained in:
@@ -225,6 +225,8 @@
|
|||||||
"RETRO_TAPPING": {"info_key": "tapping.retro", "value_type": "flag"},
|
"RETRO_TAPPING": {"info_key": "tapping.retro", "value_type": "flag"},
|
||||||
"RETRO_TAPPING_PER_KEY": {"info_key": "tapping.retro_per_key", "value_type": "flag"},
|
"RETRO_TAPPING_PER_KEY": {"info_key": "tapping.retro_per_key", "value_type": "flag"},
|
||||||
"SPECULATIVE_HOLD": {"info_key": "tapping.speculative_hold", "value_type": "flag"},
|
"SPECULATIVE_HOLD": {"info_key": "tapping.speculative_hold", "value_type": "flag"},
|
||||||
|
"SPECULATIVE_HOLD_FLOW_TERM": {"info_key": "tapping.speculative_hold_flow_term", "value_type": "int"},
|
||||||
|
"SPECULATIVE_HOLD_ONE_KEY": {"info_key": "tapping.speculative_hold_one_key", "value_type": "flag"},
|
||||||
"TAP_CODE_DELAY": {"info_key": "qmk.tap_keycode_delay", "value_type": "int"},
|
"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"},
|
"TAP_HOLD_CAPS_DELAY": {"info_key": "qmk.tap_capslock_delay", "value_type": "int"},
|
||||||
"TAPPING_TERM": {"info_key": "tapping.term", "value_type": "int"},
|
"TAPPING_TERM": {"info_key": "tapping.term", "value_type": "int"},
|
||||||
|
|||||||
@@ -993,6 +993,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"chordal_hold": {"type": "boolean"},
|
"chordal_hold": {"type": "boolean"},
|
||||||
|
"flow_tap_term": {"$ref": "./definitions.jsonschema#/unsigned_int"},
|
||||||
"force_hold": {"type": "boolean"},
|
"force_hold": {"type": "boolean"},
|
||||||
"force_hold_per_key": {"type": "boolean"},
|
"force_hold_per_key": {"type": "boolean"},
|
||||||
"ignore_mod_tap_interrupt": {"type": "boolean"},
|
"ignore_mod_tap_interrupt": {"type": "boolean"},
|
||||||
@@ -1002,6 +1003,9 @@
|
|||||||
"permissive_hold_per_key": {"type": "boolean"},
|
"permissive_hold_per_key": {"type": "boolean"},
|
||||||
"retro": {"type": "boolean"},
|
"retro": {"type": "boolean"},
|
||||||
"retro_per_key": {"type": "boolean"},
|
"retro_per_key": {"type": "boolean"},
|
||||||
|
"speculative_hold": {"type": "boolean"},
|
||||||
|
"speculative_hold_flow_term": {"$ref": "./definitions.jsonschema#/unsigned_int"},
|
||||||
|
"speculative_hold_one_key": {"type": "boolean"},
|
||||||
"term": {"$ref": "./definitions.jsonschema#/unsigned_int"},
|
"term": {"$ref": "./definitions.jsonschema#/unsigned_int"},
|
||||||
"term_per_key": {"type": "boolean"},
|
"term_per_key": {"type": "boolean"},
|
||||||
"toggle": {"$ref": "./definitions.jsonschema#/unsigned_int"}
|
"toggle": {"$ref": "./definitions.jsonschema#/unsigned_int"}
|
||||||
|
|||||||
+24
-1
@@ -824,7 +824,30 @@ bool get_speculative_hold(uint16_t keycode, keyrecord_t* record) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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).
|
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 sometimes sends a lone modifier key press, it can falsely trigger these actions (known as the "flashing mods" problem). How such an input is handled depends on the OS and application, so unfortunately, there is no universal solution.
|
||||||
|
|
||||||
|
To mitigate this issue, you can set `DUMMY_MOD_NEUTRALIZER_KEYCODE` (and optionally `MODS_TO_NEUTRALIZE`) in your `config.h`, as described above for [Retro Tapping](#retro-tapping).
|
||||||
|
|
||||||
|
You can further prevent flashing mods by restricting when Speculative Hold is allowed to trigger. There are several options for this:
|
||||||
|
|
||||||
|
* Constrain Speculative Hold to one key at a time. Add to your config.h:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define SPECULATIVE_HOLD_ONE_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
With this option, Speculative Hold does not apply when any mods are already active. Mod combinations across multiple keys can still be made after the mod-tap keys settle.
|
||||||
|
|
||||||
|
* Disable Speculative Hold during the flow of fast typing. Add to your config.h:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define SPECULATIVE_HOLD_FLOW_TERM 200
|
||||||
|
```
|
||||||
|
|
||||||
|
This value specifies a duration in milliseconds. Speculative Hold does not apply if a key is pressed within this threshold of the previous key. The effect is similar to [Flow Tap](#flow-tap); however, `SPECULATIVE_HOLD_FLOW_TERM` only restricts when speculation is allowed, without affecting how the key settles.
|
||||||
|
|
||||||
|
* Use the Flow Tap option. In the fast flow of typing, the mod-tap key is immediately settled, and therefore no speculation occurs. See [Flow Tap](#flow-tap) for details.
|
||||||
|
|
||||||
|
|
||||||
## Why do we include the key record for the per key functions?
|
## Why do we include the key record for the per key functions?
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -286,9 +286,9 @@ void process_record(keyrecord_t *record) {
|
|||||||
speculative_key_settled(record);
|
speculative_key_settled(record);
|
||||||
}
|
}
|
||||||
#endif // SPECULATIVE_HOLD
|
#endif // SPECULATIVE_HOLD
|
||||||
#ifdef FLOW_TAP_TERM
|
#if defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
flow_tap_update_last_event(record);
|
flow_tap_update_last_event(record);
|
||||||
#endif // FLOW_TAP_TERM
|
#endif // defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
|
|
||||||
if (!process_record_quantum(record)) {
|
if (!process_record_quantum(record)) {
|
||||||
#ifndef NO_ACTION_ONESHOT
|
#ifndef NO_ACTION_ONESHOT
|
||||||
|
|||||||
@@ -119,11 +119,13 @@ __attribute__((weak)) bool get_hold_on_other_key_press(uint16_t keycode, keyreco
|
|||||||
# include "process_auto_shift.h"
|
# include "process_auto_shift.h"
|
||||||
# endif
|
# endif
|
||||||
|
|
||||||
# if defined(FLOW_TAP_TERM)
|
# if defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
static uint16_t flow_tap_prev_keycode = KC_NO;
|
static uint16_t flow_tap_prev_keycode = KC_NO;
|
||||||
static uint16_t flow_tap_prev_time = 0;
|
static uint16_t flow_tap_prev_time = 0;
|
||||||
static bool flow_tap_expired = true;
|
static bool flow_tap_expired = true;
|
||||||
|
# endif // defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
|
|
||||||
|
# if defined(FLOW_TAP_TERM)
|
||||||
static bool flow_tap_key_if_within_term(keyrecord_t *record, uint16_t prev_time);
|
static bool flow_tap_key_if_within_term(keyrecord_t *record, uint16_t prev_time);
|
||||||
# endif // defined(FLOW_TAP_TERM)
|
# endif // defined(FLOW_TAP_TERM)
|
||||||
|
|
||||||
@@ -191,11 +193,11 @@ void action_tapping_process(keyrecord_t record) {
|
|||||||
if (IS_EVENT(record.event)) {
|
if (IS_EVENT(record.event)) {
|
||||||
ac_dprintf("\n");
|
ac_dprintf("\n");
|
||||||
} else {
|
} else {
|
||||||
# ifdef FLOW_TAP_TERM
|
# if defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
if (!flow_tap_expired && TIMER_DIFF_16(record.event.time, flow_tap_prev_time) >= INT16_MAX / 2) {
|
if (!flow_tap_expired && TIMER_DIFF_16(record.event.time, flow_tap_prev_time) >= INT16_MAX / 2) {
|
||||||
flow_tap_expired = true;
|
flow_tap_expired = true;
|
||||||
}
|
}
|
||||||
# endif // FLOW_TAP_TERM
|
# endif // defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,17 +772,28 @@ static void speculative_key_press(keyrecord_t *record) {
|
|||||||
if (speculative_keys_find(record->event.key) < num_speculative_keys) {
|
if (speculative_keys_find(record->event.key) < num_speculative_keys) {
|
||||||
return; // Don't trigger: key is already in speculative_keys.
|
return; // Don't trigger: key is already in speculative_keys.
|
||||||
}
|
}
|
||||||
|
# ifdef SPECULATIVE_HOLD_FLOW_TERM
|
||||||
|
if (!flow_tap_expired && TIMER_DIFF_16(record->event.time, flow_tap_prev_time) <= SPECULATIVE_HOLD_FLOW_TERM) {
|
||||||
|
return; // Don't trigger: within flow term of previous key.
|
||||||
|
}
|
||||||
|
# endif // SPECULATIVE_HOLD_FLOW_TERM
|
||||||
|
|
||||||
const uint16_t keycode = get_record_keycode(record, false);
|
const uint16_t keycode = get_record_keycode(record, false);
|
||||||
if (!IS_QK_MOD_TAP(keycode)) {
|
if (!IS_QK_MOD_TAP(keycode)) {
|
||||||
return; // Don't trigger: not a mod-tap key.
|
return; // Don't trigger: not a mod-tap key.
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t mods = mod_config(QK_MOD_TAP_GET_MODS(keycode));
|
uint8_t mods = mod_config(QK_MOD_TAP_GET_MODS(keycode));
|
||||||
|
const uint8_t active_mods = get_mods() | speculative_mods;
|
||||||
|
# ifdef SPECULATIVE_HOLD_ONE_KEY
|
||||||
|
if (active_mods != 0) {
|
||||||
|
return; // Don't trigger: some mod is already active.
|
||||||
|
}
|
||||||
|
# endif // SPECULATIVE_HOLD_ONE_KEY
|
||||||
if ((mods & 0x10) != 0) { // Unpack 5-bit mods to 8-bit representation.
|
if ((mods & 0x10) != 0) { // Unpack 5-bit mods to 8-bit representation.
|
||||||
mods <<= 4;
|
mods <<= 4;
|
||||||
}
|
}
|
||||||
if ((~(get_mods() | speculative_mods) & mods) == 0) {
|
if ((~active_mods & mods) == 0) {
|
||||||
return; // Don't trigger: mods are already active.
|
return; // Don't trigger: mods are already active.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,7 +1001,7 @@ static void waiting_buffer_process_regular(void) {
|
|||||||
}
|
}
|
||||||
# endif // CHORDAL_HOLD
|
# endif // CHORDAL_HOLD
|
||||||
|
|
||||||
# ifdef FLOW_TAP_TERM
|
# if defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
void flow_tap_update_last_event(keyrecord_t *record) {
|
void flow_tap_update_last_event(keyrecord_t *record) {
|
||||||
const uint16_t keycode = get_record_keycode(record, false);
|
const uint16_t keycode = get_record_keycode(record, false);
|
||||||
// Don't update while a tap-hold key is unsettled.
|
// Don't update while a tap-hold key is unsettled.
|
||||||
@@ -1028,7 +1041,9 @@ void flow_tap_update_last_event(keyrecord_t *record) {
|
|||||||
flow_tap_prev_time = record->event.time;
|
flow_tap_prev_time = record->event.time;
|
||||||
flow_tap_expired = false;
|
flow_tap_expired = false;
|
||||||
}
|
}
|
||||||
|
# endif // defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
|
|
||||||
|
# ifdef FLOW_TAP_TERM
|
||||||
static bool flow_tap_key_if_within_term(keyrecord_t *record, uint16_t prev_time) {
|
static bool flow_tap_key_if_within_term(keyrecord_t *record, uint16_t prev_time) {
|
||||||
const uint16_t idle_time = TIMER_DIFF_16(record->event.time, prev_time);
|
const uint16_t idle_time = TIMER_DIFF_16(record->event.time, prev_time);
|
||||||
if (flow_tap_expired || idle_time >= 500) {
|
if (flow_tap_expired || idle_time >= 500) {
|
||||||
|
|||||||
@@ -197,9 +197,6 @@ bool is_flow_tap_key(uint16_t keycode);
|
|||||||
*/
|
*/
|
||||||
uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode);
|
uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode);
|
||||||
|
|
||||||
/** Updates the Flow Tap last key and timer. */
|
|
||||||
void flow_tap_update_last_event(keyrecord_t *record);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the pressed key is within the flow tap term.
|
* Checks if the pressed key is within the flow tap term.
|
||||||
* Can be used to avoid triggering combos or other actions within the flow tap term.
|
* Can be used to avoid triggering combos or other actions within the flow tap term.
|
||||||
@@ -211,6 +208,11 @@ void flow_tap_update_last_event(keyrecord_t *record);
|
|||||||
bool within_flow_tap_term(uint16_t keycode, keyrecord_t *record);
|
bool within_flow_tap_term(uint16_t keycode, keyrecord_t *record);
|
||||||
#endif // FLOW_TAP_TERM
|
#endif // FLOW_TAP_TERM
|
||||||
|
|
||||||
|
#if defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
|
/** Updates the Flow Tap last key and timer. */
|
||||||
|
void flow_tap_update_last_event(keyrecord_t *record);
|
||||||
|
#endif // defined(FLOW_TAP_TERM) || defined(SPECULATIVE_HOLD_FLOW_TERM)
|
||||||
|
|
||||||
#ifdef DYNAMIC_TAPPING_TERM_ENABLE
|
#ifdef DYNAMIC_TAPPING_TERM_ENABLE
|
||||||
extern uint16_t g_tapping_term;
|
extern uint16_t g_tapping_term;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/* Copyright 2022 Vladislav Kucheriavykh
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
#define SPECULATIVE_HOLD
|
||||||
|
#define SPECULATIVE_HOLD_FLOW_TERM 200
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Copyright 2022 Vladislav Kucheriavykh
|
||||||
|
# Copyright 2026 Google LLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
// Copyright 2026 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 <functional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
class SpeculativeHoldFlowTerm : public TestFixture {};
|
||||||
|
|
||||||
|
TEST_F(SpeculativeHoldFlowTerm, within_flow_term_one_modtap) {
|
||||||
|
TestDriver driver;
|
||||||
|
InSequence s;
|
||||||
|
auto regular_key = KeymapKey(0, 1, 0, KC_SPC);
|
||||||
|
auto mod_tap_key = KeymapKey(0, 2, 0, RSFT_T(KC_B));
|
||||||
|
set_keymap({regular_key, mod_tap_key});
|
||||||
|
|
||||||
|
// Tap regular key.
|
||||||
|
EXPECT_REPORT(driver, (KC_SPC));
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
tap_key(regular_key);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
// Press mod-tap key.
|
||||||
|
EXPECT_NO_REPORT(driver);
|
||||||
|
mod_tap_key.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), 0); // Don't speculate.
|
||||||
|
EXPECT_EQ(get_mods(), 0);
|
||||||
|
|
||||||
|
EXPECT_REPORT(driver, (KC_RSFT));
|
||||||
|
idle_for(TAPPING_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), 0);
|
||||||
|
EXPECT_EQ(get_mods(), MOD_BIT_RSHIFT);
|
||||||
|
|
||||||
|
// Release mod-tap key.
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
mod_tap_key.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SpeculativeHoldFlowTerm, within_flow_term_two_modtaps) {
|
||||||
|
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_NO_REPORT(driver);
|
||||||
|
mod_tap_key1.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), 0); // Don't speculate.
|
||||||
|
|
||||||
|
// Press mod-tap key 2 quickly.
|
||||||
|
EXPECT_NO_REPORT(driver);
|
||||||
|
mod_tap_key2.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), 0); // Don't speculate.
|
||||||
|
|
||||||
|
// Hold for longer than the tapping term.
|
||||||
|
EXPECT_REPORT(driver, (KC_LSFT));
|
||||||
|
EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
|
||||||
|
idle_for(TAPPING_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
// Release mod-tap keys.
|
||||||
|
EXPECT_REPORT(driver, (KC_LCTL));
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
mod_tap_key1.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
mod_tap_key2.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SpeculativeHoldFlowTerm, after_flow_term_one_modtap) {
|
||||||
|
TestDriver driver;
|
||||||
|
InSequence s;
|
||||||
|
auto regular_key = KeymapKey(0, 1, 0, KC_SPC);
|
||||||
|
auto mod_tap_key = KeymapKey(0, 2, 0, RSFT_T(KC_B));
|
||||||
|
set_keymap({regular_key, mod_tap_key});
|
||||||
|
|
||||||
|
// Tap regular key.
|
||||||
|
EXPECT_REPORT(driver, (KC_SPC));
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
tap_key(regular_key);
|
||||||
|
idle_for(SPECULATIVE_HOLD_FLOW_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
// Press mod-tap key.
|
||||||
|
EXPECT_REPORT(driver, (KC_RSFT));
|
||||||
|
mod_tap_key.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), MOD_BIT_RSHIFT);
|
||||||
|
EXPECT_EQ(get_mods(), 0);
|
||||||
|
|
||||||
|
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_RSHIFT);
|
||||||
|
|
||||||
|
// Release mod-tap key.
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
mod_tap_key.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SpeculativeHoldFlowTerm, after_flow_term_two_modtaps) {
|
||||||
|
TestDriver driver;
|
||||||
|
InSequence s;
|
||||||
|
auto regular_key = KeymapKey(0, 1, 0, KC_SPC);
|
||||||
|
auto mod_tap_key1 = KeymapKey(0, 2, 0, RSFT_T(KC_B));
|
||||||
|
auto mod_tap_key2 = KeymapKey(0, 3, 0, RCTL_T(KC_C));
|
||||||
|
set_keymap({regular_key, mod_tap_key1, mod_tap_key2});
|
||||||
|
|
||||||
|
// Tap regular key.
|
||||||
|
EXPECT_REPORT(driver, (KC_SPC));
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
tap_key(regular_key);
|
||||||
|
idle_for(SPECULATIVE_HOLD_FLOW_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
// Press mod-tap key 1.
|
||||||
|
EXPECT_REPORT(driver, (KC_RSFT));
|
||||||
|
mod_tap_key1.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), MOD_BIT_RSHIFT);
|
||||||
|
EXPECT_EQ(get_mods(), 0);
|
||||||
|
|
||||||
|
// Press mod-tap key 2.
|
||||||
|
EXPECT_REPORT(driver, (KC_RSFT, KC_RCTL));
|
||||||
|
mod_tap_key2.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), MOD_BIT_RSHIFT | MOD_BIT_RCTRL);
|
||||||
|
EXPECT_EQ(get_mods(), 0);
|
||||||
|
|
||||||
|
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_RSHIFT | MOD_BIT_RCTRL);
|
||||||
|
|
||||||
|
// Release mod-tap keys.
|
||||||
|
EXPECT_REPORT(driver, (KC_RCTL));
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
mod_tap_key1.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
mod_tap_key2.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/* Copyright 2022 Vladislav Kucheriavykh
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
#define SPECULATIVE_HOLD
|
||||||
|
#define SPECULATIVE_HOLD_ONE_KEY
|
||||||
|
#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Copyright 2022 Vladislav Kucheriavykh
|
||||||
|
# Copyright 2026 Google LLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
// Copyright 2026 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 <functional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> mods_report(uint8_t mods) {
|
||||||
|
std::vector<uint8_t> keycodes;
|
||||||
|
if ((mods & MOD_BIT_LCTRL)) {
|
||||||
|
keycodes.push_back(KC_LCTL);
|
||||||
|
}
|
||||||
|
if ((mods & MOD_BIT_LSHIFT)) {
|
||||||
|
keycodes.push_back(KC_LSFT);
|
||||||
|
}
|
||||||
|
if ((mods & MOD_BIT_LALT)) {
|
||||||
|
keycodes.push_back(KC_LALT);
|
||||||
|
}
|
||||||
|
if ((mods & MOD_BIT_LGUI)) {
|
||||||
|
keycodes.push_back(KC_LGUI);
|
||||||
|
}
|
||||||
|
if ((mods & MOD_BIT_RCTRL)) {
|
||||||
|
keycodes.push_back(KC_RCTL);
|
||||||
|
}
|
||||||
|
if ((mods & MOD_BIT_RSHIFT)) {
|
||||||
|
keycodes.push_back(KC_RSFT);
|
||||||
|
}
|
||||||
|
if ((mods & MOD_BIT_RALT)) {
|
||||||
|
keycodes.push_back(KC_RALT);
|
||||||
|
}
|
||||||
|
if ((mods & MOD_BIT_RGUI)) {
|
||||||
|
keycodes.push_back(KC_RGUI);
|
||||||
|
}
|
||||||
|
return keycodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) {
|
||||||
|
return true; // Enable Speculative Hold for all mod-tap keys.
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpeculativeHoldOneKey : public TestFixture {};
|
||||||
|
|
||||||
|
TEST_F(SpeculativeHoldOneKey, modtap_dont_speculate) {
|
||||||
|
TestDriver driver;
|
||||||
|
InSequence s;
|
||||||
|
|
||||||
|
struct Params {
|
||||||
|
uint8_t initial_mods;
|
||||||
|
uint16_t key;
|
||||||
|
};
|
||||||
|
for (Params params : std::vector<Params>({
|
||||||
|
{MOD_BIT_LSHIFT, LALT_T(KC_1)},
|
||||||
|
{MOD_BIT_RCTRL, LGUI_T(KC_1)},
|
||||||
|
{MOD_BIT_LALT, LSFT_T(KC_1)},
|
||||||
|
{MOD_BIT_RALT, RCTL_T(KC_1)},
|
||||||
|
{MOD_MASK_SHIFT, RGUI_T(KC_1)},
|
||||||
|
})) {
|
||||||
|
std::string scope = "initial_mods=";
|
||||||
|
scope += testing::PrintToString(params.initial_mods);
|
||||||
|
scope += std::string(", key=") + get_keycode_string(params.key);
|
||||||
|
SCOPED_TRACE(scope);
|
||||||
|
|
||||||
|
const uint8_t initial_mods = params.initial_mods;
|
||||||
|
auto mod_tap_key = KeymapKey(0, 1, 0, params.key);
|
||||||
|
const uint8_t mod_tap_mods = unpack_mod_tap_mods(params.key);
|
||||||
|
|
||||||
|
set_keymap({mod_tap_key});
|
||||||
|
|
||||||
|
// Activate mods.
|
||||||
|
set_mods(initial_mods);
|
||||||
|
|
||||||
|
// Press mod-tap key.
|
||||||
|
EXPECT_NO_REPORT(driver);
|
||||||
|
mod_tap_key.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_mods(), initial_mods);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), 0);
|
||||||
|
|
||||||
|
EXPECT_REPORT(driver, (mods_report(initial_mods | mod_tap_mods)));
|
||||||
|
EXPECT_NO_REPORT(driver);
|
||||||
|
idle_for(TAPPING_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), 0);
|
||||||
|
EXPECT_EQ(get_mods(), initial_mods | mod_tap_mods);
|
||||||
|
|
||||||
|
// Release mod-tap key.
|
||||||
|
EXPECT_REPORT(driver, (mods_report(initial_mods & ~mod_tap_mods)));
|
||||||
|
mod_tap_key.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
clear_mods();
|
||||||
|
send_keyboard_report();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SpeculativeHoldOneKey, two_modtaps_dont_speculate_second_key) {
|
||||||
|
TestDriver driver;
|
||||||
|
InSequence s;
|
||||||
|
|
||||||
|
for (auto keys : std::vector<std::pair<uint16_t, uint16_t>>({
|
||||||
|
{LSFT_T(KC_A), LCTL_T(KC_1)},
|
||||||
|
{RSFT_T(KC_A), RCTL_T(KC_1)},
|
||||||
|
{LGUI_T(KC_A), LSFT_T(KC_1)},
|
||||||
|
{RALT_T(KC_A), RSFT_T(KC_1)},
|
||||||
|
{MEH_T(KC_A), LGUI_T(KC_1)},
|
||||||
|
{RCS_T(KC_A), RSG_T(KC_1)},
|
||||||
|
{LCS_T(KC_A), HYPR_T(KC_1)},
|
||||||
|
})) {
|
||||||
|
std::string scope = "keys = ";
|
||||||
|
scope += get_keycode_string(keys.first);
|
||||||
|
scope += std::string(", ") + get_keycode_string(keys.second);
|
||||||
|
SCOPED_TRACE(scope);
|
||||||
|
|
||||||
|
auto mod_tap_key1 = KeymapKey(0, 1, 0, keys.first);
|
||||||
|
auto mod_tap_key2 = KeymapKey(0, 2, 0, keys.second);
|
||||||
|
const uint8_t mods1 = unpack_mod_tap_mods(keys.first);
|
||||||
|
const uint8_t mods2 = unpack_mod_tap_mods(keys.second);
|
||||||
|
|
||||||
|
set_keymap({mod_tap_key1, mod_tap_key2});
|
||||||
|
|
||||||
|
// Press first mod-tap key.
|
||||||
|
EXPECT_REPORT(driver, (mods_report(mods1)));
|
||||||
|
mod_tap_key1.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), mods1);
|
||||||
|
|
||||||
|
// Press second mod-tap key.
|
||||||
|
EXPECT_NO_REPORT(driver);
|
||||||
|
mod_tap_key2.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), mods1);
|
||||||
|
|
||||||
|
EXPECT_REPORT(driver, (mods_report(mods1 | mods2)));
|
||||||
|
EXPECT_NO_REPORT(driver);
|
||||||
|
idle_for(TAPPING_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
EXPECT_EQ(get_speculative_mods(), 0);
|
||||||
|
EXPECT_EQ(get_mods(), mods1 | mods2);
|
||||||
|
|
||||||
|
// Release first mod-tap key.
|
||||||
|
EXPECT_REPORT(driver, (mods_report(mods2 & ~mods1)));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -33,6 +33,10 @@ class KeyboardReportMatcher : public testing::MatcherInterface<report_keyboard_t
|
|||||||
report_keyboard_t m_report;
|
report_keyboard_t m_report;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inline testing::Matcher<report_keyboard_t&> KeyboardReport(const std::vector<uint8_t>& keys) {
|
||||||
|
return testing::MakeMatcher(new KeyboardReportMatcher(keys));
|
||||||
|
}
|
||||||
|
|
||||||
template <typename... Ts>
|
template <typename... Ts>
|
||||||
inline testing::Matcher<report_keyboard_t&> KeyboardReport(Ts... keys) {
|
inline testing::Matcher<report_keyboard_t&> KeyboardReport(Ts... keys) {
|
||||||
return testing::MakeMatcher(new KeyboardReportMatcher({static_cast<uint8_t>(keys)...}));
|
return testing::MakeMatcher(new KeyboardReportMatcher({static_cast<uint8_t>(keys)...}));
|
||||||
|
|||||||
Reference in New Issue
Block a user