Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions quantum/action.c
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,8 @@ void process_action(keyrecord_t *record, action_t action) {
#endif

#ifndef NO_ACTION_ONESHOT
bool do_release_oneshot = false;
// Only actually consumed below when STRICT_LAYER_RELEASE is defined; see the comment there for why.
__attribute__((unused)) bool do_release_oneshot = false;
// notice we only clear the one shot layer if the pressed key is not a modifier.
if (is_oneshot_layer_active() && event.pressed &&
(action.kind.id == ACT_USAGE || !(IS_MODIFIER_KEYCODE(action.key.code)
Expand Down Expand Up @@ -902,9 +903,18 @@ void process_action(keyrecord_t *record, action_t action) {
# endif
#endif

#ifndef NO_ACTION_ONESHOT
#if !defined(NO_ACTION_ONESHOT) && defined(STRICT_LAYER_RELEASE)
/* Because we switch layers after a oneshot event, we need to release the
* key before we leave the layer or no key up event will be generated.
*
* This is only necessary when STRICT_LAYER_RELEASE is enabled, i.e. when
* store_or_get_action() (quantum/action_layer.c) is NOT using the source
* layers cache to resolve the release action from the layer that was
* active at press time. When the cache is in use (the default), the
* later physical release of this key is already correctly resolved
* against the (now inactive) one-shot layer, so replaying a release
* here would just report a duplicate, premature release event for a
* key that is still physically held down.
*/
if (do_release_oneshot && !(get_oneshot_layer_state() & ONESHOT_PRESSED)) {
record->event.pressed = false;
Expand Down
12 changes: 4 additions & 8 deletions tests/basic/test_one_shot_keys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,12 @@ TEST_F(OneShot, OSLWithAdditionalKeypress) {

/* Press regular key */
EXPECT_REPORT(driver, (regular_key.report_code)).Times(1);
EXPECT_EMPTY_REPORT(driver);
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);

/* Release regular key */
EXPECT_NO_REPORT(driver);
EXPECT_EMPTY_REPORT(driver);
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
Expand Down Expand Up @@ -341,14 +340,13 @@ TEST_F(OneShot, OSLWithOsmAndAdditionalKeypress) {

/* Press regular key */
EXPECT_REPORT(driver, (osm_key.report_code, regular_key.report_code)).Times(1);
EXPECT_EMPTY_REPORT(driver);
regular_key.press();
run_one_scan_loop();
EXPECT_FALSE(layer_state_is(1));
VERIFY_AND_CLEAR(driver);

/* Release regular key */
EXPECT_NO_REPORT(driver);
EXPECT_EMPTY_REPORT(driver);
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
Expand Down Expand Up @@ -521,13 +519,12 @@ TEST_F(OneShot, OSLChainingTwoOSLsAndAdditionalKeypress) {

/* Press regular key */
EXPECT_REPORT(driver, (regular_key.report_code)).Times(1);
EXPECT_EMPTY_REPORT(driver);
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);

/* Release regular key */
EXPECT_NO_REPORT(driver);
EXPECT_EMPTY_REPORT(driver);
regular_key.release();
run_one_scan_loop();
EXPECT_TRUE(layer_state_is(0));
Expand Down Expand Up @@ -676,13 +673,12 @@ TEST_F(OneShot, OSLWithLongModTapKeyAndRegularKey) {

/* Press regular key. */
EXPECT_REPORT(driver, (regular_key.report_code)).Times(1);
EXPECT_EMPTY_REPORT(driver);
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);

/* Release regular key. */
EXPECT_NO_REPORT(driver);
EXPECT_EMPTY_REPORT(driver);
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
Expand Down
97 changes: 97 additions & 0 deletions tests/basic/test_one_shot_layer_release_event.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* Copyright 2026 QMK
*
* 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/>.
*/

#include <functional>

#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"

namespace {

bool process_record_user_default(uint16_t keycode, keyrecord_t* record) {
return true;
}

// Indirection so that process_record_user() can be replaced per test case.
std::function<bool(uint16_t, keyrecord_t*)> process_record_user_fun = process_record_user_default;

} // namespace

extern "C" bool process_record_user(uint16_t keycode, keyrecord_t* record) {
return process_record_user_fun(keycode, record);
}

class OneShotLayerReleaseEvent : public TestFixture {
public:
void SetUp() override {
process_record_user_fun = process_record_user_default;
}
};

// Regression test for https://github.com/qmk/qmk_firmware/issues/26309
//
// When a regular (non-modifier) key press clears an active one-shot layer,
// process_record_user() must see exactly one press event followed by
// exactly one release event for that key: one for the physical press, and
// one for the physical release. It must NOT be reported as released while
// the key is still physically held down, and it must NOT be reported as
// released a second time when it is actually released.
TEST_F(OneShotLayerReleaseEvent, RegularKeyClearingOslIsNotReleasedTwice) {
TestDriver driver;
KeymapKey osl_key = KeymapKey{0, 0, 0, OSL(1)};
KeymapKey regular_key = KeymapKey{1, 1, 0, KC_A};

set_keymap({osl_key, regular_key});

int press_count = 0;
int release_count = 0;
process_record_user_fun = [&](uint16_t keycode, keyrecord_t* record) {
if (keycode == regular_key.code) {
if (record->event.pressed) {
press_count++;
} else {
release_count++;
}
}
return true;
};

/* Tap the OSL key to arm the one-shot layer. */
tap_key(osl_key);

/* Press the regular key: this is the very key press that clears the
* armed one-shot layer. */
regular_key.press();
run_one_scan_loop();

/* The key is still physically held down at this point. It must have
* been reported to process_record_user() as pressed exactly once, and
* must NOT already have been reported as released. */
EXPECT_EQ(press_count, 1);
EXPECT_EQ(release_count, 0);

/* Now physically release the key. */
regular_key.release();
run_one_scan_loop();

/* The key must now have been reported as released exactly once (not
* twice, as happened before this bug was fixed). */
EXPECT_EQ(press_count, 1);
EXPECT_EQ(release_count, 1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,12 @@ TEST_F(OneShot, OSLWithAdditionalKeypress) {

// Press regular key.
EXPECT_REPORT(driver, (regular_key1.report_code));
EXPECT_EMPTY_REPORT(driver);
regular_key1.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);

// Release regular key.
EXPECT_NO_REPORT(driver);
EXPECT_EMPTY_REPORT(driver);
regular_key1.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,12 @@ TEST_F(OneShot, OSLWithAdditionalKeypress) {

// Press regular key.
EXPECT_REPORT(driver, (regular_key1.report_code));
EXPECT_EMPTY_REPORT(driver);
regular_key1.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);

// Release regular key.
EXPECT_NO_REPORT(driver);
EXPECT_EMPTY_REPORT(driver);
regular_key1.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,12 @@ TEST_F(OneShot, OSLWithAdditionalKeypress) {

// Press regular key.
EXPECT_REPORT(driver, (regular_key1.report_code));
EXPECT_EMPTY_REPORT(driver);
regular_key1.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);

// Release regular key.
EXPECT_NO_REPORT(driver);
EXPECT_EMPTY_REPORT(driver);
regular_key1.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,13 @@ TEST_F(OneShotLayerModTap, tap_regular_key_while_mod_tap_key_is_held_tapping_ter

/* Press regular key */
EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_1));
EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
regular_key1.press();
run_one_scan_loop();
expect_layer_state(0);
testing::Mock::VerifyAndClearExpectations(&driver);

/* Release regular key */
EXPECT_NO_REPORT(driver);
EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
regular_key1.release();
run_one_scan_loop();
testing::Mock::VerifyAndClearExpectations(&driver);
Expand Down Expand Up @@ -191,8 +190,8 @@ TEST_F(OneShotLayerModTap, tap_regular_key_while_mod_tap_key_is_held) {

/* Release mod-tap-hold key */
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_0));
EXPECT_REPORT(driver, (KC_A, KC_0));
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
mod_tap_hold_key.release();
run_one_scan_loop();
Expand Down Expand Up @@ -238,8 +237,8 @@ TEST_F(OneShotLayerModTap, tap_a_mod_tap_key_while_another_mod_tap_key_is_held)

/* Release first mod-tap-hold key */
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_0));
EXPECT_REPORT(driver, (KC_A, KC_0));
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
first_mod_tap_hold_key.release();
run_one_scan_loop();
Expand Down