Skip to content

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

Open
ldsands wants to merge 134 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 134 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 397.8 KiB 406.3 KiB +2.13% ⬆️ +8356 0 +352
use_config/nrf52840_ble 439.4 KiB 447.6 KiB +1.85% ⬆️ +8000 0 +352
use_config/nrf52840_ble_split (central) 502.0 KiB 510.1 KiB +1.59% ⬆️ +7856 0 +352
use_config/nrf52840_ble_split (peripheral) 320.9 KiB 321.0 KiB +0.00% ⬆️ +32 0 0
use_config/pi_pico_w_ble 672.2 KiB 680.6 KiB +1.25% ⬆️ +8296 0 +352
use_config/rp2040 146.2 KiB 154.9 KiB +5.99% ⬆️ +8628 0 +352
use_config/rp2040_split (central) 159.7 KiB 168.7 KiB +5.59% ⬆️ +8804 0 +352
use_config/rp2040_split (peripheral) 27.9 KiB 27.9 KiB +0.00% 0 0 0
use_config/stm32f1 62.6 KiB 69.5 KiB +11.15% ⬆️ +6768 0 +376
use_config/stm32h7 100.8 KiB 109.3 KiB +8.47% ⬆️ +8372 0 +376
use_rust/nrf52832_ble 386.3 KiB 394.5 KiB +2.13% ⬆️ +8084 0 +352
use_rust/nrf52840_ble 436.2 KiB 444.8 KiB +1.97% ⬆️ +8464 0 +352
use_rust/nrf52840_ble_split (central) 505.6 KiB 513.6 KiB +1.58% ⬆️ +7836 0 +352
use_rust/nrf52840_ble_split (peripheral) 318.9 KiB 319.0 KiB +0.02% ⬆️ +76 0 0
use_rust/pi_pico_w_ble 672.0 KiB 680.6 KiB +1.28% ⬆️ +8480 0 +352
use_rust/rp2040 145.5 KiB 154.4 KiB +6.07% ⬆️ +8696 0 +352
use_rust/rp2040_split (central) 158.2 KiB 167.0 KiB +5.55% ⬆️ +8656 0 +352
use_rust/rp2040_split (peripheral) 28.2 KiB 28.2 KiB +0.00% 0 0 0
use_rust/stm32f1 61.8 KiB 68.8 KiB +11.21% ⬆️ +6728 0 +376
use_rust/stm32h7 119.6 KiB 126.9 KiB +6.04% ⬆️ +7052 0 +352
use_config/nrf52832_ble — 397.8 KiB → 406.3 KiB (+2.13% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 373632	   8620	  33824	 416076	  6594c	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 365276	   8620	  33472	 407368	  63748	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.0% +75.0Ki  [ = ]       0    .debug_str
  +2.4% +46.4Ki  [ = ]       0    .debug_info
  +2.0% +13.4Ki  [ = ]       0    .debug_loc
  +2.4% +7.57Ki  +2.4% +7.57Ki    .text
  +2.3% +7.24Ki  [ = ]       0    .debug_line
  +2.7% +5.47Ki  [ = ]       0    .debug_ranges
  +1.5% +4.92Ki  [ = ]       0    .strtab
  +1.8% +2.28Ki  [ = ]       0    .symtab
  +2.8% +1.27Ki  [ = ]       0    .debug_frame
  +1.6%    +608  +1.6%    +608    .rodata
  +1.0%    +480  [ = ]       0    .debug_aranges
  [ = ]       0  +1.1%    +352    .bss
  +0.2%     +20  [ = ]       0    .debug_abbrev
  +2.0%     +17  [ = ]       0    .defmt
  -4.3%      -3  [ = ]       0    [Unmapped]
  +2.1%  +164Ki  +2.1% +8.50Ki    TOTAL
use_config/nrf52840_ble — 439.4 KiB → 447.6 KiB (+1.85% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 405836	   8628	  43848	 458312	  6fe48	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 397836	   8628	  43496	 449960	  6dda8	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.8% +75.6Ki  [ = ]       0    .debug_str
  +2.2% +47.0Ki  [ = ]       0    .debug_info
  +1.3% +9.95Ki  [ = ]       0    .debug_loc
  +2.1% +7.22Ki  +2.1% +7.22Ki    .text
  +2.1% +6.95Ki  [ = ]       0    .debug_line
  +1.3% +4.69Ki  [ = ]       0    .strtab
  +2.0% +4.56Ki  [ = ]       0    .debug_ranges
  +1.4% +1.88Ki  [ = ]       0    .symtab
  +2.3% +1.13Ki  [ = ]       0    .debug_frame
  +1.5%    +608  +1.5%    +608    .rodata
  +0.9%    +416  [ = ]       0    .debug_aranges
  [ = ]       0  +0.8%    +352    .bss
  +0.7%     +59  [ = ]       0    .debug_abbrev
  +1.0%      +9  [ = ]       0    .defmt
  -2.2%      -1  [ = ]       0    [Unmapped]
  +1.9%  +159Ki  +1.9% +8.16Ki    TOTAL
use_config/nrf52840_ble_split (central) — 502.0 KiB → 510.1 KiB (+1.59% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 464596	   8684	  49016	 522296	  7f838	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 456740	   8684	  48664	 514088	  7d828	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.6% +78.0Ki  [ = ]       0    .debug_str
  +2.0% +47.3Ki  [ = ]       0    .debug_info
  +1.7% +13.8Ki  [ = ]       0    .debug_loc
  +1.8% +7.08Ki  +1.8% +7.08Ki    .text
  +1.8% +6.71Ki  [ = ]       0    .debug_line
  +1.2% +4.88Ki  [ = ]       0    .strtab
  +1.9% +4.87Ki  [ = ]       0    .debug_ranges
  +1.5% +2.20Ki  [ = ]       0    .symtab
  +2.4% +1.28Ki  [ = ]       0    .debug_frame
  +1.4%    +608  +1.4%    +608    .rodata
  +1.0%    +496  [ = ]       0    .debug_aranges
  [ = ]       0  +0.7%    +352    .bss
  +1.0%      +9  [ = ]       0    .defmt
 -25.4%     -17  [ = ]       0    [Unmapped]
  +1.7%  +167Ki  +1.6% +8.02Ki    TOTAL
use_config/nrf52840_ble_split (peripheral) — 320.9 KiB → 321.0 KiB (+0.00% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 292748	   8500	  27424	 328672	  503e0	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 292716	   8500	  27424	 328640	  503c0	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1% +3.86Ki  [ = ]       0    .debug_str
  +0.1% +1.31Ki  [ = ]       0    .debug_info
  +0.1%    +447  [ = ]       0    .debug_loc
  +0.1%    +176  [ = ]       0    .debug_ranges
  +0.2%    +104  [ = ]       0    .debug_aranges
  +0.0%     +33  [ = ]       0    .debug_line
  +0.0%     +32  +0.0%     +32    .text
  +0.3%     +20  [ = ]       0    .debug_abbrev
  -2.0%      -1  [ = ]       0    [Unmapped]
  -0.1%     -44  [ = ]       0    .debug_frame
  -0.2%    -481  [ = ]       0    .strtab
  +0.1% +5.45Ki  +0.0%     +32    TOTAL
use_config/pi_pico_w_ble — 672.2 KiB → 680.6 KiB (+1.25% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 643608	      0	  53376	 696984	  aa298	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 635312	      0	  53024	 688336	  a80d0	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.8% +76.5Ki  [ = ]       0    .debug_str
  +2.0% +47.8Ki  [ = ]       0    .debug_info
  +1.4% +15.1Ki  [ = ]       0    .debug_loc
  +2.3% +7.51Ki  +2.3% +7.51Ki    .text
  +1.7% +7.14Ki  [ = ]       0    .debug_line
  +1.4% +4.88Ki  [ = ]       0    .strtab
  +1.8% +4.48Ki  [ = ]       0    .debug_ranges
  +2.2% +1.83Ki  [ = ]       0    .symtab
  +2.2%    +944  [ = ]       0    .debug_frame
  +0.2%    +604  +0.2%    +604    .rodata
  [ = ]       0  +0.7%    +352    .bss
  +0.7%    +328  [ = ]       0    .debug_aranges
  +0.2%     +20  [ = ]       0    .debug_abbrev
  +1.8%     +17  [ = ]       0    .defmt
 -12.1%      -8  [ = ]       0    [Unmapped]
  +1.7%  +167Ki  +1.3% +8.45Ki    TOTAL
use_config/rp2040 — 146.2 KiB → 154.9 KiB (+5.99% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 144416	      0	  14244	 158660	  26bc4	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 135788	      0	  13892	 149680	  248b0	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +4.5% +75.1Ki  [ = ]       0    .debug_str
  +4.9% +47.7Ki  [ = ]       0    .debug_info
  +5.3% +15.9Ki  [ = ]       0    .debug_loc
  +6.7% +7.83Ki  +6.7% +7.83Ki    .text
  +4.5% +7.49Ki  [ = ]       0    .debug_line
  +5.7% +4.70Ki  [ = ]       0    .debug_ranges
  +3.8% +4.49Ki  [ = ]       0    .strtab
  +5.4% +1.73Ki  [ = ]       0    .symtab
  +5.5%    +944  [ = ]       0    .debug_frame
  +3.7%    +608  +3.7%    +608    .rodata
  [ = ]       0  +2.7%    +352    .bss
  +1.9%    +328  [ = ]       0    .debug_aranges
  +4.1%     +17  [ = ]       0    .defmt
   +28%     +11  [ = ]       0    [Unmapped]
  -0.0%      -2  [ = ]       0    .debug_abbrev
  +4.7%  +166Ki  +6.0% +8.77Ki    TOTAL
use_config/rp2040_split (central) — 159.7 KiB → 168.7 KiB (+5.59% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 157552	      0	  15180	 172732	  2a2bc	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 148748	      0	  14828	 163576	  27ef8	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +3.7% +74.3Ki  [ = ]       0    .debug_str
  +4.3% +47.7Ki  [ = ]       0    .debug_info
  +4.5% +15.6Ki  [ = ]       0    .debug_loc
  +6.3% +8.00Ki  +6.3% +8.00Ki    .text
  +4.2% +7.64Ki  [ = ]       0    .debug_line
  +5.2% +4.71Ki  [ = ]       0    .debug_ranges
  +3.4% +4.47Ki  [ = ]       0    .strtab
  +4.9% +1.73Ki  [ = ]       0    .symtab
  +4.9%    +944  [ = ]       0    .debug_frame
  +3.3%    +608  +3.3%    +608    .rodata
  [ = ]       0  +2.5%    +352    .bss
  +1.8%    +328  [ = ]       0    .debug_aranges
   +68%     +26  [ = ]       0    [Unmapped]
  +2.0%      +9  [ = ]       0    .defmt
  -0.0%      -2  [ = ]       0    .debug_abbrev
  +4.1%  +165Ki  +5.6% +8.94Ki    TOTAL
use_config/rp2040_split (peripheral) — 27.9 KiB → 27.9 KiB (+0.00%)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  25696	     60	   2804	  28560	   6f90	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25696	     60	   2804	  28560	   6f90	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +2.82Ki  [ = ]       0    .debug_str
  +0.1%    +553  [ = ]       0    .debug_info
  +0.8%    +112  [ = ]       0    .debug_aranges
  +0.0%     +20  [ = ]       0    .debug_line
  +0.0%      +5  [ = ]       0    .strtab
  +3.9%      +2  [ = ]       0    [Unmapped]
  +0.2% +3.50Ki  [ = ]       0    TOTAL
use_config/stm32f1 — 62.6 KiB → 69.5 KiB (+11.15% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  64456	     28	   6712	  71196	  1161c	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  57688	     28	   6336	  64052	   fa34	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +7.9% +72.1Ki  [ = ]       0    .debug_str
  +8.1% +45.7Ki  [ = ]       0    .debug_info
  +9.4% +11.7Ki  [ = ]       0    .debug_loc
  +8.2% +7.25Ki  [ = ]       0    .debug_line
   +12% +6.61Ki   +12% +6.61Ki    .text
  +9.6% +4.02Ki  [ = ]       0    .debug_ranges
  +7.8% +3.48Ki  [ = ]       0    .strtab
   +11% +2.16Ki  [ = ]       0    .symtab
   +12% +1.49Ki  [ = ]       0    .debug_frame
  +7.3%    +464  [ = ]       0    .debug_aranges
  [ = ]       0  +5.9%    +376    .bss
  +0.7%     +44  [ = ]       0    .debug_abbrev
  +0.5%      +4  +0.5%      +4    .rodata
 -26.7%     -16  [ = ]       0    [Unmapped]
  +8.2%  +154Ki   +11% +6.98Ki    TOTAL
use_config/stm32h7 — 100.8 KiB → 109.3 KiB (+8.47% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 103216	    264	   8488	 111968	  1b560	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  94844	    264	   8112	 103220	  19334	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +3.7% +71.8Ki  [ = ]       0    .debug_str
  +4.8% +44.6Ki  [ = ]       0    .debug_info
  +6.6% +12.0Ki  [ = ]       0    .debug_loc
  +9.1% +7.21Ki  +9.1% +7.21Ki    .text
  +5.7% +6.88Ki  [ = ]       0    .debug_line
  +6.1% +4.21Ki  [ = ]       0    .debug_ranges
  +5.2% +3.28Ki  [ = ]       0    .strtab
  +8.3% +2.22Ki  [ = ]       0    .symtab
  +9.7% +1.43Ki  [ = ]       0    .debug_frame
  +7.5%    +988  +7.5%    +988    .rodata
  +1.3%    +448  [ = ]       0    .debug_aranges
  [ = ]       0  +4.6%    +376    .bss
   +20%      +9  [ = ]       0    [Unmapped]
  -0.0%      -3  [ = ]       0    .debug_abbrev
  +4.4%  +155Ki  +8.5% +8.54Ki    TOTAL
use_rust/nrf52832_ble — 386.3 KiB → 394.5 KiB (+2.13% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 361592	   8620	  33800	 404012	  62a2c	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 353508	   8620	  33448	 395576	  60938	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.1% +73.4Ki  [ = ]       0    .debug_str
  +2.4% +45.2Ki  [ = ]       0    .debug_info
  +1.8% +11.9Ki  [ = ]       0    .debug_loc
  +2.4% +7.30Ki  +2.4% +7.30Ki    .text
  +2.4% +7.29Ki  [ = ]       0    .debug_line
  +2.4% +4.81Ki  [ = ]       0    .debug_ranges
  +1.5% +4.02Ki  [ = ]       0    .strtab
  +2.1% +2.39Ki  [ = ]       0    .symtab
  +3.4% +1.43Ki  [ = ]       0    .debug_frame
  +1.7%    +608  +1.7%    +608    .rodata
  +1.3%    +536  [ = ]       0    .debug_aranges
  [ = ]       0  +1.1%    +352    .bss
  +0.9%     +74  [ = ]       0    .debug_abbrev
  +1.7%      +8  [ = ]       0    .defmt
 -32.3%     -21  [ = ]       0    [Unmapped]
  +2.1%  +158Ki  +2.1% +8.24Ki    TOTAL
use_rust/nrf52840_ble — 436.2 KiB → 444.8 KiB (+1.97% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 405732	   8628	  41096	 455456	  6f320	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 397268	   8628	  40744	 446640	  6d0b0	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.9% +76.2Ki  [ = ]       0    .debug_str
  +2.2% +47.5Ki  [ = ]       0    .debug_info
  +1.8% +13.0Ki  [ = ]       0    .debug_loc
  +2.2% +7.67Ki  +2.2% +7.67Ki    .text
  +2.2% +7.32Ki  [ = ]       0    .debug_line
  +2.3% +5.32Ki  [ = ]       0    .debug_ranges
  +1.3% +4.64Ki  [ = ]       0    .strtab
  +1.4% +1.81Ki  [ = ]       0    .symtab
  +2.2% +1.07Ki  [ = ]       0    .debug_frame
  +1.5%    +608  +1.5%    +608    .rodata
  +0.8%    +384  [ = ]       0    .debug_aranges
  [ = ]       0  +0.9%    +352    .bss
  +0.6%     +52  [ = ]       0    .debug_abbrev
  +2.0%     +17  [ = ]       0    .defmt
 -28.9%     -22  [ = ]       0    [Unmapped]
  +2.0%  +165Ki  +2.0% +8.61Ki    TOTAL
use_rust/nrf52840_ble_split (central) — 505.6 KiB → 513.6 KiB (+1.58% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 466244	   8684	  50984	 525912	  80658	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 458408	   8684	  50632	 517724	  7e65c	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.7% +78.9Ki  [ = ]       0    .debug_str
  +2.0% +48.2Ki  [ = ]       0    .debug_info
  +1.6% +13.1Ki  [ = ]       0    .debug_loc
  +2.0% +7.20Ki  [ = ]       0    .debug_line
  +1.7% +7.06Ki  +1.7% +7.06Ki    .text
  +2.0% +5.17Ki  [ = ]       0    .debug_ranges
  +1.2% +4.74Ki  [ = ]       0    .strtab
  +1.2% +1.88Ki  [ = ]       0    .symtab
  +2.1% +1.14Ki  [ = ]       0    .debug_frame
  +1.3%    +608  +1.3%    +608    .rodata
  +0.9%    +416  [ = ]       0    .debug_aranges
  [ = ]       0  +0.7%    +352    .bss
  +1.8%     +17  [ = ]       0    .defmt
  +8.5%      +5  [ = ]       0    [Unmapped]
  -0.4%     -39  [ = ]       0    .debug_abbrev
  +1.7%  +168Ki  +1.6% +8.00Ki    TOTAL
use_rust/nrf52840_ble_split (peripheral) — 318.9 KiB → 319.0 KiB (+0.02% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 290948	   8500	  27192	 326640	  4fbf0	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 290872	   8500	  27192	 326564	  4fba4	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +7.02Ki  [ = ]       0    .debug_str
  +0.1%   +1010  [ = ]       0    .debug_info
  +0.2%    +280  [ = ]       0    .debug_ranges
  +0.2%     +96  [ = ]       0    .debug_aranges
  +0.0%     +78  [ = ]       0    .debug_line
  +0.0%     +76  +0.0%     +76    .text
  +0.0%     +75  [ = ]       0    .debug_loc
  +0.3%     +20  [ = ]       0    .debug_abbrev
 -10.4%      -7  [ = ]       0    [Unmapped]
  -0.2%     -60  [ = ]       0    .debug_frame
  -0.1%     -64  [ = ]       0    .symtab
  -0.1%    -144  [ = ]       0    .strtab
  +0.1% +8.35Ki  +0.0%     +76    TOTAL
use_rust/pi_pico_w_ble — 672.0 KiB → 680.6 KiB (+1.28% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 643488	      0	  53480	 696968	  aa288	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 635008	      0	  53128	 688136	  a8008	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.7% +74.7Ki  [ = ]       0    .debug_str
  +2.0% +47.8Ki  [ = ]       0    .debug_info
  +1.3% +14.5Ki  [ = ]       0    .debug_loc
  +2.3% +7.68Ki  +2.3% +7.68Ki    .text
  +1.8% +7.45Ki  [ = ]       0    .debug_line
  +1.3% +4.51Ki  [ = ]       0    .strtab
  +1.7% +4.16Ki  [ = ]       0    .debug_ranges
  +2.0% +1.67Ki  [ = ]       0    .symtab
  +2.2%    +944  [ = ]       0    .debug_frame
  +0.2%    +612  +0.2%    +612    .rodata
  [ = ]       0  +0.7%    +352    .bss
  +0.8%    +328  [ = ]       0    .debug_aranges
  +0.2%     +20  [ = ]       0    .debug_abbrev
  +1.8%     +17  [ = ]       0    .defmt
  -4.1%      -2  [ = ]       0    [Unmapped]
  +1.7%  +164Ki  +1.3% +8.62Ki    TOTAL
use_rust/rp2040 — 145.5 KiB → 154.4 KiB (+6.07% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 143984	      0	  14084	 158068	  26974	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 135288	      0	  13732	 149020	  2461c	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +4.6% +76.1Ki  [ = ]       0    .debug_str
  +4.9% +47.9Ki  [ = ]       0    .debug_info
  +5.1% +15.1Ki  [ = ]       0    .debug_loc
  +6.8% +7.90Ki  +6.8% +7.90Ki    .text
  +4.6% +7.52Ki  [ = ]       0    .debug_line
  +3.8% +4.50Ki  [ = ]       0    .strtab
  +5.4% +4.39Ki  [ = ]       0    .debug_ranges
  +5.5% +1.77Ki  [ = ]       0    .symtab
  +5.5%    +944  [ = ]       0    .debug_frame
  +3.7%    +608  +3.7%    +608    .rodata
  [ = ]       0  +2.8%    +352    .bss
  +2.0%    +328  [ = ]       0    .debug_aranges
  +4.1%     +17  [ = ]       0    .defmt
   +18%     +10  [ = ]       0    [Unmapped]
  -0.0%      -2  [ = ]       0    .debug_abbrev
  +4.8%  +167Ki  +6.1% +8.84Ki    TOTAL
use_rust/rp2040_split (central) — 158.2 KiB → 167.0 KiB (+5.55% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 156132	      0	  14924	 171056	  29c30	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 147476	      0	  14572	 162048	  27900	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +3.7% +73.4Ki  [ = ]       0    .debug_str
  +4.3% +47.6Ki  [ = ]       0    .debug_info
  +4.0% +13.6Ki  [ = ]       0    .debug_loc
  +6.3% +7.86Ki  +6.3% +7.86Ki    .text
  +4.2% +7.64Ki  [ = ]       0    .debug_line
  +5.3% +4.84Ki  [ = ]       0    .debug_ranges
  +3.4% +4.42Ki  [ = ]       0    .strtab
  +4.2% +1.48Ki  [ = ]       0    .symtab
  +5.0%    +944  [ = ]       0    .debug_frame
  +3.2%    +604  +3.3%    +604    .rodata
  [ = ]       0  +2.6%    +352    .bss
  +1.8%    +328  [ = ]       0    .debug_aranges
  +2.0%      +9  [ = ]       0    .defmt
  -0.0%      -2  [ = ]       0    .debug_abbrev
 -32.8%     -20  [ = ]       0    [Unmapped]
  +4.0%  +162Ki  +5.6% +8.80Ki    TOTAL
use_rust/rp2040_split (peripheral) — 28.2 KiB → 28.2 KiB (+0.00%)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  25796	     60	   3068	  28924	   70fc	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25796	     60	   3068	  28924	   70fc	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +2.82Ki  [ = ]       0    .debug_str
  +0.1%    +553  [ = ]       0    .debug_info
  +0.8%    +112  [ = ]       0    .debug_aranges
  +0.0%     +20  [ = ]       0    .debug_line
  +0.0%      +5  [ = ]       0    .strtab
  -3.8%      -2  [ = ]       0    [Unmapped]
  +0.2% +3.49Ki  [ = ]       0    TOTAL
use_rust/stm32f1 — 61.8 KiB → 68.8 KiB (+11.21% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  63744	     28	   6648	  70420	  11314	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  57016	     28	   6272	  63316	   f754	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +8.2% +72.6Ki  [ = ]       0    .debug_str
  +8.4% +46.1Ki  [ = ]       0    .debug_info
   +10% +12.6Ki  [ = ]       0    .debug_loc
  +8.3% +7.25Ki  [ = ]       0    .debug_line
   +12% +6.57Ki   +12% +6.57Ki    .text
   +11% +4.56Ki  [ = ]       0    .debug_ranges
  +8.1% +3.53Ki  [ = ]       0    .strtab
   +12% +2.28Ki  [ = ]       0    .symtab
   +12% +1.51Ki  [ = ]       0    .debug_frame
   +10%    +472  [ = ]       0    .debug_aranges
  [ = ]       0  +6.0%    +376    .bss
  +0.8%     +44  [ = ]       0    .debug_abbrev
  +0.5%      +4  +0.6%      +4    .rodata
 -18.3%     -11  [ = ]       0    [Unmapped]
  +8.6%  +157Ki   +11% +6.94Ki    TOTAL
use_rust/stm32h7 — 119.6 KiB → 126.9 KiB (+6.04% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 115228	    320	  14364	 129912	  1fb78	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 108176	    320	  14012	 122508	  1de8c	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +3.0% +74.1Ki  [ = ]       0    .debug_str
  +3.9% +45.6Ki  [ = ]       0    .debug_info
  +5.2% +12.3Ki  [ = ]       0    .debug_loc
  +4.6% +6.98Ki  [ = ]       0    .debug_line
  +6.8% +6.89Ki  +6.8% +6.89Ki    .text
  +6.6% +5.23Ki  [ = ]       0    .debug_ranges
  +3.6% +3.63Ki  [ = ]       0    .strtab
  +5.3% +1.86Ki  [ = ]       0    .symtab
  +6.1% +1.19Ki  [ = ]       0    .debug_frame
  +1.1%    +456  [ = ]       0    .debug_aranges
  [ = ]       0  +2.7%    +352    .bss
  +2.6%      +8  [ = ]       0    .defmt
 -15.9%     -10  [ = ]       0    [Unmapped]
  -0.3%     -22  [ = ]       0    .debug_abbrev
  +3.6%  +158Ki  +6.0% +7.23Ki    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.

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