Skip to content

Fix option+letter shortcuts being intercepted by IME layer#236

Open
BashkaMen wants to merge 2 commits intosindresorhus:mainfrom
BashkaMen:fix/option-letter-ime-conflict
Open

Fix option+letter shortcuts being intercepted by IME layer#236
BashkaMen wants to merge 2 commits intosindresorhus:mainfrom
BashkaMen:fix/option-letter-ime-conflict

Conversation

@BashkaMen
Copy link

Fixes #235

Problem

Shortcuts using option + letter (e.g. option + q) stop working intermittently. option + number works reliably. Re-opening the app's settings window temporarily restores them.

Root cause: Carbon's RegisterEventHotKey doesn't receive events for option + letter combinations because macOS processes them through the Text Services Manager / IME layer first (e.g. option + qœ). The Carbon event never fires. This doesn't affect option + number because digits don't participate in IME character composition.

The existing RunLoopLocalEventMonitor with .eventTracking already solves this correctly for the menu-open case, since it intercepts raw key events before IME. This PR extends the same approach to the normal (non-menu) mode.

Fix

When at least one registered shortcut uses option without command or control (the only combinations affected by IME), a NSEvent.addGlobalMonitorForEvents listener is activated alongside the normal Carbon hotkey handler.

The global monitor handles key matching via the existing handleRawKeyEvent path — no new matching logic needed. It is automatically enabled/disabled as shortcuts are registered/unregistered, and torn down entirely when no option-only shortcuts remain.

Scope

Only option-only shortcuts (no cmd/ctrl) are affected. Adding command or control prevents IME interception — those shortcuts continue using Carbon exclusively.

Carbon's RegisterEventHotKey doesn't reliably fire for option+letter
shortcuts because macOS processes them through the IME layer first
(e.g. option+q → œ). This causes such shortcuts to stop working
intermittently depending on the active application's input handling.

The existing RunLoopLocalEventMonitor with .eventTracking already
solves this for the menu-open case. This fix extends that approach
by adding an NSEvent.addGlobalMonitorForEvents listener whenever
there are registered shortcuts that use Option without Command or
Control — the only combinations affected by IME interception.

The global monitor is enabled/disabled alongside normal hotkey
mode, and is torn down automatically when no option-only shortcuts
are registered.

Fixes sindresorhus#235
Comment on lines +333 to +344
/**
Returns true if any registered shortcut uses Option as the only modifier (no Command or Control).
Such shortcuts are prone to being intercepted by the IME layer before Carbon receives them.
*/
private var hasOptionOnlyShortcuts: Bool {
hotKeys.values.compactMap(\.value).contains { hotKey in
let modifiers = NSEvent.ModifierFlags(carbon: hotKey.carbonModifiers)
return modifiers.contains(.option)
&& !modifiers.contains(.command)
&& !modifiers.contains(.control)
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't work if a user has also shortcuts with different modifiers. It would be best to combine both the current solution and the new one, I guess.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe your application is more important and should run globally.
I removed all the hotkeys I found while setting up the workspace.

Comment on lines +340 to +342
return modifiers.contains(.option)
&& !modifiers.contains(.command)
&& !modifiers.contains(.control)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a more appropriate check would be:

modifiers == [.option]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

private func setGlobalOptionKeyMonitorEnabled(_ isEnabled: Bool) {
if isEnabled, globalEventMonitor == nil {
globalEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown, .keyUp]) { [weak self] event in
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires accessibility access, so it's a no-go. From docs:

Key-related events may only be monitored if accessibility is enabled or if your application is trusted for accessibility access (see AXIsProcessTrusted()).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many apps use accessibility API. How about making it optional? Clients could decide wheter they want to use this API or not.

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.

Option + letter shortcuts stop working intermittently (Carbon RegisterEventHotKey + IME conflict)

3 participants