feat(hook,agent,gui): vibrate mice when the cursor hovers a window's close button - #987
feat(hook,agent,gui): vibrate mice when the cursor hovers a window's close button#987cddu33 wants to merge 13 commits into
Conversation
…close button Adds a Logi Options+ parity feature: a single haptic pulse (HapticWaveform::SubtleCollision) fires when the cursor enters a window's close control. macOS uses an Accessibility hit-test (objc2-application-services, typed AXUIElement bindings); Windows uses the OS's own WM_NCHITTEST answer via WindowFromPoint + SendMessageTimeoutW; Linux stays a clean no-op (Wayland can't answer this, X11 support is out of scope). - openlogi-hook: new HookBackend::cursor_is_over_close_button capability, default false, implemented per-platform. - openlogi-agent-core: a new close_button_haptics watcher (Poll::on_change, dedupes on the false->true/true->false transition) and Orchestrator::close_button_haptic_routes, which returns every online, haptic-capable, opted-in device (there is one cursor, so every eligible device buzzes together). - openlogi-agent: the watcher is started/stopped dynamically - only while at least one connected device has the new per-device setting enabled, rechecked on a cheap 500ms interval - rather than polling the OS unconditionally. RingHapticPlayer is generalized from a single DeviceRoute to Vec<DeviceRoute> (play_many/arm_many) so this feature and the Actions Ring share one single-flight coalescing worker instead of two near-duplicate ones. - openlogi-core: new per-device DeviceConfig::close_button_haptics bool, following the existing invert_scroll field's exact serde shape. - openlogi-desktop: a new settings card (Pointer tab) toggles the setting per device, gated on the same haptic-feedback capability check the Actions Ring already uses (now shared via AppState::current_haptics_supported). - i18n: three new strings added to every locale catalog.
The close-button haptics feature shipped its three new keys as English fill-ins in every non-English catalog, so the card read English in all 21 locales. Translate them, reusing each catalog's established terminology for "haptic feedback" (from the Actions Ring card) and for the cursor/pointer, and the existing "This device does not ..." phrasing for the unsupported case. en.yml is unchanged.
# Conflicts: # crates/openlogi-agent/src/main.rs # crates/openlogi-desktop/src/state.rs
The upstream merge (2c3c893) made AppState.config a ConfigState wrapper that no longer allows direct mutation; this call site still used the pre-refactor pattern and didn't compile until fixed inline during the merge — commit it separately since it landed after `git commit --no-edit`.
Greptile SummaryThe PR adds configurable close-button hover haptics across native hit-testing, agent-side device routing and playback, desktop settings, and localization. The latest changes attempt to rearm devices across eligibility transitions and generalize the haptic worker to multiple routes, but both previously reported arming failures remain reachable.
Confidence Score: 3/5The PR is not yet safe to merge because stable routes can remain disarmed after power transitions and concurrent arming requests can discard required routes. The current route-difference logic does not reassert firmware state when a wake preserves route identity, while the shared pending arming slot still lets the Actions Ring and close-button watcher overwrite each other; both paths can leave enabled haptic interactions physically silent. Files Needing Attention: crates/openlogi-agent/src/main.rs, crates/openlogi-agent/src/server.rs
|
| Filename | Overview |
|---|---|
| crates/openlogi-agent/src/main.rs | Adds the close-button watcher lifecycle, but its route-difference rearming still misses firmware resets that preserve route identity. |
| crates/openlogi-agent/src/server.rs | Generalizes haptic playback to route batches, but concurrent arming producers still overwrite the shared pending request. |
| crates/openlogi-agent-core/src/orchestrator.rs | Adds filtering for online, opted-in, haptic-capable device routes without an identified blocking defect. |
| crates/openlogi-hook/src/macos.rs | Implements best-effort macOS Accessibility close-button hit testing without an identified blocking defect. |
| crates/openlogi-hook/src/windows.rs | Implements bounded Windows non-client hit testing without an identified blocking defect. |
| crates/openlogi-core/src/config/device.rs | Adds an off-by-default, serialization-compatible per-device setting without an identified blocking defect. |
| crates/openlogi-desktop/src/state/close_button_haptics.rs | Adds capability-gated persistence and agent reload behavior without an identified blocking defect. |
Sequence Diagram
sequenceDiagram
participant Inventory
participant Watcher as CloseButtonWatcher
participant Ring as Actions Ring
participant Player as RingHapticPlayer
participant Firmware
Inventory->>Watcher: Same eligible route after wake
Note over Firmware: Power transition clears armed state
Watcher->>Watcher: Route found in armed_routes
Note over Watcher: Rearming skipped
par Concurrent producers
Ring->>Player: arm(route A)
Watcher->>Player: arm_many(routes B)
end
Note over Player: Later pending_arm assignment replaces earlier routes
Player->>Firmware: Arm only surviving vector
Watcher->>Player: play_many(current routes)
Player->>Firmware: Play waveform
Note over Firmware: Unarmed route remains physically silent
Reviews (5): Last reviewed commit: "Merge branch 'master' into master" | Re-trigger Greptile
…come eligible The dynamic close-button watcher started polling and played haptic pulses without ever calling RingHapticPlayer::arm_many, so a device whose firmware haptic engine got disarmed by a power transition (sleep/wake, reconnect) accepted play commands silently, with no physical feedback. Re-assert arming for any route that wasn't already eligible on the last check — covers first activation, a newly opted-in device, and a reconnect that drops a route out of and back into the eligible set, even while other devices keep the watcher running. Reported by Greptile on PR AprilNEA#987.
Summary
WM_NCHITTESTon Windows) and only polls while at least one connected device has the setting enabled — no idle cost otherwise. Linux is a clean no-op (Wayland can't answer this; X11 was intentionally left out of scope).Changes
openlogi-hook: newHookBackend::cursor_is_over_close_buttoncapability — macOS viaobjc2-application-services(AXUIElementCopyElementAtPosition+AXSubrole), Windows viaWindowFromPoint+SendMessageTimeoutW(WM_NCHITTEST)(bounded timeout so a hung target window can't stall the watcher thread).openlogi-agent-core: newwatchers::close_button_hapticspoll watcher (dedupes hover-enter/exit via the existingPoll::on_change);Orchestrator::close_button_haptic_routes()resolves every online, opted-in, haptic-capable device.openlogi-agent: dynamically starts/stops the watcher based on eligibility (checked on a cheap in-memory interval, no OS calls when nothing is enabled); generalized the existing single-flightRingHapticPlayertoplay_many/arm_manyso every eligible device buzzes together for one hover-enter (there's only one cursor).openlogi-core: new per-deviceclose_button_haptics: boolconfig field (DeviceConfig), off by default, omitted fromconfig.tomlwhen unset.openlogi-desktop: new toggle card in the Pointer tab, gated on the same haptic-feedback capability check the Actions Ring already uses (extracted into a sharedAppState::current_haptics_supported()instead of duplicating it).openlogi-ui: new locale keys across all supported languages.