Skip to content

Space-as-modifier (hold-layer) still misbehaves after restructuring activation/cleanup per wiki "ExtMode/ExtBypass" pattern #369

@confluencepoint

Description

@confluencepoint

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

  1. Tap < 100 ms on Space → emits a regular Space keystroke. No layer activation. No residual Virtual1/spacemod state.
  2. 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.
  3. 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.
  4. 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

  1. Load the config below in keymapper 5.5.0 on Windows 11.
  2. Hold Space for ≥ 100 ms; while still holding, tap T.
  3. Release Space.
  4. 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

  1. 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.

  2. 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.

  3. 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.

  4. Verified @virtual-keys-toggle false is set globally — spacemod
    should behave as a hold-modifier, not a toggle.

  5. 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:

  1. Hold Space, tap R (emits Control{Space}), release Space.
  2. Press plain A.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions