[Bug][Core] Fix for Flow Tap: fix handling of distinct taps and timer updates. (#25175)

* Flow Tap bug fix.

As reported by @amarz45 and @mwpardue, there is a bug where if two
tap-hold keys are pressed in distinct taps back to back, then Flow Tap
is not applied on the second tap-hold key, but it should be.

In a related bug reported by @NikGovorov, if a tap-hold key is held
followed by a tap of a tap-hold key, then Flow Tap updates its timer on
the release of the held tap-hold key, but it should be ignored.

The problem common to both these bugs is that I incorrectly assumed
`tapping_key` is cleared to noevent once it is released, when actually
`tapping_key` is still maintained for `TAPPING_TERM` ms after release
(for Quick Tap). This commit fixes that. Thanks to @amarz45, @mwpardue,
and @NikGovorov for reporting!

Details:

* Logic for converting the current tap-hold event to a tap is extracted
  to `flow_tap_key_if_within_term()`, which is now invoked also in the
  post-release "interfered with other tap key" case. This fixes the
  distinct taps bug.

* The Flow Tap timer is now updated at the beginning of each call to
  `process_record()`, provided that there is no unsettled tap-hold key
  at that time and that the record is not for a mod or layer switch key.
  By moving this update logic to `process_record()`, it is conceptually
  simpler and more robust.

* Unit tests extended to cover the reported scenarios.

* Fix formatting.

* Revision to fix @NikGovorov's scenario.

The issue is that when another key is pressed while a layer-tap hasn't
been settled yet, the `prev_keycode` remembers the keycode from before
the layer switched. This can then enable Flow Tap for the following key
when it shouldn't, or vice versa.

Thanks to @NikGovorov for reporting!

This commit revises Flow Tap in the following ways:

* The previous key and timer are both updated from `process_record()`.
  This is slightly later in the sequence of processing than before, and
  by this point, a just-settled layer-tap should have taken effect so
  that the keycode from the correct layer is remembered.

* The Flow Tap previous key and timer are updated now also on key
  release events, except for releases of modifiers and held layer
  switches.

* The Flow Tap previous key and timer are now updated together, for
  simplicity. This makes the logic easier to think about.

* A few additional unit tests, including @NikGovorov's scenario as
  "layer_tap_ignored_with_disabled_key_complex."
This commit is contained in:
Pascal Getreuer 2025-04-22 00:59:49 -07:00 committed by GitHub
parent b5f8f4d6a2
commit 73e2ef486a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 410 additions and 33 deletions

View file

@ -20,3 +20,4 @@
#include "test_common.h"
#define FLOW_TAP_TERM 150
#define PERMISSIVE_HOLD

View file

@ -25,7 +25,169 @@ using testing::InSequence;
class FlowTapTest : public TestFixture {};
TEST_F(FlowTapTest, short_flow_tap_settled_as_tapped) {
// Test an input of quick distinct taps. All should be settled as tapped.
TEST_F(FlowTapTest, distinct_taps) {
TestDriver driver;
InSequence s;
auto regular_key = KeymapKey(0, 0, 0, KC_A);
auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_B));
auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_C));
auto mod_tap_key3 = KeymapKey(0, 3, 0, ALT_T(KC_D));
set_keymap({regular_key, mod_tap_key1, mod_tap_key2, mod_tap_key3});
// Tap regular key.
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
tap_key(regular_key, FLOW_TAP_TERM + 1);
VERIFY_AND_CLEAR(driver);
// Tap mod-tap 1.
EXPECT_REPORT(driver, (KC_B));
mod_tap_key1.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
idle_for(FLOW_TAP_TERM + 1);
mod_tap_key1.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Tap mod-tap 2.
EXPECT_REPORT(driver, (KC_C));
mod_tap_key2.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
idle_for(FLOW_TAP_TERM + 1);
mod_tap_key2.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Tap mod-tap 3.
EXPECT_REPORT(driver, (KC_D));
mod_tap_key3.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
idle_for(FLOW_TAP_TERM + 1);
mod_tap_key3.release();
idle_for(FLOW_TAP_TERM + 1); // Pause between taps.
VERIFY_AND_CLEAR(driver);
// Tap mod-tap 1.
EXPECT_NO_REPORT(driver);
mod_tap_key1.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
idle_for(FLOW_TAP_TERM + 1);
mod_tap_key1.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Tap mod-tap 2.
EXPECT_REPORT(driver, (KC_C));
mod_tap_key2.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
idle_for(FLOW_TAP_TERM + 1);
mod_tap_key2.release();
idle_for(FLOW_TAP_TERM + 1);
VERIFY_AND_CLEAR(driver);
}
// By default, Flow Tap is disabled when mods other than Shift and AltGr are on.
TEST_F(FlowTapTest, hotkey_taps) {
TestDriver driver;
InSequence s;
auto ctrl_key = KeymapKey(0, 0, 0, KC_LCTL);
auto shft_key = KeymapKey(0, 1, 0, KC_LSFT);
auto alt_key = KeymapKey(0, 2, 0, KC_LALT);
auto gui_key = KeymapKey(0, 3, 0, KC_LGUI);
auto regular_key = KeymapKey(0, 4, 0, KC_A);
auto mod_tap_key = KeymapKey(0, 5, 0, RCTL_T(KC_B));
set_keymap({ctrl_key, shft_key, alt_key, gui_key, regular_key, mod_tap_key});
for (KeymapKey* mod_key : {&ctrl_key, &alt_key, &gui_key}) {
// Hold mod key.
EXPECT_REPORT(driver, (mod_key->code));
mod_key->press();
run_one_scan_loop();
// Tap regular key.
EXPECT_REPORT(driver, (mod_key->code, KC_A));
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (mod_key->code));
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap, where Flow Tap is disabled due to the held mod.
EXPECT_REPORT(driver, (mod_key->code, KC_RCTL));
mod_tap_key.press();
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
// Release mod-tap.
EXPECT_REPORT(driver, (mod_key->code));
mod_tap_key.release();
run_one_scan_loop();
// Release mod key.
EXPECT_EMPTY_REPORT(driver);
mod_key->release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
// Hold Shift key.
EXPECT_REPORT(driver, (KC_LSFT));
shft_key.press();
run_one_scan_loop();
// Tap regular key.
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT));
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap, where Flow Tap applies to settle as tapped.
EXPECT_REPORT(driver, (KC_LSFT, KC_B));
mod_tap_key.press();
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
// Release mod-tap.
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.release();
run_one_scan_loop();
// Release Shift key.
EXPECT_EMPTY_REPORT(driver);
shft_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
// Test input with two mod-taps in a rolled press quickly after a regular key.
TEST_F(FlowTapTest, rolled_press) {
TestDriver driver;
InSequence s;
auto regular_key = KeymapKey(0, 0, 0, KC_A);
@ -152,6 +314,53 @@ TEST_F(FlowTapTest, holding_multiple_mod_taps) {
VERIFY_AND_CLEAR(driver);
}
TEST_F(FlowTapTest, holding_mod_tap_with_regular_mod) {
TestDriver driver;
InSequence s;
auto regular_key = KeymapKey(0, 0, 0, KC_A);
auto mod_key = KeymapKey(0, 1, 0, KC_LSFT);
auto mod_tap_key = KeymapKey(0, 2, 0, CTL_T(KC_C));
set_keymap({regular_key, mod_key, mod_tap_key});
// Tap regular key.
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
tap_key(regular_key);
VERIFY_AND_CLEAR(driver);
EXPECT_NO_REPORT(driver);
idle_for(FLOW_TAP_TERM + 1);
VERIFY_AND_CLEAR(driver);
// Press mod and mod-tap keys.
EXPECT_REPORT(driver, (KC_LSFT));
mod_key.press();
run_one_scan_loop();
mod_tap_key.press();
idle_for(TAPPING_TERM - 5); // Hold almost until tapping term.
VERIFY_AND_CLEAR(driver);
// Press regular key.
EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL, KC_A));
regular_key.press();
idle_for(10);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
EXPECT_REPORT(driver, (KC_LCTL));
EXPECT_EMPTY_REPORT(driver);
regular_key.release();
run_one_scan_loop();
mod_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(FlowTapTest, layer_tap_key) {
TestDriver driver;
InSequence s;
@ -216,6 +425,125 @@ TEST_F(FlowTapTest, layer_tap_key) {
VERIFY_AND_CLEAR(driver);
}
TEST_F(FlowTapTest, layer_tap_ignored_with_disabled_key) {
TestDriver driver;
InSequence s;
auto no_key = KeymapKey(0, 0, 0, KC_NO);
auto regular_key = KeymapKey(1, 0, 0, KC_ESC);
auto layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 2, 0, CTL_T(KC_B));
set_keymap({no_key, regular_key, layer_tap_key, mod_tap_key});
EXPECT_REPORT(driver, (KC_ESC));
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
tap_key(regular_key);
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(FlowTapTest, layer_tap_ignored_with_disabled_key_complex) {
TestDriver driver;
InSequence s;
auto regular_key1 = KeymapKey(0, 0, 0, KC_Q);
auto layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_SPC));
auto mod_tap_key1 = KeymapKey(0, 2, 0, CTL_T(KC_T));
// Place RALT_T(KC_I), where Flow Tap is enabled, in the same position on
// layer 0 as KC_RGHT, where Flow Tap is disabled. This tests that Flow Tap
// tracks the keycode from the correct layer.
auto mod_tap_key2 = KeymapKey(0, 3, 0, RALT_T(KC_I));
auto regular_key2 = KeymapKey(1, 3, 0, KC_RGHT);
set_keymap({regular_key1, layer_tap_key, mod_tap_key1, mod_tap_key2, regular_key2});
// Tap regular key 1.
EXPECT_REPORT(driver, (KC_Q));
EXPECT_EMPTY_REPORT(driver);
tap_key(regular_key1);
idle_for(FLOW_TAP_TERM + 1);
VERIFY_AND_CLEAR(driver);
// Hold layer-tap key.
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
// idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
// Tap regular key 2.
EXPECT_REPORT(driver, (KC_RGHT));
EXPECT_EMPTY_REPORT(driver);
tap_key(regular_key2);
VERIFY_AND_CLEAR(driver);
// Release layer-tap key.
EXPECT_NO_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Quickly hold mod-tap key 1.
EXPECT_NO_REPORT(driver);
mod_tap_key1.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LCTL));
EXPECT_REPORT(driver, (KC_LCTL, KC_Q));
EXPECT_REPORT(driver, (KC_LCTL));
tap_key(regular_key1);
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
mod_tap_key1.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(FlowTapTest, layer_tap_ignored_with_enabled_key) {
TestDriver driver;
InSequence s;
auto no_key = KeymapKey(0, 0, 0, KC_NO);
auto regular_key = KeymapKey(1, 0, 0, KC_C);
auto layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 2, 0, CTL_T(KC_B));
set_keymap({no_key, regular_key, layer_tap_key, mod_tap_key});
EXPECT_REPORT(driver, (KC_C));
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
tap_key(regular_key);
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_B));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
idle_for(TAPPING_TERM + 1);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(FlowTapTest, combo_key) {
TestDriver driver;
InSequence s;
@ -275,6 +603,7 @@ TEST_F(FlowTapTest, oneshot_mod_key) {
// Nested press of OSM and regular keys.
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
EXPECT_EMPTY_REPORT(driver);
osm_key.press();
run_one_scan_loop();