fix(remap): wire Accessibility (TCC) gate so button remap actually fires#10
Merged
Conversation
The button-remap engine in #8 dispatched actions with CGEvent.post(), which silently drops every event when the process isn't trusted under System Settings → Privacy & Security → Accessibility. There was no AX check and no UI signal — the toggle in the remap menu would persist, the firmware would correctly divert the button, but pressing it did nothing because macOS dropped the synthesized keystroke into the void. Worse: WelcomeWindow.swift literally said "Optune does not require accessibility" — a leftover from before the remap engine landed. Worst: macOS keys Accessibility grants on the binary's signature/path. Every brew upgrade or rebuild invalidates the prior grant while the toggle in the System Settings list visibly stays on, leaving the user thinking they've granted permission when they haven't. Changes: - Add AccessibilityChecker singleton (@mainactor ObservableObject) that polls AXIsProcessTrusted() on a 2s timer, stops polling once trusted, and exposes requestPrompt() / openSettingsPane() helpers. - Gate every CGEvent-dispatching action in RemapEngine on AXIsProcessTrusted(); if denied, refresh the checker and pop the system prompt instead of silently no-op'ing. - Replace the lying Welcome 'permissions' page: now lists Input Monitoring AND Accessibility as separate rows, with live trust status, a Grant button that triggers the prompt, and a tip about re-adding after updates. - Add an Accessibility nag banner at the top of ButtonsPane when remap bindings exist but the trust grant is missing, so the user sees the exact recovery path right where they're about to be confused. - Bump CFBundleVersion to 0.6.0 in bundle-app.sh.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
After granting whichever permission System Settings asked for, button remap toggles still saved fine but pressing the button did nothing. No error, no log, just silence.
Root cause
Three stacked failures:
RemapEngine.fire()callsCGEvent.post(tap: .cgSessionEventTap)directly. macOS silently drops every event a process tries to post unless it has the Accessibility (TCC) trust grant. There's no return code, no exception, no log — events vanish.WelcomeWindow.swiftlied. It read 'Optune does not require accessibility, screen recording, or any other privileged scope' — leftover copy from before the remap engine landed in feat(remap): button remap engine — HID++ 0x1B04 setControlReporting + CGEvent dispatch #8./Applications/Optune.app, the prior grant is invalidated, but the toggle in System Settings → Accessibility visibly stays on. The user thinks they've granted it, the OS thinks they haven't, and there's no signal anywhere except 'pressing the button does nothing'.Fix
AccessibilityChecker.swift(new):@MainActor ObservableObjectsingleton that pollsAXIsProcessTrusted()every 2 seconds, stops polling once trust flips on, exposesrequestPrompt()(which fires the system prompt + falls back to opening the pane after 600 ms in case the prompt is suppressed), andopenSettingsPane().RemapEngine.fire(): before any CGEvent action (keystroke/systemSwipe), guard onAXIsProcessTrusted(). If denied, refresh the checker and trigger the system prompt instead of silently no-op'ing.WelcomeWindow: rewrote the permissions page. Now lists Input Monitoring AND Accessibility as separate rows, with live trust state, a 'Grant' button that triggers the prompt, and an explicit warning that grants invalidate on every update + how to re-add.ButtonsPane: in-pane Accessibility banner when remap bindings exist but trust is missing — orange exclamation card with 'Grant Accessibility' / 'Open Settings' buttons. Disappears the second the trust toggle flips on.CFBundleVersionto 0.6.0.Verified locally
swift buildclean (Swift 6 strict-concurrency forced literal-string for the AX prompt key — see comment inAccessibilityChecker.swift)./Applications/Optune.app; menu-bar agent launches; banner appears on Buttons pane when no AX grant; clicking 'Grant Accessibility' triggers the system prompt and deep-links to the pane.Why bother polling
The alternative is one-shot check at app launch, but TCC grants are reactive — they flip on the moment the user clicks the toggle in System Settings, and we want the banner to disappear immediately when that happens. 2 s timer, single XPC ping, stops as soon as we go from
false → true. Cheap.Test plan
CI builds. For manual verification: revoke Accessibility for Optune in System Settings, launch the app, open Buttons pane → banner appears. Click 'Grant Accessibility' → system prompt + pane opens. Toggle on → banner disappears within 2 s. Bind any button to ⌘C → press it on the mouse → keystroke fires.