From d4275701b792cad7d9b680ed3c57d14697a60f0a Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 10 Mar 2026 22:01:47 +1300 Subject: [PATCH 1/2] fix: stale keys playing when pressing mod layer keys --- behaviors/behavior_os_layer_mod.c | 7 ++ run-tests | 65 +++++++++++-------- .../events.patterns | 1 + .../keycode_events.snapshot | 4 ++ .../native_posix_64.keymap | 27 ++++++++ 5 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/events.patterns create mode 100644 tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/keycode_events.snapshot create mode 100644 tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/native_posix_64.keymap diff --git a/behaviors/behavior_os_layer_mod.c b/behaviors/behavior_os_layer_mod.c index 3d8b88b..48f30a3 100644 --- a/behaviors/behavior_os_layer_mod.c +++ b/behaviors/behavior_os_layer_mod.c @@ -204,6 +204,10 @@ static void os_layer_mod_work_handler(struct k_work *work) { } } else { do_hold(olm); + /* Replay any events captured before the timer fired — they + * must be processed under the now-active layer, and won't be + * replayed by the release path (which sees OLM_HOLD). */ + release_captured_events(); } } } @@ -263,6 +267,9 @@ static int on_os_layer_mod_binding_released(struct zmk_behavior_binding *binding * modifier, dismissing the OS app-switcher menu. */ zmk_keymap_layer_deactivate(olm->layer); zmk_behavior_invoke_binding(&olm->mod_binding, event, false); + /* Safety: discard any captured events that were not replayed + * (e.g. timer fired while an interrupt key was still down). */ + captured_events_count = 0; } olm->active = false; diff --git a/run-tests b/run-tests index b77f205..359ebc5 100755 --- a/run-tests +++ b/run-tests @@ -3,15 +3,26 @@ set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -# Resolve the test path to an absolute path -if [ -n "$1" ]; then - case "$1" in - /*) TEST_PATH="$1" ;; - *) TEST_PATH="$SCRIPT_DIR/$1" ;; - esac -else - TEST_PATH="$SCRIPT_DIR/tests/oskey/" -fi +run_suite() { + TEST_PATH="$1" + DOCKER_TEST_PATH="/workspace${TEST_PATH#$SCRIPT_DIR}" + docker run --rm \ + -v "$SCRIPT_DIR:/workspace" \ + -e "ZMK_TEST_PATH=$DOCKER_TEST_PATH" \ + -e ZMK_TESTS_AUTO_ACCEPT \ + zmkfirmware/zmk-build-arm:3.5 \ + bash -c ' + git config --global --add safe.directory "*" + set -e + cd /workspace/tests + cmake -P /workspace/tests/zephyr/share/zephyr-package/cmake/zephyr_export.cmake + cmake -P /workspace/tests/zephyr/share/zephyrunittest-package/cmake/zephyr_export.cmake + ZEPHYR_TOOLCHAIN_VARIANT=host \ + ZMK_EXTRA_MODULES=/workspace \ + ZMK_SRC_DIR=/workspace/tests/zmk/app \ + /workspace/tests/zmk/app/run-test.sh "$ZMK_TEST_PATH" + ' +} if ! command -v docker >/dev/null 2>&1; then echo "Error: Docker is required to run tests." @@ -19,20 +30,22 @@ if ! command -v docker >/dev/null 2>&1; then exit 1 fi -DOCKER_TEST_PATH="/workspace${TEST_PATH#$SCRIPT_DIR}" -exec docker run --rm \ - -v "$SCRIPT_DIR:/workspace" \ - -e "ZMK_TEST_PATH=$DOCKER_TEST_PATH" \ - -e ZMK_TESTS_AUTO_ACCEPT \ - zmkfirmware/zmk-build-arm:3.5 \ - bash -c ' - git config --global --add safe.directory "*" - set -e - cd /workspace/tests - cmake -P /workspace/tests/zephyr/share/zephyr-package/cmake/zephyr_export.cmake - cmake -P /workspace/tests/zephyr/share/zephyrunittest-package/cmake/zephyr_export.cmake - ZEPHYR_TOOLCHAIN_VARIANT=host \ - ZMK_EXTRA_MODULES=/workspace \ - ZMK_SRC_DIR=/workspace/tests/zmk/app \ - /workspace/tests/zmk/app/run-test.sh "$ZMK_TEST_PATH" - ' +if [ -n "$1" ]; then + # Single path supplied — resolve and run it directly. + case "$1" in + /*) TEST_PATH="$1" ;; + *) TEST_PATH="$SCRIPT_DIR/$1" ;; + esac + run_suite "$TEST_PATH" +else + # No argument — run every test suite. + FAIL=0 + for suite in "$SCRIPT_DIR"/tests/*/; do + # Skip non-suite directories (no test cases inside). + if ! find "$suite" -name "native_posix_64.keymap" | grep -q .; then + continue + fi + run_suite "$suite" || FAIL=1 + done + exit $FAIL +fi diff --git a/tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/events.patterns b/tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/events.patterns new file mode 100644 index 0000000..b1342af --- /dev/null +++ b/tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/keycode_events.snapshot b/tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/keycode_events.snapshot new file mode 100644 index 0000000..47b48d1 --- /dev/null +++ b/tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0xE2 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x17 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x17 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0xE2 implicit_mods 0x00 explicit_mods 0x00 diff --git a/tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/native_posix_64.keymap b/tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/native_posix_64.keymap new file mode 100644 index 0000000..356ad3b --- /dev/null +++ b/tests/os-layer-mod/balanced-timer-fires-while-interrupt-held/native_posix_64.keymap @@ -0,0 +1,27 @@ +#include "../behavior_keymap.dtsi" + +/* Set flavor to balanced. */ +&ok_ltm_test { + flavor = "balanced"; +}; + +/* Default OS = Windows. Hold (1,1), press (0,0) as an interrupt key, then + * let the tapping-term timer expire while (0,0) is STILL held. + * + * Before the fix, the captured (0,0) key-down event was never replayed when + * the timer triggered the hold — it stayed in the buffer and leaked into the + * next interaction. After the fix, release_captured_events() is called + * immediately after do_hold() in the timer handler. + * + * Position (0,0) maps to &kp T on app_switch_layer (layer 1), so after the + * hold fires the replayed key-down should produce a T press. The subsequent + * (0,0) key-up arrives while layer 1 is still active, producing a T release. + * Releasing (1,1) then deactivates the layer and releases Left Alt. */ +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,50) + ZMK_MOCK_PRESS(0,0,300) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(1,1,10) + >; +}; From 27c0b9f6dd6d92ea7dc0cd487a79dbfe47316372 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 10 Mar 2026 22:28:22 +1300 Subject: [PATCH 2/2] Fixed test script (don't run all the ZMK tests) --- run-tests | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/run-tests b/run-tests index 359ebc5..ca0dc92 100755 --- a/run-tests +++ b/run-tests @@ -30,22 +30,17 @@ if ! command -v docker >/dev/null 2>&1; then exit 1 fi -if [ -n "$1" ]; then - # Single path supplied — resolve and run it directly. - case "$1" in - /*) TEST_PATH="$1" ;; - *) TEST_PATH="$SCRIPT_DIR/$1" ;; - esac - run_suite "$TEST_PATH" -else - # No argument — run every test suite. - FAIL=0 - for suite in "$SCRIPT_DIR"/tests/*/; do - # Skip non-suite directories (no test cases inside). - if ! find "$suite" -name "native_posix_64.keymap" | grep -q .; then - continue - fi - run_suite "$suite" || FAIL=1 - done - exit $FAIL +SUITES="$*" +if [ -z "$SUITES" ]; then + SUITES="oskey os-layer-mod" fi + +FAIL=0 +for suite in $SUITES; do + case "$suite" in + /*) TEST_PATH="$suite" ;; + *) TEST_PATH="$SCRIPT_DIR/tests/$suite" ;; + esac + run_suite "$TEST_PATH" || FAIL=1 +done +exit $FAIL