The fastest thing you can do in a terminal is the thing you never have to leave it for. herdr gives you a lot of actions and then buries them: the ones you reach for most sit below help, settings and detach in its own list, moving a pane between tabs exists only on the command line, and even splits have no action, keybinding or CLI at all. Every one of those is a reason to lift your hand off the keyboard.
This plugin is about not doing that. One key shows every binding you have - not the defaults, your resolved keymap - ordered so the things you reach for are at the top. Three more keys do the pane work herdr leaves to the CLI: send a pane to another tab, walk it through its neighbours, set every split back to half.
Nothing to configure beyond the bindings you want, because the list is not stored anywhere. It is derived from your own config every time it opens.
One key, and the whole keymap
herdr will not print your resolved keybindings. There is no CLI for it. What exists is herdr --default-config, where every action appears as a commented line:
# action = "binding"
and ~/.config/herdr/config.toml, which overrides those. The set your fingers actually know is the merge of the two, and nothing in the tool shows it. So the plugin does the merge and renders it - which is also why it cannot go stale: bind a new chord, and the popup is right on the next keypress.
The popup has three columns under one title. KEYCAPS is what your config adds, PREFIX is herdr's stock binding for the same action, spelled out as ctrl+b h rather than prefix+h because the prefix is released before the next key and that is how the chord is typed. Keeping them apart is what makes the page scannable: your own chords line up down the left, instead of being pushed around by the longer prefix forms. An action with nothing under KEYCAPS is one still waiting for a direct chord.
Your own commands come first, because herdr's config order opens with help, settings and detach - the things you reach for least - and pushes pane and tab movement to the bottom of a popup that already has limited height.
Colours are ANSI 16 only, so the popup resolves through the terminal's own palette and follows whatever herdr theme is active. NO_COLOR=1 turns that off; piped output is plain either way.
The pane work herdr leaves to the CLI, as keys
Three things, and the third has no CLI at all.
Move a pane to another tab on alt+m. A picker, arrows or j/k to move, a number to jump straight there, Enter to confirm, q or Esc to cancel. Destinations are grouped by workspace, so a pane can cross into another one:
MOVE PANE pick a destination
Redesign
› 1 Claude1 (2 panes)
2 a new tab here
Infra
3 Notes (1 pane)
4 a new tab here
5 a new workspace
Headings are not selectable and the cursor skips them, the tab the pane is already in is never offered, and "a new tab here" carries the workspace it sits under, so it lands there rather than in whichever workspace has focus.
Swap a pane with its neighbour on alt+shift+h/j/k/l. herdr's own move_tab_previous and move_tab_next move whole tabs, not panes. Hold the same direction and the pane keeps walking, and at the edge it wraps to the far end instead of stopping dead, so a row behaves as a closed cycle - because herdr reports a missing neighbour as changed: false with exit code 0 rather than as an error, and that is the cue to wrap. The walk is bounded so a misbehaving swap cannot spin forever.
Even out the panes on alt+shift+e, which sets every split in the tab back to half. pane resize only shifts a divider by a relative amount, so there is nothing to call with: this one goes through the socket API's layout.set_split_ratio, over newline-delimited JSON on ~/.config/herdr/herdr.sock. A split is addressed by the path taken to reach it - [] is the root, then false descends into first and true into second.
The parts that took longest are the parts herdr does not document
Keybindings can only target plugin actions, never plugin panes. The popup is not what you bind. You bind an action, and that action opens the pane:
alt+h -> action "show" -> show.py -> herdr plugin pane open
-> pane "keys" (popup)
-> keycaps.py
placement = "popup" has to live in the manifest, not on the open request, because the CLI's --placement flag has no popup value. A popup also takes all terminal input, Escape included, and closes the moment its command exits - so handing the list to a pager is out. It pages in-process instead: Escape closes it, q and ctrl+c too, arrows and page keys and g/G scroll when it does not fit. (less would have needed --lesskey-src to rebind Escape, which arrived in less 582 while macOS still ships 581 - a plugin that works on one machine only is not a plugin.)
The install has a step herdr cannot tell you about. A plugin manifest has no field for a keybinding, and herdr plugin install never touches config.toml. So after the install every action is registered and no key reaches any of them, which looks exactly like a broken plugin. Hence the second command:
herdr plugin install macd2/herdr-keycaps
python3 ~/.config/herdr/plugins/github/macd2.keycaps-*/setup.py
setup.py merges this plugin's seven bindings - plus the single-modifier alt+ layer that fills the KEYCAPS column for herdr's own actions - into your config.toml. It backs the file up first, keeps your comments and ordering, adds only what is missing, and leaves any chord you already spent where it is, naming it on the way out. Running it twice changes nothing.
herdr cannot print a warning at install time either: the install preview emits only herdr's own fields, and a build command's output is piped and discarded unless the command fails, which aborts the install. So the plugin waits for the next server start and puts one notification in the top right, naming the command - and only while nothing at all is bound. Bind a subset on purpose and it stays quiet, because a half-spent config is a choice, not a mistake.
Both scripts would rather fail than mangle the file
Your whole herdr setup lives in that one file, so a wrong edit is worse than no edit. setup.py and uninstall.py both state what they intend, build the result in memory, re-parse it and compare it against that intent before anything is written. One unexplained difference - another table changed, a value that came out wrong, output that is not valid TOML - and nothing is written at all. Shapes a line-based edit cannot see are refused rather than guessed at: a keys.command inline array, a value spread over several lines.
A refusal exits 1 and prints the reason, the path, and the steps to do it by hand - addressed to an agent, telling it to ask the human first rather than editing the file to get past the refusal. Stock config, then setup.py, then uninstall.py --also-direct-layer gives back the original file byte for byte.
The failure that is worth a paragraph
If you attach with herdr --remote, nothing in this plugin works until you pass one flag:
herdr --remote <target> --remote-keybindings server
herdr --remote uses your local keybindings by default, and local custom command bindings are never sent to the remote host. This fails silently, which is what makes it worth knowing: herdr config check says config: ok, herdr server reload-config says applied, the plugin links fine and lists its actions - and no key does anything.
Tests
Four of them, because a plugin that edits someone's config should be boring to trust:
python3 tests/test_manifest.py # the required metadata the marketplace parses
python3 tests/test_grouping.py # group order, custom first, unknown actions kept
python3 tests/test_popup_pager.py # stays open on a real pty, closes on esc
python3 tests/test_show_action.py # action opens the right pane, opens no popup
The manifest one matters more than it sounds: the herdr marketplace indexes public repositories tagged herdr-plugin whose default branch carries a herdr-plugin.toml. Discovery is automatic and unreviewed, so a typo there is an invisible delisting rather than an error anyone sees.
An action no group knows about still renders, under OTHER, so a binding added by a future herdr release is never silently dropped.
Try it
herdr plugin install macd2/herdr-keycaps
python3 ~/.config/herdr/plugins/github/macd2.keycaps-*/setup.py
MIT, Linux and macOS, built and verified against herdr 0.9.3. The source is github.com/macd2/herdr-keycaps.
So the question I would like answered in the comments: which action in your setup is still not one keypress away? That list is what this plugin is built around, and for me it was moving panes - the one thing I did with a right-click for months because herdr only exposed it on the command line.
Drafted with an AI assistant and edited by me.


Top comments (0)