Skip to content

Fix key: permission warning and support modifier-only chords - #3

Open
nanderss wants to merge 1 commit into
okoker:mainfrom
nanderss:key-accessibility-modifier-chords
Open

Fix key: permission warning and support modifier-only chords#3
nanderss wants to merge 1 commit into
okoker:mainfrom
nanderss:key-accessibility-modifier-chords

Conversation

@nanderss

Copy link
Copy Markdown

Two issues with key: actions.

The permission warning names the wrong setting

The startup check tests AXIsProcessTrusted(), which reports Accessibility, but the message tells the user to grant Input Monitoring:

if hasKeyActions && !AXIsProcessTrusted() {
    print("WARNING: ... Grant access in System Settings → Privacy & Security → Input Monitoring.")
}

CGEvent.post(tap:) needs Accessibility. Input Monitoring gates listening to input, which this daemon never does — and the deck itself is a vendor-usage-page device (0xFFA0), which macOS doesn't gate at all, so its events flow with no permission granted.

Following the message grants a permission that changes nothing, after which post() silently no-ops and every key: action dies with no error anywhere. Note the README currently states "Accessibility — not required", which is the opposite of what the code checks; happy to send a README fix separately.

Modifier-only chords are unparseable

parts.last! is taken as the key and everything before it as modifiers, so ctrl+shift parses as "key ctrl with modifier shift" and is rejected with unknown key 'ctrl'. Such a chord has no key to press, so CGEvent(virtualKey:) can't express it.

These are sent as flagsChanged instead, and held for as long as the deck button is held — press builds the chord up one modifier at a time carrying the cumulative flags, release unwinds it in reverse, mirroring how real hardware reports a chord. This drives apps that trigger on a held modifier combo. Shortcuts containing a real key are unaffected and take the original path.

buttonRelease previously returned immediately; it's now wired and matched by button ID, so releasing a different button can't drop the chord.

Latched modifiers are nasty to recover from, so there are three guards:

  • a 30s watchdog releases the chord if a buttonRelease packet is ever dropped, and logs that it did
  • pressing a second chord button releases the first rather than stacking
  • shutdown() releases before teardown — already called on config reload, and now also after the run loop exits, so SIGTERM and launchctl bootout unwind cleanly

SIGKILL is the one case nothing can cover.

Tested on an AJAZZ AKP03, macOS 26.5: key:cmd+space opens Spotlight, and key:shift+ctrl on a button holds the chord for the duration of the press and triggers a third-party app listening for it.

Two problems with key: actions.

The startup warning tests AXIsProcessTrusted(), which reports
Accessibility, but told the user to grant Input Monitoring. CGEvent.post
needs Accessibility; nothing here listens to input, so Input Monitoring
is never required. Granting the advertised permission left post()
silently doing nothing, with no error anywhere.

A shortcut naming only modifiers ("ctrl+shift") was rejected, because
the last +-separated component is taken as the key and no modifier
appears in the keycode table. Such a chord has no key to press, so send
it as flagsChanged instead and hold it for as long as the deck button is
held: press builds the chord up one modifier at a time carrying the
cumulative flags, release unwinds it in reverse. Shortcuts containing a
real key are unaffected.

Latched modifiers are difficult to recover from, so guard three ways: a
30s watchdog releases the chord if a buttonRelease packet is ever
dropped, pressing a second chord button releases the first rather than
stacking, and shutdown() releases before teardown -- called on config
reload and now after the run loop exits, so SIGTERM unwinds cleanly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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