Skip to content

fix(remap): wire Accessibility (TCC) gate so button remap actually fires#10

Merged
Sanjays2402 merged 1 commit into
mainfrom
v0.6-accessibility-fix
May 10, 2026
Merged

fix(remap): wire Accessibility (TCC) gate so button remap actually fires#10
Sanjays2402 merged 1 commit into
mainfrom
v0.6-accessibility-fix

Conversation

@Sanjays2402

Copy link
Copy Markdown
Owner

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:

  1. No AX check anywhere. RemapEngine.fire() calls CGEvent.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.
  2. WelcomeWindow.swift lied. 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.
  3. TCC reset trap. macOS keys Accessibility grants on the binary's signature/path. The moment a new release replaces /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 ObservableObject singleton that polls AXIsProcessTrusted() every 2 seconds, stops polling once trust flips on, exposes requestPrompt() (which fires the system prompt + falls back to opening the pane after 600 ms in case the prompt is suppressed), and openSettingsPane().
  • RemapEngine.fire(): before any CGEvent action (keystroke / systemSwipe), guard on AXIsProcessTrusted(). 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.
  • Bumped CFBundleVersion to 0.6.0.

Verified locally

  • swift build clean (Swift 6 strict-concurrency forced literal-string for the AX prompt key — see comment in AccessibilityChecker.swift).
  • Bundled and replaced /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.

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.
@Sanjays2402 Sanjays2402 merged commit 9b6aeb1 into main May 10, 2026
1 check passed
@Sanjays2402 Sanjays2402 deleted the v0.6-accessibility-fix branch May 10, 2026 00:09
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