Skip to content

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

Open
ldsands wants to merge 137 commits into
rmk-rs:mainfrom
ldsands:feat/sticky-mod
Open

feat: add StickyMod (SM) action for Alt+Tab-style modifier cycling#859
ldsands wants to merge 137 commits into
rmk-rs: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 399.5 KiB 407.9 KiB +2.10% ⬆️ +8272 0 +352
use_config/nrf52840_ble 441.5 KiB 450.1 KiB +1.94% ⬆️ +8440 0 +352
use_config/nrf52840_ble_split (central) 510.9 KiB 518.7 KiB +1.53% ⬆️ +7660 0 +352
use_config/nrf52840_ble_split (peripheral) 320.8 KiB 320.8 KiB +0.01% ⬆️ +36 0 0
use_config/pi_pico_w_ble 674.8 KiB 683.4 KiB +1.27% ⬆️ +8452 0 +352
use_config/rp2040 146.1 KiB 155.2 KiB +6.27% ⬆️ +9028 0 +352
use_config/rp2040_split (central) 159.7 KiB 168.8 KiB +5.71% ⬆️ +8988 0 +352
use_config/rp2040_split (peripheral) 27.8 KiB 27.8 KiB +0.00% 0 0 0
use_config/stm32f1 62.5 KiB 69.5 KiB +11.20% ⬆️ +6788 0 +376
use_config/stm32h7 100.6 KiB 109.2 KiB +8.49% ⬆️ +8376 0 +376
use_rust/nrf52832_ble 387.5 KiB 395.6 KiB +2.08% ⬆️ +7912 0 +352
use_rust/nrf52840_ble 436.9 KiB 445.2 KiB +1.88% ⬆️ +8092 0 +352
use_rust/nrf52840_ble_split (central) 510.3 KiB 518.1 KiB +1.52% ⬆️ +7620 0 +352
use_rust/nrf52840_ble_split (peripheral) 316.7 KiB 316.7 KiB +0.01% ⬆️ +40 0 0
use_rust/pi_pico_w_ble 674.2 KiB 682.8 KiB +1.27% ⬆️ +8432 0 +352
use_rust/rp2040 145.6 KiB 154.4 KiB +6.08% ⬆️ +8716 0 +352
use_rust/rp2040_split (central) 158.4 KiB 167.2 KiB +5.58% ⬆️ +8712 0 +352
use_rust/rp2040_split (peripheral) 28.1 KiB 28.1 KiB +0.00% 0 0 0
use_rust/stm32f1 61.7 KiB 68.6 KiB +11.20% ⬆️ +6708 0 +376
use_rust/stm32h7 119.1 KiB 126.8 KiB +6.48% ⬆️ +7560 0 +352
use_config/nrf52832_ble — 399.5 KiB → 407.9 KiB (+2.10% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 374752	   8620	  34304	 417676	  65f8c	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 366480	   8620	  33952	 409052	  63ddc	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.1% +77.4Ki  [ = ]       0    .debug_str
  +2.4% +46.7Ki  [ = ]       0    .debug_info
  +1.9% +13.7Ki  [ = ]       0    .debug_loc
  +2.3% +7.48Ki  +2.3% +7.48Ki    .text
  +2.4% +7.33Ki  [ = ]       0    .debug_line
  +2.6% +5.35Ki  [ = ]       0    .debug_ranges
  +1.5% +4.91Ki  [ = ]       0    .strtab
  +1.7% +2.12Ki  [ = ]       0    .symtab
  +2.8% +1.23Ki  [ = ]       0    .debug_frame
  +1.6%    +608  +1.6%    +608    .rodata
  +1.0%    +456  [ = ]       0    .debug_aranges
  [ = ]       0  +1.1%    +352    .bss
  +0.2%     +20  [ = ]       0    .debug_abbrev
   +19%     +10  [ = ]       0    [Unmapped]
  +1.1%      +9  [ = ]       0    .defmt
  +2.2%  +167Ki  +2.1% +8.42Ki    TOTAL
use_config/nrf52840_ble — 441.5 KiB → 450.1 KiB (+1.94% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 407936	   8628	  44336	 460900	  70864	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 399496	   8628	  43984	 452108	  6e60c	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.8% +73.2Ki  [ = ]       0    .debug_str
  +2.2% +47.4Ki  [ = ]       0    .debug_info
  +1.5% +12.1Ki  [ = ]       0    .debug_loc
  +2.2% +7.65Ki  +2.2% +7.65Ki    .text
  +2.2% +7.54Ki  [ = ]       0    .debug_line
  +2.6% +6.09Ki  [ = ]       0    .debug_ranges
  +1.2% +4.49Ki  [ = ]       0    .strtab
  +1.6% +2.06Ki  [ = ]       0    .symtab
  +2.6% +1.26Ki  [ = ]       0    .debug_frame
  +1.4%    +608  +1.4%    +608    .rodata
  +1.0%    +472  [ = ]       0    .debug_aranges
  [ = ]       0  +0.8%    +352    .bss
  +0.7%     +59  [ = ]       0    .debug_abbrev
  +1.8%     +17  [ = ]       0    .defmt
  +7.1%      +4  [ = ]       0    [Unmapped]
  +1.9%  +162Ki  +1.9% +8.59Ki    TOTAL
use_config/nrf52840_ble_split (central) — 510.9 KiB → 518.7 KiB (+1.53% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 472084	   8684	  50424	 531192	  81af8	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 464424	   8684	  50072	 523180	  7fbac	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.6% +77.2Ki  [ = ]       0    .debug_str
  +1.9% +46.8Ki  [ = ]       0    .debug_info
  +1.4% +12.4Ki  [ = ]       0    .debug_loc
  +1.9% +7.00Ki  [ = ]       0    .debug_line
  +1.7% +6.89Ki  +1.7% +6.89Ki    .text
  +2.2% +5.59Ki  [ = ]       0    .debug_ranges
  +1.2% +4.92Ki  [ = ]       0    .strtab
  +1.5% +2.27Ki  [ = ]       0    .symtab
  +2.4% +1.30Ki  [ = ]       0    .debug_frame
  +1.3%    +608  +1.3%    +608    .rodata
  +1.0%    +496  [ = ]       0    .debug_aranges
  [ = ]       0  +0.7%    +352    .bss
  +0.9%     +77  [ = ]       0    .debug_abbrev
  +1.8%     +17  [ = ]       0    .defmt
 -20.0%     -12  [ = ]       0    [Unmapped]
  +1.7%  +165Ki  +1.5% +7.82Ki    TOTAL
use_config/nrf52840_ble_split (peripheral) — 320.8 KiB → 320.8 KiB (+0.01% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 294680	   8508	  25328	 328516	  50344	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 294644	   8508	  25328	 328480	  50320	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1% +3.84Ki  [ = ]       0    .debug_str
  +0.1% +1.31Ki  [ = ]       0    .debug_info
  +0.1%    +376  [ = ]       0    .debug_loc
  +0.1%    +128  [ = ]       0    .debug_ranges
  +0.2%    +104  [ = ]       0    .debug_aranges
  +0.0%     +36  +0.0%     +36    .text
  +0.3%     +20  [ = ]       0    .debug_abbrev
  +0.0%      +9  [ = ]       0    .debug_line
 -13.6%      -9  [ = ]       0    [Unmapped]
  -0.0%     -32  [ = ]       0    .symtab
  -0.1%     -44  [ = ]       0    .debug_frame
  -0.2%    -490  [ = ]       0    .strtab
  +0.1% +5.25Ki  +0.0%     +36    TOTAL
use_config/pi_pico_w_ble — 674.8 KiB → 683.4 KiB (+1.27% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 645472	      0	  54368	 699840	  aadc0	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 637020	      0	  54016	 691036	  a8b5c	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.8% +78.0Ki  [ = ]       0    .debug_str
  +2.0% +48.3Ki  [ = ]       0    .debug_info
  +1.2% +13.9Ki  [ = ]       0    .debug_loc
  +2.3% +7.66Ki  +2.3% +7.66Ki    .text
  +1.7% +6.90Ki  [ = ]       0    .debug_line
  +1.3% +4.58Ki  [ = ]       0    .strtab
  +1.8% +4.42Ki  [ = ]       0    .debug_ranges
  +2.3% +1.91Ki  [ = ]       0    .symtab
  +2.3%    +976  [ = ]       0    .debug_frame
  +0.2%    +612  +0.2%    +612    .rodata
  [ = ]       0  +0.7%    +352    .bss
  +0.8%    +336  [ = ]       0    .debug_aranges
  +1.0%      +9  [ = ]       0    .defmt
  -9.6%      -5  [ = ]       0    [Unmapped]
  -0.2%     -19  [ = ]       0    .debug_abbrev
  +1.7%  +167Ki  +1.3% +8.60Ki    TOTAL
use_config/rp2040 — 146.1 KiB → 155.2 KiB (+6.27% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 144696	      0	  14252	 158948	  26ce4	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 135668	      0	  13900	 149568	  24840	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +4.5% +75.0Ki  [ = ]       0    .debug_str
  +4.9% +47.9Ki  [ = ]       0    .debug_info
  +5.2% +16.4Ki  [ = ]       0    .debug_loc
  +7.1% +8.22Ki  +7.1% +8.22Ki    .text
  +4.5% +7.46Ki  [ = ]       0    .debug_line
  +5.8% +4.79Ki  [ = ]       0    .debug_ranges
  +3.9% +4.60Ki  [ = ]       0    .strtab
  +5.9% +1.92Ki  [ = ]       0    .symtab
  +5.7%    +976  [ = ]       0    .debug_frame
  +3.7%    +608  +3.7%    +608    .rodata
  [ = ]       0  +2.7%    +352    .bss
  +2.0%    +336  [ = ]       0    .debug_aranges
  +2.1%      +9  [ = ]       0    .defmt
  -1.8%      -1  [ = ]       0    [Unmapped]
  -0.2%     -16  [ = ]       0    .debug_abbrev
  +4.7%  +168Ki  +6.3% +9.16Ki    TOTAL
use_config/rp2040_split (central) — 159.7 KiB → 168.8 KiB (+5.71% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 157676	      0	  15196	 172872	  2a348	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 148688	      0	  14844	 163532	  27ecc	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +3.7% +74.0Ki  [ = ]       0    .debug_str
  +4.3% +47.8Ki  [ = ]       0    .debug_info
  +4.5% +16.1Ki  [ = ]       0    .debug_loc
  +6.5% +8.18Ki  +6.5% +8.18Ki    .text
  +4.3% +7.73Ki  [ = ]       0    .debug_line
  +5.2% +4.76Ki  [ = ]       0    .debug_ranges
  +3.4% +4.54Ki  [ = ]       0    .strtab
  +4.8% +1.70Ki  [ = ]       0    .symtab
  +5.1%    +976  [ = ]       0    .debug_frame
  +3.3%    +608  +3.3%    +608    .rodata
  [ = ]       0  +2.5%    +352    .bss
  +1.9%    +336  [ = ]       0    .debug_aranges
  +3.8%     +17  [ = ]       0    .defmt
  -0.2%     -16  [ = ]       0    .debug_abbrev
 -39.1%     -25  [ = ]       0    [Unmapped]
  +4.1%  +166Ki  +5.7% +9.12Ki    TOTAL
use_config/rp2040_split (peripheral) — 27.8 KiB → 27.8 KiB (+0.00%)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  25592	     60	   2804	  28456	   6f28	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25592	     60	   2804	  28456	   6f28	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +2.84Ki  [ = ]       0    .debug_str
  +0.1%    +553  [ = ]       0    .debug_info
  +0.8%    +112  [ = ]       0    .debug_aranges
  +0.0%     +20  [ = ]       0    .debug_line
  +1.6%      +1  [ = ]       0    [Unmapped]
  +0.2% +3.51Ki  [ = ]       0    TOTAL
use_config/stm32f1 — 62.5 KiB → 69.5 KiB (+11.20% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  64360	     28	   6736	  71124	  115d4	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  57572	     28	   6360	  63960	   f9d8	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +7.9% +72.1Ki  [ = ]       0    .debug_str
  +8.1% +45.9Ki  [ = ]       0    .debug_info
  +8.9% +11.3Ki  [ = ]       0    .debug_loc
  +7.9% +6.98Ki  [ = ]       0    .debug_line
   +12% +6.62Ki   +12% +6.62Ki    .text
  +9.5% +3.99Ki  [ = ]       0    .debug_ranges
  +8.1% +3.61Ki  [ = ]       0    .strtab
   +11% +2.19Ki  [ = ]       0    .symtab
   +12% +1.57Ki  [ = ]       0    .debug_frame
  +7.8%    +496  [ = ]       0    .debug_aranges
  [ = ]       0  +5.9%    +376    .bss
  +0.5%     +30  [ = ]       0    .debug_abbrev
  +0.5%      +4  +0.5%      +4    .rodata
 -10.0%      -5  [ = ]       0    [Unmapped]
  +8.2%  +154Ki   +11% +7.00Ki    TOTAL
use_config/stm32h7 — 100.6 KiB → 109.2 KiB (+8.49% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 103060	    264	   8488	 111812	  1b4c4	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  94684	    264	   8112	 103060	  19294	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +3.6% +71.3Ki  [ = ]       0    .debug_str
  +4.7% +43.9Ki  [ = ]       0    .debug_info
  +6.6% +11.9Ki  [ = ]       0    .debug_loc
  +9.1% +7.21Ki  +9.1% +7.21Ki    .text
  +5.4% +6.51Ki  [ = ]       0    .debug_line
  +6.2% +4.31Ki  [ = ]       0    .debug_ranges
  +5.3% +3.36Ki  [ = ]       0    .strtab
  +8.3% +2.22Ki  [ = ]       0    .symtab
   +10% +1.48Ki  [ = ]       0    .debug_frame
  +7.5%    +988  +7.5%    +988    .rodata
  +1.4%    +464  [ = ]       0    .debug_aranges
  [ = ]       0  +4.6%    +376    .bss
   +11%      +5  [ = ]       0    [Unmapped]
  -0.0%      -3  [ = ]       0    .debug_abbrev
  +4.4%  +153Ki  +8.5% +8.55Ki    TOTAL
use_rust/nrf52832_ble — 387.5 KiB → 395.6 KiB (+2.08% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 362240	   8620	  34248	 405108	  62e74	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 354328	   8620	  33896	 396844	  60e2c	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.0% +72.5Ki  [ = ]       0    .debug_str
  +2.5% +46.8Ki  [ = ]       0    .debug_info
  +1.9% +13.2Ki  [ = ]       0    .debug_loc
  +2.4% +7.39Ki  [ = ]       0    .debug_line
  +2.3% +7.13Ki  +2.3% +7.13Ki    .text
  +2.6% +5.23Ki  [ = ]       0    .debug_ranges
  +1.3% +3.58Ki  [ = ]       0    .strtab
  +2.1% +2.39Ki  [ = ]       0    .symtab
  +3.3% +1.41Ki  [ = ]       0    .debug_frame
  +1.6%    +608  +1.6%    +608    .rodata
  +1.3%    +520  [ = ]       0    .debug_aranges
  [ = ]       0  +1.1%    +352    .bss
   +48%     +22  [ = ]       0    [Unmapped]
  +0.2%     +20  [ = ]       0    .debug_abbrev
  +1.7%      +8  [ = ]       0    .defmt
  +2.2%  +160Ki  +2.1% +8.07Ki    TOTAL
use_rust/nrf52840_ble — 436.9 KiB → 445.2 KiB (+1.88% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 405672	   8628	  41568	 455868	  6f4bc	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 397580	   8628	  41216	 447424	  6d3c0	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.9% +77.4Ki  [ = ]       0    .debug_str
  +2.2% +46.7Ki  [ = ]       0    .debug_info
  +1.6% +12.1Ki  [ = ]       0    .debug_loc
  +2.1% +7.31Ki  +2.1% +7.31Ki    .text
  +2.0% +6.71Ki  [ = ]       0    .debug_line
  +2.3% +5.33Ki  [ = ]       0    .debug_ranges
  +1.3% +4.71Ki  [ = ]       0    .strtab
  +1.3% +1.75Ki  [ = ]       0    .symtab
  +2.2% +1.07Ki  [ = ]       0    .debug_frame
  +1.4%    +608  +1.4%    +608    .rodata
  +0.8%    +376  [ = ]       0    .debug_aranges
  [ = ]       0  +0.9%    +352    .bss
  +0.2%     +20  [ = ]       0    .debug_abbrev
  +1.9%     +17  [ = ]       0    .defmt
   +16%      +7  [ = ]       0    [Unmapped]
  +1.9%  +163Ki  +1.9% +8.25Ki    TOTAL
use_rust/nrf52840_ble_split (central) — 510.3 KiB → 518.1 KiB (+1.52% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 470028	   8684	  51840	 530552	  81878	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 462408	   8684	  51488	 522580	  7f954	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.6% +76.2Ki  [ = ]       0    .debug_str
  +1.9% +45.6Ki  [ = ]       0    .debug_info
  +1.2% +10.8Ki  [ = ]       0    .debug_loc
  +1.7% +6.86Ki  +1.7% +6.86Ki    .text
  +1.8% +6.61Ki  [ = ]       0    .debug_line
  +1.3% +5.23Ki  [ = ]       0    .strtab
  +1.9% +4.78Ki  [ = ]       0    .debug_ranges
  +1.6% +2.47Ki  [ = ]       0    .symtab
  +2.6% +1.42Ki  [ = ]       0    .debug_frame
  +1.3%    +600  +1.3%    +600    .rodata
  +1.1%    +552  [ = ]       0    .debug_aranges
  [ = ]       0  +0.7%    +352    .bss
  +1.8%     +17  [ = ]       0    .defmt
  -0.0%      -1  [ = ]       0    .debug_abbrev
  -8.2%      -5  [ = ]       0    [Unmapped]
  +1.6%  +161Ki  +1.5% +7.79Ki    TOTAL
use_rust/nrf52840_ble_split (peripheral) — 316.7 KiB → 316.7 KiB (+0.01% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 290824	   8508	  24960	 324292	  4f2c4	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 290784	   8508	  24960	 324252	  4f29c	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1% +3.88Ki  [ = ]       0    .debug_str
  +0.1% +1.31Ki  [ = ]       0    .debug_info
  +0.1%    +458  [ = ]       0    .debug_loc
  +0.1%    +128  [ = ]       0    .debug_ranges
  +0.2%    +104  [ = ]       0    .debug_aranges
  +0.0%     +40  +0.0%     +40    .text
  +0.3%     +20  [ = ]       0    .debug_abbrev
  +0.0%     +12  [ = ]       0    .debug_line
 -16.4%      -9  [ = ]       0    [Unmapped]
  -0.1%     -44  [ = ]       0    .debug_frame
  -0.2%    -469  [ = ]       0    .strtab
  +0.1% +5.42Ki  +0.0%     +40    TOTAL
use_rust/pi_pico_w_ble — 674.2 KiB → 682.8 KiB (+1.27% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 645164	      0	  54048	 699212	  aab4c	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 636732	      0	  53696	 690428	  a88fc	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.8% +77.9Ki  [ = ]       0    .debug_str
  +2.0% +48.7Ki  [ = ]       0    .debug_info
  +1.4% +15.4Ki  [ = ]       0    .debug_loc
  +2.3% +7.64Ki  +2.3% +7.64Ki    .text
  +1.8% +7.31Ki  [ = ]       0    .debug_line
  +1.9% +4.60Ki  [ = ]       0    .debug_ranges
  +1.3% +4.56Ki  [ = ]       0    .strtab
  +2.1% +1.78Ki  [ = ]       0    .symtab
  +2.3%    +976  [ = ]       0    .debug_frame
  +0.2%    +604  +0.2%    +604    .rodata
  [ = ]       0  +0.7%    +352    .bss
  +0.8%    +336  [ = ]       0    .debug_aranges
  +0.2%     +20  [ = ]       0    .debug_abbrev
   +28%     +15  [ = ]       0    [Unmapped]
  +1.0%      +9  [ = ]       0    .defmt
  +1.8%  +169Ki  +1.3% +8.58Ki    TOTAL
use_rust/rp2040 — 145.6 KiB → 154.4 KiB (+6.08% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 144048	      0	  14084	 158132	  269b4	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 135332	      0	  13732	 149064	  24648	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +4.5% +73.9Ki  [ = ]       0    .debug_str
  +4.9% +47.9Ki  [ = ]       0    .debug_info
  +5.3% +16.5Ki  [ = ]       0    .debug_loc
  +6.8% +7.92Ki  +6.8% +7.92Ki    .text
  +4.5% +7.47Ki  [ = ]       0    .debug_line
  +5.8% +4.73Ki  [ = ]       0    .debug_ranges
  +3.9% +4.59Ki  [ = ]       0    .strtab
  +6.0% +1.94Ki  [ = ]       0    .symtab
  +5.7%    +976  [ = ]       0    .debug_frame
  +3.7%    +608  +3.7%    +608    .rodata
  [ = ]       0  +2.8%    +352    .bss
  +2.0%    +336  [ = ]       0    .debug_aranges
   +63%     +26  [ = ]       0    [Unmapped]
  +2.1%      +9  [ = ]       0    .defmt
  -0.2%     -16  [ = ]       0    .debug_abbrev
  +4.7%  +166Ki  +6.1% +8.86Ki    TOTAL
use_rust/rp2040_split (central) — 158.4 KiB → 167.2 KiB (+5.58% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 156288	      0	  14940	 171228	  29cdc	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 147576	      0	  14588	 162164	  27974	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +3.7% +74.2Ki  [ = ]       0    .debug_str
  +4.3% +47.8Ki  [ = ]       0    .debug_info
  +4.9% +17.6Ki  [ = ]       0    .debug_loc
  +6.3% +7.92Ki  +6.3% +7.92Ki    .text
  +4.1% +7.38Ki  [ = ]       0    .debug_line
  +5.1% +4.66Ki  [ = ]       0    .debug_ranges
  +3.5% +4.60Ki  [ = ]       0    .strtab
  +5.7% +1.98Ki  [ = ]       0    .symtab
  +5.2%    +976  [ = ]       0    .debug_frame
  +3.2%    +604  +3.3%    +604    .rodata
  [ = ]       0  +2.6%    +352    .bss
  +1.9%    +336  [ = ]       0    .debug_aranges
  +3.8%     +17  [ = ]       0    .defmt
 -17.5%     -10  [ = ]       0    [Unmapped]
  -0.2%     -16  [ = ]       0    .debug_abbrev
  +4.1%  +167Ki  +5.6% +8.85Ki    TOTAL
use_rust/rp2040_split (peripheral) — 28.1 KiB → 28.1 KiB (+0.00%)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  25692	     60	   3068	  28820	   7094	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25692	     60	   3068	  28820	   7094	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +2.84Ki  [ = ]       0    .debug_str
  +0.1%    +553  [ = ]       0    .debug_info
  +0.8%    +112  [ = ]       0    .debug_aranges
  +0.0%     +20  [ = ]       0    .debug_line
  +1.8%      +1  [ = ]       0    [Unmapped]
  +0.2% +3.51Ki  [ = ]       0    TOTAL
use_rust/stm32f1 — 61.7 KiB → 68.6 KiB (+11.20% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  63612	     28	   6656	  70296	  11298	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  56904	     28	   6280	  63212	   f6ec	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +8.3% +72.6Ki  [ = ]       0    .debug_str
  +8.4% +46.4Ki  [ = ]       0    .debug_info
  +9.3% +11.7Ki  [ = ]       0    .debug_loc
  +8.0% +6.99Ki  [ = ]       0    .debug_line
   +12% +6.55Ki   +12% +6.55Ki    .text
  +9.6% +3.98Ki  [ = ]       0    .debug_ranges
  +8.3% +3.60Ki  [ = ]       0    .strtab
   +12% +2.25Ki  [ = ]       0    .symtab
   +13% +1.55Ki  [ = ]       0    .debug_frame
   +10%    +488  [ = ]       0    .debug_aranges
  [ = ]       0  +6.0%    +376    .bss
  +0.5%     +30  [ = ]       0    .debug_abbrev
   +35%     +15  [ = ]       0    [Unmapped]
  +0.5%      +4  +0.6%      +4    .rodata
  +8.5%  +156Ki   +11% +6.92Ki    TOTAL
use_rust/stm32h7 — 119.1 KiB → 126.8 KiB (+6.48% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 115200	    320	  14364	 129884	  1fb5c	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 107640	    320	  14012	 121972	  1dc74	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +3.0% +74.2Ki  [ = ]       0    .debug_str
  +3.9% +45.9Ki  [ = ]       0    .debug_info
  +5.3% +13.0Ki  [ = ]       0    .debug_loc
  +7.3% +7.38Ki  +7.3% +7.38Ki    .text
  +4.8% +7.29Ki  [ = ]       0    .debug_line
  +6.5% +5.11Ki  [ = ]       0    .debug_ranges
  +3.6% +3.65Ki  [ = ]       0    .strtab
  +5.1% +1.80Ki  [ = ]       0    .symtab
  +6.2% +1.19Ki  [ = ]       0    .debug_frame
  +1.1%    +448  [ = ]       0    .debug_aranges
  [ = ]       0  +2.7%    +352    .bss
  +2.6%      +8  [ = ]       0    .defmt
  -7.3%      -4  [ = ]       0    [Unmapped]
  -0.3%     -22  [ = ]       0    .debug_abbrev
  +3.6%  +159Ki  +6.5% +7.73Ki    TOTAL

@HaoboGu

HaoboGu commented May 21, 2026

Copy link
Copy Markdown
Collaborator

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
Collaborator

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
Collaborator

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
Collaborator

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
Collaborator

#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

ldsands commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Okay I think that this implementation is much better (at least from a code smell perspective). I've done some of my normal manual testing in part using my own keyboard and I've been running it with profiles since yesterday afternoon (before the conflicts that came up) and I haven't had any issues as far as I can tell it works as intended.

Again, thanks for your patience with me on this pr. I have never done anything else in rust and I'm much more of a data scientist and thus my training is really limited in this area.

Let me know what changes (if any 🤞) are needed.

@HaoboGu

HaoboGu commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Thanks a lot! The current implementation looks great! I'll review it today and I'm really excited to get it merged soon. Thanks again for your effort on this.

@HaoboGu HaoboGu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall this version is much better 👍.

I think it can be even cleaner by removing layer_generations/layer_transition_generation/event, making this PR more focused

Comment thread rmk/src/keyboard.rs Outdated
Comment thread rmk/src/config/behavior.rs Outdated
Comment thread rmk-config/src/resolved/behavior.rs
Comment thread rmk/src/event/state.rs Outdated
Comment thread rmk/src/keymap.rs Outdated
Comment thread rmk-types/src/action/mod.rs Outdated
Comment thread rmk-types/src/action/mod.rs Outdated
@ldsands

ldsands commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

I removed the layer generation/transition machinery and unified the old one-shot paths under KeyAction::Sticky, substantially reducing the implementation scope. The full RMK feature matrices and focused Sticky, OSL/OSM, and Vial tests pass.

@ldsands

ldsands commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Maybe I should have just created a separate pr for this but I won't be around for a couple weeks starting in a couple of days. I realized that after I used OSM keys (e.g. ctrl+shift+arrow the timeout would start as I released those keys but then it causes issues when using the mouse or other keys right after I'm done with holding any of these OSM keys. Below is the explanation for what I've added.

Added an optional Sticky Key behavior, release_on_keyup_after_timeout.

When enabled, holding an OSM or OSL beyond its configured timeout and then releasing it immediately releases the modifier or layer. Previously, releasing the key would start a new timeout, which could leave modifiers or layers unexpectedly active.

[behavior.sticky_key]
release_on_keyup_after_timeout = true

The option defaults to false for backward compatibility and does not affect modified tap keys. Tests cover OSM and OSL behavior, default compatibility, and timeout/key-up ordering.

@ldsands

ldsands commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

I made a follow-up change to release_on_keyup_after_timeout that in actual use I liked better.

Using the normal Sticky Key timeout as the hold threshold turned out to be too restrictive. I still need a relatively long timeout for tapping and stacking OSM/OSL keys, but
I want a physically held modifier to stop immediately when I release it—even if it was held for less than that normal timeout.

I replaced the boolean option with an optional, independently configurable duration:

[behavior.sticky_key]
timeout = "1s"
release_on_keyup_after = "300ms"

The resulting behavior is:

  • If the Sticky Key is released before 300ms, it is treated as a tap and receives the complete normal 1s timeout starting from key-up.
  • If it is held for at least 300ms, releasing it immediately releases the modifier or layer.
  • Omitting release_on_keyup_after preserves the existing behavior.
  • Modified tap keys are still excluded from this behavior.

The primary use case is holding an OSM such as Ctrl, clicking a link with the mouse, and then releasing Ctrl. RMK cannot observe the external mouse click, so physical hold
duration is used to distinguish this from a tap. Separating the hold threshold from the normal timeout lets Ctrl release promptly without shortening the timeout needed for
tapping or stacking Sticky Keys.

The implementation also preserves the original physical press time when combo processing delays dispatch and handles overlapping Sticky Modifier producers consistently. I
added coverage for short taps, the exact threshold boundary, independent timeout/hold-threshold durations, timeout/event ordering, combo-buffered input, overlapping release
orders, and mixed profiles.

Comment thread rmk/src/keyboard.rs
/// Modifier and layer effects can coexist and therefore own distinct policies,
/// phases, sources, and deadlines. A tap key is exclusive with both.
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct StickyKeyState {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think sticky key should be general enough to process all held Actions. Distinguishing between modifier/layer/keys makes the current implementation not general at all. That means, this state should not be the current shape.

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.

Thanks — I reworked the state around that model in 03eb1a91. StickyKeyState now owns a uniform bounded collection of StickyEntry values, and every entry stores its canonical held Action together with the shared phase, policy, deadline, repetition, and buffered-claim lifecycle. OSM, OSL, and modified tap keys no longer have separate top-level state slots or lifecycle types.

Action-specific handling is now limited to applying and releasing the concrete effect. The small amount of modifier-only metadata remains because modifiers can have multiple physical producers and must remember whether their HID effect has already been reported; layer and tap-key effects are represented completely by their stored Action. The OSM and OSL aliases remain unchanged. 46673893 adds parity and edge-case coverage to ensure the consolidation does not remove existing behavior.

@HaoboGu HaoboGu Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

sorry maybe I didn't make it clear, the current code still has modifier/layer/tap-key processing logic in sticky key, which leads to the binary size bloats -- these actions' processing logic is duplicate in normal path and sticky key path.

Actually sticky key itself should not execute/classify any Action variant, all "effect" should be removed. Only a thin state like sticky: (Action, u8) is needed.

@HaoboGu HaoboGu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For the release_on_keyup_after, I feel it's more like a Morse/Tap-Hold behavior, not Sticky Key's. But since we've promoted the StickyKey as a KeyAction, I don't know if there's other solutions, so your code might be right and the only way to achieve this.

I prefer to have a name like release_after_hold and set a longer threshold?

@ldsands

ldsands commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

For the release_on_keyup_after, I feel it's more like a Morse/Tap-Hold behavior, not Sticky Key's. But since we've promoted the StickyKey as a KeyAction, I don't know if there's other solutions, so your code might be right and the only way to achieve this.

I prefer to have a name like release_after_hold and set a longer threshold?

Works for me.

  • Renamed release_on_keyup_after to release_after_hold throughout.
  • Changed the documented example threshold from 300ms to 500ms.
  • Kept the threshold optional and disabled by default.

@braindefender

Copy link
Copy Markdown
Contributor

@HaoboGu @ldsands What is the current status of this PR?

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.

3 participants