openlogi-hook 0.8.13

OS-level mouse-event hook for OpenLogi. macOS via CGEventTap; Linux via evdev+uinput; Windows via WH_MOUSE_LL.
Documentation
# openlogi-hook — OS input capture

The event-tap crate: CGEventTap on macOS, evdev+uinput on Linux, WH_MOUSE_LL
on Windows. Cross-platform cfg discipline is
[`.agents/rules/cross-platform.md`](../../.agents/rules/cross-platform.md), and
the macOS FFI contract is
[`.agents/rules/objc-ffi.md`](../../.agents/rules/objc-ffi.md); this file is the
crate's own load-bearing behavior.

- macOS: the CGEventTap freeze-hazard state machine is load-bearing. The tap must
  self-disable when Accessibility is revoked, on its own thread, with the bounded
  run-loop slice — a stopped watcher after grant once froze all input on the machine.
  Don't restructure it casually, and don't migrate the tap to `objc2-core-graphics`.
  The `NSWorkspace` read and the Accessibility-trust check/prompt are the parts that
  did move to the objc2 framework crates — see `.agents/rules/objc-ffi.md` for the rule that
  every TCC call uses a typed binding rather than a hand-written `extern` block.
- `AXIsProcessTrusted()` is **not** a revocation signal: it keeps returning `true`
  after the user deletes the app's row from System Settings, which is how #674 froze
  clicks machine-wide. `has_accessibility` pairs it with a throwaway filtering tap
  (`CGEventTapCreate` → NULL when the grant is gone); keep both, in that order — the
  trust read is the cheap short-circuit, the probe is the truth. Never probe with
  `ListenOnly`: that asks about Input Monitoring, a different grant.
- The live tap probes on cue, not every slice. `grant::ProbeCue` owns an `axwatch`
  watch — the `com.apple.accessibility.api` distributed notification plus tccd's
  `com.apple.tcc.access.changed` Darwin notification, the only one that fires when the
  row is *removed* — and a `PROBE_HEARTBEAT` backstop; each probe is a WindowServer
  round trip that stalls for seconds around a sleep transition (#952). Keep the
  heartbeat: the notifications are freshness, not a completeness proof, and the probe
  stays the authority. Delivery needs the agent's main-thread `NSApplication` loop,
  which `run_app_loop` starts once the core arms.
- Re-enabling the tap each slice is idempotent and recovers a disable the OS never
  reported. Charge `RearmBudget` from both `TapDisabledBy*` and
  `CGEventTapIsEnabled`: a tap the system keeps disabling must be let go instead of
  fought over, even when CoreGraphics omits the callback.
- Keep the `CGEventTap` owned by its run-loop thread. Normal teardown disables it
  synchronously there and `Drop` invalidates its Mach port; a watchdog whose tap
  thread is wedged force-exits so the OS destroys that process-owned port. Do not
  add cross-thread tap ownership solely to pre-invalidate it during process exit —
  Core Graphics does not document that operation as thread-safe.
- The tap callback must never block and never panic: use `try_read`/`try_lock` only,
  queue bound actions off-thread, wrap the user callback in `catch_unwind`, and keep
  the stuck-callback watchdog that force-exits the agent if the budget is exceeded.
  An active HID-level tap serialises every pointer event; a hang freezes clicks
  machine-wide. Only suppress events from remappable Logitech sources
  (`source_is_remappable`) — never the built-in trackpad.
- A macOS tap stop request is not proof of teardown. Keep the independent lifecycle
  watchdog armed until the tap thread reports the tap destroyed (and, for an explicit
  stop, the thread exited). The watchdog must not call the Accessibility trust API —
  that query can stall during TCC revocation; monitor tap-thread progress instead, and
  force-exit the agent if revocation or shutdown stalls so macOS releases the HID tap.
- A watchdog budget — lifecycle and callback alike — is charged only for stall the
  watchdog was awake to see, and a gap in a watchdog's own schedule is discounted only
  when a kernel sleep or wake fell inside it (`kern.sleeptime` / `kern.waketime`
  moved). Around such a transition the lifecycle watchdog thread has fired 1.0 s and
  4.8 s past its budget, which a thread evaluating every 100 ms cannot do: it was not
  running, and neither was the tap thread it judges. Reading its own schedule gap as
  tap-thread stall force-exited healthy agents on lid close (#952); the callback
  watchdog would read a callback entered just before the same freeze the same way. A
  gap with no transition in it is charged in full, so a watchdog merely delayed by
  scheduling still catches a wedged tap on schedule. Likewise the between-slice
  capability probe runs under `TapPhase::Probing` with its own budget: it is a
  WindowServer/TCC round trip, not tap servicing — and a stop that waited out a slow
  probe gets the short budget afresh for the teardown. Refresh progress *before*
  publishing `Armed` again, or the watchdog judges a probe that already returned against
  the pre-probe mark. Both exit logs carry `stalled_ms` (uptime since the stall began)
  and `watched_ms`; a wide gap between them is the process-frozen signature.
- The off-main `frontmost_application` read keeps its explicit `autoreleasepool` — the
  watcher thread has no run loop; that is the only place in this crate a pool belongs.
  Every string it copies out (bundle id *and* localized name) must be owned before the
  pool drops.
- This crate ships non-macOS implementations (evdev/uinput, WH_MOUSE_LL) that a
  macOS-green build never compiles. CI lints them; treat the linux/windows CI jobs as
  the check, not local builds.