Drive Herdr with a game controller. Patrol your AI agents, split panes, and switch workspaces from the couch — any gamepad, mapped by you in 60 seconds.
Open the configurator —
plug a pad in, press buttons, pick actions from a dropdown, copy the generated
gamepad.toml.
Paste this at your coding agent (Claude Code, Codex, Cursor — anything with a shell):
Install the herdr-gamepad plugin from https://github.com/htlin222/herdr-gamepad
into my Herdr setup on macOS. Work through it step by step and stop to tell me
whenever you need something from me:
1. Check that `herdr` is on PATH and is version 0.7.0 or newer (`herdr --version`),
and that `swiftc` exists (`swiftc --version`). If swiftc is missing, tell me to
run `xcode-select --install` and wait.
2. Install the plugin: `herdr plugin install https://github.com/htlin222/herdr-gamepad`
This runs build.sh, which compiles a single dependency-free Swift binary.
3. Copy the sample config into place:
`cp config/gamepad.toml "$(herdr plugin config-dir gamepad)/gamepad.toml"`
4. Add a keybinding to ~/.config/herdr/config.toml so I can reach learn mode:
[[keys.command]]
key = "prefix+g"
type = "plugin_action"
command = "gamepad.learn"
description = "Gamepad learn mode"
then `herdr server reload-config`.
5. Start the daemon: `herdr plugin action invoke gamepad.start`, then confirm with
`herdr plugin action invoke gamepad.status`.
6. Tell me to grant Accessibility permission to the terminal that started the daemon
(System Settings → Privacy & Security → Accessibility). This is only needed for the
[input] block — arrow keys, Return/Escape/Space and scrolling. Herdr actions work
without it. Restart the daemon after I approve.
7. Verify: `herdr plugin action invoke gamepad.learn`, and tell me to press a few
buttons and report what names appear.
Or do it yourself:
herdr plugin install https://github.com/htlin222/herdr-gamepad
cp config/gamepad.toml "$(herdr plugin config-dir gamepad)/gamepad.toml"
herdr plugin action invoke gamepad.startRequires macOS, Herdr ≥ 0.7.0, and Swift (ships with the Xcode command line tools).
No npm, no native module, no runtime dependency — build.sh produces one binary.
Bindings split by who is being talked to, and both use the same shape as Herdr's own
[keys]:
[herdr] # operations on Herdr
next_agent = "rt"
next_tab = "rb"
focus_pane_left = "dpad_left"
[input] # pretend to be a keyboard or mouse
return = "a"
up = { input = "right_up", repeat = true }Everything in [herdr] is sent as your keybinding for it, so the controller behaves
exactly like the keyboard. Rebind Herdr and this follows along — nothing to update here.
Bind several inputs to one behaviour with a list, exactly like Herdr:
focus_pane_left = ["dpad_left", "left_left"]
focus_pane_down = { input = "dpad_down", repeat = true }One button gives every other input a second meaning, the way tmux's prefix key works:
split_vertical = { input = "b", hold = "back" } # back THEN bhold is reached two ways, and both are always live:
- Hold — keep
backdown, pressb - Tap — press and release
backon its own, then pressbwithinprefix_timeout_ms
Tapping the armed prefix again backs out of it. Anything else you press spends it, so a stray tap costs you one button, not a mode you are stuck in.
The layer is Herdr's prefix mode. Pressing the pad's prefix button sends your Herdr
prefix (ctrl+a) for real, the moment your thumb lands — so Herdr shows the mode exactly
as it does from the keyboard, and the second press is a plain key rather than a chord this
plugin fakes.
The price: everything in the layer has to be a Herdr action you bound through the
prefix, e.g. split_vertical = "prefix+b" in config.toml. Prefix mode eats the next
key, so these are refused at startup, each with a message naming the binding:
| Refused | Why |
|---|---|
an [input] keystroke |
it would go to Herdr, not to your pane |
a built-in (agent_*) |
talks over the socket and sends no key at all, so the mode would stay open |
| a non-prefix binding | next_tab = "shift+right" — Herdr would eat the chord and match nothing |
Put those in the base layer instead; that is what the base layer is for.
herdr plugin action invoke gamepad.learn # press anything, see its name
herdr plugin action invoke gamepad.setup # guided, writes your profileInput names follow the W3C standard gamepad layout that browsers report, so any online
gamepad tester agrees with gamepad.toml:
a b x y face buttons (a = down, b = right, x = left, y = up)
lb rb shoulders
lt rt analog triggers
back start little centre buttons
l3 r3 stick clicks — many pads never send these
dpad_up dpad_down dpad_left dpad_right
guide big middle button, usually eaten by macOS
left_up left_down left_left left_right sticks, per direction
right_up right_down right_left right_right
Herdr has 85 socket methods (herdr api schema --json). Anything the one-liner form
cannot express goes here:
[[bind]]
button = "x"
hold = "lb"
method = "pane.split"
params = { direction = "right", ratio = 0.5 }In params, "$focused" becomes the pane that currently has focus.
| Action | What it does |
|---|---|
gamepad.setup |
guided setup, writes your controller profile |
gamepad.learn |
show button IDs as you press them |
gamepad.start |
start the daemon |
gamepad.stop |
stop the daemon |
gamepad.status |
connected pads, daemon state |
Herdr 0.7 does not bind keys declared in a plugin manifest, and actions run without a TTY
— which is why every interactive flow reports through notifications instead of printing to
a terminal. Bind gamepad.learn to a key yourself if you want it at hand.
The [input] block synthesises real key and wheel events, and macOS will not let any
process do that without permission:
System Settings → Privacy & Security → Accessibility
The prompt names the terminal that started the daemon, not this plugin. Approve that,
then restart the daemon. The [herdr] block needs none of this.
- Configurator, manual and rationale
config/gamepad.toml— the annotated reference configdocs/gamepad.svg— the binding diagram, as vector
MIT

