Skip to content

macos: fix Option+Arrow chords + modifier-state corruption in repeat-task cleanup#441

Open
tyvsmith wants to merge 2 commits into
feschber:mainfrom
tyvsmith:fix/macos-arrow-numericpad-flags
Open

macos: fix Option+Arrow chords + modifier-state corruption in repeat-task cleanup#441
tyvsmith wants to merge 2 commits into
feschber:mainfrom
tyvsmith:fix/macos-arrow-numericpad-flags

Conversation

@tyvsmith
Copy link
Copy Markdown
Contributor

Fixes #440.

Two related bugs in the macOS input-emulation backend, each addressed in its own commit so they can be reviewed and bisected independently.

Commit 1 — macos: post NumericPad and SecondaryFn flags for synthesized arrow keys

Hardware-generated arrow key CGEvents on macOS carry kCGEventFlagMaskNumericPad and kCGEventFlagMaskSecondaryFn in addition to any user-pressed modifiers. CGEventTap-based hotkey matchers (tiling window managers, accessibility tools) often check for those flags to disambiguate navigation keys from generic chords. Synthesized arrow chords without them fall through to the focused app instead of being captured by the WM.

Fix: set both flags on arrow key events (Mac key codes 0x7B0x7E).

if is_arrow_key(key) {
    flags |= CGEventFlags::CGEventFlagNumericPad | CGEventFlags::CGEventFlagSecondaryFn;
}

Commit 2 — macos: stop corrupting modifier state in repeat-task cleanup

spawn_repeat_task() takes a Mac CGKeyCode, but the cleanup block was passing that value to update_modifiers(), which expects a Linux evdev scancode (it calls scancode::Linux::try_from(key)). The two codespaces collide on several values, silently clearing still-held modifiers when the repeat task is cancelled by a follow-up key:

Mac code Mac key Linux scancode Cleanup effect
56 LeftShift KeyLeftAlt (56) clears Mod1Mask
125 Down arrow KeyLeftMeta (125) clears Mod4Mask
126 Up arrow KeyRightMeta (126) clears Mod4Mask

This made Shift+Option+X arrive as Shift+X and Cmd+Up/Cmd+Down arrive without Cmd — see issue #440 for the full trace.

Fix: remove the buggy update_modifiers call. The main consume() loop already updates modifier state with the correct Linux scancode on the real release event.

Verification

  • Built with cargo build --workspace and cargo bundle --release on macOS 26 (Sequoia, arm64)
  • cargo clippy --workspace --all-targets --all-features: clean
  • cargo fmt --check: clean
  • cargo test --workspace: passes (no unit tests for the macOS backend; AGENTS.md says "OS-specific backends: test via GTK/CLI on target OS or document manual verification")
  • Manual verification on a Mac receiver with OmniWM and a Linux sender:
    • ✅ Option+Arrow → WM focus moves (was: focused app got the arrow)
    • ✅ Shift+Option+Arrow → both modifiers in flags
    • ✅ Option+1/2/3/4 → WM workspace switch (regression check, still works)
    • ✅ Shift+Option+1/2/3/4 → both modifiers in flags (was: only Shift)
    • ✅ Cmd+Up/Cmd+Down → Cmd in flags (was: missing)
    • ✅ Plain typing and Down-arrow auto-repeat behave normally (regression checks)

Notes / follow-ups deliberately left out of this PR

  • Other navigation keys (Home/End/PgUp/PgDn/ForwardDelete/Help/F-row) also carry SecondaryFn on hardware. They aren't part of the reported symptom, but a follow-up could extend the same pattern.
  • Numeric-keypad number keys also carry NumericPad. Same situation.
  • The cleanup still posts a KeyUp for the cancelled key even when the user hasn't released it (because a different key was pressed). This is independently questionable behavior but doesn't visibly misbehave once the codespace bug is fixed, so left alone.

tyvsmith and others added 2 commits May 19, 2026 16:22
Hardware-generated arrow key events on macOS carry the NumericPad and
SecondaryFn flags in addition to any user-pressed modifiers. CGEventTap-
based hotkey matchers (tiling window managers, accessibility tools, etc.)
commonly check those flags to disambiguate navigation arrows from generic
chords, and reject events that lack them.

Before this change, synthesized Option+Arrow chords were silently
swallowed by the focused application instead of being captured by the
window manager, because the events arrived with only the Alternate flag
set. Hardware Option+Arrow chords on the local keyboard worked because
the OS itself set the missing flags.

Add NumericPad + SecondaryFn to the flags posted with arrow key events
(Mac key codes 0x7B-0x7E) so synthesized arrow chords match hardware
chords on the wire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
spawn_repeat_task() takes a Mac CGKeyCode, but the cleanup block was
passing that value to update_modifiers(), which expects a Linux evdev
scancode (it calls scancode::Linux::try_from(key)). The two codespaces
collide on several values, so cancelling the repeat task could
silently clear a still-held modifier:

  Mac LeftShift   = 56  == Linux KeyLeftAlt   = 56  -> clears Mod1Mask
  Mac Down arrow  = 125 == Linux KeyLeftMeta  = 125 -> clears Mod4Mask
  Mac Up arrow    = 126 == Linux KeyRightMeta = 126 -> clears Mod4Mask
  Mac Backslash   = 42  == Linux KeyLeftShift = 42  -> clears ShiftMask
  Mac "9"         = 29  == Linux KeyLeftCtrl  = 29  -> clears ControlMask

In practice this broke chords such as Shift+Option+X and Cmd+Down:
pressing Shift while holding Option cancels Option's repeat task and
runs the buggy cleanup, which then interprets Mac LeftShift's code
(56) as Linux KeyLeftAlt and removes Option from the modifier state.
The next key arrives with Shift only, so window-manager bindings on
the original Option chord never fire.

Remove the buggy update_modifiers() call. Modifier state is owned by
the main consume() loop, which already calls update_modifiers() with
the correct Linux scancode on the real release event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tyvsmith tyvsmith marked this pull request as ready for review May 19, 2026 23:29
Copilot AI review requested due to automatic review settings May 19, 2026 23:29
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes two macOS input-emulation issues that affected modifier correctness and global hotkey recognition in CGEventTap-based tools (issue #440), by aligning synthesized events more closely with hardware-generated events and preventing accidental modifier-state mutation during repeat-task cleanup.

Changes:

  • Stop calling update_modifiers() in repeat-task cleanup (it expects Linux evdev scancodes, but the repeat task is keyed by macOS CGKeyCode).
  • Add detection for arrow-key macOS virtual keycodes and attach NumericPad + SecondaryFn flags to synthesized arrow-key events.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@feschber
Copy link
Copy Markdown
Owner

Thanks for catching this! I've been wondering whats up with the arrow keys ...
We should probably still release the key repeat task but with the correct code.

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.

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

3 participants