A Raspberry Pi Pico + Pimoroni RGB Keypad that provides dynamic, app-specific shortcut keys driven by a Mac watchdog script.
If you find this useful, consider giving it a ⭐️ on GitHub or buying me a ☕️.
- App-specific key layouts that switch automatically when you change applications
- Four key types: shortcut, application launch, folder (sub-pages), and action (plugin commands)
- Global
_defaultkeys added to every app/folder, with per-entry opt-out viaignore_default - Fallback
_otherwiselayout for apps without a specific definition - Plugins for Spotify, Philips Hue, and audio playback
- Rotate the layout CW or CCW for 3D-printed cases
- All configuration lives in a single
key_def.jsonon the Pico
- Raspberry Pi Pico
- Pimoroni RGB Keypad for Raspberry Pi Pico
- Micro-USB cable
- A Mac running
watchdog.py(required for app detection and plugins)
code.py (CircuitPython, runs on the Pico) reads key_def.json and maps keys to sequences and LED colors. It listens over USB serial for the active app name and updates the layout accordingly.
watchdog.py (Python, runs on Mac) monitors the active application using Cocoa notifications and sends it to the Pico. It also handles plugin commands. Without it, the keypad works as a static shortcut pad only.
- Install CircuitPython on the Pico.
- Install required libraries into
lib/:adafruit_dotstar.mpy,adafruit_hid, and rgbkeypad-circuitpython. - Copy
src/pi_pico/to the Pico root. - Edit
key_def.jsonto set up your layouts. Key0is top-left,15is bottom-right. Thonny makes this easy.
cd src/mac
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# Run — port is auto-detected
python3 watchdog.py --verbose
# Or specify a port explicitly
python3 watchdog.py --port /dev/cu.usbmodem2101 --verboseOr use the launcher script from the repo root:
# Auto-detect
./run-mac-watchdog.sh --verbose
# Or specify a port
./run-mac-watchdog.sh --port /dev/cu.usbmodem2101 --verboseWatchdog flags:
| Flag | Description |
|---|---|
--port |
Serial port for the Pico (optional — auto-detected if omitted) |
--speed |
Baud rate (default: 9600) |
--verbose |
Print active app name to console |
--rotate |
Rotate layout: CW or CCW |
The file has four top-level sections: settings, applications, folders, urls.
{ "settings": { "rotate": "CCW" } }rotate accepts CW, CCW, or omit for no rotation.
| Type | Field | Behaviour |
|---|---|---|
| Shortcut | key_sequence |
Sends a key combination |
| App launch | application |
Opens or focuses an app |
| Folder | folder |
Opens a sub-page of keys |
| Action | action |
Runs a plugin command or close_folder |
Common fields (any key type):
color— LED color as#RRGGBBpressedColor— color while heldtoggleColor— color for toggled/active statedescription— label printed in--verboseoutput
Shortcut-specific:
key_sequence— keycode string"CTRL+ALT+T", array["CTRL","ALT","T"], or array with float delays between pressespressedUntilReleased: true— holds the key until physically released
App-specific:
alias_of— copy another app's layout instead of defining keys
Folder-specific:
autoclose: false— keep folder open after a key press (default: auto-closes)- Folders without
autoclosemust include aclose_folderaction key
App/folder/URL:
ignore_default: true— skip global_defaultkeys for this entry
{
"settings": { "rotate": "CCW" },
"applications": {
"_default": {
"15": { "key_sequence": "GUI+Q", "color": "#FF0000", "description": "Close App" }
},
"zoom.us": {
"0": { "key_sequence": "GUI+SHIFT+A", "color": "#FFFF00", "description": "Mute/Unmute" },
"1": { "key_sequence": "GUI+SHIFT+V", "color": "#FFFF00", "description": "Start/Stop Video" },
"15": { "key_sequence": ["GUI+W", 0.1, "RETURN"], "color": "#FF0000", "description": "End Meeting" }
},
"_otherwise": {
"0": { "key_sequence": "GUI+SPACE", "color": "#FFFFFF", "description": "Spotlight" },
"4": { "action": "spotify.prev", "color": "#00FF00", "description": "Previous" },
"5": { "action": "spotify.playpause", "color": "#00FF00", "description": "Play/Pause" },
"6": { "action": "spotify.next", "color": "#00FF00", "description": "Next" },
"13": { "folder": "apps", "color": "#FFFFFF", "description": "Apps" }
}
},
"folders": {
"apps": {
"0": { "action": "close_folder", "color": "#FFFFFF", "description": "Close" },
"12": { "application": "zoom.us", "color": "#0000FF", "description": "Zoom" },
"13": { "application": "Slack", "color": "#FF0000", "description": "Slack" }
}
}
}Note:
watchdog.pydetects URL changes only when Chrome or Safari becomes active, not on tab switches.
Plugins live in src/mac/plugins/ and extend BasePlugin. Config files go in src/mac/plugins_config/ (git-ignored — copy from the .json.example templates).
Requires a Spotify Premium account. Add credentials to plugins_config/spotify.json.
| Command | Description |
|---|---|
spotify.play |
Resume playback |
spotify.pause |
Pause |
spotify.playpause |
Toggle play/pause |
spotify.next |
Next track |
spotify.prev |
Previous track |
spotify.volume_up |
Volume +10% |
spotify.volume_down |
Volume −10% |
Set the bridge IP in plugins_config/hue.json and press the bridge button on first run. Lamp identifier can be a name in single quotes or a numeric index.
| Command | Description |
|---|---|
hue.turn_on 'Lamp Name' |
Turn on a light |
hue.turn_off 'Lamp Name' |
Turn off a light |
hue.toggle 'Lamp Name' |
Toggle a light |
Place .wav or .mp3 files in src/mac/sounds/ (configure path in plugins_config/sounds.json).
| Command | Description |
|---|---|
sounds.play 'file.mp3' |
Play a sound file |
sounds.stop |
Stop playback |
# First-time setup
python3 -m venv test_venv && source test_venv/bin/activate
pip install -r tests/requirements_test.txt
# Run tests
./run-tests.sh all # everything (218 tests)
./run-tests.sh pico # Pi Pico only
./run-tests.sh mac # Mac/watchdog only
./run-tests.sh security # security tests onlySee tests/CLAUDE.md for full details.
The case in the photo is this Printables model. It rotates the keypad — use "rotate": "CCW" (or "CW") in settings to compensate.
