A herdr plugin that tells PadIO which app is running in the focused pane, so your game controller switches mode automatically as you move around.
Focus a Claude Code pane and the controller is in agent mode. Move to a shell pane and it is in shell mode. Launch nvim in that pane and it becomes vim mode. No manual mode picking.
The bridge polls herdr for the focused pane and writes a single token to ~/.config/padio/context. PadIO watches that file and maps the token to a mode via context_modes.
herdr bridge PadIO
───── ────── ─────
poll pane.current @5Hz ──> agent field, else argv0 ──> write ──> file watch
where pid == pgid context → mode
The token is resolved in two steps:
- If herdr has detected an agent in the pane, its
agentfield is used directly (claude, and so on). herdr does this detection itself, so no process inspection is needed. - Otherwise the bridge asks for
pane.process_infoand takes theargv0of the process whosepidequalsforeground_process_group_id— the process actually driving the pane. That yieldszshfor a shell,sshfor a remote session,nvimfor an editor.
herdr has an event stream, and pane.focused is an accurate focus signal: measured on 0.7.5, four real focus changes produced exactly four events, in order, each arriving ahead of a 2 Hz poll, with no spurious events. An event-driven bridge is therefore possible. This one still polls, for two reasons.
events.subscribe replays retained history (#1270). Every new subscription first re-delivers a buffer of past events — 116 of them on the session this was developed against — paced at about 100 ms, before any live event arrives. It is per subscription rather than per connection, and nothing marks where the replay ends, so a subscriber cannot cleanly tell stale from live. Acting on the replay would walk the mode through every historical focus change in turn. Working around it means a heuristic, such as treating a quiet gap as the end of history, and reapplying it after every reconnect.
Polling is cheaper than that heuristic. Connect plus one pane.current costs about 0.14 ms, so 5 Hz is roughly 1 ms of work per second. It also collapses two cases into one call: pane switches and launching a program inside a pane you are already in. The latter has no reliable event at all, since pane.updated is touch-driven and a process change on its own emits nothing (#2115).
So the tradeoff is a millisecond a second against replay-detection logic and a second code path. If #1270 is fixed with an explicit "live only" or cursor option, switching to events would be worthwhile.
herdr plugin install vgreg/herdr-padio
Requires python3 on PATH (no packages, stdlib only) and PadIO 2026.3.0 or later.
To run it outside herdr, or to develop against a checkout:
git clone https://github.com/vgreg/herdr-padio
herdr plugin link ./herdr-padio
Map tokens to modes in the profile for your terminal, in ~/.config/padio/config.json. Put herdr's own workspace/tab/pane navigation in global so it stays available in every mode:
{
"profiles": {
"ghostty": {
"apps": ["com.mitchellh.ghostty"],
"default_mode": "herdr-nav",
"global": { "…": "workspace / tab / pane navigation" },
"context_modes": {
"claude": "agent",
"nvim": "vim",
"zsh": "shell",
"ssh": "shell"
},
"hidden_modes": ["agent", "vim"],
"modes": { "herdr-nav": {}, "agent": {}, "vim": {}, "shell": {} }
}
}
}Matching is exact. Three PadIO behaviours are worth knowing:
- A token with no
context_modesentry leaves the current mode alone. It does not fall back todefault_mode, so you only need entries for apps you care about. - A mode picked by hand sticks until the context token changes.
hidden_modeskeeps context-driven modes out of the mode picker while leaving them reachable. Useful once you have one mode per app and only want to pick a couple by hand.
Some tools run under a wrapper, so argv0 is unhelpful: node for a JavaScript CLI, uv for uv run python …. Add substring rules in ~/.config/padio/bridge.json:
{
"rules": [
{ "match": "bin/some-cli", "token": "some-cli" },
{ "match": "uv run python train", "token": "training" }
]
}Each rule's match is tested as a plain substring against the process command line, most specific first; the first hit wins. Rules are tried against the pane's main process, then any other foreground process. If none match, the bridge falls back to argv0.
Identifying the app is the bridge's job; mapping that name to a mode is PadIO's. Keep rules here and modes in context_modes.
See what the bridge resolves right now, without writing anything:
python3 padio_bridge.py --once
Inspect a specific pane instead of the focused one:
python3 padio_bridge.py --once --pane w5:p2
Read the plugin's output:
herdr plugin log
Other flags: --interval (poll seconds, default 0.2), --context (output path), --rules (rules file), --quiet.
- The bridge writes only when the resolved token changes, via a temp file plus
rename, which is the atomic-replace contract PadIO's file watcher expects. - A pane it cannot identify leaves the context file untouched, matching PadIO's "no match leaves the mode alone" rule.
- If herdr is unreachable it retries with backoff and says so once. PadIO keeps working with manual mode switching, since a missing or stale context file is a no-op.
- The context file is not cleared on exit, so the last mode persists. This is deliberate and consistent with the rule above.
- The bridge owns the context file. If something else overwrites it, the bridge will not restore it until the resolved token next changes.
MIT