ITADN

macOS emulation: Option+Arrow chords don't reach window managers; Shift+Option+X and Cmd+Up/Down lose their leading modifier

#440Opentyvsmith 创建于 2026-05-19
T
tyvsmithcommented
## 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 条评论