Summary
A Space-as-modifier ("spacemod") hold-layer, built around Virtual1, does not behave reliably even after restructuring activation and cleanup along the lines of the wiki's "Using CapsLock as an extension key while keeping its original behavior" (ExtMode/ExtBypass) pattern.
The earlier, well-known failure mode of this kind of layer — virtual key latching after a layer mapping that synthesizes an output containing the trigger key — was the original motivation for the restructure. The new structure separates physical-key cleanup from the timing matcher:
Space{!100ms} >> Space
Space{100ms} >> spacemod ^
spacemod !Space >> !spacemod
The intent is that !spacemod fires on the physical Space release, independent of any synthetic Space outputs (e.g. Control{Space}, !Space) emitted by mappings inside the [modifier = "spacemod"] block or by app-specific contexts.
In practice this still does not produce a stable hold-layer. Single tap on Space sometimes outputs nothing / a stuck modifier / a duplicated character sequence, and combinations like Space+T, Space+A, Space+R either fail to fire the layer mapping or leave the layer active afterwards — depending on timing and on which app currently owns focus.
I would like to know whether this construction is expected to work in v5.5.0, and if so, what the canonical pattern is for a Space hold-layer where layer mappings synthesize outputs that include Space itself (Control{Space}, !Space ..., etc.). The wiki's CapsLock example is clean precisely because CapsLock is rarely needed as a synthetic output in the layer's own mappings; for Space that is not realistic.
Environment
- keymapper version: 5.5.0 (Windows x86_64, prebuilt release zip)
- Running binaries:
C:\Portable\keymapper\keymapper.exe,
C:\Portable\keymapper\keymapperd.exe
- Service:
keymapperd running as service, keymapper running per user
- OS: Windows 11 Pro 26200 (10.0.26200)
- Keyboard layout: German (QWERTZ), physical layout US is not in use
@options: update, @enforce-lowercase-commands,
@virtual-keys-toggle false
- Forwarded modifiers:
ShiftLeft ControlLeft AltRight
(asymmetric — ShiftRight, ControlRight, AltLeft are deliberately
used as DoubleTap triggers further down)
Expected behavior
- Tap < 100 ms on Space → emits a regular
Space keystroke. No layer activation. No residual Virtual1/spacemod state.
- Hold ≥ 100 ms on Space, then press a layer key (e.g.
T, R, Comma) → fires the [modifier = "spacemod"] mapping for that key exactly once. Releasing Space immediately deactivates the layer.
- After
Space is physically released, a subsequent isolated press of any layer key (e.g. plain A, plain T) emits its base character and does not trigger the spacemod mapping.
- Synthetic outputs that include
Space (e.g. R >> Control{Space}, A >> Control{Space} Tab, or app-specific !Space (Control Alt Shift){C} for Microsoft Teams) inside the layer must not feed back into the layer's own activation/cleanup logic.
Actual behavior
- "After
Space+T, releasing Space, then pressing plain A, the layer mapping for A (Control{Space} Tab) fires instead of a."
- "Tapping Space < 100 ms produces nothing / a stuck
Shift / two spaces."
- "The first
Space+R works; the second never fires the layer mapping."
Steps to reproduce
- Load the config below in keymapper 5.5.0 on Windows 11.
- Hold
Space for ≥ 100 ms; while still holding, tap T.
- Release
Space.
- Press plain
A.
Expected at step 4: literal a.
Actual at step 4 (TODO confirm): Control{Space} Tab (i.e. the
spacemod-A mapping) — would indicate the layer is still latching.
Config excerpts (from working config)
These are the relevant excerpts from my real config. Comments are kept in
German because they are part of my working file; semantics should still be
clear.
Global options and virtual-key declarations
@options update
@enforce-lowercase-commands
# Asymmetric: ShiftRight, ControlRight, AltLeft are deliberately NOT
# forwarded because they are used as DoubleTap triggers further down.
@forward-modifiers ShiftLeft ControlLeft AltRight
# Virtual keys are used as hold-modifiers, not toggles.
@virtual-keys-toggle false
# Macro: two presses of the same key within 100 ms.
DoubleTap = $0{!150ms} !100ms $0{!150ms}
spacemod = Virtual1
combomod = Virtual3
Spacemode activation/cleanup (the part this issue is about)
[default]
NumpadDecimal >> Period
# Spacemode (Space-as-modifier layer)
# - Tap < 100 ms emits Space normally.
# - Hold >= 100 ms activates spacemod.
# - Cleanup is on the separate "spacemod !Space >> !spacemod" rule and
# therefore on the PHYSICAL Space release event, NOT on the release-half
# of "Space{100ms} >> ... ^ ...". Synthetic Space outputs inside layer
# mappings (e.g. Control{Space} Tab in A, Control{Space} in R) and
# app-specific !Space injections (Teams block below) must not be able
# to defeat that.
# - Pattern from the wiki section
# "Using CapsLock as an extension key while keeping it's original
# behavior" (ExtMode/ExtBypass).
Space{!100ms} >> Space
Space{100ms} >> spacemod ^
spacemod !Space >> !spacemod
Spacemode layer (relevant subset)
[modifier = "spacemod"]
1 >> (Alt MetaLeft Control Shift){1}
# ...digits 2–9, 0 same shape...
T >> $("C:\Portable\AutoHotkey64.exe C:\Portable\TotalCommander.ahk") ^
# A >> Control{Space} Tab # currently commented out for testing
R >> Control{Space}
V >> !Shift Control{Semicolon} Shift{F3} !Shift ArrowDown ArrowDown ArrowDown Enter
M >> $("C:\Portable\AutoHotkey64.exe C:\Portable\minimize.ahk")
Comma >> Control{Escape}
App-specific block that injects !Space (relevant for the latch
question)
# Teams: app-specific triggers in addition to passthrough.
# - Space+C -> Ctrl+Alt+Shift+C
# - Space+S -> Ctrl+Alt+X
# - ,+/+. -> Win+ArrowDown
# 200 ms (vs. global 100 ms) so normal typing with Space here does not
# accidentally fire the triggers.
[system = "Windows" path = "ms-teams.exe"]
Space{!200ms C} >> !Space (Control Alt Shift){C}
Space{!200ms S} >> !Space (Control Alt){X}
(Comma Slash Period) >> MetaLeft{ArrowDown}
passthrough = true
What I already tried
-
Original pattern (latched on synthetic Space outputs):
Space{100ms} >> spacemod ^ !spacemod
Symptom: after Space+T or Space+A, spacemod stayed active. Pressing
plain A afterwards therefore triggered the spacemod-A mapping
(Control{Space} Tab) instead of emitting a. Workaround: tap Space
again, or restart keymapper.
-
Per-key cleanup hypothesis: I assumed !Space-prefixed outputs
inside the Teams block were the sole cause and tried adding explicit
!spacemod cleanup at the end of those mappings. This fixed the Teams
case but did not generalise: any layer mapping that synthesizes
Space (Control{Space}, Control{Space} Tab) reproduced the latch
in non-Teams apps.
-
Current structural fix (the one in the excerpt above), inspired by
the wiki's CapsLock ExtMode/ExtBypass example. The intent: cleanup
anchored to the physical Space release event, decoupled from the
timing matcher. This is the configuration in which I am still seeing
problems.
-
Verified @virtual-keys-toggle false is set globally — spacemod
should behave as a hold-modifier, not a toggle.
-
Rebuilt the layer in isolation (commented out CapsWord layer, left
combomod unchanged because it has no synthetic Comma/Period outputs
and is not symptomatic).
Hypothesis
The wiki ExtMode/ExtBypass pattern works for CapsLock because
CapsLock is essentially never the output of any layer mapping. For
Space that is not true: Control{Space}, !Space, and timing-gated
Space{!200ms X} are common.
I suspect that the second rule
Space{100ms} >> spacemod ^
still consumes the physical Space press in a way that prevents the
matching Space release from arriving as a clean !Space event for the
third rule
spacemod !Space >> !spacemod
…specifically when an intermediate layer mapping (R >> Control{Space},
A >> Control{Space} Tab) injects synthetic Space press/release pairs
between the original physical press and release.
If that is correct, the right primitive is probably "cleanup on the
physical key, addressed by code rather than by name", e.g. an !Any
fallback inside the spacemod block, or a dedicated escape-hatch that the
wiki does not currently document for Space-shaped triggers. I would
appreciate guidance on the recommended construction.
Minimal repro
TODO: replace with the smallest standalone keymapper.conf that still
reproduces. Suggested skeleton — please verify it actually fails
standalone before submitting:
@options update
@enforce-lowercase-commands
@forward-modifiers ShiftLeft ControlLeft AltRight
@virtual-keys-toggle false
spacemod = Virtual1
[default]
Space{!100ms} >> Space
Space{100ms} >> spacemod ^
spacemod !Space >> !spacemod
[modifier = "spacemod"]
A >> Control{Space} Tab
R >> Control{Space}
T >> Control{Space}
Reproduction:
- Hold
Space, tap R (emits Control{Space}), release Space.
- Press plain
A.
- Observe whether
A emits a (correct) or Control{Space} Tab
(latch — bug).
Additional notes
- The
combomod ((Comma Period)) layer in my real config has the same
fragile shape but is currently asymptomatic because none of its layer
mappings synthesize Comma/Period.
- Restarting
keymapperd clears any latched state, which is consistent
with the latch being purely runtime virtual-key state.
- I am happy to capture
keymapper -v / keymapperd -v traces of the
failing sequence if that would help.
Summary
A Space-as-modifier ("spacemod") hold-layer, built around
Virtual1, does not behave reliably even after restructuring activation and cleanup along the lines of the wiki's "Using CapsLock as an extension key while keeping its original behavior" (ExtMode/ExtBypass) pattern.The earlier, well-known failure mode of this kind of layer — virtual key latching after a layer mapping that synthesizes an output containing the trigger key — was the original motivation for the restructure. The new structure separates physical-key cleanup from the timing matcher:
The intent is that
!spacemodfires on the physicalSpacerelease, independent of any syntheticSpaceoutputs (e.g.Control{Space},!Space) emitted by mappings inside the[modifier = "spacemod"]block or by app-specific contexts.In practice this still does not produce a stable hold-layer. Single tap on Space sometimes outputs nothing / a stuck modifier / a duplicated character sequence, and combinations like
Space+T,Space+A,Space+Reither fail to fire the layer mapping or leave the layer active afterwards — depending on timing and on which app currently owns focus.I would like to know whether this construction is expected to work in v5.5.0, and if so, what the canonical pattern is for a Space hold-layer where layer mappings synthesize outputs that include
Spaceitself (Control{Space},!Space ..., etc.). The wiki's CapsLock example is clean precisely because CapsLock is rarely needed as a synthetic output in the layer's own mappings; for Space that is not realistic.Environment
C:\Portable\keymapper\keymapper.exe,C:\Portable\keymapper\keymapperd.exekeymapperdrunning as service,keymapperrunning per user@options:update,@enforce-lowercase-commands,@virtual-keys-toggle falseShiftLeft ControlLeft AltRight(asymmetric —
ShiftRight,ControlRight,AltLeftare deliberatelyused as DoubleTap triggers further down)
Expected behavior
Spacekeystroke. No layer activation. No residualVirtual1/spacemodstate.T,R,Comma) → fires the[modifier = "spacemod"]mapping for that key exactly once. Releasing Space immediately deactivates the layer.Spaceis physically released, a subsequent isolated press of any layer key (e.g. plainA, plainT) emits its base character and does not trigger thespacemodmapping.Space(e.g.R >> Control{Space},A >> Control{Space} Tab, or app-specific!Space (Control Alt Shift){C}for Microsoft Teams) inside the layer must not feed back into the layer's own activation/cleanup logic.Actual behavior
Space+T, releasing Space, then pressing plainA, the layer mapping forA(Control{Space} Tab) fires instead ofa."Shift/ two spaces."Space+Rworks; the second never fires the layer mapping."Steps to reproduce
Spacefor ≥ 100 ms; while still holding, tapT.Space.A.Expected at step 4: literal
a.Actual at step 4 (TODO confirm):
Control{Space} Tab(i.e. thespacemod-Amapping) — would indicate the layer is still latching.Config excerpts (from working config)
These are the relevant excerpts from my real config. Comments are kept in
German because they are part of my working file; semantics should still be
clear.
Global options and virtual-key declarations
Spacemode activation/cleanup (the part this issue is about)
Spacemode layer (relevant subset)
App-specific block that injects
!Space(relevant for the latchquestion)
What I already tried
Original pattern (latched on synthetic Space outputs):
Symptom: after
Space+TorSpace+A,spacemodstayed active. Pressingplain
Aafterwards therefore triggered thespacemod-Amapping(
Control{Space} Tab) instead of emittinga. Workaround: tap Spaceagain, or restart
keymapper.Per-key cleanup hypothesis: I assumed
!Space-prefixed outputsinside the Teams block were the sole cause and tried adding explicit
!spacemodcleanup at the end of those mappings. This fixed the Teamscase but did not generalise: any layer mapping that synthesizes
Space(Control{Space},Control{Space} Tab) reproduced the latchin non-Teams apps.
Current structural fix (the one in the excerpt above), inspired by
the wiki's CapsLock ExtMode/ExtBypass example. The intent: cleanup
anchored to the physical Space release event, decoupled from the
timing matcher. This is the configuration in which I am still seeing
problems.
Verified
@virtual-keys-toggle falseis set globally —spacemodshould behave as a hold-modifier, not a toggle.
Rebuilt the layer in isolation (commented out CapsWord layer, left
combomodunchanged because it has no synthetic Comma/Period outputsand is not symptomatic).
Hypothesis
The wiki ExtMode/ExtBypass pattern works for
CapsLockbecauseCapsLockis essentially never the output of any layer mapping. ForSpacethat is not true:Control{Space},!Space, and timing-gatedSpace{!200ms X}are common.I suspect that the second rule
still consumes the physical
Spacepress in a way that prevents thematching
Spacerelease from arriving as a clean!Spaceevent for thethird rule
…specifically when an intermediate layer mapping (
R >> Control{Space},A >> Control{Space} Tab) injects syntheticSpacepress/release pairsbetween the original physical press and release.
If that is correct, the right primitive is probably "cleanup on the
physical key, addressed by code rather than by name", e.g. an
!Anyfallback inside the spacemod block, or a dedicated escape-hatch that the
wiki does not currently document for
Space-shaped triggers. I wouldappreciate guidance on the recommended construction.
Minimal repro
Reproduction:
Space, tapR(emitsControl{Space}), releaseSpace.A.Aemitsa(correct) orControl{Space} Tab(latch — bug).
Additional notes
combomod((Comma Period)) layer in my real config has the samefragile shape but is currently asymptomatic because none of its layer
mappings synthesize
Comma/Period.keymapperdclears any latched state, which is consistentwith the latch being purely runtime virtual-key state.
keymapper -v/keymapperd -vtraces of thefailing sequence if that would help.