herdr-simple-switcher is a Herdr plugin that adds four interactive fuzzy-select popups for navigating a Herdr session: switch workspace, switch tab, switch agent, and open project. The three switch actions query the herdr CLI, format with jq, present via fzf, and apply with a herdr … focus call. Open project discovers git repos under a configurable root and opens the chosen one in a new tab (creating the workspace if needed). Pure bash — no build step, no compiled source. MIT licensed.
The plugin is manifest-driven. herdr-plugin.toml declares four [[panes]] (each a bash script rendered as a popup) and four matching [[actions]]. A keybinding invokes an action (type = "plugin_action", id simple-switcher.<id>); the action runs scripts/open-pane.sh <id>, which calls herdr plugin pane open to launch the popup pane. This indirection exists because Herdr keybindings can invoke an action but cannot open a pane directly — panes render the popup UI, actions only run commands. (You can also open a pane straight from a type = "shell" keybind, skipping the action.) The three switch scripts follow the identical pipeline:
herdr <entity> list # query state as JSON
→ jq -r '…' # format into "display\tID" lines
→ fzf --with-nth=1 # interactive fuzzy select (display col 1)
→ cut -f2 # extract the ID (col 2)
→ herdr <entity> focus <ID> # apply the selection
Pattern: query → format → interact → apply. open-project.sh extends it with a discovery + create step:
fd -H -t d '^\.git$' $PROJECTS_ROOT # find repos, emit "ws/proj\tws\tabs-path"
→ fzf --with-nth=1 # pick a project
→ herdr workspace list | jq # find workspace whose label == ws
→ herdr tab create --workspace ID --cwd PATH --label PROJ # reuse workspace
(or herdr workspace create --cwd PATH --label ws, then tab rename) # create it
→ herdr workspace focus / herdr tab focus # apply
A [[startup]] hook (scripts/startup-check.sh) runs once per session and posts a herdr notification show toast when the projects root is unconfigured — startup stdout only reaches the plugin log, so user-facing messages must go through the notification CLI. Action commands are likewise non-interactive (no TTY); interactive fzf must run inside a pane, which is why actions delegate to plugin pane open.
Scripts are independent, stateless, argument-free, and interactive-only (no automated assertions).
scripts/— four pane scripts (one bash file per popup),open-pane.sh(action→pane launcher), andstartup-check.sh(startup hook)..claude/— local agent permission config (settings.local.json).- Repo root — plugin manifest,
config.example.env, and docs. Nosrc/, notests/.
herdr-plugin.toml— plugin manifest and entry points. Top-level:id = "simple-switcher",version = "0.4.0",min_herdr_version = "0.7.0",platforms = ["linux", "macos"]. One[[startup]]hook, four[[actions]](eachcommand = ["bash", "scripts/open-pane.sh", "<id>"]), and four[[panes]](placement = "popup",command = ["bash", "scripts/<name>.sh"]). Action ids and pane ids share names but live in separate id namespaces.scripts/switch-workspace.sh— lists workspaces (number, label, tab count withtab/tabspluralization); focuses viaherdr workspace focus <workspace_id>.scripts/switch-tab.sh— resolves the current workspace, lists its tabs (herdr tab list --workspace <WID>), shows the workspace label in the fzf header; focuses viaherdr tab focus <tab_id>.scripts/switch-agent.sh— lists agents across all workspaces (name,[agent_status], cwd), filters empty agents; focuses viaherdr agent focus <pane_id>.scripts/open-project.sh—fd-discovers git repos underPROJECTS_ROOT(<workspace>/<project>layout), picks one, then reuses or creates the matching-label workspace and opens the project in a new tab viaherdr tab create --cwd <path> --label <project>(parses.result.tab.tab_id/.result.workspace.workspace_id). Projects root comes solely fromPROJECTS_ROOTin$HERDR_PLUGIN_CONFIG_DIR/config.env(no env override, no default); if unset the popup prints setup instructions and exits. Error paths usedie()to pause (read) so the message stays readable before the popup closes.scripts/startup-check.sh— startup hook; ifPROJECTS_ROOTis not defined inconfig.env, firesherdr notification showto prompt setup. Exits 0 otherwise.scripts/open-pane.sh— action launcher;exec "$HERDR" plugin pane open --plugin simple-switcher --entrypoint "$1". One generic script backs all four actions.config.example.env— template for the user'sconfig.env(copied into the plugin config dir); setsPROJECTS_ROOT.README.md— install (herdr plugin install hapham/herdr-simple-switcher), requirements, and example keybindings.
There is no build, lint, or test tooling configured.
# Install the plugin into Herdr
herdr plugin install hapham/herdr-simple-switcher
# Run an action script directly (for manual smoke testing)
bash scripts/switch-workspace.sh
bash scripts/switch-tab.sh
bash scripts/switch-agent.sh
bash scripts/open-project.sh
# Point scripts at a specific herdr binary
HERDR_BIN_PATH=/path/to/herdr bash scripts/switch-agent.sh
# Run Open Project (reads PROJECTS_ROOT from the plugin config.env)
HERDR_PLUGIN_CONFIG_DIR=/path/to/config bash scripts/open-project.shActions are also invoked from user keymaps as simple-switcher.<pane-id> (e.g. simple-switcher.switch-workspace).
All scripts are deliberately uniform — match this style exactly when adding or editing:
- Shebang / strict mode:
#!/usr/bin/env bashthenset -euo pipefail. - Dependency check at top:
for cmd in fzf jq; do command -v "$cmd" >/dev/null 2>&1 || { echo "$cmd is required but not found" >&2 exit 1 } done
- Binary override:
HERDR="${HERDR_BIN_PATH:-herdr}"; always call"$HERDR"(quoted). - Formatting:
jq -rbuilds tab-delimiteddisplay\tIDlines; focused rows are prefixed with"* "vs" "via(if .focused then "* " else " " end). - Selection:
fzf --delimiter=$'\t' --with-nth=1 --prompt="<Label> > " --reverse --no-multi --exit-0 || true—|| trueswallows fzf exit 130 on Esc soset -edoesn't abort. - Apply: guard with
if [[ -n "$selected" ]]; then …, extract ID withcut -f2, redirect focus/mutation output to>/dev/null. - Config: plugin owns its config format (Herdr has no config API). Read user config from
$HERDR_PLUGIN_CONFIG_DIR/config.env(sourced); require it explicitly and fail with a clear message when unset (no silent default) — seeopen-project.shresolvingPROJECTS_ROOT. - Naming: hyphenated script files (
<verb>-<entity>.sh); lowercase locals (selected,pane_id,workspace_id,tab_id,project_path); uppercase constants (HERDR); no spaces around=; double-quote all variable expansions.
- Runtime:
bash. No Node, Bun, Rust, or package manager — do not introduce one. - Required tools on the host:
herdrCLI (>= 0.7.0),fzf,jq, andfd(Open Project only). - Manifest schema: Herdr
[[panes]],[[actions]], and[[startup]](v0.7.0+). Panes:id,title,placement = "popup",command. Actions:id,title,contexts = ["workspace"],command— invoked viaplugin_actionkeybindings; run a command with no TTY, so they delegate interactive UI to a pane. Startup:command(one-shot, async; output goes to the plugin log, not the user — useherdr notification showfor user-facing messages). .claude/settings.local.jsonrestricts agent Bash tortk ls *andherdr agent *, and WebFetch to theherdr.devdomain.- Plugin runtime env (injected by Herdr): call the CLI via
HERDR_BIN_PATH; read user config fromHERDR_PLUGIN_CONFIG_DIR; durable state (if ever needed) goes inHERDR_PLUGIN_STATE_DIR— never write intoHERDR_PLUGIN_ROOT(managed checkout).
No tests, test framework, or CI exist in this repo. QA is manual: run a script and exercise selection + Esc against a live herdr session.
If adding automated coverage, the idiomatic choice for these scripts is shellcheck (static lint) plus bats (mock the herdr CLI, assert jq formatting and the cut -f2 ID extraction, verify the correct focus invocation). Do not claim tests exist until they do.