Conversation
The app previously sat in the system keystroke-delivery path: one filter tap on keyDown/keyUp/flagsChanged, serviced on the main run loop, saw every keystroke in the session and could stall input everywhere if the main thread was busy. Key chords now register with RegisterEventHotKey instead. The window server matches them and delivers them as ordinary app events, so no keystroke waits on this process, and no Accessibility grant is needed — the default shortcuts work on first launch. An event tap remains only for bare-modifier shortcuts (Fn/Globe push-to-talk), which Carbon cannot express. It is created only when such a shortcut is bound, its mask is exactly .flagsChanged, and it runs on a dedicated thread. A second listen-only tap watches for ordinary keys, and only while the modifier is held, to tell Fn-as-a-shortcut from Fn+arrow. Because a Carbon registration swallows its chord unconditionally, gating a shortcut now means registering and unregistering it rather than filtering at fire time, so settings and recording-state changes re-derive the registration set. Breaking change: Fn+key chords can no longer be bound. The hot-key API has no Fn modifier, and registering the chord without it would swallow the bare key system-wide. Stored Fn chords are flagged in the UI for re-recording; arrow and F-keys saved by older builds are migrated, since those report the Fn flag intrinsically and were never real Fn chords. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Summary
The app sat in the system keystroke-delivery path: one filter tap on
keyDown/keyUp/flagsChanged, serviced on the main run loop, saw every keystroke in the session and could stall input everywhere if the main thread was busy.Key chords now register with
RegisterEventHotKey. The window server matches them and delivers them as ordinary app events, so no keystroke waits on this process — and no Accessibility grant is needed, so the default shortcuts work on first launch.An event tap remains only for bare-modifier shortcuts (Fn/Globe push-to-talk), which Carbon cannot express. It is created only when such a shortcut is bound, its mask is exactly
.flagsChanged, and it runs on a dedicated thread rather than the main run loop. A second listen-only tap watches for ordinary keys, and only while the modifier is held, to tell Fn-as-a-shortcut from Fn+arrow.A Carbon registration swallows its chord unconditionally, so gating a shortcut now means registering and unregistering it rather than filtering at fire time. Settings changes and recording-state edges re-derive the registration set.
Breaking change
Fn+key chords can no longer be bound. The hot-key API has no Fn modifier, and registering the chord without it would swallow the bare key system-wide — binding Fn+W would stop W reaching any app.
Worth a release-notes line for existing users. Suggests a minor version bump rather than a patch.
Notes for review
ShortcutBackenddecides which mechanism serves a shortcut;HotKeyBindingPlanresolves collisions deterministically, since the OS awards a contested chord by arrival order.HotKeyPressTracker), so a registration that disappears under a held key gets its release completed by hand instead of leaving a hold-style action running.ModifierTapMonitorand its policy is now unit-testable (TapStarvationPolicy).KeyDownObserverdeliberately has none — it is listen-only and disabled outside modifier holds.CGEventcopied out before any hop.Testing
swift build— no warnings.swift test— 146 tests across 29 suites pass, including new coverage for backend classification, binding-plan precedence, press tracking, Fn press/release swallow symmetry, and migration of stored Fn flags on function-group keys.🤖 Generated with Claude Code