Skip to content

fix: re-enable CGEventTap on timeout and skip trackpad scroll events#64

Open
hieshima wants to merge 1 commit intoTomBadash:masterfrom
hieshima:fix/cgeventtap-timeout-and-trackpad-scroll
Open

fix: re-enable CGEventTap on timeout and skip trackpad scroll events#64
hieshima wants to merge 1 commit intoTomBadash:masterfrom
hieshima:fix/cgeventtap-timeout-and-trackpad-scroll

Conversation

@hieshima
Copy link
Copy Markdown
Contributor

Problem

Two macOS CGEventTap issues that affect daily use:

  1. Button/scroll remapping silently stops working after running for a while.
    macOS disables a CGEventTap when the callback responds too slowly (kCGEventTapDisabledByTimeout). The existing wake observer (NSWorkspaceDidWakeNotification) only covers sleep/wake — it does not handle runtime timeout disabling. Once disabled, all remapping permanently stops until restart.

  2. Trackpad gestures and scroll direction break while Mouser is running.
    The kCGEventScrollWheel handler intercepts all scroll events — including trackpad two-finger scrolls — without distinguishing input source. This causes:

    • Trackpad horizontal swipes trigger mapped mouse actions (e.g., browser back/forward)
    • invert_vscroll applies to trackpad, overriding the system scroll direction
    • macOS gesture recognition breaks because scroll phase sequences are interrupted

Related: #35, PR #45

Fix

1. Handle kCGEventTapDisabledByTimeout / kCGEventTapDisabledByUserInput

At the top of _event_tap_callback, detect the two disabled signal event types (0xFFFFFFFE, 0xFFFFFFFF) and immediately call CGEventTapEnable(tap, True). This is the Apple-recommended pattern for keeping an event tap alive.

2. Filter trackpad events via kCGScrollWheelEventIsContinuous

At the top of the kCGEventScrollWheel handler (before any dispatch/block/inversion logic), check CGEvent field 88 (kCGScrollWheelEventIsContinuous):

  • 0 → discrete mouse wheel event → process normally (dispatch, block, invert)
  • 1 → continuous trackpad/Magic Mouse event → return cg_event (pass through untouched)

This is the same approach used by Scroll Reverser, Mos, and LinearMouse. Field 88 is a documented Apple API stable since macOS 10.5.

Verification

CGEventTap re-enable (manual test on macOS 15.5)

Added a temporary time.sleep(5) inside _event_tap_callback to force macOS to trigger the timeout mechanism. Result:

07:51:01 [MouseHook] CGEventTap: first event received
07:51:06 [MouseHook] CGEventTap disabled by system (type=0xFFFFFFFE), re-enabling
07:51:17 [MouseHook] CGEventTap disabled by system (type=0xFFFFFFFE), re-enabling

The tap was automatically re-enabled each time macOS disabled it. Remapping resumed without restart.

Trackpad filtering (manual test on macOS 15.5)

  • Trackpad two-finger horizontal swipe: system gestures work normally, no spurious actions triggered ✅
  • Trackpad vertical scroll: follows system scroll direction setting (standard/natural) ✅
  • Mouse wheel horizontal tilt: mapped action fires correctly ✅
  • Mouse wheel vertical scroll with invert_vscroll: inversion applies correctly ✅

Automated tests

$ uv run python -m unittest discover -s tests -v
Ran 176 tests in 0.051s
OK (skipped=28)

New tests added:

  • test_reenable_on_timeout — callback receives 0xFFFFFFFE, asserts CGEventTapEnable(tap, True) is called
  • test_reenable_on_user_input — same for 0xFFFFFFFF
  • test_normal_event_does_not_reenable — normal events don't trigger re-enable
  • test_trackpad_scroll_passes_through_callback — continuous scroll returns cg_event as-is
  • test_trackpad_hscroll_not_blocked — trackpad horizontal scroll not dispatched as HSCROLL action
  • test_mouse_wheel_hscroll_dispatched_and_blocked — discrete mouse wheel horizontal scroll dispatches and blocks correctly

Two fixes for macOS CGEventTap reliability:

1. Handle kCGEventTapDisabledByTimeout (0xFFFFFFFE) and
   kCGEventTapDisabledByUserInput (0xFFFFFFFF) in the event tap
   callback by immediately calling CGEventTapEnable to re-activate.
   Without this, macOS silently disables the tap when the callback
   runs too slowly, permanently losing all button/scroll remapping.

2. Check kCGScrollWheelEventIsContinuous (field 88) at the top of
   the kCGEventScrollWheel handler to pass through trackpad and
   Magic Mouse events untouched. Previously all scroll events were
   intercepted regardless of source, which broke trackpad gestures,
   applied scroll inversion to trackpad, and triggered hscroll
   actions on two-finger swipes.
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.

1 participant