Skip to content

macOS: fix stuck drag and drop caused by clicks waiting on the state mutex - #32

Open
rufy89 wants to merge 1 commit into
handy-computer:mainfrom
rufy89:macos-click-fast-path
Open

macOS: fix stuck drag and drop caused by clicks waiting on the state mutex#32
rufy89 wants to merge 1 commit into
handy-computer:mainfrom
rufy89:macos-click-fast-path

Conversation

@rufy89

@rufy89 rufy89 commented Aug 2, 2026

Copy link
Copy Markdown

The macOS tap runs in Default mode and its mask includes the left and right
mouse buttons, so every click is held until the callback returns. The callback
takes the shared state mutex before it looks at the event type. That means a
plain click waits on the manager thread, even though an unmodified click ends
up in the empty match arm and does nothing.

When the manager thread holds the lock at the wrong moment, the delayed event
can be the LeftMouseUp that ends a drag and drop. The source app never sees the
release, its tracking loop stays open, and the dragged item stays stuck to the
cursor in whatever app started the drag. Quitting Handy frees it. Most people
just log out, since nothing else works.

I hit this repeatedly on 0.8.3 and went looking. While a drag was stuck,
CGGetEventTapList showed:

PID    APP     ON    MODE    avg ms
63479  handy   True  ACTIVE  327.6

327 ms average per event. A separate listen-only tap placed downstream logged
nothing at all for the 53 seconds the drag was stuck, so the events were not
lost, they were sitting in the callback. kill -TERM on Handy released them and
the drag finished on its own, no logout needed.

Not device specific: it happens with an external mouse and with the built-in
trackpad, which makes sense since the tap sits below where those converge.

The fix returns unmodified left/right clicks before the lock is taken. Same
behaviour as now, those clicks already reached a no-op arm, but they stop
queueing behind the mutex. Clicks with modifiers still go down the normal path,
so Cmd+Click style bindings keep working.

Two things this does not touch:

One thing worth flagging: the macos-modifier-tracking branch rewrites most of
this file, and it keeps the same shape, mask still lists the mouse buttons, tap
still created with CGEventTapOptions::Default, callback still takes the lock
before dispatching. So this fix will need carrying over there rather than
merging cleanly. Happy to rebase onto that branch instead if you prefer, or to
redo it there once it lands.

Tested on macOS 14.6.1, arm64:

  • cargo build and cargo clippy --all-targets are clean, cargo fmt reports no
    diff
  • cargo test --lib passes, 54 tests, including one I added for the predicate
    that guards the fast path
  • tests/macos_ipc_quiescence.rs: idle_listener_sends_no_recurring_ipc is flaky
    on my machine. It fails on unpatched main too. I ran patched and unpatched
    alternately and got failures on both sides, with counts swinging from 28 to
    205 messages between runs, so it looks sensitive to what else the machine is
    doing rather than related to this change. I see that file is deleted on the
    macos-modifier-tracking branch, so this may already be known.

Disclosure: I ran into this as a user and worked out the diagnosis with Claude
(Opus 5, via Claude Code), patch included. Everything reported here was measured
and run on my machine, macOS 14.6.1 on arm64.

The tap runs in Default mode with the mouse buttons in its mask, so every click is held until the callback returns. The callback takes the state mutex before dispatching on event type, so a plain click waits on the manager thread even though it ends up in the empty match arm and does nothing.

When that wait lands on the LeftMouseUp that ends a drag and drop, the source app never sees the release. Its tracking loop stays open and the dragged item stays stuck to the cursor until the tap goes away, which for most people means logging out.

Unmodified left and right clicks cannot match a binding, so return them before the lock is taken. Clicks with modifiers still take the normal path.
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