Requirements
Describe the bug
When a one-shot-release modifier is activated by pressing key K on layer L1 (where K is mapped to the one-shot action), then the active layer changes to L2 (where K is mapped to a regular key), pressing and releasing K on L2 does not terminate the one-shot. The modifier remains active across all subsequent keypresses until either (a) a key whose keycode differs from the activator's is released, or (b) the timeout fires.
The bug affects both EndOnFirstRelease (one-shot-release) and EndOnFirstReleaseOrRepress (one-shot-release-pcancel). EndOnFirstPress (one-shot) and EndOnFirstPressOrRepress (one-shot-press-pcancel) are not affected.
Root cause
OneShotState::handle_release in keyberon/src/layout.rs (~line 1100):
fn handle_release(&mut self, (i, j): KCoord) -> (bool, Option<KCoord>) {
if self.keys.is_empty() {
return (true, None);
}
if !self.keys.contains(&(i, j)) {
if matches!(
self.end_config,
OneShotEndConfig::EndOnFirstRelease | OneShotEndConfig::EndOnFirstReleaseOrRepress
) && self.other_pressed_keys.contains(&(i, j))
{
self.release_on_next_tick = true;
}
(true, None)
} else {
// delay release for one shot keys
(false, self.released_keys.push_back((i, j)))
}
}
When the user re-presses the activator's physical key on a different layer where it produces a normal key event:
handle_press(Other(coord_D)) correctly pushes coord_D into other_pressed_keys.
- But
keys (the activator-tracking list) still contains coord_D from the original activation; it's only cleared by tick_osh on timeout/explicit termination.
So at release time both lists contain coord_D. The current ordering checks keys.contains(&(i, j)) first, which is true, so execution falls into the else branch ("delay release for one shot keys") at line 1115 and the other_pressed_keys check at line 1108 is never reached. Termination doesn't fire.
For EndOnFirstPress the equivalent termination check is in handle_press(Other(...)) (around line 1081), which doesn't consult keys membership and triggers correctly even when the second press's coord matches the activator's. That's why EndOnFirstPress is not affected.
Suggested fix
In EndOnFirstRelease mode, prefer the other_pressed_keys check over the keys membership check. A release that satisfies "non-oneshot key was pressed after activation" should win over "this coord is also tracked as the activator":
fn handle_release(&mut self, (i, j): KCoord) -> (bool, Option<KCoord>) {
if self.keys.is_empty() {
return (true, None);
}
// A coord may legitimately appear in both `keys` (as the original activator)
// and `other_pressed_keys` (if the same physical key was re-pressed for a
// non-oneshot action after the activator's lifecycle began, on a different
// layer). In that case, the release should terminate the one-shot, not be
// filtered as "activator release."
if matches!(
self.end_config,
OneShotEndConfig::EndOnFirstRelease | OneShotEndConfig::EndOnFirstReleaseOrRepress
) && self.other_pressed_keys.contains(&(i, j))
{
self.release_on_next_tick = true;
return (true, None);
}
if !self.keys.contains(&(i, j)) {
(true, None)
} else {
(false, self.released_keys.push_back((i, j)))
}
}
Relevant kanata config
(defcfg
process-unmapped-keys yes
)
(defsrc
tab d
)
(deflayer base
(layer-while-held nav) d
)
(deflayer nav
_ (one-shot-release 2000 lalt)
)
To Reproduce
Trace 1: Single re-press
Sequence: hold tab, press d, release d, release tab, press d, release d.
TAB Press
D Press → key press LAlt (one-shot active)
D Release → (no LAlt release; activator)
TAB Release → (no emission)
D Press → key press D (D emitted with LAlt held)
D Release → key release D
→ *** no LAlt release ***
~2s elapse → key release LAlt (timeout fired)
lalt is released only by the 2000ms timeout, not by d's release.
Trace 2: Modifier leak across non-activator keys
Sequence: hold tab, press d, release d, release tab, press d, release d, press p, release p.
TAB Press
D Press → key press LAlt
D Release
TAB Release
D Press → key press D (with LAlt held)
D Release → key release D (no LAlt release — BUG)
P Press → key press P (with LAlt held — UNINTENDED)
P Release → key release P
→ key release LAlt (terminates here)
The unrelated p keypress receives the modifier even though the user clearly intended only alt+d. The modifier persists across every keypress between the bogus d release and the next non-activator-keycode release.
Trace 3: Same scenario with one-shot (EndOnFirstPress) instead
Same minimal config, but with (one-shot 2000 lalt) substituted for (one-shot-release 2000 lalt).
Same sequence as Trace 2: hold tab, d, release, release tab, d, release, p, release.
Result: only the first key after activation receives the modifier. p is plain.
EndOnFirstPress terminates the one-shot in handle_press (when the second d is pressed), not in handle_release, and does not consult keys membership for that termination, so the bug doesn't manifest. The bug is exclusive to release-path termination, which pins it to handle_release rather than to one-shot tracking generally.
Expected behavior
With one-shot-release activated, the modifier should remain active until the next non-activator key is released, then deactivate. Pressing and releasing the activator's physical keycode after the activating layer is gone (i.e., as a normal key event on a different layer) should terminate the one-shot just like any other release would.
Kanata version
1.10.1 and 1.12.0-prerelease-2
Debug logs
Operating system and I/O mechanism
macOS 26.4.1 (Apple Silicon, ARM64)
Additional context
Discussion #1218: Same root mechanism, narrower symptom (a one-shot accent layer where the activator was re-pressed on the secondary layer). Resolved with a (unicode <char>) workaround. The underlying bug was not addressed.
Requirements
Describe the bug
When a
one-shot-releasemodifier is activated by pressing keyKon layerL1(whereKis mapped to the one-shot action), then the active layer changes toL2(whereKis mapped to a regular key), pressing and releasingKonL2does not terminate the one-shot. The modifier remains active across all subsequent keypresses until either (a) a key whose keycode differs from the activator's is released, or (b) the timeout fires.The bug affects both
EndOnFirstRelease(one-shot-release) andEndOnFirstReleaseOrRepress(one-shot-release-pcancel).EndOnFirstPress(one-shot) andEndOnFirstPressOrRepress(one-shot-press-pcancel) are not affected.Root cause
OneShotState::handle_releaseinkeyberon/src/layout.rs(~line 1100):When the user re-presses the activator's physical key on a different layer where it produces a normal key event:
handle_press(Other(coord_D))correctly pushescoord_Dintoother_pressed_keys.keys(the activator-tracking list) still containscoord_Dfrom the original activation; it's only cleared bytick_oshon timeout/explicit termination.So at release time both lists contain
coord_D. The current ordering checkskeys.contains(&(i, j))first, which is true, so execution falls into theelsebranch ("delay release for one shot keys") at line 1115 and theother_pressed_keyscheck at line 1108 is never reached. Termination doesn't fire.For
EndOnFirstPressthe equivalent termination check is inhandle_press(Other(...))(around line 1081), which doesn't consultkeysmembership and triggers correctly even when the second press's coord matches the activator's. That's whyEndOnFirstPressis not affected.Suggested fix
In
EndOnFirstReleasemode, prefer theother_pressed_keyscheck over thekeysmembership check. A release that satisfies "non-oneshot key was pressed after activation" should win over "this coord is also tracked as the activator":Relevant kanata config
To Reproduce
Trace 1: Single re-press
Sequence: hold
tab, pressd, released, releasetab, pressd, released.laltis released only by the 2000ms timeout, not byd's release.Trace 2: Modifier leak across non-activator keys
Sequence: hold
tab, pressd, released, releasetab, pressd, released, pressp, releasep.The unrelated
pkeypress receives the modifier even though the user clearly intended onlyalt+d. The modifier persists across every keypress between the bogusdrelease and the next non-activator-keycode release.Trace 3: Same scenario with
one-shot(EndOnFirstPress) insteadSame minimal config, but with
(one-shot 2000 lalt)substituted for(one-shot-release 2000 lalt).Same sequence as Trace 2: hold
tab,d, release, releasetab,d, release,p, release.Result: only the first key after activation receives the modifier.
pis plain.EndOnFirstPressterminates the one-shot inhandle_press(when the seconddis pressed), not inhandle_release, and does not consultkeysmembership for that termination, so the bug doesn't manifest. The bug is exclusive to release-path termination, which pins it tohandle_releaserather than to one-shot tracking generally.Expected behavior
With
one-shot-releaseactivated, the modifier should remain active until the next non-activator key is released, then deactivate. Pressing and releasing the activator's physical keycode after the activating layer is gone (i.e., as a normal key event on a different layer) should terminate theone-shotjust like any other release would.Kanata version
1.10.1 and 1.12.0-prerelease-2
Debug logs
Operating system and I/O mechanism
macOS 26.4.1 (Apple Silicon, ARM64)
Additional context
Discussion #1218: Same root mechanism, narrower symptom (a one-shot accent layer where the activator was re-pressed on the secondary layer). Resolved with a
(unicode <char>)workaround. The underlying bug was not addressed.