Add three new configuration options for one-shot modifiers (OSM):
- `tap_on_timeout` (default: false) — when the one-shot timeout expires
with no follow-up key, send a bare modifier tap to the host instead of
silently cancelling. Useful for triggering OS actions tied to a lone
modifier press (e.g., tapping LGui to open Start menu).
- `tap_on_double_press` (default: false) — when the same OSM key is
pressed again while one-shot is active, send a bare modifier tap and
consume the one-shot state. Provides a quick way to intentionally fire
the modifier by itself.
- `retap_cancel` (default: false) — when the same OSM key is pressed
again while one-shot is active, cancel the one-shot silently without
sending anything. When both `tap_on_double_press` and `retap_cancel`
are enabled, `retap_cancel` takes priority.
These replace the previous unconditional "unstick on re-press" behavior
(added after v0.8.2) with explicit, opt-in configuration. Users who
relied on the unstick behavior can restore it by setting
`tap_on_double_press = true`.
Also includes:
- Fix for select race condition in OSM timeout: guard against the timer
winning the select even though a key event arrived simultaneously, by
checking try_next_message_pure() before treating a timeout as genuine.
- Fix for update_osm: clear Single state on any key event (not just
release), since resolve_modifiers already included the modifier in the
HID report before update_osm runs.
- Tests for tap_on_timeout, tap_on_double_press, retap_cancel, and
stacking different OSM modifiers with tap_on_double_press enabled.
Configuration example:
```toml
[behavior.one_shot_modifiers]
tap_on_timeout = true
tap_on_double_press = true
```
Again, I'm trying to replicate all the features I was using previously on KMK. I've only got one more after this. I think I tested this fairly extensively on my own keyboard, and almost everything worked perfectly. I add this so that it is much easier to send a bare one-shot key to the OS than it was before. This allows you to double tap a one-shot key to immediately send that one-shot key to the os with no other keys. It also allows for sending the one-shot key once after tapping it when the timeout is reached. This is useful for using the gui (start) key which I couldn't use on its own to get to the start menu (on Windows and similar on Linux) while keeping the one-shot functionality. Thanks again.
Add three new configuration options for one-shot modifiers (OSM) and fix OSM interaction with mouse keys.
New OSM configuration options
All options default to
false, preserving existing behavior for current users.Re-press behavior — what happens when you press the same OSM key again while one-shot is active:
tap_on_double_press— send a bare modifier tap to the host and consume the one-shot state. Useful for intentionally firing the modifier by itself (e.g., tapping LGui twice to open the Start menu).retap_cancel— cancel the one-shot silently without sending anything. Takes priority overtap_on_double_pressif both are enabled.Timeout behavior:
tap_on_timeout— when the one-shot timeout expires with no follow-up key, send a bare modifier tap instead of silently cancelling. Useful for triggering OS actions tied to a lone modifier press.Behavior change: unconditional unstick-on-repress removed
The
mainbranch (post-v0.8.2) added unconditional logic inprocess_action_osm()where re-pressing the same OSM key always removed the matching modifier bits and sent a report pair. This PR replaces that unconditional behavior with the explicittap_on_double_pressandretap_canceloptions above.Since the unstick behavior was added after v0.8.2 and has not been in a published release, this should not affect any released users. Users who adopted the behavior from
maincan restore it by addingtap_on_double_press = trueto their config.Bug fixes
OSM + mouse click: Mouse HID reports don't carry keyboard modifier bits. When an OSM modifier was active and a mouse button was pressed, the host received the click without modifier context, so combinations like Ctrl+Click didn't work. Fixed by sending a keyboard report with resolved OSM modifiers before the mouse report in
process_action_mouse.Select race in OSM timeout: Guard against the
selectrace where the timer wins even though a key event arrived at the subscriber simultaneously. Now checkstry_next_message_pure()before treating a timeout as genuine.update_osm timing: Clear
Singlestate on any key event (press or release), not just release. Sinceresolve_modifiersruns beforeupdate_osm, the modifier is already included in the HID report by the timeupdate_osmexecutes, so clearing on press prevents the state from lingering between key press and release.Tests
Four new tests covering the configurable behaviors:
test_osm_tap_on_timeout— bare modifier tap on timeout expirytest_osm_tap_on_double_press— bare modifier tap on re-presstest_osm_retap_cancel— silent cancellation on re-presstest_osm_double_press_different_modifier_still_stacks— stacking different OSM modifiers still works withtap_on_double_pressenabledContext
I'm using a Sofle V2 split keyboard with sticky modifier keys (OSM) and needed these behaviors to match what I had in KMK firmware (
KC.SKwithretap_cancel=Trueand timeout tap). The mouse fix was discovered during daily use — Ctrl+Click for opening links in new tabs wasn't working with OSM-Ctrl.Changes
rmk/src/keyboard/oneshot.rs— core OSM state machine changesrmk/src/keyboard.rs— mouse + OSM modifier fixrmk/src/config/behavior.rs— new config struct fieldsrmk-config/src/lib.rs— TOML deserializationrmk-config/src/resolved/behavior.rs— resolved configrmk-macro/src/codegen/behavior.rs— proc macro codegenrmk/tests/keyboard_one_shot_test.rs— 4 new testsdocs/docs/main/docs/configuration/behavior.md— user-facing docsdocs/docs/main/docs/configuration/appendix.md— full config referenceMinor Issue with Windows
The one "issue" is something that I'm pretty sure is an OS issue (with Windows gui+arrow keys) anyway. It works normally but when tapping (once) the one-shot gui key then layer key (MO) then tap an arrow key with another tap. It sends the correct combination but it also sends the gui key (seemingly independently). I realized that my KMK firmware also acted weird when doing this as well. This does not occur with any other key as far as I can tell (e.g. I tried using the exact same sequence but with the
akey and it worked perfectly).So again the only reason I came across it was due to trying to be comprehensive in my testing so it doesn't effect me but I figured I would be transparent. I tired to fix it for a while but couldn't figure out anything that worked. If anyone wants to take a stab at it feel free.