Skip to content

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

Open
ldsands wants to merge 33 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 33 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 372.1 KiB 377.1 KiB +1.33% ⬆️ +3524 0 +1544
use_config/nrf52840_ble 421.7 KiB 431.8 KiB +2.39% ⬆️ +2652 0 +7704
use_config/nrf52840_ble_split (central) 497.2 KiB 501.5 KiB +0.86% ⬆️ +2860 0 +1544
use_config/nrf52840_ble_split (peripheral) 322.2 KiB 323.2 KiB +0.29% ⬆️ +964 0 0
use_config/pi_pico_w_ble 659.3 KiB 664.0 KiB +0.71% ⬆️ +3296 0 +1544
use_config/rp2040 147.2 KiB 151.6 KiB +3.00% ⬆️ +2988 0 +1544
use_config/rp2040_split (central) 160.5 KiB 164.9 KiB +2.77% ⬆️ +3016 0 +1544
use_config/rp2040_split (peripheral) 27.4 KiB 27.4 KiB -0.01% ⬇️ -4 0 0
use_config/stm32f1 62.6 KiB 66.0 KiB +5.35% ⬆️ +2008 0 +1424
use_config/stm32h7 99.7 KiB 103.8 KiB +4.10% ⬆️ +2688 0 +1504
use_rust/nrf52832_ble 359.6 KiB 363.9 KiB +1.19% ⬆️ +2860 0 +1544
use_rust/nrf52840_ble 417.8 KiB 426.0 KiB +1.97% ⬆️ +2352 0 +6088
use_rust/nrf52840_ble_split (central) 506.6 KiB 512.8 KiB +1.22% ⬆️ +3068 0 +3272
use_rust/nrf52840_ble_split (peripheral) 319.0 KiB 319.8 KiB +0.26% ⬆️ +852 0 0
use_rust/pi_pico_w_ble 659.9 KiB 664.7 KiB +0.73% ⬆️ +3424 0 +1544
use_rust/rp2040 147.2 KiB 151.7 KiB +3.00% ⬆️ +2980 0 +1544
use_rust/rp2040_split (central) 159.6 KiB 164.1 KiB +2.82% ⬆️ +3068 0 +1544
use_rust/rp2040_split (peripheral) 27.8 KiB 27.8 KiB +0.00% 0 0 0
use_rust/stm32f1 62.6 KiB 65.6 KiB +4.65% ⬆️ +1564 0 +1424
use_rust/stm32h7 121.2 KiB 125.4 KiB +3.46% ⬆️ +2760 0 +1544
use_config/nrf52832_ble — 372.1 KiB → 377.1 KiB (+1.33% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 344940	   5256	  35920	 386116	  5e444	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 341416	   5256	  34376	 381048	  5d078	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.6% +18.1Ki  [ = ]       0    .debug_str
  +0.6% +10.5Ki  [ = ]       0    .debug_info
  +0.5% +3.45Ki  [ = ]       0    .debug_loc
  +1.1% +3.31Ki  +1.1% +3.31Ki    .text
  +0.7% +2.04Ki  [ = ]       0    .debug_line
  [ = ]       0  +4.6% +1.51Ki    .bss
  +0.3%    +908  [ = ]       0    .strtab
  +0.4%    +888  [ = ]       0    .debug_ranges
  +0.4%    +480  [ = ]       0    .symtab
  +0.6%    +280  [ = ]       0    .debug_frame
  +0.3%    +132  +0.3%    +132    .rodata
  +0.3%    +120  [ = ]       0    .debug_aranges
  +0.4%     +34  [ = ]       0    .debug_abbrev
  +0.1%      +1  [ = ]       0    .defmt
  -6.2%      -4  [ = ]       0    [Unmapped]
  +0.6% +40.1Ki  +1.3% +4.95Ki    TOTAL
use_config/nrf52840_ble — 421.7 KiB → 431.8 KiB (+2.39% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 378056	   5264	  58880	 442200	  6bf58	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 375404	   5264	  51176	 431844	  696e4	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.6% +19.3Ki  [ = ]       0    .debug_str
  +0.5% +10.5Ki  [ = ]       0    .debug_info
  [ = ]       0   +15% +7.52Ki    .bss
  +0.8% +2.46Ki  +0.8% +2.46Ki    .text
  +0.2% +1.58Ki  [ = ]       0    .debug_loc
  +0.4% +1.41Ki  [ = ]       0    .debug_line
  +0.4% +1.06Ki  [ = ]       0    .strtab
  +0.3%    +720  [ = ]       0    .debug_ranges
  +0.5%    +672  [ = ]       0    .symtab
  +0.9%    +428  [ = ]       0    .debug_frame
  +0.4%    +192  [ = ]       0    .debug_aranges
  +0.3%    +132  +0.3%    +132    .rodata
  +5.6%      +3  [ = ]       0    [Unmapped]
  +0.1%      +1  [ = ]       0    .defmt
  -0.4%     -38  [ = ]       0    .debug_abbrev
  +0.5% +38.4Ki  +2.4% +10.1Ki    TOTAL
use_config/nrf52840_ble_split (central) — 497.2 KiB → 501.5 KiB (+0.86% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 458428	   6588	  48536	 513552	  7d610	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 455568	   6588	  46992	 509148	  7c4dc	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.4% +15.4Ki  [ = ]       0    .debug_str
  +0.6% +14.7Ki  [ = ]       0    .debug_info
  +0.7% +5.59Ki  [ = ]       0    .debug_loc
  +0.7% +2.67Ki  +0.7% +2.67Ki    .text
  +0.5% +1.96Ki  [ = ]       0    .debug_line
  [ = ]       0  +3.4% +1.51Ki    .bss
  +0.3%    +894  [ = ]       0    .strtab
  +0.2%    +568  [ = ]       0    .debug_ranges
  +0.3%    +448  [ = ]       0    .symtab
  +0.6%    +312  [ = ]       0    .debug_frame
  +0.3%    +128  [ = ]       0    .debug_aranges
  +0.3%    +124  +0.3%    +124    .rodata
  +0.1%      +1  [ = ]       0    .defmt
 -17.1%     -12  [ = ]       0    [Unmapped]
  -0.6%     -55  [ = ]       0    .debug_abbrev
  +0.5% +42.7Ki  +0.9% +4.30Ki    TOTAL
use_config/nrf52840_ble_split (peripheral) — 322.2 KiB → 323.2 KiB (+0.29% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 297508	   5920	  27512	 330940	  50cbc	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 296544	   5920	  27512	 329976	  508f8	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1% +3.66Ki  [ = ]       0    .debug_str
  +0.2% +2.98Ki  [ = ]       0    .debug_info
  +0.4%    +964  +0.4%    +964    .text
  +0.1%    +461  [ = ]       0    .debug_loc
  +0.2%    +343  [ = ]       0    .strtab
  +0.1%    +112  [ = ]       0    .symtab
  +0.1%     +44  [ = ]       0    .debug_frame
  +0.1%     +32  [ = ]       0    .debug_aranges
  +0.2%      +1  [ = ]       0    .defmt
  -0.0%      -2  [ = ]       0    .debug_abbrev
 -12.2%      -6  [ = ]       0    [Unmapped]
  -0.0%     -56  [ = ]       0    .debug_ranges
  -0.0%     -83  [ = ]       0    .debug_line
  +0.1% +8.41Ki  +0.3%    +964    TOTAL
use_config/pi_pico_w_ble — 659.3 KiB → 664.0 KiB (+0.71% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 622816	      0	  57116	 679932	  a5ffc	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 619520	      0	  55572	 675092	  a4d14	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +16.9Ki  [ = ]       0    .debug_str
  +0.5% +12.6Ki  [ = ]       0    .debug_info
  +1.0% +3.09Ki  +1.0% +3.09Ki    .text
  +0.9% +2.15Ki  [ = ]       0    .debug_ranges
  +0.5% +1.90Ki  [ = ]       0    .debug_line
  [ = ]       0  +2.8% +1.51Ki    .bss
  +0.2%    +673  [ = ]       0    .strtab
  +0.4%    +304  [ = ]       0    .symtab
  +0.0%    +132  +0.0%    +132    .rodata
  +0.2%     +88  [ = ]       0    .debug_frame
  +0.1%     +40  [ = ]       0    .debug_aranges
  +0.1%      +1  [ = ]       0    .defmt
  -5.7%      -3  [ = ]       0    [Unmapped]
  -0.0%    -269  [ = ]       0    .debug_loc
  +0.4% +37.7Ki  +0.7% +4.73Ki    TOTAL
use_config/rp2040 — 147.2 KiB → 151.6 KiB (+3.00% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 138328	      0	  16916	 155244	  25e6c	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 135340	      0	  15372	 150712	  24cb8	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.2% +15.7Ki  [ = ]       0    .debug_str
  +1.3% +12.3Ki  [ = ]       0    .debug_info
  +2.4% +2.79Ki  +2.4% +2.79Ki    .text
  +2.5% +2.04Ki  [ = ]       0    .debug_ranges
  [ = ]       0   +11% +1.51Ki    .bss
  +0.9% +1.48Ki  [ = ]       0    .debug_line
  +0.7%    +690  [ = ]       0    .strtab
  +1.3%    +432  [ = ]       0    .symtab
  +0.1%    +216  [ = ]       0    .debug_loc
  +0.7%    +128  +0.7%    +128    .rodata
  +0.5%     +88  [ = ]       0    .debug_frame
  +0.2%     +40  [ = ]       0    .debug_aranges
   +57%     +20  [ = ]       0    [Unmapped]
  +2.2%      +9  [ = ]       0    .defmt
  +1.1% +35.9Ki  +3.0% +4.43Ki    TOTAL
use_config/rp2040_split (central) — 160.5 KiB → 164.9 KiB (+2.77% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 151140	      0	  17728	 168868	  293a4	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 148124	      0	  16184	 164308	  281d4	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.0% +15.2Ki  [ = ]       0    .debug_str
  +1.1% +11.7Ki  [ = ]       0    .debug_info
  +2.3% +2.82Ki  +2.3% +2.82Ki    .text
  +2.7% +2.44Ki  [ = ]       0    .debug_ranges
  +1.2% +2.20Ki  [ = ]       0    .debug_line
  [ = ]       0   +10% +1.51Ki    .bss
  +0.7%    +735  [ = ]       0    .strtab
  +1.7%    +592  [ = ]       0    .symtab
  +0.6%    +124  +0.6%    +124    .rodata
  +0.5%     +88  [ = ]       0    .debug_frame
  +0.2%     +40  [ = ]       0    .debug_aranges
  +0.2%      +1  [ = ]       0    .defmt
 -19.2%     -10  [ = ]       0    [Unmapped]
  -0.5% -1.59Ki  [ = ]       0    .debug_loc
  +0.9% +34.3Ki  +2.8% +4.45Ki    TOTAL
use_config/rp2040_split (peripheral) — 27.4 KiB → 27.4 KiB (-0.01% ⬇️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  25252	     60	   2764	  28076	   6dac	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25256	     60	   2764	  28080	   6db0	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1%    +692  [ = ]       0    .debug_str
  +0.1%    +214  [ = ]       0    .debug_info
  +0.2%     +24  [ = ]       0    .debug_aranges
  +0.0%     +17  [ = ]       0    .debug_line
  +6.8%      +3  [ = ]       0    [Unmapped]
  -0.1%      -4  -0.1%      -4    .rodata
  -0.0%      -6  [ = ]       0    .strtab
  +0.1%    +940  -0.0%      -4    TOTAL
use_config/stm32f1 — 62.6 KiB → 66.0 KiB (+5.35% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  58600	     28	   8928	  67556	  107e4	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  56592	     28	   7504	  64124	   fa7c	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.9% +13.6Ki  [ = ]       0    .debug_str
  +1.4% +7.67Ki  [ = ]       0    .debug_info
  +2.1% +2.42Ki  [ = ]       0    .debug_loc
  +3.6% +1.95Ki  +3.6% +1.95Ki    .text
  [ = ]       0   +19% +1.39Ki    .bss
  +1.4% +1.20Ki  [ = ]       0    .debug_line
  +1.4%    +600  [ = ]       0    .debug_ranges
  +1.3%    +420  [ = ]       0    .strtab
  +1.7%    +336  [ = ]       0    .symtab
  +1.6%    +208  [ = ]       0    .debug_frame
  +1.4%     +88  [ = ]       0    .debug_aranges
  +1.1%      +8  +1.1%      +8    .rodata
 -30.8%     -20  [ = ]       0    [Unmapped]
  +1.7% +28.4Ki  +5.4% +3.35Ki    TOTAL
use_config/stm32h7 — 99.7 KiB → 103.8 KiB (+4.10% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  94824	    268	  11240	 106332	  19f5c	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  92136	    268	   9736	 102140	  18efc	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.8% +14.1Ki  [ = ]       0    .debug_str
  +1.0% +9.59Ki  [ = ]       0    .debug_info
  +2.4% +4.04Ki  [ = ]       0    .debug_loc
  +3.2% +2.50Ki  +3.2% +2.50Ki    .text
  +1.3% +1.61Ki  [ = ]       0    .debug_line
  [ = ]       0   +15% +1.47Ki    .bss
  +0.6%    +376  [ = ]       0    .debug_ranges
  +0.6%    +335  [ = ]       0    .strtab
  +1.0%    +288  [ = ]       0    .symtab
  +1.0%    +152  [ = ]       0    .debug_frame
  +1.0%    +132  +1.1%    +132    .rodata
  +0.2%     +56  [ = ]       0    .debug_aranges
  +3.4%      +2  [ = ]       0    [Unmapped]
  +1.0% +33.1Ki  +4.1% +4.09Ki    TOTAL
use_rust/nrf52832_ble — 359.6 KiB → 363.9 KiB (+1.19% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 332412	   5264	  34912	 372588	  5af6c	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 329552	   5264	  33368	 368184	  59e38	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.6% +17.0Ki  [ = ]       0    .debug_str
  +0.6% +10.4Ki  [ = ]       0    .debug_info
  +0.9% +2.67Ki  +0.9% +2.67Ki    .text
  +0.3% +2.12Ki  [ = ]       0    .debug_loc
  +0.6% +1.85Ki  [ = ]       0    .debug_line
  [ = ]       0  +4.8% +1.51Ki    .bss
  +0.7% +1.43Ki  [ = ]       0    .debug_ranges
  +0.4%    +841  [ = ]       0    .strtab
  +0.4%    +496  [ = ]       0    .symtab
  +0.8%    +336  [ = ]       0    .debug_frame
  +0.4%    +144  [ = ]       0    .debug_aranges
  +0.3%    +124  +0.3%    +124    .rodata
  +1.3%    +100  [ = ]       0    .debug_abbrev
  +0.2%      +1  [ = ]       0    .defmt
 -34.0%     -17  [ = ]       0    [Unmapped]
  +0.6% +37.4Ki  +1.2% +4.30Ki    TOTAL
use_rust/nrf52840_ble — 417.8 KiB → 426.0 KiB (+1.97% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 378236	   5264	  52744	 436244	  6a814	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 375884	   5264	  46656	 427804	  6871c	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +17.5Ki  [ = ]       0    .debug_str
  +0.5% +11.3Ki  [ = ]       0    .debug_info
  [ = ]       0   +13% +5.95Ki    .bss
  +0.5% +3.54Ki  [ = ]       0    .debug_loc
  +0.7% +2.17Ki  +0.7% +2.17Ki    .text
  +0.5% +1.53Ki  [ = ]       0    .debug_line
  +0.3%    +784  [ = ]       0    .debug_ranges
  +0.2%    +485  [ = ]       0    .strtab
  +0.3%    +132  +0.3%    +132    .rodata
  +0.8%     +68  [ = ]       0    .debug_abbrev
  +0.0%     +24  [ = ]       0    .debug_frame
   +28%     +15  [ = ]       0    [Unmapped]
  +0.1%      +1  [ = ]       0    .defmt
  -0.0%      -8  [ = ]       0    .debug_aranges
  -0.0%     -64  [ = ]       0    .symtab
  +0.5% +37.5Ki  +2.0% +8.24Ki    TOTAL
use_rust/nrf52840_ble_split (central) — 506.6 KiB → 512.8 KiB (+1.22% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 462508	   6588	  56000	 525096	  80328	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 459440	   6588	  52728	 518756	  7ea64	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.4% +14.7Ki  [ = ]       0    .debug_str
  +0.5% +10.9Ki  [ = ]       0    .debug_info
  +0.9% +7.04Ki  [ = ]       0    .debug_loc
  [ = ]       0  +6.3% +3.20Ki    .bss
  +0.7% +2.88Ki  +0.7% +2.88Ki    .text
  +0.5% +1.94Ki  [ = ]       0    .debug_line
  +0.2%    +874  [ = ]       0    .strtab
  +0.3%    +632  [ = ]       0    .debug_ranges
  +0.3%    +448  [ = ]       0    .symtab
  +0.5%    +280  [ = ]       0    .debug_frame
  +0.3%    +124  +0.3%    +124    .rodata
  +0.3%    +120  [ = ]       0    .debug_aranges
  +0.1%      +6  [ = ]       0    .debug_abbrev
  +0.1%      +1  [ = ]       0    .defmt
 -39.4%     -28  [ = ]       0    [Unmapped]
  +0.5% +39.9Ki  +1.2% +6.19Ki    TOTAL
use_rust/nrf52840_ble_split (peripheral) — 319.0 KiB → 319.8 KiB (+0.26% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 295844	   5360	  26272	 327476	  4ff34	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 294992	   5360	  26272	 326624	  4fbe0	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +2.42Ki  [ = ]       0    .debug_info
  +0.1% +2.02Ki  [ = ]       0    .debug_str
  +0.2% +1.38Ki  [ = ]       0    .debug_loc
  +0.3%    +852  +0.3%    +852    .text
  +0.2%    +392  [ = ]       0    .debug_ranges
  +0.1%    +261  [ = ]       0    .strtab
   +26%     +13  [ = ]       0    [Unmapped]
  +0.0%      +8  [ = ]       0    .debug_aranges
  +0.2%      +1  [ = ]       0    .defmt
  -0.0%      -2  [ = ]       0    .debug_abbrev
  -0.0%      -4  [ = ]       0    .debug_frame
  -0.0%     -16  [ = ]       0    .symtab
  -0.0%     -87  [ = ]       0    .debug_line
  +0.1% +7.20Ki  +0.3%    +852    TOTAL
use_rust/pi_pico_w_ble — 659.9 KiB → 664.7 KiB (+0.73% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 623404	      0	  57260	 680664	  a62d8	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 619980	      0	  55716	 675696	  a4f70	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +15.4Ki  [ = ]       0    .debug_str
  +0.5% +11.7Ki  [ = ]       0    .debug_info
  +1.0% +3.21Ki  +1.0% +3.21Ki    .text
  +0.5% +1.92Ki  [ = ]       0    .debug_line
  +0.8% +1.82Ki  [ = ]       0    .debug_ranges
  [ = ]       0  +2.8% +1.51Ki    .bss
  +0.2%    +691  [ = ]       0    .strtab
  +0.5%    +400  [ = ]       0    .symtab
  +0.0%    +367  [ = ]       0    .debug_loc
  +0.0%    +132  +0.0%    +132    .rodata
  +0.2%     +88  [ = ]       0    .debug_frame
  +0.1%     +40  [ = ]       0    .debug_aranges
  +5.8%      +4  [ = ]       0    [Unmapped]
  +0.1%      +1  [ = ]       0    .defmt
  +0.4% +35.8Ki  +0.7% +4.85Ki    TOTAL
use_rust/rp2040 — 147.2 KiB → 151.7 KiB (+3.00% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 138504	      0	  16796	 155300	  25ea4	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 135524	      0	  15252	 150776	  24cf8	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.2% +16.1Ki  [ = ]       0    .debug_str
  +1.4% +13.3Ki  [ = ]       0    .debug_info
  +2.4% +2.79Ki  +2.4% +2.79Ki    .text
  +2.3% +1.85Ki  [ = ]       0    .debug_ranges
  +1.0% +1.67Ki  [ = ]       0    .debug_line
  [ = ]       0   +11% +1.51Ki    .bss
  +0.4% +1.31Ki  [ = ]       0    .debug_loc
  +0.7%    +673  [ = ]       0    .strtab
  +1.0%    +336  [ = ]       0    .symtab
  +0.7%    +128  +0.7%    +128    .rodata
  +0.5%     +88  [ = ]       0    .debug_frame
  +0.2%     +40  [ = ]       0    .debug_aranges
  +0.2%      +1  [ = ]       0    .defmt
 -10.9%      -5  [ = ]       0    [Unmapped]
  +1.2% +38.2Ki  +3.0% +4.42Ki    TOTAL
use_rust/rp2040_split (central) — 159.6 KiB → 164.1 KiB (+2.82% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 150556	      0	  17508	 168064	  29080	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 147488	      0	  15964	 163452	  27e7c	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.0% +15.7Ki  [ = ]       0    .debug_str
  +1.1% +11.7Ki  [ = ]       0    .debug_info
  +5.0% +4.45Ki  [ = ]       0    .debug_ranges
  +2.3% +2.88Ki  +2.3% +2.88Ki    .text
  +1.1% +1.91Ki  [ = ]       0    .debug_line
  [ = ]       0   +10% +1.51Ki    .bss
  +0.7%    +730  [ = ]       0    .strtab
  +1.7%    +592  [ = ]       0    .symtab
  +0.6%    +124  +0.6%    +124    .rodata
  +0.5%     +88  [ = ]       0    .debug_frame
  +0.2%     +40  [ = ]       0    .debug_aranges
  +4.1%      +2  [ = ]       0    [Unmapped]
  +0.2%      +1  [ = ]       0    .defmt
  -0.1%    -262  [ = ]       0    .debug_loc
  +1.1% +37.9Ki  +2.8% +4.50Ki    TOTAL
use_rust/rp2040_split (peripheral) — 27.8 KiB → 27.8 KiB (+0.00%)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  25372	     60	   3028	  28460	   6f2c	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25372	     60	   3028	  28460	   6f2c	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1%    +692  [ = ]       0    .debug_str
  +0.1%    +214  [ = ]       0    .debug_info
  +0.2%     +24  [ = ]       0    .debug_aranges
  +0.0%     +17  [ = ]       0    .debug_line
  -8.6%      -5  [ = ]       0    [Unmapped]
  -0.0%      -6  [ = ]       0    .strtab
  +0.1%    +936  [ = ]       0    TOTAL
use_rust/stm32f1 — 62.6 KiB → 65.6 KiB (+4.65% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  58208	     28	   8904	  67140	  10644	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  56644	     28	   7480	  64152	   fa98	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.9% +13.4Ki  [ = ]       0    .debug_str
  +1.3% +6.77Ki  [ = ]       0    .debug_info
  +2.8% +1.52Ki  +2.8% +1.52Ki    .text
  [ = ]       0   +19% +1.39Ki    .bss
  +1.1% +1.31Ki  [ = ]       0    .debug_loc
  +1.3% +1.14Ki  [ = ]       0    .debug_line
  +2.0%    +848  [ = ]       0    .debug_ranges
  +1.4%    +452  [ = ]       0    .strtab
  +1.5%    +304  [ = ]       0    .symtab
  +1.9%    +240  [ = ]       0    .debug_frame
  +2.2%    +104  [ = ]       0    .debug_aranges
  +1.2%      +8  +1.2%      +8    .rodata
 +10.0%      +5  [ = ]       0    [Unmapped]
  +1.6% +26.0Ki  +4.7% +2.92Ki    TOTAL
use_rust/stm32h7 — 121.2 KiB → 125.4 KiB (+3.46% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 111012	    324	  17068	 128404	  1f594	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 108252	    324	  15524	 124100	  1e4c4	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.8% +16.9Ki  [ = ]       0    .debug_str
  +1.0% +11.1Ki  [ = ]       0    .debug_info
  +1.8% +4.09Ki  [ = ]       0    .debug_loc
  +2.7% +2.67Ki  +2.7% +2.67Ki    .text
  +1.4% +2.09Ki  [ = ]       0    .debug_line
  +2.1% +1.67Ki  [ = ]       0    .debug_ranges
  [ = ]       0   +11% +1.51Ki    .bss
  +0.7%    +607  [ = ]       0    .strtab
  +0.7%    +256  [ = ]       0    .symtab
  +0.9%    +176  [ = ]       0    .debug_frame
  +0.2%     +64  [ = ]       0    .debug_aranges
  +0.5%     +24  +0.5%     +24    .rodata
  +0.3%      +1  [ = ]       0    .defmt
 -12.5%      -6  [ = ]       0    [Unmapped]
  +1.0% +39.7Ki  +3.5% +4.20Ki    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 and others added 15 commits June 2, 2026 11:05
When a non-SK key press triggers SK release, the implementation emits
two separate HID reports: first the SK release (modifier cleared), then
the new key registration. Update the test to expect both reports rather
than a single combined report.
…d SK PEG grammar

- Rename StickyModConfig → StickyKeyConfig in rmk-config/src/lib.rs
- Rename BehaviorConfig.sticky_mod → sticky_key field
- Remove sm_action PEG rule; add boolean, modifier_keep_list, sk_action rules
- Replace Rule::sm_action arm with Rule::sk_action in layout.rs
- Replace SM tests with SK grammar/parsing tests
- Rename sticky_mod_timeout_ms → sticky_key_timeout_ms in resolved/behavior.rs
- Fix cross-crate breakage in rmk-macro: update field reference to sticky_key_timeout_ms
…opts) syntax

- action_parser.rs: replace SM( arm with SK( arm; parses SK(key,[keep_mods],max_repeat,timeout_ms,exit_on_layer_change) using bracket-delimited keep-mod list and optional trailing args; emits ::rmk::sk!()
- behavior.rs: rename expand_sticky_mod→expand_sticky_key, use StickyKeyConfig, update field to sticky_key: in BehaviorConfig quote block; add sticky_mod: StickyModConfig::default() for remaining field
…ire-format snapshots

Snapshots changed because SK adds fields to the behavior wire type,
shifting endpoint key hashes.
… nightly rustfmt

- endpoint_keys_bulk.snap: the committed bytes were generated under a
  non-host BULK_SIZE; CI runs rmk-types with --features host, where
  BULK_SIZE = MAX_BULK_SIZE = 16, so the postcard schema hashes for the
  bulk_get RESP / bulk_set REQ endpoints differed. Regenerated with
  --features host to match CI.
- Apply nightly rustfmt to sticky_key.rs, layout_macro.rs,
  action_parser.rs, and keyboard_sticky_key_test.rs (CI uses nightly).
@ldsands ldsands force-pushed the feat/sticky-mod branch from cdee132 to 61aa4cf Compare June 2, 2026 16:06
@HaoboGu

HaoboGu commented Jun 3, 2026

Copy link
Copy Markdown
Owner

I was imaging SK would replace one-shot, but it seems in current implementation one-shot key is kept.

@ldsands

ldsands commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

I was imaging SK would replace one-shot, but it seems in current implementation one-shot key is kept.

@HaoboGu

I'm glad you brought this up. So as I mentioned earlier, I'm relying heavily on Claude, and so I clearly did not quite define well enough what I wanted it to do. Also, I somehow forgot that you were okay with completely replacing the one shot with sticky key, and so I was aiming to be completely backwards compatible. But re reading our earlier conversation, I realized that is not exactly what you wanted. I decided to merge in what I already had anyway because there were some changes that I thought were worth it to get into this branch and PR, and then I can now take another step of completely eliminating the one shot and migrating all those features into the sticky keys.

So I do still have a couple of questions for you. Mostly it's about how we want this formatted. I did make a couple of changes based on your recommendations from earlier, but you didn't quite cover everything in terms of how they should look for the new sticky key only migration. Below are examples of everything we'll need (I think).

old: OSM(LGui)
new: SK(LGui)

old: OSL(1)
new: SK(1) or SK(MO(1)) or SK(TO(1)) or SK("L1") or SK(Layer = 1) or SK(Layer, [1])

old: SK(Tab, [LAlt], 0, 0, true) or SK(Tab, [LCtrl])
new: SK(Tab, [LAlt])

So the OSM is very straightforward I assume no issues there though you had previously suggested SK(LGui [], 1) I think this simpler version is probably the way to go.

For OSL There are many different options here. I'm not really sure which one you'd prefer to take. If we just use SK(1) that'll be a very straightforward translation. However, I'm not sure that this is the way I would prefer. I think I prefer one of these two: SK(MO(1)) or SK(TO(1)). Do you have a strong preference for any of the options I put above, or if you have some other option you would prefer?

Finally, for the sticky mod I think just simplifying from your suggested SK(Tab, [LAlt], MAX_REPEAT) I would prefer to set the max repeat in the behavior profile.

Which brings us to the other major area where I would like your opinion.

As of right now (for the most part) we have three Toml profiles that handle the three different sets of one shot/sticky keys. Do note that the [behavior.sticky_key] options are implemented on the key definition itself but I will change that to just use the toml profile instead (more on this below).

old:

[behavior.one_shot]
timeout = "1s"                  # default: 1s   (Duration::from_secs(1))

[behavior.one_shot_modifiers]
activate_on_keypress = false    # default: false
quick_release = false           # default: false

[behavior.sticky_key]
timeout = "5s"                  # default: no timeout
max_repeat = 0                  # default: 0 = infinite (fires key on presses 1..=max_repeat, releases on max_repeat+1)
exit_on_layer_change = false    # default: false = SK survives layer changes (release only on key/timeout)

new:

[behavior.sticky_key]
timeout = "1s"                  # default: 1s   (Duration::from_secs(1))
activate_on_keypress = false    # default: false
quick_release = false           # default: false
max_repeat = 0                  # default: 0 = infinite (fires key on presses 1..=max_repeat, releases on max_repeat+1)
exit_on_layer_change = false    # default: false = SK survives layer changes (release only on key/timeout)

So I'm fairly certain that the behavior.one_shot and the behavior.one_shot_modifiers can easily be combined and just move the timeout into the behavior one shot modifiers and apply that default timeout to the one shot modifiers. If you have any issues with that, let me know. Now the behavior.sticky_key is a bit more complicated. I don't see any large issue with timeout being combined and applied to all one-shot/sticky keys, especially if down the road I implement an individual key override (which I do want so that I can increase the timeout of the alt tab sticky key/mod). The max repeat can be set there and does not interfere with the current one shot modifiers, but will only apply to the current sticky keys, so as I see it no issue combining there. Now the exit_on_layer_change could be more complicated. I could see some people not wanting a one-shot modifier to be held across a layer change. However, I would personally prefer that, but for my sticky mods, I do not want that. So once again, I would like to see an individual key override for this, but, I do not think it's a bad idea to keep this as a default that you set in the toml profile. Also, I'm not sure I actually like exit_on_layer_change as the name for this parameter. So if you have other suggestions for what to change this name to, let me know.

For now I don't think I'm going to implement any key level overrides to rather keep the combining as simple as possible. Then once I've confirmed that the consolidation is working well, then I would like to see what kind of effect adding these individual key overrides has on the RAM and flash.

What do you think?

@HaoboGu

HaoboGu commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Thanks for your detailed explanation! Yes some details needs to be clarified.

  1. For OSM/OSL, I prefer SK(LGui) and SK(Mo(1)). And if we do this, the default value of keep is [] and max_repeat is 1. It requires a change of default max_repeat value from current implementation.
  2. Yes all configurations should be merged into a single behavior.sticky_key, but I prefer to keep max_repeat in per-key config, because the max_repeat value should be always 1 for one-shot keys. It's impossible to have other values if we don't do per-key max_repeat config.
  3. I'm fine with exit_on_layer_change and current behavior.

Thanks again for your effort! Please feel free to discuss anything you're unclear about.

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