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
4 changes: 2 additions & 2 deletions docs/docs/main/docs/configuration/behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ If you press One-Shot Modifier again, it will be sent as a normal modifier key p

The `quick_release` option controls when the one-shot modifier is released:

- `false` (default): the modifier is released when the next key is **released** (chain mode, equivalent to ZMK `&skn`). The modifier stays active for the entire duration of the next keypress, including key repeat.
- `true`: the modifier is released when the next key is **pressed** (equivalent to ZMK `&skq`). Only the initial press of the next key is modified; key repeat will not include the modifier.
- `false` (default): the modifier is included in the next key's press report and stays part of that report for as long as the key is held, including key repeat (chain mode, equivalent to ZMK `&skn`). No separate report is sent when the key is released.
- `true`: an extra report is sent right after the next key's press with the modifier removed (equivalent to ZMK `&skq`). Only the initial press of the next key is modified; key repeat will not include the modifier.

Default values:

Expand Down
10 changes: 4 additions & 6 deletions rmk/src/keyboard/oneshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,15 @@ impl<'a> Keyboard<'a> {
/// Update OSM state based on the keyboard event.
/// Returns `true` if the OSM was consumed (transitioned from Single to None).
pub(crate) fn update_osm(&mut self, event: KeyboardEvent) -> bool {
let quick_release = self.keymap.one_shot_modifiers_config().quick_release;
match self.osm_state {
OneShotState::Initial(m) => {
self.osm_state = OneShotState::Held(m);
false
}
OneShotState::Single(_) if quick_release && event.pressed => {
self.osm_state = OneShotState::None;
true
}
OneShotState::Single(_) if !quick_release && !event.pressed => {
// Consume on press so a key rolled over before this one releases doesn't
// also see the modifier (resolve_explicit_modifiers only applies `Single`
// on the pressed report anyway, so the release report is unaffected).
OneShotState::Single(_) if event.pressed => {
self.osm_state = OneShotState::None;
true
}
Expand Down
35 changes: 35 additions & 0 deletions rmk/tests/scenarios/one_shot.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ expect = [
[],
]

# Rollover: a second key goes down before the first shifted key comes up.
# `update_osm` in oneshot.rs consumes the one-shot state on that first key's
# press, so a key rolled over before the release doesn't also get shifted.
[[test]]
name = "osm_rollover_second_key_not_shifted"
steps = [
{ tap = { pos = [0, 0], duration = 10 } }, # OSM(LShift)
{ press = [0, 2] }, # Press A
{ press = [1, 3] }, # Press W before A is released (rollover)
{ release = [0, 2] },
{ release = [1, 3] },
]
expect = [
["LShift", "A"], # A gets the one-shot modifier
["A", "W"], # W does not, even though A is still held
["W"],
[],
]

# OSM released while the tap-hold key is still down: the tap still carries it.
[[test]]
name = "osm_rolling_with_tap_hold"
Expand Down Expand Up @@ -160,6 +179,22 @@ steps = [
]
expect = [["C"], []]

# A held (not tapped) OSL consumes a pending OSM at the OSL's own press, same
# as the tapped case above. The key typed under the held layer must not see
# the modifier either.
[[test]]
name = "osm_then_held_osl"
steps = [
{ tap = { pos = [0, 0], duration = 10 } }, # OSM(LShift)
{ press = [0, 1] }, # Press OSL(1), held rather than tapped
{ tap = { pos = [0, 2], duration = 10 } }, # C from layer 1
{ release = [0, 1] }, # Release OSL(1)
]
expect = [
["C"], # OSM already consumed by the OSL press
[],
]

# OSL first, so (0,0) resolves to layer 1's OSM(LShift | LCtrl); the layer is
# spent by then, so the next key comes from layer 0.
[[test]]
Expand Down
Loading