C.W.K.
Stream
Lesson 01 of 04 · published

Three Doors to a Global Key

~12 min · carbon, cgevent-tap, nsevent, deprecated-not-removed

Level 0Cold Flint
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The modern frameworks can watch the key. Only the old one can take it."

Three Doors, One Job

Flint needs a key chord that fires everywhere — in any app, whether or not Flint is frontmost — and, crucially, gets consumed so it doesn't also type into whatever you're using. macOS gives you three doors to that, and they are not equivalent. The surprise, coming from modern Swift, is that the newest door is the wrong one and the oldest door is the right one.

Observe Is Not Consume

The first door is NSEvent.addGlobalMonitorForEvents. It is modern, easy, and Swift-native — and it is passive. A global NSEvent monitor lets you observe key events happening in other apps, but it cannot intercept or block them. If you used it for a hotkey, your action would fire and the keystroke would also reach the focused app. That's fine for a heads-up display that reacts to activity; it's useless for a command shortcut, which must swallow the keys it claims.

The Firehose and the Scalpel

The second door is a CGEvent tap. It is powerful: it can intercept and consume events system-wide. But it's a firehose — it sees every event and you filter for yours — it requires Accessibility permission, and its lifecycle is fragile. For a handful of discrete chords, that's a heavy, error-prone way in. The third door, RegisterEventHotKey, is a scalpel: you register one specific chord with the OS, your handler fires when it's pressed, and the chord is consumed. It's lightweight and needs no Accessibility just to register the hotkey. For Flint's job — a few named chords — the scalpel wins cleanly.

DOOR                         consumes?  needs AX?  weight   fit for Flint
---------------------------  ---------  ---------  -------  -------------
NSEvent global monitor       no         no         light    no (passive)
CGEvent tap                  yes        yes        heavy    overkill
RegisterEventHotKey (Carbon) yes        no*        light    yes

* Flint still needs Accessibility to READ and INSERT text —
  just not to register the chord itself.

Deprecated Is Not Removed

RegisterEventHotKey lives in Carbon's HIToolbox, an API family Apple marked deprecated a very long time ago. That word scares people off, but deprecated is not the same as removed: it still ships, still works, and Apple never wrote a modern replacement for this exact narrow need. The mature engineering move is to use the stable, purpose-built API even though its label says 'old,' and to wall off that one Carbon dependency behind a small Swift type so the deprecation lives in one file instead of leaking through the app.

Use the boring, purpose-built API — and isolate it. A newer, general mechanism is not automatically better than an older, specific one. When the specific API does exactly your job and the general one is a firehose, pick the specific one — then quarantine it behind a narrow interface so 'deprecated' is a fact about one file, not a property of your whole codebase.
Why Apple never replaced it, in one line: global hotkeys are a small, stable need that the modern event-tap and shortcut systems technically cover, so a dedicated modern API was never worth shipping — leaving the old one as the pragmatic path. That's common in mature platforms: the right tool is sometimes the un-renovated one.

Code

The three doors, as the choice Flint actually makes·swift
// DOOR 1 — passive: sees the key, can't take it. Wrong for a command.
NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
    // observe only; the keystroke still goes to the focused app
}

// DOOR 2 — CGEvent tap: consumes, but a firehose + needs Accessibility.
// Overkill for a few named chords.

// DOOR 3 — RegisterEventHotKey (Carbon): registers ONE chord, consumes it,
// lightweight, no Accessibility for the registration. Flint's choice.
import Carbon.HIToolbox   // the one deprecated import, quarantined here

External links

Exercise

You need a system-wide shortcut that must NOT also reach the app the user is typing in. Walk the three doors — passive global monitor, event tap, registered hotkey — and for each, state exactly why it does or doesn't satisfy 'consume the chord.' Then decide where you'd draw the line between 'a few named chords' and 'so many chords that a firehose starts to make sense.'
Hint
Consume-vs-observe eliminates the passive monitor immediately. Between the tap and RegisterEventHotKey, the deciding factors are permission cost, lifecycle fragility, and how many distinct chords you have: a small fixed set favors the scalpel; something like a full remapping layer that reinterprets many keys is where a tap's firehose starts to earn its weight.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.