The Problem: macOS Doesn't Let You Remap Keys Natively
If you've ever switched from Windows/Linux to a Mac, you know the pain. Caps Lock sits where your muscle memory wants Ctrl. Your external keyboard's modifier layout fights with the built-in one.
Apple gives you exactly one remap option: Caps Lock → Escape. Everything else requires kernel extensions (KEXTs) — which Apple is actively killing off — or complex third-party tools like Karabiner-Elements (free but JSON-config hell) or BetterTouchTool ($24).
I wanted something in between: a native, lightweight, menu-bar app that does one thing perfectly.
What KeyMapper Does
KeyMapper is a native macOS menu-bar utility (Swift + AppKit, no Electron) that lets you:
- Remap any key to any key — including modifiers, function keys, and media keys
- Per-device profiles — different mappings for built-in keyboard vs. external keyboards
- Application-specific overrides — Caps Lock = Escape in VS Code, Caps Lock = Ctrl in terminal
- Menu-bar only — no dock icon, no clutter, 4MB memory footprint
- No KEXT — uses Apple's CGEventTap API, fully sandboxed and notarized
The Technical Approach
1. Event Interception with CGEventTap
The core of any key remapper is intercepting key events before macOS processes them. Apple deprecated KEXT-based interception in Catalina, so we use CGEventTap:
let eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: eventMask,
callback: { (proxy, type, event, refcon) -> Unmanaged<CGEvent>? in
return KeyMapperEngine.shared.process(event: event)
},
userInfo: nil
)
Key detail: headInsertEventTap places our tap before system processing. Our remapped events appear to macOS as if they came from the hardware directly — no conflicts with other input monitoring apps.
2. Per-Device Identification
Modern Macs often have 2-3 keyboards connected: built-in, Bluetooth, USB. Each needs different mappings. We use IOHIDDevice properties to uniquely identify each input source:
func deviceIdentifier(for event: CGEvent) -> String? {
guard let nsEvent = NSEvent(cgEvent: event) else { return nil }
return "\(nsEvent.deviceID)"
}
This gives us stable per-device persistence across reboots and reconnections.
3. Application-Specific Profiles
For per-app overrides, we monitor the frontmost application via NSWorkspace notifications:
NotificationCenter.default.addObserver(
forName: NSWorkspace.didActivateApplicationNotification,
object: nil, queue: .main
) { notification in
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
KeyMapperEngine.shared.switchProfile(for: app.bundleIdentifier)
}
Profile switching is instantaneous (< 1ms) because we pre-compile all active mappings into a lookup table keyed by (deviceID, keyCode).
4. The Memory Footprint Trick
Most key remappers leak memory because they recreate event tap contexts on every config change. We use a single long-lived tap and mutate only the lookup table. Total memory: 3.8MB idle, 4.2MB under load.
Compare: Karabiner-Elements uses 45-80MB. BetterTouchTool uses 120MB+.
Why This Isn't Free
Karabiner-Elements is free and open source. Why pay $7.99 for KeyMapper?
- Karabiner requires editing JSON config files — KeyMapper has a native GUI
- Karabiner breaks on major macOS updates — KeyMapper uses stable public APIs only
- Signed and notarized — no Gatekeeper warnings, no security exceptions
- Support — email me, get a fix within 24 hours
The target audience isn't developers who love editing JSON. It's designers, writers, and Windows switchers who want it to just work.
Try It
KeyMapper is available on Gumroad for $7.99 (one-time purchase, no subscription):
Includes: lifetime updates, email support, 30-day money-back guarantee.
If you want the full toolkit (KeyMapper + 4 other Mac automation utilities), check out the bundle:
Built by a solo dev. Follow @agentechip on X for more Mac automation tools.
Top comments (0)