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

When Two Chords Want the Same Keys

~10 min · collisions, osstatus, reserved-shortcuts, honesty

Level 0Cold Flint
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The strike that does nothing is the worst bug in a hotkey app — because the user blames the whole app, not the one chord."

Two Kinds of Collision

A chord can collide two ways. Internal: two of your own macros are assigned the same keys. External: the system, or another running app, already owns the chord. These are not the same problem, and the honest thing is to admit that one is easy to catch and the other is genuinely hard.

The Internal One Is Easy

Internal collisions you own completely. You hold the registry of every macro's chord, so before you register a new one you check whether any other macro already claims those exact keys. If so, refuse and tell the user which macro has it. This is a simple lookup in the map you already keep — there's no excuse for shipping two of your own macros that fight over one chord.

The External One Is Honest-Hard

External collisions are where you have to be truthful about the platform. RegisterEventHotKey does not reliably fail when another app already holds the chord — it can return noErr and simply never fire, or fire unpredictably, depending on who registered first. So you cannot fully detect an external collision from the return code alone. What you can do is check the chord against the known reserved system shortcuts, warn when a macro maps to one of those, and — because you can't promise a chord is free — make sure there is always another way to run the macro that doesn't depend on the chord firing at all.

A noErr return does not prove the chord is yours. The tempting mistake is to treat RegisterEventHotKey returning success as 'the chord works.' It only means the OS accepted your registration — not that your handler will win when the keys are pressed. Design for the case where registration succeeds and the chord still never fires, because that case is real and undetectable from the status code.

Never Swallow the Strike

The failure to design against is the silent one: a user assigns a chord, it quietly loses to another app, they press it, nothing happens, and they conclude Flint is broken. The fix is twofold — surface every collision you can detect on the specific macro, and give every macro a chord-independent path to run (the palette, next lesson). A user who can always fall back to the palette never experiences 'the app is broken,' only 'this particular chord is taken.'

Code

Catch the collision you own; be honest about the one you don't·swift
enum ChordStatus {
    case free
    case internalConflict(macro: String)   // another Flint macro owns it
    case reservedSystem                     // a known macOS system shortcut
    case registeredButUnverifiable          // noErr, but may lose to another app
}

func classify(_ chord: HotkeyBinding, against macros: [FlintMacro]) -> ChordStatus {
    // 1. Internal collision: fully knowable from our own registry.
    if let owner = macros.first(where: { $0.hotkey == chord }) {
        return .internalConflict(macro: owner.name)
    }
    // 2. Reserved system chord: check a known list, warn (don't hard-block).
    if ReservedShortcuts.contains(chord) { return .reservedSystem }
    // 3. Everything else: we can register, but RegisterEventHotKey may return
    //    noErr and still lose to another app. We cannot prove it's free.
    return .registeredButUnverifiable
}

External links

Exercise

A user reports 'my Correct macro stopped working' after installing a new app. Walk the diagnosis without assuming a bug in your code: which kind of collision is this, why couldn't RegisterEventHotKey's return code have warned you, and what two things should the app have done so the user never reached 'it's broken' in the first place?
Hint
It's an external collision — the new app grabbed the same chord, and RegisterEventHotKey likely returned noErr for both, so the status code never flagged it. The two mitigations are: surface reserved/known conflicts up front, and guarantee a chord-independent way to run every macro (the palette), so a lost chord degrades to 'use the palette' instead of 'the app is dead.'

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.