Turn a spare Apple Magic Keyboard into a macropad. Click a key in the UI, assign it an action, and that key — on the Magic Keyboard only — launches apps, opens URLs, runs shell commands, Apple Shortcuts, or sends keystrokes. The MacBook's built-in keyboard is never touched.
Triggers can be bare keys (T) or combos (⇧T, ⌘⇧F8, …) — toggle fn/⌃/⌥/⇧/⌘ chips in the key editor to edit each combo slot; one physical key can hold several mappings. The kill unassigned keys switch (02 / board) makes every unmapped key do nothing at all (no stray letters); modifier caps stay live so combos keep working.
- macOS 12+ with Karabiner-Elements installed
- Node.js 20.19+ (or 22.12+)
- A Magic Keyboard you can dedicate to macros — any model, Bluetooth or USB
git clone https://github.com/stearyl/magickeys.git
cd magickeys
npm install
npm start # build + serve → http://localhost:4477
npm run dev # dev mode (vite on :5177, api on :4477)The app only needs to be running while editing mappings. The mappings themselves live inside Karabiner-Elements and keep working with MagicKeys closed.
~/Applications/MagicKeys.app — drag it into the Dock. Clicking it starts the server if it isn't running (finds node via fnm/homebrew/nvm even in the Dock's bare environment, builds dist/ if missing, logs to ~/.config/magickeys/launcher.log) and opens the UI as a chromeless Chrome app window. Rebuild/reinstall it any time with npm run install-app; icon source lives in assets/icon.html (regenerate via headless-Chrome screenshot → iconutil).
MagicKeys writes complex modification rules into ~/.config/karabiner/karabiner.json. Every rule carries a device_if condition pinned to the Magic Keyboard's vendor/product id (detected via hidutil list), which is what keeps the built-in keyboard separate. Karabiner watches the file and reloads automatically — no restarts.
- Rules are tagged with a
MagicKeys:description prefix; anything else in karabiner.json (other rules, settings, profiles) is preserved verbatim. - Every real write is preceded by a timestamped backup →
~/.config/magickeys/backups/(last 20 kept). - App state (chosen device + mappings) lives in
~/.config/magickeys/config.json; karabiner.json is regenerated from it, so the two can always be re-synced ("re-apply" in the UI). - Writes are atomic (tmp file + rename).
| type | karabiner to |
|---|---|
| launch app | shell_command: open <path> |
| open URL | shell_command: open <url> (any scheme — https:, raycast:, …) |
| shell | shell_command verbatim |
| send keys | key_code + modifiers |
| apple shortcut | shell_command: shortcuts run '<name>' (picker fed by shortcuts list) |
| app menu item | shell_command: osascript … click menu item "Save…" of menu "File" of menu bar 1 |
| disable | key_code: vk_none |
Combo triggers compile to from.modifiers.mandatory (generic names, so either shift key works). Mapping ids are canonical strings — p, shift+t, shift+command+f8 — in fn→⌃→⌥→⇧→⌘ order. "Kill unassigned keys" appends one catch-all rule after all mappings: every non-modifier key (plus the top-row media events) with optional: any modifiers → vk_none, so anything an earlier rule didn't claim is swallowed.
The media glyphs (F1–F12) don't send F-key codes unless fn is held — they arrive as consumer/apple-vendor events. Each F-key mapping therefore includes extra manipulators matching the media variant (brightness, mission control, play/pause, volume, …), so the key fires in either mode. F4 covers both the 2015 (launchpad) and 2021 (spotlight) models. F5/F6 only match as plain F-keys.
Binds any menu command of any app — e.g. TextEdit's File → Save… — including commands with no keyboard shortcut. The editor has a live menu browser (pick a running app, drill into its menus); at trigger time the action optionally activates the app, then clicks the item via System Events. Deep submenu paths work (Format → Font → Bold).
Permissions (one-time, macOS requires them for UI scripting):
- Browsing/testing from the MagicKeys UI — the app you run MagicKeys from (your terminal) needs: the Automation → System Events consent (macOS prompts; click Allow) and Privacy & Security → Accessibility enabled (no prompt — toggle it manually).
- Key presses — Karabiner runs the click, so the first press pops an Automation prompt for
karabiner_console_user_server; allow it. If clicks still do nothing, also addkarabiner_console_user_server(in/Library/Application Support/org.pqrs/Karabiner-Elements/bin/) or Karabiner-Elements to the Accessibility list.
Menu names must match exactly — Apple uses the single ellipsis character (Save…, not Save...); the browser always captures names exactly, so prefer it over manual typing.
- Mapping doesn't fire → Karabiner-Elements Settings → Devices: make sure Modify events is enabled for the Magic Keyboard. Check EventViewer to confirm Karabiner sees the key.
- Device not detected → it must be connected (Bluetooth or cable); hit rescan. Worst case, enter vendor/product ids manually (EventViewer shows them).
- Restore an old config → copy a file from
~/.config/magickeys/backups/over~/.config/karabiner/karabiner.json. Karabiner also keeps its own backups in~/.config/karabiner/automatic_backups/. - Uninstall → remove all mappings in the UI (or delete the
MagicKeys:-prefixed rules from karabiner.json), then delete~/.config/magickeys/.
Vite + React + TypeScript + Tailwind v4 front, Express 5 + tsx back, bound to 127.0.0.1:4477. No database — karabiner.json is the runtime state, ~/.config/magickeys/config.json the source of truth. npm run typecheck && npm run build is the verification gate.
Not affiliated with Apple Inc. “Magic Keyboard” is a trademark of Apple Inc. Powered by Karabiner-Elements.
