macOS emulation: Option+Arrow chords don't reach window managers; Shift+Option+X and Cmd+Up/Down lose their leading modifier
## Environment
- macOS receiver running latest `main` (commit `a9461ae`), CLI mode
- Sender: Linux host (lan-mouse capture) with hardware keyboard attached
- Window manager: tiling WM that registers global hotkeys via `CGEventTap` (reproduced with [OmniWM](https://omniwm.app/), should reproduce with yabai/Amethyst/AeroSpace as well)
## Symptoms
### Bug 1 — Option+Arrow chords are not captured by the window manager
Pressing **Option+Left/Right/Up/Down** on the *Mac's built-in keyboard* correctly fires the window manager's focus-move bindings.
Pressing the same chord on the *remote Linux keyboard via lan-mouse* falls through to the focused application instead (e.g. in VS Code it performs the default word-jump). Other Option chords like Option+1/2/3/4 work fine through lan-mouse on the same setup.
### Bug 2 — Shift+Option+X and Cmd+Up/Down lose their leading modifier
Pressing **Shift+Option+1** (or any Shift+Option chord) on the remote keyboard arrives at the focused app as **Shift+1** — the Option flag is missing on the wire. Similarly, **Cmd+Down** / **Cmd+Up** arrives without the Cmd flag.
Plain Option+1 works. Plain Shift+1 works. Plain Cmd+key (non-arrow) works. The pattern is specifically:
- Holding a modifier (Option), then pressing another modifier (Shift), then a regular key → first modifier vanishes
- Cmd+Up or Cmd+Down specifically → Cmd vanishes
## Root causes (one per bug)
### Bug 1
`input-emulation/src/macos.rs::key_event()` posts arrow key `CGEvent`s with only the user-pressed modifier flags. Hardware arrow events on macOS additionally carry `kCGEventFlagMaskNumericPad` and `kCGEventFlagMaskSecondaryFn`, and hotkey-matching `CGEventTap`s (window managers, accessibility tools) often check for those flags to disambiguate navigation keys from generic chords. Without them, synthesized arrow chords fall through to the focused app.
References: [Apple `CGEvent.h`](https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.9.sdk/System/Library/Frameworks/CoreGraphics.framework/Versions/A/Headers/CGEvent.h), [keybd_event issue #44](https://github.com/micmonay/keybd_event/issues/44) (same pattern in another keyboard-synth library).
### Bug 2
The repeat-task cleanup at `input-emulation/src/macos.rs:110` calls:
\`\`\`rust
update_modifiers(&modifiers, key as u32, 0);
\`\`\`
`key` here is a **Mac `CGKeyCode`** (passed into `spawn_repeat_task`), but `update_modifiers()` internally does `scancode::Linux::try_from(key)` — it expects a **Linux evdev scancode**. The two codespaces accidentally collide on several values:
| Mac key code (dec) | Mac key | Linux scancode same value | Side effect of cleanup |
|---:|---|---|---|
| 56 | LeftShift | KeyLeftAlt (56) | **clears Mod1Mask (Option)** |
| 125 | Down arrow | KeyLeftMeta (125) | **clears Mod4Mask (Cmd)** |
| 126 | Up arrow | KeyRightMeta (126) | **clears Mod4Mask (Cmd)** |
| 42 | Backslash | KeyLeftShift (42) | clears ShiftMask |
| 29 | "9" | KeyLeftCtrl (29) | clears ControlMask |
| 58 | LeftOption | KeyCapsLock (58) | harmless (LockMask rarely held) |
Symptoms trace exactly:
- **Shift+Option+1**: pressing Shift cancels Option's repeat task → cleanup runs `update_modifiers(58, 0)` → no-op (CapsLock not held). Then pressing 1 cancels Shift's repeat task → cleanup runs `update_modifiers(56, 0)` → treated as `KeyLeftAlt` → **clears Mod1Mask (Option)**. The 1 KeyDown then goes out with only Shift in the flags.
- **Cmd+Down**: pressing Down cancels Cmd's repeat task → cleanup runs `update_modifiers(125, 0)` → treated as `KeyLeftMeta` → **clears Mod4Mask (Cmd)**. Down then goes out without Cmd.
- **Option+1** (works): pressing 1 cancels Option's repeat task → cleanup runs `update_modifiers(58, 0)` → no-op (collides with CapsLock, not held). State preserved.
The cleanup also doesn't actually need this call: when a key is genuinely released, the main `consume()` loop already calls `update_modifiers()` with the **correct Linux scancode** beforehand.
## Reproduction
1. Build and run lan-mouse on a Mac with a tiling WM (or any CGEventTap-based hotkey matcher) configured with Option+Arrow focus bindings
2. Connect a Linux sender with hardware keyboard
3. From the Linux keyboard, press Option+Arrow → expected: WM moves focus. Actual: focused app handles the arrow (e.g. caret moves a word in VS Code)
4. From the Linux keyboard, press Shift+Option+1 → expected: WM fires Shift+Option+1 binding, or focused app receives Shift+Option+1. Actual: focused app receives Shift+1 only
## Fix
PR incoming (one commit per bug):
1. Set `NumericPad | SecondaryFn` flags on synthesized arrow events
2. Remove the buggy `update_modifiers(key as u32, 0)` from the repeat-task cleanup
0 条评论