No behavior changes. Brings three files back in line with the
project's `style_edition = "2024"` rustfmt config so subsequent
edits don't carry unrelated formatting in their diffs.
InputCaptureState fetches the active-display bounds once in new()
via CGDisplay::active_displays(), and crossed() / start_capture()
both consult those frozen bounds for every barrier check and
cursor warp. Plug in a monitor, change resolution, or rearrange
displays in System Settings and the bounds go stale immediately:
the cursor either stops crossing at the new edge or warps to the
old edge coordinates.
Register a Quartz CGDisplayRegisterReconfigurationCallback on the
event-tap thread's CFRunLoop and route a new
ProducerEvent::DisplayReconfigured into the existing producer
channel. The producer task re-runs update_bounds() on the live
state, so subsequent barrier checks use the current geometry.
The callback fires twice per change — once with the
kCGDisplayBeginConfigurationFlag (BEFORE the bounds update) and
once after — we filter the begin-phase out and only refresh on
the post-change notification. The callback registration is
removed and the leaked sender Box is reclaimed when the run loop
exits, so create/destroy cycles don't leak channel senders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The kernel disables a session-level CGEventTap when its callback
runs longer than ~1 s on a single event — typical causes are heavy
load, scheduler contention, or the process being briefly suspended
(App Nap on a long idle, debugger pause). It is not a fatal
condition: Apple's documented recovery is to call CGEventTapEnable
and resume processing. Before this change the tap stayed dead until
the user manually clicked Re-enable from the menubar.
Stash the tap's mach port pointer in an Arc<OnceLock<usize>> set
immediately after CGEventTap::new returns, and on
TapDisabledByTimeout call CGEventTapEnable from the callback to
revive the tap while preserving capture state — the user doesn't
see the cursor pop back to the local screen mid-session for a
transient slow callback.
TapDisabledByUserInput keeps the existing teardown path: those
causes (TCC Accessibility revoked mid-session, secure-input mode,
explicit kill) are not safely recoverable from inside the
callback, and the existing fallthrough-fix from
59d9e45 / d1e963e still applies there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user pressed the release-bind chord (typically all four
modifiers) the down events for the chord were forwarded to the peer
while capture was active, but the matching up events arrived after
the local tap flipped to passthrough and were never forwarded. The
peer was left with phantom held modifiers until either its watchdog
ran (1+ s) or the Leave message was processed — and Leave is sent
over UDP/DTLS and can be lost.
Drain the capture's pressed_keys set in release_capture and emit a
KeyboardEvent::Key{state: 0} for every still-held key, plus a
zeroed KeyboardEvent::Modifiers, before sending Leave. The receiver
already maintains pressed_keys per handle and processes these
key-up events through its normal path, so no protocol change is
required and an unmodified peer picks up the fix automatically.
The receiver-side release_keys safety net stays in place for the
genuine packet-loss / disconnect-without-Leave cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user revokes Accessibility in System Settings while Lan
Mouse is in a captured session (cursor on a remote client), the
session-level event tap receives `TapDisabledByUserInput`. The
previous flow was:
1. Callback sends `ProducerEvent::EventTapDisabled` to notify_tx.
2. Callback falls through the `current_pos.is_some()` branch and
returns `CallbackResult::Drop` — *this very event*, plus any
racing callback still in flight, get `set_type(Null)`'d and
consumed.
3. Outer task calls `handle_producer_event(..).unwrap_or_else(|e|
log::error!(..))` — the `EventTapDisabled` error is just logged,
the loop keeps running, `current_pos` stays `Some`, cursor
stays hidden.
Net effect for the user: mouse motion keeps working (pass-through
when the tap is fully dead), but clicks and keypresses in the brief
disable window silently disappear, and the cursor is still hidden
where the captured session left it. Input looks broken until the
app is force-quit.
Fix:
- In the callback, when `TapDisabled*` fires, clear `current_pos`
and `CGDisplay::show_cursor` synchronously, then return
`CallbackResult::Keep` so this event (and any subsequent racing
one) can't hit the drop branch.
- Mirror the cleanup in `handle_producer_event`'s
`EventTapDisabled` arm so even if the outer task only logs the
error, state is still released.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On macOS the three TCC grants (Accessibility, Input Monitoring, Post
Event) live in separate Privacy panes. Before this change the
"Reenable" row sent the user to Accessibility regardless of which
grant was actually missing, and the daemon's own permission checks
re-fired the Accessibility prompt on every retry.
- lan-mouse-gtk/src/macos_privacy.rs: new module that exposes silent
preflight checks (AXIsProcessTrusted, CGPreflightListenEventAccess,
CGPreflightPostEventAccess), per-pane URL-scheme navigation, and
a Once-guarded fire_initial_prompts() called from build_ui. The
initial-prompt path only fires the Accessibility prompt if AX is
missing and then returns; secondary registrations run only after
AX is granted, which prevents a double Accessibility alert on
Sequoia where Post Event is nested under Accessibility.
- Input Monitoring registration attempts CGEventTapCreate at
kCGSessionEventTap (not kCGHIDEventTap) so a failure surfaces as
an Input Monitoring signal rather than triggering an Accessibility
prompt as a side effect.
- lan-mouse-gtk/src/window/imp.rs: handle_capture / handle_emulation
switch on the missing-pane enum and navigate to the specific pane
via x-apple.systempreferences:... URLs before re-requesting.
- lan-mouse-gtk/resources/window.ui: pill class on the Reenable
buttons so the hover padding matches the rest of libadwaita.
- input-capture/src/macos.rs, input-emulation/src/macos.rs: make
request_*_permission() a silent preflight (AXIsProcessTrusted /
CGPreflightListenEventAccess / CGPreflightPostEventAccess), so the
daemon no longer fires TCC prompts on retry — all prompting is
owned by the GUI.
- input-capture/src/error.rs, input-emulation/src/error.rs: new
error variants so the GUI can distinguish missing-AX from
missing-IM / missing-PostEvent for pane routing.
Verified on macOS 15.5: first launch fires a single AX prompt;
second launch (AX granted) registers under Input Monitoring via the
session-tap attempt and requests Post Event. Sequoia auto-grants the
listen-only path via AX so the IM list may stay empty, which is the
intended OS behavior and no longer blocks capture.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(macos): forward back/forward mouse buttons in capture and emulation
OtherMouseDown/Up events on macOS carry a button number field that
distinguishes middle (2), back (3), and forward (4) buttons. The
capture backend was unconditionally mapping all OtherMouse events to
BTN_MIDDLE, silently dropping back/forward. The emulation backend had
no match arms for BTN_BACK/BTN_FORWARD, causing them to be dropped
with a warning.
Fix capture by reading MOUSE_EVENT_BUTTON_NUMBER and mapping 3->BTN_BACK,
4->BTN_FORWARD. Fix emulation by adding match arms for BTN_BACK/BTN_FORWARD
and setting MOUSE_EVENT_BUTTON_NUMBER on the emitted CGEvent so macOS
apps receive the correct button identity.
* fix(macos): track button state and double-clicks by evdev code instead of CGMouseButton
Back, forward, and middle buttons all map to CGMouseButton::Center on
macOS, which caused them to share a single pressed-state boolean and
alias in double-click detection. Replace the ButtonState struct with a
HashSet<u32> keyed by evdev button code so each button is tracked
independently.
---------
Co-authored-by: Ferdinand Schober <ferdinandschober20@gmail.com>
* reference count capture
Multiple captures can now be created at the same position.
Captures at the same position are reference counted.
* update testcase
will be required by #200 / #164