Skip to content

feat: add StickyMod (SM) action for Alt+Tab-style modifier cycling#859

Open
ldsands wants to merge 92 commits into
HaoboGu:mainfrom
ldsands:feat/sticky-mod
Open

feat: add StickyMod (SM) action for Alt+Tab-style modifier cycling#859
ldsands wants to merge 92 commits into
HaoboGu:mainfrom
ldsands:feat/sticky-mod

Conversation

@ldsands

@ldsands ldsands commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new SM(key, modifier) action — StickyMod — that holds a modifier
across repeated presses of the same key, then releases it automatically when
any non-SM, non-modifier key is pressed, the active layer changes, or an
optional timeout expires.

Primary use case: Alt+Tab window/tab cycling. Bind SM(Tab, LAlt) to a
key; the first press sends Alt+Tab, subsequent presses send Tab (Alt stays
held), and Alt releases as soon as you press any other key or switch layers.

Motivation

This is a direct port of the KC.SK() (Sticky Key) behavior from KMK firmware.
For those migrating from KMK, SM(Tab, LAlt) replicates KC.SK(KC.LALT) used
in conjunction with Tab for Alt+Tab cycling. This was the last regularly-used
KMK feature I needed in order to fully replicate my KMK keymap in RMK
(though others may find additional gaps).

This feature covers similar ground to #724 (Tabber), which I was aware of
before writing this implementation but found didn't quite fit my needs — I
preferred this approach because it generalizes to any key+modifier combination
rather than being Tab-specific, and includes timeout support. That said, I
have no attachment to the name SM or StickyMod — happy to rename this to
whatever fits best if this is merged.

How it differs from OSM

Behavior OSM(mod) SM(key, mod)
Modifier release After the next single keypress After any non-SM/non-modifier press, or layer change, or timeout
Key bundled No — modifier only Yes — modifier + key in one action
Repeatable cycling No Yes

Behavior details

  • First press: activates SM state, sends modifier + key
  • Release: unregisters key, modifier stays held
  • Subsequent presses: modifier already active, sends key again; timeout resets
  • Release triggers: any non-SM, non-modifier keypress; any layer deactivation; timeout expiry
  • Modifier exclusion: Modifier actions and HID modifier keycodes (Shift, Ctrl, etc.)
    do not release SM — this lets Shift+Tab work for reverse cycling

Optional timeout

Timeout is measured from the last SM press — repeated presses extend the
hold window. Implemented via the main run() loop deadline (same pattern as
mouse repeat), so it fires reliably regardless of how many press/release
cycles have occurred.

[behavior.sticky_mod]
timeout = "5s"   # auto-release modifier after 5s since last SM press

Default: no timeout (modifier held until released by keypress or layer change).

TOML syntax

SM(Tab, LAlt)           # Alt+Tab cycling
SM(Tab, LCtrl)          # Ctrl+Tab cycling
SM(Tab, LCtrl|LShift)   # Ctrl+Shift+Tab (reverse cycling)

Changes

  • rmk-types: new Action::StickyMod(KeyCode, ModifierCombination) variant
  • rmk: new keyboard/sticky_mod.rs module — StickyModState with optional deadline, state machine, and processing logic
  • rmk: integrated into keyboard.rs — dispatch, modifier resolution, release triggers on layer deactivation, deadline-based timeout in run() loop
  • rmk-config: TOML grammar (keymap.pest) and parser for SM(key, mod) syntax
  • rmk-macro: codegen support for SM in action_parser.rs and behavior.rs
  • rmk: sm!() macro in layout_macro.rs
  • docs: behavior.md (Sticky Modifiers section), layout.md (SM syntax entry)

Tests

7 integration tests in rmk/tests/keyboard_sticky_mod_test.rs:

  1. Basic flow: press SM twice while MO held
  2. Layer change cleanup: MO release triggers SM release
  3. Shift integration: Shift key does not release SM (enables reverse cycling)
  4. Rapid presses: 3× SM press/release cycle
  5. Combined modifiers: LCtrl|LShift combination
  6. Timeout: modifier auto-releases after inactivity
  7. Timeout reset: pressing SM again extends the timeout window

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown

Size Report

Example main PR Diff .text .data .bss
use_config/nrf52832_ble 394.4 KiB 398.7 KiB +1.08% ⬆️ +3508 0 +880
use_config/nrf52840_ble 443.5 KiB 450.1 KiB +1.48% ⬆️ +2776 0 +3960
use_config/nrf52840_ble_split (central) 493.2 KiB 496.6 KiB +0.68% ⬆️ +2568 0 +880
use_config/nrf52840_ble_split (peripheral) 317.3 KiB 318.0 KiB +0.22% ⬆️ +744 0 0
use_config/pi_pico_w_ble 667.9 KiB 671.9 KiB +0.59% ⬆️ +3180 0 +880
use_config/rp2040 149.3 KiB 153.2 KiB +2.55% ⬆️ +3032 0 +880
use_config/rp2040_split (central) 162.8 KiB 166.6 KiB +2.28% ⬆️ +2928 0 +880
use_config/rp2040_split (peripheral) 27.8 KiB 27.8 KiB +0.00% 0 0 0
use_config/stm32f1 63.1 KiB 65.8 KiB +4.19% ⬆️ +1840 0 +872
use_config/stm32h7 100.2 KiB 103.4 KiB +3.25% ⬆️ +2428 0 +912
use_rust/nrf52832_ble 382.4 KiB 385.5 KiB +0.82% ⬆️ +2360 0 +880
use_rust/nrf52840_ble 439.8 KiB 444.9 KiB +1.16% ⬆️ +2108 0 +3152
use_rust/nrf52840_ble_split (central) 503.3 KiB 507.6 KiB +0.86% ⬆️ +2728 0 +1744
use_rust/nrf52840_ble_split (peripheral) 313.7 KiB 314.5 KiB +0.24% ⬆️ +800 0 0
use_rust/pi_pico_w_ble 668.2 KiB 672.0 KiB +0.57% ⬆️ +3036 0 +880
use_rust/rp2040 148.7 KiB 152.9 KiB +2.77% ⬆️ +3340 0 +880
use_rust/rp2040_split (central) 161.5 KiB 165.4 KiB +2.41% ⬆️ +3120 0 +880
use_rust/rp2040_split (peripheral) 28.2 KiB 28.2 KiB +0.00% 0 0 0
use_rust/stm32f1 62.7 KiB 65.2 KiB +4.00% ⬆️ +1696 0 +872
use_rust/stm32h7 122.4 KiB 126.0 KiB +2.94% ⬆️ +2812 0 +880
use_config/nrf52832_ble — 394.4 KiB → 398.7 KiB (+1.08% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 367948	   4588	  35720	 408256	  63ac0	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 364440	   4588	  34840	 403868	  6299c	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +8.40Ki  [ = ]       0    .debug_str
  +0.3% +5.47Ki  [ = ]       0    .debug_info
  +1.0% +3.29Ki  +1.0% +3.29Ki    .text
  +0.2% +1.37Ki  [ = ]       0    .debug_loc
  +0.4% +1.12Ki  [ = ]       0    .debug_line
  +0.5%    +960  [ = ]       0    .debug_ranges
  [ = ]       0  +2.6%    +880    .bss
  +0.2%    +563  [ = ]       0    .strtab
  +0.4%    +448  [ = ]       0    .symtab
  +0.5%    +212  [ = ]       0    .debug_frame
  +0.4%    +140  +0.4%    +140    .rodata
  +0.2%     +96  [ = ]       0    .debug_aranges
   +31%     +14  [ = ]       0    [Unmapped]
  +0.1%      +1  [ = ]       0    .defmt
  -1.1%     -86  [ = ]       0    .debug_abbrev
  +0.3% +21.9Ki  +1.1% +4.29Ki    TOTAL
use_config/nrf52840_ble — 443.5 KiB → 450.1 KiB (+1.48% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 400816	   4596	  55456	 460868	  70844	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 398040	   4596	  51496	 454132	  6edf4	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +9.72Ki  [ = ]       0    .debug_str
  +0.3% +6.19Ki  [ = ]       0    .debug_info
  [ = ]       0  +7.8% +3.87Ki    .bss
  +0.7% +2.57Ki  +0.7% +2.57Ki    .text
  +0.1% +1.03Ki  [ = ]       0    .debug_loc
  +0.3%    +933  [ = ]       0    .debug_line
  +0.2%    +683  [ = ]       0    .strtab
  +0.2%    +432  [ = ]       0    .debug_ranges
  +0.3%    +140  +0.3%    +140    .rodata
  +0.0%     +64  [ = ]       0    .symtab
  +0.5%     +39  [ = ]       0    .debug_abbrev
  +0.0%     +20  [ = ]       0    .debug_frame
  +0.1%      +1  [ = ]       0    .defmt
 -33.8%     -22  [ = ]       0    [Unmapped]
  +0.3% +21.7Ki  +1.5% +6.58Ki    TOTAL
use_config/nrf52840_ble_split (central) — 493.2 KiB → 496.6 KiB (+0.68% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 456040	   4652	  47776	 508468	  7c234	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 453472	   4652	  46896	 505020	  7b4bc	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +10.0Ki  [ = ]       0    .debug_str
  +0.3% +7.58Ki  [ = ]       0    .debug_info
  +0.6% +2.37Ki  +0.6% +2.37Ki    .text
  +0.3% +2.18Ki  [ = ]       0    .debug_loc
  +0.4% +1.50Ki  [ = ]       0    .debug_line
  [ = ]       0  +1.9%    +880    .bss
  +0.2%    +860  [ = ]       0    .strtab
  +0.3%    +856  [ = ]       0    .debug_ranges
  +0.4%    +656  [ = ]       0    .symtab
  +0.5%    +272  [ = ]       0    .debug_frame
  +0.3%    +152  [ = ]       0    .debug_aranges
  +0.3%    +140  +0.3%    +140    .rodata
  +1.0%      +9  [ = ]       0    .defmt
 -12.9%      -9  [ = ]       0    [Unmapped]
  +0.3% +26.5Ki  +0.7% +3.37Ki    TOTAL
use_config/nrf52840_ble_split (peripheral) — 317.3 KiB → 318.0 KiB (+0.22% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 294036	   4468	  27152	 325656	  4f818	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 293292	   4468	  27152	 324912	  4f530	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1% +3.97Ki  [ = ]       0    .debug_str
  +0.1% +1.83Ki  [ = ]       0    .debug_info
  +0.3%    +744  +0.3%    +744    .text
  +0.1%    +225  [ = ]       0    .strtab
  +0.1%    +224  [ = ]       0    .debug_ranges
  +0.2%    +176  [ = ]       0    .symtab
  +0.2%     +60  [ = ]       0    .debug_frame
  +0.1%     +56  [ = ]       0    .debug_aranges
   +53%     +25  [ = ]       0    [Unmapped]
  +0.2%      +1  [ = ]       0    .defmt
  -0.0%      -2  [ = ]       0    .debug_abbrev
  -0.0%     -82  [ = ]       0    .debug_line
  -0.3% -1.90Ki  [ = ]       0    .debug_loc
  +0.1% +5.29Ki  +0.2%    +744    TOTAL
use_config/pi_pico_w_ble — 667.9 KiB → 671.9 KiB (+0.59% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 632456	      0	  55580	 688036	  a7fa4	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 629276	      0	  54700	 683976	  a6fc8	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +5.87Ki  [ = ]       0    .debug_info
  +0.9% +2.97Ki  +0.9% +2.97Ki    .text
  +1.1% +2.67Ki  [ = ]       0    .debug_ranges
  +0.1% +2.23Ki  [ = ]       0    .debug_str
  +0.5% +2.17Ki  [ = ]       0    .debug_line
  [ = ]       0  +1.6%    +880    .bss
  +0.6%    +496  [ = ]       0    .symtab
  +0.1%    +195  [ = ]       0    .strtab
  +0.0%    +140  +0.0%    +140    .rodata
  +0.2%     +72  [ = ]       0    .debug_frame
  +0.1%     +32  [ = ]       0    .debug_aranges
  +1.0%      +9  [ = ]       0    .defmt
 -21.2%     -11  [ = ]       0    [Unmapped]
  -0.2%     -16  [ = ]       0    .debug_abbrev
  -0.0%    -266  [ = ]       0    .debug_loc
  +0.2% +16.5Ki  +0.6% +3.96Ki    TOTAL
use_config/rp2040 — 149.3 KiB → 153.2 KiB (+2.55% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 140336	      0	  16492	 156828	  2649c	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 137304	      0	  15612	 152916	  25554	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.6% +6.30Ki  [ = ]       0    .debug_info
  +2.4% +2.82Ki  +2.4% +2.82Ki    .text
  +0.1% +2.49Ki  [ = ]       0    .debug_str
  +2.6% +2.11Ki  [ = ]       0    .debug_ranges
  +0.7% +1.99Ki  [ = ]       0    .debug_loc
  +0.9% +1.46Ki  [ = ]       0    .debug_line
  [ = ]       0  +6.0%    +880    .bss
  +1.4%    +464  [ = ]       0    .symtab
  +0.4%    +425  [ = ]       0    .strtab
  +0.8%    +140  +0.8%    +140    .rodata
  +0.4%     +72  [ = ]       0    .debug_frame
  +0.2%     +32  [ = ]       0    .debug_aranges
   +14%      +8  [ = ]       0    [Unmapped]
  +0.2%      +1  [ = ]       0    .defmt
  +0.5% +18.3Ki  +2.6% +3.82Ki    TOTAL
use_config/rp2040_split (central) — 162.8 KiB → 166.6 KiB (+2.28% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 153216	      0	  17340	 170556	  29a3c	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 150288	      0	  16460	 166748	  28b5c	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +6.01Ki  [ = ]       0    .debug_info
  +5.6% +5.02Ki  [ = ]       0    .debug_ranges
  +0.2% +3.29Ki  [ = ]       0    .debug_str
  +2.1% +2.72Ki  +2.1% +2.72Ki    .text
  +1.2% +2.11Ki  [ = ]       0    .debug_line
  [ = ]       0  +5.7%    +880    .bss
  +0.9%    +336  [ = ]       0    .symtab
  +0.2%    +287  [ = ]       0    .strtab
  +0.7%    +140  +0.7%    +140    .rodata
  +0.4%     +72  [ = ]       0    .debug_frame
  +0.2%     +32  [ = ]       0    .debug_aranges
  +0.2%      +1  [ = ]       0    .defmt
 -22.7%     -15  [ = ]       0    [Unmapped]
  -1.5% -5.51Ki  [ = ]       0    .debug_loc
  +0.4% +14.5Ki  +2.3% +3.72Ki    TOTAL
use_config/rp2040_split (peripheral) — 27.8 KiB → 27.8 KiB (+0.00%)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  25612	     60	   2796	  28468	   6f34	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25612	     60	   2796	  28468	   6f34	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1%    +561  [ = ]       0    .debug_str
  +0.0%    +130  [ = ]       0    .debug_info
  +0.2%     +24  [ = ]       0    .debug_aranges
  +0.0%      +3  [ = ]       0    .debug_line
  +2.5%      +1  [ = ]       0    [Unmapped]
  -0.0%      -3  [ = ]       0    .strtab
  +0.0%    +716  [ = ]       0    TOTAL
use_config/stm32f1 — 63.1 KiB → 65.8 KiB (+4.19% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  58888	     28	   8460	  67376	  10730	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  57048	     28	   7588	  64664	   fc98	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.0% +2.42Ki  [ = ]       0    .debug_loc
  +0.4% +2.02Ki  [ = ]       0    .debug_info
  +3.3% +1.79Ki  +3.3% +1.79Ki    .text
  +1.1%    +962  [ = ]       0    .debug_line
  +2.1%    +896  [ = ]       0    .debug_ranges
  [ = ]       0   +11%    +872    .bss
  +3.8%    +784  [ = ]       0    .symtab
  +2.8%    +364  [ = ]       0    .debug_frame
  +2.7%    +176  [ = ]       0    .debug_aranges
  +0.5%      +4  +0.6%      +4    .rodata
 -15.8%      -9  [ = ]       0    [Unmapped]
  -0.0%     -15  [ = ]       0    .strtab
  -0.1%    -663  [ = ]       0    .debug_str
  +0.5% +8.67Ki  +4.2% +2.65Ki    TOTAL
use_config/stm32h7 — 100.2 KiB → 103.4 KiB (+3.25% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  94932	    264	  10728	 105924	  19dc4	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  92504	    264	   9816	 102584	  190b8	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +4.94Ki  [ = ]       0    .debug_info
  +2.9% +4.86Ki  [ = ]       0    .debug_loc
  +2.9% +2.21Ki  +2.9% +2.21Ki    .text
  +1.2% +1.45Ki  [ = ]       0    .debug_line
  +1.5%   +1016  [ = ]       0    .debug_ranges
  [ = ]       0  +9.3%    +912    .bss
  +1.2%    +352  [ = ]       0    .symtab
  +1.3%    +160  +1.3%    +160    .rodata
  +0.9%    +136  [ = ]       0    .debug_frame
  +0.2%     +72  [ = ]       0    .debug_aranges
   +15%      +7  [ = ]       0    [Unmapped]
  -0.3%     -19  [ = ]       0    .debug_abbrev
  -0.9%    -622  [ = ]       0    .strtab
  -0.1% -2.54Ki  [ = ]       0    .debug_str
  +0.3% +12.0Ki  +3.3% +3.26Ki    TOTAL
use_rust/nrf52832_ble — 382.4 KiB → 385.5 KiB (+0.82% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 355480	   4588	  34704	 394772	  60614	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 353120	   4588	  33824	 391532	  5f96c	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +7.71Ki  [ = ]       0    .debug_str
  +0.3% +6.14Ki  [ = ]       0    .debug_info
  +0.7% +2.17Ki  +0.7% +2.17Ki    .text
  +0.3%    +939  [ = ]       0    .debug_line
  [ = ]       0  +2.7%    +880    .bss
  +0.1%    +258  [ = ]       0    .strtab
  +0.4%    +136  +0.4%    +136    .rodata
  +1.9%      +9  [ = ]       0    .defmt
  +0.0%      +2  [ = ]       0    .debug_abbrev
 -37.3%     -25  [ = ]       0    [Unmapped]
  -0.3%    -128  [ = ]       0    .debug_aranges
  -0.5%    -228  [ = ]       0    .debug_frame
  -0.3%    -384  [ = ]       0    .symtab
  -0.5% -1.01Ki  [ = ]       0    .debug_ranges
  -0.6% -4.19Ki  [ = ]       0    .debug_loc
  +0.2% +11.4Ki  +0.8% +3.16Ki    TOTAL
use_rust/nrf52840_ble — 439.8 KiB → 444.9 KiB (+1.16% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 400884	   4588	  50128	 455600	  6f3b0	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 398776	   4588	  46976	 450340	  6df24	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +5.88Ki  [ = ]       0    .debug_info
  +0.1% +3.54Ki  [ = ]       0    .debug_str
  [ = ]       0  +6.9% +3.08Ki    .bss
  +0.4% +2.94Ki  [ = ]       0    .debug_loc
  +0.6% +1.92Ki  +0.6% +1.92Ki    .text
  +0.2%    +734  [ = ]       0    .debug_line
  +0.4%    +544  [ = ]       0    .symtab
  +0.2%    +416  [ = ]       0    .debug_ranges
  +0.5%    +228  [ = ]       0    .debug_frame
  +0.3%    +144  +0.3%    +144    .rodata
  +0.0%    +139  [ = ]       0    .strtab
  +0.2%    +104  [ = ]       0    .debug_aranges
  +0.1%      +1  [ = ]       0    .defmt
  +2.2%      +1  [ = ]       0    [Unmapped]
  +0.2% +16.5Ki  +1.2% +5.14Ki    TOTAL
use_rust/nrf52840_ble_split (central) — 503.3 KiB → 507.6 KiB (+0.86% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 460792	   4652	  54384	 519828	  7ee94	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 458064	   4652	  52640	 515356	  7dd1c	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +5.86Ki  [ = ]       0    .debug_info
  +0.6% +4.81Ki  [ = ]       0    .debug_loc
  +0.1% +3.00Ki  [ = ]       0    .debug_str
  +0.6% +2.53Ki  +0.6% +2.53Ki    .text
  [ = ]       0  +3.4% +1.70Ki    .bss
  +0.2%    +612  [ = ]       0    .debug_line
  +0.2%    +304  [ = ]       0    .symtab
  +0.1%    +208  [ = ]       0    .debug_ranges
  +0.3%    +136  +0.3%    +136    .rodata
  +0.1%     +64  [ = ]       0    .debug_frame
  +0.1%     +48  [ = ]       0    .debug_aranges
  +0.1%      +1  [ = ]       0    .defmt
 -21.4%     -12  [ = ]       0    [Unmapped]
  -0.0%     -57  [ = ]       0    .strtab
  +0.2% +17.5Ki  +0.9% +4.37Ki    TOTAL
use_rust/nrf52840_ble_split (peripheral) — 313.7 KiB → 314.5 KiB (+0.24% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 292156	   4468	  25400	 322024	  4e9e8	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 291356	   4468	  25400	 321224	  4e6c8	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +7.31Ki  [ = ]       0    .debug_str
  +0.7% +3.72Ki  [ = ]       0    .debug_loc
  +0.1% +2.04Ki  [ = ]       0    .debug_info
  +0.3%    +800  +0.3%    +800    .text
  +0.2%    +556  [ = ]       0    .strtab
  +0.1%    +112  [ = ]       0    .symtab
  +0.1%     +40  [ = ]       0    .debug_aranges
  +0.1%     +28  [ = ]       0    .debug_frame
  +3.1%      +2  [ = ]       0    [Unmapped]
  +0.2%      +1  [ = ]       0    .defmt
  -0.0%      -2  [ = ]       0    .debug_abbrev
  -0.1%    -135  [ = ]       0    .debug_line
  -0.2%    -288  [ = ]       0    .debug_ranges
  +0.2% +14.2Ki  +0.2%    +800    TOTAL
use_rust/pi_pico_w_ble — 668.2 KiB → 672.0 KiB (+0.57% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 632404	      0	  55724	 688128	  a8000	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 629368	      0	  54844	 684212	  a70b4	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +6.06Ki  [ = ]       0    .debug_info
  +0.9% +2.83Ki  +0.9% +2.83Ki    .text
  +0.1% +2.82Ki  [ = ]       0    .debug_str
  +1.1% +2.81Ki  [ = ]       0    .debug_ranges
  +0.5% +1.96Ki  [ = ]       0    .debug_line
  [ = ]       0  +1.6%    +880    .bss
  +0.1%    +391  [ = ]       0    .strtab
  +0.3%    +272  [ = ]       0    .symtab
  +0.0%    +136  +0.0%    +136    .rodata
  +0.2%     +72  [ = ]       0    .debug_frame
  +0.1%     +32  [ = ]       0    .debug_aranges
  +1.0%      +9  [ = ]       0    .defmt
  +5.2%      +3  [ = ]       0    [Unmapped]
  -0.1% -1.28Ki  [ = ]       0    .debug_loc
  +0.2% +16.1Ki  +0.6% +3.82Ki    TOTAL
use_rust/rp2040 — 148.7 KiB → 152.9 KiB (+2.77% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 140160	      0	  16364	 156524	  2636c	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 136820	      0	  15484	 152304	  252f0	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.6% +6.30Ki  [ = ]       0    .debug_info
  +2.7% +3.12Ki  +2.7% +3.12Ki    .text
  +0.2% +2.85Ki  [ = ]       0    .debug_str
  +3.3% +2.62Ki  [ = ]       0    .debug_ranges
  +1.1% +1.81Ki  [ = ]       0    .debug_line
  [ = ]       0  +6.1%    +880    .bss
  +0.3%    +833  [ = ]       0    .debug_loc
  +1.4%    +464  [ = ]       0    .symtab
  +0.2%    +278  [ = ]       0    .strtab
  +0.8%    +140  +0.8%    +140    .rodata
  +0.4%     +72  [ = ]       0    .debug_frame
  +0.2%     +32  [ = ]       0    .debug_aranges
  +0.2%      +1  [ = ]       0    .defmt
 -20.0%     -12  [ = ]       0    [Unmapped]
  +0.5% +18.5Ki  +2.8% +4.12Ki    TOTAL
use_rust/rp2040_split (central) — 161.5 KiB → 165.4 KiB (+2.41% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 152260	      0	  17124	 169384	  295a8	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 149140	      0	  16244	 165384	  28608	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +6.06Ki  [ = ]       0    .debug_info
  +0.2% +3.32Ki  [ = ]       0    .debug_str
  +2.3% +2.91Ki  +2.3% +2.91Ki    .text
  +2.4% +2.18Ki  [ = ]       0    .debug_ranges
  +0.8% +1.51Ki  [ = ]       0    .debug_line
  [ = ]       0  +5.8%    +880    .bss
  +1.2%    +432  [ = ]       0    .symtab
  +0.2%    +261  [ = ]       0    .strtab
  +0.7%    +136  +0.7%    +136    .rodata
  +0.4%     +72  [ = ]       0    .debug_frame
  +0.2%     +32  [ = ]       0    .debug_aranges
  +2.1%      +9  [ = ]       0    .defmt
 -27.9%     -17  [ = ]       0    [Unmapped]
  -1.5% -5.05Ki  [ = ]       0    .debug_loc
  +0.3% +11.8Ki  +2.4% +3.91Ki    TOTAL
use_rust/rp2040_split (peripheral) — 28.2 KiB → 28.2 KiB (+0.00%)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  25712	     60	   3060	  28832	   70a0	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25712	     60	   3060	  28832	   70a0	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1%    +561  [ = ]       0    .debug_str
  +0.0%    +130  [ = ]       0    .debug_info
  +0.2%     +24  [ = ]       0    .debug_aranges
  +0.0%      +3  [ = ]       0    .debug_line
  -0.0%      -3  [ = ]       0    .strtab
  -7.7%      -3  [ = ]       0    [Unmapped]
  +0.0%    +712  [ = ]       0    TOTAL
use_rust/stm32f1 — 62.7 KiB → 65.2 KiB (+4.00% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  58276	     28	   8428	  66732	  104ac	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  56580	     28	   7556	  64164	   faa4	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +2.65Ki  [ = ]       0    .debug_info
  +3.0% +1.65Ki  +3.0% +1.65Ki    .text
  [ = ]       0   +12%    +872    .bss
  +0.7%    +843  [ = ]       0    .debug_loc
  +0.9%    +821  [ = ]       0    .debug_line
  +1.9%    +808  [ = ]       0    .debug_ranges
  +3.3%    +656  [ = ]       0    .symtab
  +2.4%    +316  [ = ]       0    .debug_frame
  +3.2%    +152  [ = ]       0    .debug_aranges
  +0.1%      +8  [ = ]       0    .debug_abbrev
  +0.6%      +4  +0.6%      +4    .rodata
  -3.9%      -2  [ = ]       0    [Unmapped]
  -0.2%     -96  [ = ]       0    .strtab
  -0.0%    -191  [ = ]       0    .debug_str
  +0.4% +7.54Ki  +4.0% +2.51Ki    TOTAL
use_rust/stm32h7 — 122.4 KiB → 126.0 KiB (+2.94% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 112088	    320	  16644	 129052	  1f81c	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 109276	    320	  15764	 125360	  1e9b0	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.4% +4.78Ki  [ = ]       0    .debug_info
  +0.1% +3.43Ki  [ = ]       0    .debug_str
  +1.4% +3.36Ki  [ = ]       0    .debug_loc
  +2.7% +2.73Ki  +2.7% +2.73Ki    .text
  +2.2% +1.79Ki  [ = ]       0    .debug_ranges
  +0.8% +1.27Ki  [ = ]       0    .debug_line
  [ = ]       0  +6.0%    +880    .bss
  +0.3%     +16  +0.3%     +16    .rodata
  +9.3%      +5  [ = ]       0    [Unmapped]
  +0.3%      +1  [ = ]       0    .defmt
  -0.0%     -47  [ = ]       0    .strtab
  -0.1%     -56  [ = ]       0    .debug_aranges
  -0.3%     -96  [ = ]       0    .symtab
  -0.6%    -116  [ = ]       0    .debug_frame
  +0.4% +17.1Ki  +2.9% +3.61Ki    TOTAL

@HaoboGu

HaoboGu commented May 21, 2026

Copy link
Copy Markdown
Owner

There is One Shot Sticky Modifier in main branch, what's the difference?

@ldsands

ldsands commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

There is One Shot Sticky Modifier in main branch, what's the difference?

OSM(LAlt) releases the modifier after one keypress — press OSM, release, press Tab, Alt+Tab fires once and Alt is gone. SM(Tab, LAlt) bundles the modifier and key together and keeps the modifier held across repeated presses of the same SM key: first press sends Alt+Tab, second press sends Alt+Tab again (Alt still held), and Alt only releases when you press something else entirely. OSM is for one-shot use; SM is specifically for cycling (Alt+Tab, Ctrl+Tab) where you need the modifier to persist across multiple presses of the same key.

@HaoboGu

HaoboGu commented May 21, 2026

Copy link
Copy Markdown
Owner

OSM(LAlt) releases the modifier after one keypress — press OSM, release, press Tab, Alt+Tab fires once and Alt is gone. SM(Tab, LAlt) bundles the modifier and key together and keeps the modifier held across repeated presses of the same SM key: first press sends Alt+Tab, second press sends Alt+Tab again (Alt still held), and Alt only releases when you press something else entirely. OSM is for one-shot use; SM is specifically for cycling (Alt+Tab, Ctrl+Tab) where you need the modifier to persist across multiple presses of the same key.

Great, maybe the two types be merged into a single type of behavior, i.e. a general "Sticky Key"?

I'm imaging some like SK(key, keep, max_repeat):

  • When this SK is triggered, key is activated
  • key keeps to be activated if keys in keep list is pressed
  • key is released when keep is pressed for max_repeat times, or any other keys is triggered

With this, one-shot mod can be represented as SK(mod, [], 1), and SM(Tab, LAlt) can be represented as SK(Tab, [LAlt], MAX_REPEAT)

What do you think?

@ldsands

ldsands commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

OSM(LAlt) releases the modifier after one keypress — press OSM, release, press Tab, Alt+Tab fires once and Alt is gone. SM(Tab, LAlt) bundles the modifier and key together and keeps the modifier held across repeated presses of the same SM key: first press sends Alt+Tab, second press sends Alt+Tab again (Alt still held), and Alt only releases when you press something else entirely. OSM is for one-shot use; SM is specifically for cycling (Alt+Tab, Ctrl+Tab) where you need the modifier to persist across multiple presses of the same key.

Great, maybe the two types be merged into a single type of behavior, i.e. a general "Sticky Key"?

I'm imaging some like SK(key, keep, max_repeat):

  • When this SK is triggered, key is activated
  • key keeps to be activated if keys in keep list is pressed
  • key is released when keep is pressed for max_repeat times, or any other keys is triggered

With this, one-shot mod can be represented as SK(mod, [], 1), and SM(Tab, LAlt) can be represented as SK(Tab, [LAlt], MAX_REPEAT)

What do you think?

I like that idea, since the two are conceptually similar. Let me think through what implementing this in the one-shot would need (and what I'd want from it).

Max repeat, on the other hand, isn't something I'd personally use, though I see the utility. Right now I'm thinking of the browser tabs I have open. As long as we can set "no max repeat" or "infinite" as an option, that works for me.

I'd also like a timeout that's independent of the one-shot timeout. This probably isn't strictly necessary, but I suspect most people who use this would want a different timeout than the one for one-shot keys. I usually exit a sticky mod with my layer button, but when cycling through browser tabs I'll sometimes go through several, pause to look at the screen, then continue. Again, a personal preference I could make work with a shared timeout, but worth mentioning.

I also wonder how this interacts with layer changes. I use this key on another layer (via MO) so that returning to the base layer automatically exits the sticky mod and I can resume typing immediately. So I'd want an optional per-key feature to exit on layer change. I wouldn't want this on my other one-shot keys, though, since I use those across layers constantly; I added it specifically to this sticky mod implementation.

Forgive the verbosity; talking through it helped. I'm happy to fold this into the current one-shot keys implementation, but if you want to go that route, I would prefer a per-key timeout option and a per-key "exit on layer change" option. Adding max retries is a great idea. I don't know what you'd want as the default, but I'd like to have the max retries configurable to have infinite, or to assume infinite until the timeout from the last sticky mod key press.

What do you think?

@HaoboGu

HaoboGu commented May 21, 2026

Copy link
Copy Markdown
Owner

Max repeat, on the other hand, isn't something I'd personally use, though I see the utility. Right now I'm thinking of the browser tabs I have open. As long as we can set "no max repeat" or "infinite" as an option, that works for me.

Yes, I agree. Omitting it means "infinite", i.e. SK(Tab, [LAlt], MAX_REPEAT) == SK(Tab, [LAlt]).
But I'm not sure what the default behavior should be: max_repeat == 1 or infinite?

About the per-key timeout, I have no strong opinion on it. Using the shared timeout as the default and overriding it when a per-key timeout exists is fine with me. But it does increase the complexity and RAM/Flash usage.

I also wonder how this interacts with layer changes.

Should it be included in the keep list?

Also, the length of keep list is a bit tricky. I think it should be calculated at compile-time and applied to the type. TBH I don't know if the idea works, but I think it's worth a try at least.

@ldsands

ldsands commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

Just a quick update, I think I'll be done with this by the end of the week or earlier for you to look at. I may also wait until #854 is done as well and make sure that I have it working with that merge.

@HaoboGu

HaoboGu commented May 29, 2026

Copy link
Copy Markdown
Owner

Great!

Only a minor comment left in #854, I think we can get it merged first.

@HaoboGu

HaoboGu commented May 29, 2026

Copy link
Copy Markdown
Owner

#854 is merged

ldsands added 15 commits May 30, 2026 16:48
- Restore pre-existing comments in LayerOff, LayerToggle, DefaultLayer arms
- Fix typo in LayerToggle comment ("release" → "released")
- Remove unused HidKeyCode import from sticky_mod.rs
…ase guard

Action::Key(KeyCode::Hid(LShift)) etc. are modifier keys expressed via
the Key action rather than the Modifier action. The SM release guard now
checks hid_key.is_modifier() so that holding Shift for reverse-Tab cycling
doesn't break StickyMod state.
Five rusty_fork_test cases covering: basic two-press flow, layer-change
cleanup, Shift-does-not-release-SM, rapid triple presses, and combined
LCtrl|LShift modifier.
rusty-fork was used by keyboard_sticky_mod_test but missing from
Cargo.toml dev-dependencies. Also add missing .await on
process_action_layer_switch call in test_key_action_transparent
(function became async in upstream refactor).
…d StickyMod docs

- Make DurationMillis pub (was pub(crate), caused visibility warning via StickyModConfig pub field)
- Add test_sm_action_parsing and test_sm_action_grammar to rmk-config/src/layout.rs
- Add Sticky Modifiers section to behavior.md
- Add SM(key, modifier) entry to layout.md advanced layer operations list
Move timeout tracking out of the release handler's blocking select and into
the main run() loop, following the same pattern as mouse repeat deadlines.

- StickyModState::Active now stores an optional Instant deadline
- Deadline is set (and reset) on each SM key PRESS, so repeated presses
  extend the hold window rather than starting from the release
- run() combines SM and mouse deadlines and uses with_deadline(); on expiry
  it calls release_sticky_mod_if_active() before continuing
- Remove embassy_futures select from release handler (was fragile: any
  event arriving cancelled the timer, preventing timeout on 2nd+ press)
- Add sticky_mod_timeout() accessor to KeyMap
- Add 2 integration tests: test_sm_timeout and test_sm_timeout_resets_on_press
…shots

- Replace map_or(false, ...) with is_some_and(...) in keyboard.rs run() loop
- Regenerate endpoint key snapshots in rmk-types: Action::StickyMod added a
  variant to the Action enum, changing the postcard schema hash for keymap,
  combo, and morse endpoints
@ldsands

ldsands commented May 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback on consolidating SM and OSM! I've rebased this branch onto the latest main (which includes PR #854's quick_release feature) and reworked the implementation to address your request.

What changed:

  • Removed StickyMod entirely — replaced by the unified StickyKey (SK) action
  • SK(key, [modifier], max_repeat, timeout_ms, exit_on_layer_change) — all args after key are optional
    • max_repeat = 0 → infinite repeats (default)
    • timeout_ms = 0 → uses global [behavior.sticky_key] timeout (default; no timeout if unset)
    • exit_on_layer_change → defaults to false (modifier survives layer changes)
  • OSM remains as-is for the one-shot use case; SK handles the "hold modifier across repeated presses" use case
  • 11 integration tests added for SK behavior; all 450 tests passing
  • Docs updated in behavior.md and layout.md

Example usage: SK(Tab, [LAlt]) for Alt+Tab window cycling — first press sends Alt+Tab, each subsequent press sends Tab again while holding Alt, releases when you press anything else.

@ldsands ldsands force-pushed the feat/sticky-mod branch from 2a2f462 to 8d51d88 Compare May 30, 2026 23:56
ldsands added 3 commits June 2, 2026 11:05
…es not yet implemented)

Tests cover: basic flow, layer-change cleanup, shift coexistence, rapid presses,
combined modifiers, global timeout, timeout reset, max_repeat, per-key timeout,
exit_on_layer_change=true, and exit_on_layer_change=false (survives layer change).
Compile fails on StickyKeyConfig, StickyKeyAction, sk! macro, and
BehaviorConfig::sticky_key — all to be added in Tasks 3–8.
ldsands added 6 commits July 1, 2026 22:49
Per HaoboGu's review of HaoboGu#859: consolidate the five flat
sticky_key_timeout_ms/activate_on_keypress/quick_release/max_repeat/
release_on_layer_change fields on Behavior into a StickyKeyConfig
struct, matching the runtime StickyKeyConfig already in
rmk/src/config/behavior.rs.
Per HaoboGu's review of HaoboGu#859: keep OSM/OSL in user space by adding
thin macro aliases that delegate to the SK backend macros.
- osm!(M) = sk_mod!(M)
- osl!(n) = sk_layer!(n)
Per HaoboGu's review of HaoboGu#859: rusty-fork is not needed with
cargo-nextest which handles test isolation natively.

Removed the dep, the import, and the rusty_fork_test! wrapper from
keyboard_sticky_key_test.rs.
…n pure-mod release

Item 5: release_sticky_key_if_active() now skips if phase is Pressed
(the key is still physically held) — defers cleanup to the physical
release handler so the modifier doesn't disappear mid-hold.

Item 6: pure-mod deadline refreshed on release-to-Latched, matching
existing layer behavior. Previously the deadline was set on press and
never updated, so holding OSM for 5s then releasing caused a near-
immediate timeout.
…n pure-mod release

Item 5: release_sticky_key_if_active() now skips if phase is Pressed
(the key is still physically held) and clears the deadline to avoid
busy-looping — defers cleanup to the physical release handler so the
modifier doesn't disappear mid-hold.

Item 6: pure-mod deadline refreshed on release-to-Latched, matching
existing layer behavior. Previously the deadline was set on press and
never updated, so holding OSM for 5s then releasing caused a near-
immediate timeout.
…while-held

Fix A: Add trailing newline to keyboard_sticky_key_test.rs
Fix B: Restructure with_deadline to only wrap subscriber wait, not
       process_inner, eliminating spurious timeout risk during processing
Fix C: Add test_sk_timeout_while_held — exercises the Item 5 guard
       (timeout fires while SK physically held in Pressed phase)
@ldsands

ldsands commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Okay I think I got it all. I also did some testing on my own keyboard and everything worked as intended except for "stacking" OSM and OSL I'm still working on that right now.

All 9 review items from HaoboGu's Jun 27-29 review on commit ebfe286 are resolved. Changes are on branch feat/sticky-mod at these commits:

# Item Commit
1 Group resolved Behavior fields into StickyKeyConfig struct 34a65599
2 Vial OSM/OSL → SK backend 5ac8d57f (pre-existing)
3 Add osl!/osm! macros d2d35f49
4 Remove rusty-fork dev-dependency 42dec113
5 Guard timeout while key held (Pressed phase) acbe4f35
6 Refresh deadline on pure-mod release (release_time + timeout) acbe4f35
7 Simplify run loop — single with_deadline race + post-check f7bf7903
8 release_on_layer_change on layer activation f7bf7903

Post-review fixes (self-discovered during code review)

Fix Description Commit
A Trailing newline in keyboard_sticky_key_test.rs 2f30a0c2
B with_deadline restructured to only wrap subscriber (not process_inner) to avoid spurious timeout during processing 2f30a0c2
C Added test_sk_timeout_while_held — exercises the Item 5 guard path 2f30a0c2

…od and layer behavior

The previous single-latch design forced mutual exclusion: pressing a
layer SK (OSL) while a pure-mod SK (OSM) was latched would overwrite
the state, dropping the modifier. The old parallel OSM/OSL state
machines allowed them to coexist — a common QMK one-shot workflow.

Fix allows pure-mod (modifiers) and layer (OSL) shapes to compose:
they affect different dimensions and are naturally compatible. Tap-key
remains exclusive with both.

Changes:
- process_sticky_pure_mod: skip release guard when active state is a
  layer (compatible)
- process_sticky_layer: capture existing_mods from prior state and
  carry them forward when overwriting
- update_sticky_key: new quick_release arm deactivates layer before
  clearing state when both are active
- resolve_explicit_modifiers: treat layer shapes same as pure-mod
  (only add modifiers on press/Held, not release)
- test: updated osm_then_osl/osl_then_osm expectations to reflect
  coexistence behavior
@ldsands

ldsands commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Okay I think I got the OSL and OSM "stacking" working. I've manually confirmed that any combination of tapping and/or holding either of those in any sequence worked as intended (brief summary of changes below).

OSM + OSL Coexistence (commit 231df5c)

The single-latch design forced mutual exclusion between OSM and OSL. Fixed by allowing pure-mod (modifiers) and layer shapes to compose — they affect different dimensions and are naturally compatible. Tap-key remains exclusive with both.

Changes:

  • process_sticky_pure_mod — skip release guard when active state is a layer (compatible)
  • process_sticky_layer — capture existing mods from prior state and carry them forward
  • update_sticky_key — new quick_release arm deactivates layer before clearing state when both are active
  • resolve_explicit_modifiers — treat layer shapes same as pure-mod (only add modifiers on press/Held)
  • Tests updated for coexistence behavior

@HaoboGu

HaoboGu commented Jul 3, 2026

Copy link
Copy Markdown
Owner

I just checked, some comments are still unresolved?

@ldsands

ldsands commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

I just checked, some comments are still unresolved?

You're right, my bad, I'm working on resolving the last few and I'll post another comment when I think I got all of them.

Thanks for your patience!

ldsands and others added 2 commits July 6, 2026 10:25
The previous commit (5ac8d57: feat(sticky-key): restore OSM/OSL as
aliases) only added the macro-level aliases but did NOT touch the Vial
keycode conversion layer.

This completes Item 2 of HaoboGu's review: the Vial keycode_convert.rs
now properly maps:
  to_via_keycode: Action::StickyKey → 0x52A0|bits (OSM) / 0x5280|l (OSL)
  from_via_keycode: 0x52A0..0x52BF → Action::StickyKey (OSM)
                     0x5280..0x529F → Action::StickyKey (OSL)

Uses VIA-defined ranges 0x5280/0x52A0 (same as old OneShotLayer/
OneShotModifier) rather than QMK's 0x7C02/0x7C03 ranges, because VIA
ranges avoid prefix-vs-payload bit overlap and round-trip cleanly
through packed bits.

Round-trip tests added for OSM(LCtrl), OSM(LShift), OSM(LAlt),
OSL(0), and OSL(5).

Per HaoboGu: 'Keep Vial OSM/OSL supported, just migrate to SK backend'
522/522 tests pass.
…avoid stale-read race

The shared  captured at loop iteration start can go stale when the first async post-check (release_sticky_key_if_active) sends HID reports and takes real time. The second post-check (fire_mouse_repeat) then compares against a stale timestamp, potentially missing its deadline by one iteration. Using inline Instant::now() in each closure ensures every deadline check uses current wall-clock time.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@ldsands ldsands force-pushed the feat/sticky-mod branch from 447537a to 07b2ac6 Compare July 7, 2026 00:46
# Conflicts:
#	rmk-config/src/lib.rs
#	rmk-config/src/resolved/behavior.rs
#	rmk-macro/src/codegen/behavior.rs
#	rmk/src/config/behavior.rs
#	rmk/src/config/mod.rs
@ldsands ldsands force-pushed the feat/sticky-mod branch from 358b65a to 2d6a2b2 Compare July 7, 2026 01:48
@ldsands

ldsands commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Okay I think that is everything Not sure why that one test failed it says what I copied below so just rerunning it should work.

[warn]: Failed to get latest Xtensa Rust version: HTTP GET Error: GitHub API returned status code: 403 Forbidden

Let me know if I missed something again.

@HaoboGu

HaoboGu commented Jul 7, 2026

Copy link
Copy Markdown
Owner

That's because the unstable network? of the github action server I think. Reran it.

Comment thread .github/workflows/user_build.yml
Comment thread .vscode/settings.json Outdated
Comment thread rmk/src/host/via/keycode_convert.rs Outdated
self.keymap.deactivate_layer(layer_num);
}

self.sticky_key_state = StickyKeyState::None;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the code


let mut should_deactivate = false;

match &mut self.sticky_key_state {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are 2 stickykeys are pressed, the second key's state is wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a05a9942.

StickyKey state now tracks the owning physical key, so a second tap-key StickyKey replaces the first cleanly and the first key’s later release does not affect the second.

ldsands added 5 commits July 9, 2026 17:23
Addresses HaoboGu's review comments from PR HaoboGu#859:
- Restore CI artifact upload steps accidentally deleted during merge conflict resolution (comment 1)
- Remove personal VS Code theme customizations accidentally committed (comment 2)
# Conflicts:
#	rmk/tests/keyboard_combo_test.rs
@ldsands

ldsands commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Resolved in 301c6352.

Merged the latest upstream changes and resolved the combo-test conflict by retaining the upstream combo regressions while migrating the one-shot test setup to StickyKey.
Also adjusted pure-mod StickyKey releases so combo-generated press/release events from different trigger keys are handled correctly.

@HaoboGu

HaoboGu commented Jul 11, 2026

Copy link
Copy Markdown
Owner

The overall implementation is nice, but I'm worried about the increased FLASH/RAM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants