Regression
Fast double-taps on Shift do not enable Caps Lock. This is still reproducible in v4.1.8.
The regression was introduced by ec1de5fa84fd819b93e3ba904f17445f7424f61f (feat: tune double-tap shift timing and keep llamacpp proguard), which changed KeyboardState.onPressShift() to require both an active double-tap timer and now - lastShiftPressTime > 100.
That 100 ms minimum was intended to address the single-tap Caps Lock failures reported in #186 and #188, but it rejects legitimate fast double-taps. It also does not mechanically solve duplicate press delivery: a second press event without a release can still lock Caps after 100 ms.
Deterministic reproduction
A KeyboardStateTest using the real onPressKey/onReleaseKey sequence, a fake clock, and a controllable 300 ms timer produces these pre-fix results:
fastDoubleTapShiftLocksCaps (50 ms between distinct taps): fails
ordinaryDoubleTapShiftWithinTimerLocksCaps (150 ms): passes
singleTapShiftStaysTemporarilyShifted: passes
duplicateShiftPressWithoutReleaseDoesNotLockCaps (150 ms between DOWN events, no UP): fails
Pre-fix targeted result: 5 tests, 2 failed. After the patch below, the complete KeyboardStateTest passes, including lock-then-single-tap unlock after the timer window. The broader fork gate also reports no new failures across 369 tests.
Suggested patch
A double-tap should be identified by the invariant that two presses have an intervening release boundary, while the existing TimerHandler supplies the maximum 300 ms window:
// A second tap must have a release boundary; repeated press events are not double taps.
if (!shiftKeyState.isReleasing) return
isInDoubleTapShiftKey = switchActions.isInDoubleTapShiftKeyTimeout
This replaces the SystemClock/lastShiftPressTime minimum check and removes lastShiftPressTime. It restores legitimate sub-100 ms double-taps while preserving the purpose of #186/#188: duplicate pointer-down/key-repeat delivery without a release cannot become Caps Lock.
Regression
Fast double-taps on Shift do not enable Caps Lock. This is still reproducible in v4.1.8.
The regression was introduced by
ec1de5fa84fd819b93e3ba904f17445f7424f61f(feat: tune double-tap shift timing and keep llamacpp proguard), which changedKeyboardState.onPressShift()to require both an active double-tap timer andnow - lastShiftPressTime > 100.That 100 ms minimum was intended to address the single-tap Caps Lock failures reported in #186 and #188, but it rejects legitimate fast double-taps. It also does not mechanically solve duplicate press delivery: a second press event without a release can still lock Caps after 100 ms.
Deterministic reproduction
A
KeyboardStateTestusing the realonPressKey/onReleaseKeysequence, a fake clock, and a controllable 300 ms timer produces these pre-fix results:fastDoubleTapShiftLocksCaps(50 ms between distinct taps): failsordinaryDoubleTapShiftWithinTimerLocksCaps(150 ms): passessingleTapShiftStaysTemporarilyShifted: passesduplicateShiftPressWithoutReleaseDoesNotLockCaps(150 ms between DOWN events, no UP): failsPre-fix targeted result: 5 tests, 2 failed. After the patch below, the complete
KeyboardStateTestpasses, including lock-then-single-tap unlock after the timer window. The broader fork gate also reports no new failures across 369 tests.Suggested patch
A double-tap should be identified by the invariant that two presses have an intervening release boundary, while the existing
TimerHandlersupplies the maximum 300 ms window:This replaces the
SystemClock/lastShiftPressTimeminimum check and removeslastShiftPressTime. It restores legitimate sub-100 ms double-taps while preserving the purpose of #186/#188: duplicate pointer-down/key-repeat delivery without a release cannot become Caps Lock.