From 9fe3975fea6cdcc526e0490e1d80ff2438f865a4 Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Mon, 20 Jul 2026 11:06:06 +1000 Subject: [PATCH 1/4] fix: zellij session must detach, not quit, when the client dies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit on_force_close "quit" killed the whole session (and every process in it) whenever the ssh/mosh chain dropped — the opposite of what a multiplexer is for. Default to "detach" and self-heal existing configs on upgraded containers, mirroring the tmux mouse self-heal. Also bind ? to a floating configuration plugin as a keybinding help popup (clear-defaults=true leaves no discoverable help otherwise). Co-Authored-By: Claude Fable 5 --- setup.sh | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/setup.sh b/setup.sh index b87f08b..12466d0 100755 --- a/setup.sh +++ b/setup.sh @@ -1023,7 +1023,9 @@ _install_zellij_inner() { scroll_buffer_size 50000 pane_frames false auto_layout true - on_force_close "quit" + // "detach" not "quit" — a dropped client (ssh/mosh dying) must not kill + // the session and everything running in it + on_force_close "detach" simplified_ui true session_serialization true support_kitty_keyboard_protocol true @@ -1161,6 +1163,9 @@ _install_zellij_inner() { // Enter scroll/copy mode (vi-style — like tmux [ ) bind "[" { SwitchToMode "Scroll"; } + + // Keybinding help — floating configuration plugin + bind "?" { LaunchOrFocusPlugin "configuration" { floating true; }; SwitchToMode "Normal"; } } // ── Scroll mode (vi-style copy mode) ──────────────────────── @@ -1236,9 +1241,19 @@ _install_zellij_inner() { fi } +_ensure_zellij_defaults() { + local conf="$HOME/.config/zellij/config.kdl" + [ -f "$conf" ] || return 0 + sed -i 's/^on_force_close "quit"/on_force_close "detach"/' "$conf" + if ! grep -q 'bind "?"' "$conf"; then + sed -i '/bind "\[" { SwitchToMode "Scroll"; }/a\ bind "?" { LaunchOrFocusPlugin "configuration" { floating true; }; SwitchToMode "Normal"; }' "$conf" + fi +} + install_zellij() { - if command -v zellij &>/dev/null; then echo "Zellij already installed, skipping."; return 0; fi + if command -v zellij &>/dev/null; then echo "Zellij already installed, skipping."; _ensure_zellij_defaults; return 0; fi run_with_spinner "Installing Zellij..." _install_zellij_inner + _ensure_zellij_defaults } for mux in $(echo "$mux_list" | tr ',' ' '); do From bf7c31f05fb6462568b84a2ece672f09a47f0204 Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Mon, 20 Jul 2026 11:14:39 +1000 Subject: [PATCH 2/4] feat: Ctrl+b as a second zellij leader alongside Ctrl+Space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ctrl+Space is transmitted as a NUL byte that some client stacks never deliver (iPadOS grabs it for input-source switching before Blink sees it). Mirror every Ctrl+Space mode bind with Ctrl+b — the classic tmux prefix — in the shipped config and the self-heal. Scroll/search modes keep their explicit Ctrl+b page-up binds, which take precedence over the shared exit bind. Co-Authored-By: Claude Fable 5 --- setup.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 12466d0..6921f8e 100755 --- a/setup.sh +++ b/setup.sh @@ -1105,16 +1105,21 @@ _install_zellij_inner() { }; } - // Prefix-style bindings via Ctrl+Space (tmux-like leader) + // Prefix-style bindings via Ctrl+Space (tmux-like leader). + // Ctrl+b as well: Ctrl+Space is a NUL byte some clients never + // deliver (iPadOS grabs it for input-source switching) bind "Ctrl Space" { SwitchToMode "Tmux"; } + bind "Ctrl b" { SwitchToMode "Tmux"; } } locked { bind "Ctrl Space" { SwitchToMode "Normal"; } + bind "Ctrl b" { SwitchToMode "Normal"; } } tmux { bind "Ctrl Space" { SwitchToMode "Normal"; } + bind "Ctrl b" { SwitchToMode "Normal"; } bind "Esc" { SwitchToMode "Normal"; } // Pane splitting (h=horizontal, v=vertical — matching tmux) @@ -1222,6 +1227,7 @@ _install_zellij_inner() { // Allow Ctrl+Space to return to normal from any non-normal mode shared_except "normal" "locked" "tmux" { bind "Ctrl Space" { SwitchToMode "Normal"; } + bind "Ctrl b" { SwitchToMode "Normal"; } } } ZELLIJCONF @@ -1248,6 +1254,10 @@ _ensure_zellij_defaults() { if ! grep -q 'bind "?"' "$conf"; then sed -i '/bind "\[" { SwitchToMode "Scroll"; }/a\ bind "?" { LaunchOrFocusPlugin "configuration" { floating true; }; SwitchToMode "Normal"; }' "$conf" fi + if ! grep -q 'bind "Ctrl b" { SwitchToMode "Tmux"; }' "$conf"; then + sed -i 's|^\(\s*\)bind "Ctrl Space" { SwitchToMode "Tmux"; }$|&\n\1bind "Ctrl b" { SwitchToMode "Tmux"; }|' "$conf" + sed -i 's|^\(\s*\)bind "Ctrl Space" { SwitchToMode "Normal"; }$|&\n\1bind "Ctrl b" { SwitchToMode "Normal"; }|' "$conf" + fi } install_zellij() { From 91ff11b58e090f517a6b28260d16002cd8bd1eda Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Thu, 13 Aug 2026 19:10:40 +1000 Subject: [PATCH 3/4] Add Herdr keyboard shortcuts --- README.md | 24 ++++++++++- demo/setup-demo.sh | 3 +- scripts/e2e-test.sh | 5 ++- scripts/lib/tools.yaml | 9 +++++ scripts/sqrbx-learn | 2 +- scripts/squarebox-setup.sh | 2 +- scripts/squarebox-update.sh | 7 ++-- setup.sh | 80 +++++++++++++++++++++++++++++++++++-- 8 files changed, 119 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a04eaef..8e0d1c9 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ scripted installs). Values use the same keys as `sqrbx-setup`: | `SQUAREBOX_SDKS` | language SDKs (`node,python,go,dotnet,rust`) | | `SQUAREBOX_EDITORS` | editors (`micro,edit,fresh,nvim`) | | `SQUAREBOX_TUIS` | TUI tools (`lazygit,gh-dash,yazi`) | -| `SQUAREBOX_MULTIPLEXERS` | multiplexers (`tmux,zellij`) | +| `SQUAREBOX_MULTIPLEXERS` | multiplexers (`tmux,zellij,herdr`) | | `SQUAREBOX_GIT_NAME` / `SQUAREBOX_GIT_EMAIL` | git identity (when no host gitconfig) | Example: `SQUAREBOX_AI=claude SQUAREBOX_SDKS=node,python curl -fsSL …/install.sh | bash` @@ -257,12 +257,32 @@ Installed during first-run setup. Choose any combination: ### Terminal Multiplexers -Installed during first-run setup. Choose either, both, or neither: +Installed during first-run setup. Choose any combination, or none: | Name | Description | |------|-------------| | [tmux](https://github.com/tmux/tmux) | Classic terminal multiplexer | | [zellij](https://github.com/zellij-org/zellij) | Friendly terminal workspace | +| [herdr](https://herdr.dev) | Agent multiplexer — run multiple coding agents from one terminal | + +### Shared Herdr Keyboard Language (Experimental) + +Herdr uses `Ctrl` for immediate actions in the focused application and `Shift` +to modify or move the target. Its `F12` prefix remains available as a +compatibility fallback: + +| Feature | Herdr | +|---------|-------| +| New / close | `Ctrl+T` / `Ctrl+W` | +| Cycle / select tabs | `Ctrl+Tab`, `Ctrl+Shift+Tab` / `Ctrl+1-9` | +| Focus / swap pane | `Ctrl+Arrow` / `Ctrl+Shift+Arrow` | +| Sidebar / go to | `Ctrl+B` / `Ctrl+G` | +| Split right / down | `Ctrl+\\` / `Ctrl+Shift+\\` | +| Zoom / new workspace | `Ctrl+Shift+Z` / `Ctrl+Shift+N` | +| Compatibility prefix | `F12` | + +Existing `~/.config/herdr/config.toml` files remain user-managed and are not +replaced by setup. ### Shell (Experimental) diff --git a/demo/setup-demo.sh b/demo/setup-demo.sh index 434f979..5f120aa 100755 --- a/demo/setup-demo.sh +++ b/demo/setup-demo.sh @@ -80,13 +80,14 @@ echo section_header "Terminal Multiplexers" selected=$(gum choose --no-limit \ --header "Select terminal multiplexer:" \ - "tmux" "zellij") || true + "tmux" "zellij" "herdr") || true while IFS= read -r line; do [ -z "$line" ] && continue case "$line" in tmux) run_with_spinner "Installing tmux..." 0.6 ;; zellij) run_with_spinner "Installing Zellij v0.44.0..." 0.6 ;; + herdr) run_with_spinner "Installing Herdr..." 0.6 ;; esac done <<< "$selected" diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh index dd9af3e..757bddd 100755 --- a/scripts/e2e-test.sh +++ b/scripts/e2e-test.sh @@ -185,7 +185,7 @@ suite_setup_editors() { echo "opencode,pi,paseo" > /workspace/.squarebox/ai-tool echo "micro,edit,fresh,nvim" > /workspace/.squarebox/editors echo "lazygit,gh-dash,yazi" > /workspace/.squarebox/tuis - echo "tmux,zellij" > /workspace/.squarebox/multiplexer + echo "tmux,zellij,herdr" > /workspace/.squarebox/multiplexer echo "node,go" > /workspace/.squarebox/sdks # Shell section (experimental): exercise the bash path here. The zsh # install (apt zsh + Oh My Zsh + two plugin clones) is network-heavy and @@ -231,6 +231,9 @@ suite_setup_editors() { # 3.8 multiplexers installed run_test "3.8a tmux installed" command -v tmux run_test "3.8b zellij installed" command -v zellij + run_test "3.8c herdr installed" command -v herdr + run_test "3.8d herdr config installed" test -f "$HOME/.config/herdr/config.toml" + run_test_grep "3.8e herdr uses Ctrl+T for new tab" 'new_tab = ["ctrl+t", "prefix+c"]' cat "$HOME/.config/herdr/config.toml" # 3.9 SDKs installed (via mise) run_test "3.9a node installed (via mise)" command -v node diff --git a/scripts/lib/tools.yaml b/scripts/lib/tools.yaml index 1ce504d..b562b3a 100644 --- a/scripts/lib/tools.yaml +++ b/scripts/lib/tools.yaml @@ -206,3 +206,12 @@ tools: tar_extract: zellij dest: user group: setup + + herdr: + repo: ogulcancelik/herdr + version_prefix: v + artifact: herdr-linux-{zarch} + method: binary + binaries: herdr + dest: user + group: setup diff --git a/scripts/sqrbx-learn b/scripts/sqrbx-learn index b27682b..4d2e047 100755 --- a/scripts/sqrbx-learn +++ b/scripts/sqrbx-learn @@ -83,7 +83,7 @@ has_tool() { case "$tool" in lazygit|gh-dash|yazi) grep -qw "$tool" "$SQUAREBOX_DIR/tuis" 2>/dev/null ;; - tmux|zellij) + tmux|zellij|herdr) grep -qw "$tool" "$SQUAREBOX_DIR/multiplexer" 2>/dev/null ;; *) command -v "$tool" &>/dev/null ;; diff --git a/scripts/squarebox-setup.sh b/scripts/squarebox-setup.sh index ba4390d..2043b08 100644 --- a/scripts/squarebox-setup.sh +++ b/scripts/squarebox-setup.sh @@ -34,7 +34,7 @@ usage() { ai AI coding assistants (claude, copilot, gemini, codex, opencode, pi, paseo) editors Text editors (micro, edit, fresh, nvim) tuis TUI tools (lazygit, gh-dash, yazi) - multiplexers Terminal multiplexers (tmux, zellij) + multiplexers Terminal multiplexers (tmux, zellij, herdr) sdks SDKs (node, python, go, dotnet, rust) shell Default shell (bash, zsh/fish — experimental) learn Interactive terminal learning guide (sqrbx-learn) diff --git a/scripts/squarebox-update.sh b/scripts/squarebox-update.sh index dffccfd..0f72ad3 100755 --- a/scripts/squarebox-update.sh +++ b/scripts/squarebox-update.sh @@ -139,6 +139,7 @@ helix_current() { hx --version 2>/dev/null | head -1 | awk '{print $2}' || echo nvim_current() { nvim --version 2>/dev/null | head -1 | awk '{print $2}' | sed 's/^v//' || echo "not installed"; } opencode_current() { opencode --version 2>/dev/null | grep -oP '[\d.]+' | head -1 || echo "not installed"; } zellij_current() { zellij --version 2>/dev/null | head -1 | awk '{print $2}' || echo "not installed"; } +herdr_current() { herdr --version 2>/dev/null | head -1 | awk '{print $2}' || echo "not installed"; } just_current() { just --version 2>/dev/null | awk '{print $2}' || echo "not installed"; } difftastic_current() { difft --version 2>/dev/null | head -1 | awk '{print $2}' || echo "not installed"; } mise_current() { mise --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "not installed"; } @@ -161,8 +162,8 @@ tool_latest() { # ── Tool registry ────────────────────────────────────────────────────── -TOOLS=(delta yq lazygit xh yazi starship ghdash glow gum just difftastic mise micro fresh edit helix nvim opencode zellij) -TOOL_DISPLAY_NAMES=(delta yq lazygit xh yazi starship gh-dash glow gum just difftastic mise micro fresh edit helix nvim opencode zellij) +TOOLS=(delta yq lazygit xh yazi starship ghdash glow gum just difftastic mise micro fresh edit helix nvim opencode zellij herdr) +TOOL_DISPLAY_NAMES=(delta yq lazygit xh yazi starship gh-dash glow gum just difftastic mise micro fresh edit helix nvim opencode zellij herdr) # Map display names to tools.yaml names (ghdash → gh-dash) yaml_name() { @@ -186,7 +187,7 @@ usage() { sqrbx-update --help Show this help ${BOLD}Tools:${RESET} - delta, yq, lazygit, xh, yazi, starship, gh-dash, glow, gum, just, difftastic, mise, micro, fresh, edit, helix, nvim, opencode, zellij + delta, yq, lazygit, xh, yazi, starship, gh-dash, glow, gum, just, difftastic, mise, micro, fresh, edit, helix, nvim, opencode, zellij, herdr EOF } diff --git a/setup.sh b/setup.sh index 6921f8e..5705d71 100755 --- a/setup.sh +++ b/setup.sh @@ -841,12 +841,13 @@ if $INTERACTIVE; then case "$mux" in tmux) gum_selected="${gum_selected:+$gum_selected,}tmux" ;; zellij) gum_selected="${gum_selected:+$gum_selected,}zellij" ;; + herdr) gum_selected="${gum_selected:+$gum_selected,}herdr" ;; esac done gum_args=(--no-limit --header "Select terminal multiplexer:") [ -n "$gum_selected" ] && gum_args+=(--selected "$gum_selected") selected=$(gum choose "${gum_args[@]}" \ - "tmux" "zellij") || true + "tmux" "zellij" "herdr") || true mux_list="" while IFS= read -r line; do [ -z "$line" ] && continue @@ -854,7 +855,7 @@ if $INTERACTIVE; then done <<< "$selected" else echo "Select terminal multiplexer (comma-separated, or 'all', or press Enter to skip):" - for mux_item in "1:tmux:classic terminal multiplexer" "2:zellij:friendly terminal workspace"; do + for mux_item in "1:tmux:classic terminal multiplexer" "2:zellij:friendly terminal workspace" "3:herdr:agent multiplexer for coding agents"; do num="${mux_item%%:*}"; rest="${mux_item#*:}"; key="${rest%%:*}"; desc="${rest#*:}" if [[ ",$mux_prev," == *",${key},"* ]]; then echo " ${num}) ${key} — ${desc} [installed]" @@ -862,18 +863,19 @@ if $INTERACTIVE; then echo " ${num}) ${key} — ${desc}" fi done - read -rp "Selection [1,2/all/skip]: " mux_selection + read -rp "Selection [1,2,3/all/skip]: " mux_selection if [ -z "$mux_selection" ] && [ -n "$mux_prev" ]; then mux_list="$mux_prev" else mux_list="" if [ "$mux_selection" = "all" ]; then - mux_list="tmux,zellij" + mux_list="tmux,zellij,herdr" elif [ -n "$mux_selection" ]; then for item in $(echo "$mux_selection" | tr ',' ' '); do case "$item" in 1) mux_list="${mux_list:+$mux_list,}tmux" ;; 2) mux_list="${mux_list:+$mux_list,}zellij" ;; + 3) mux_list="${mux_list:+$mux_list,}herdr" ;; esac done fi @@ -1266,10 +1268,80 @@ install_zellij() { _ensure_zellij_defaults } +_install_herdr_inner() { + command -v herdr >/dev/null 2>&1 || sb_install herdr latest || return 1 + command -v herdr >/dev/null 2>&1 || return 1 + mkdir -p "$HOME/.config/herdr" || return 1 + if [ ! -e "$HOME/.config/herdr/config.toml" ] && [ ! -L "$HOME/.config/herdr/config.toml" ]; then + if ! ( + config_tmp="" + cleanup_herdr_stage() { + [ -z "$config_tmp" ] || rm -f -- "$config_tmp" + } + trap cleanup_herdr_stage EXIT + trap 'exit 1' HUP INT TERM + config_tmp=$(mktemp "$HOME/.config/herdr/.config.toml.squarebox-tmp.XXXXXX") || exit 1 + cat > "$config_tmp" <<-'HERDRCONF' || exit 1 + [ui] + agent_panel_sort = "spaces" + + # Super belongs to the OS/window manager. Ctrl acts immediately in Herdr; + # Shift modifies or moves. F12 keeps the upstream-style prefix fallbacks. + [keys] + prefix = "f12" + help = ["ctrl+?", "prefix+?"] + goto = ["ctrl+g", "prefix+g"] + workspace_picker = "prefix+w" + toggle_sidebar = ["ctrl+b", "prefix+b"] + detach = ["ctrl+shift+q", "prefix+q"] + + new_tab = ["ctrl+t", "prefix+c"] + previous_tab = ["ctrl+shift+tab", "prefix+p"] + next_tab = ["ctrl+tab", "prefix+n"] + switch_tab = ["ctrl+1..9", "prefix+1..9"] + rename_tab = "prefix+shift+t" + close_tab = "prefix+shift+x" + + new_workspace = ["ctrl+shift+n", "prefix+shift+n"] + rename_workspace = "prefix+shift+w" + close_workspace = "prefix+shift+d" + + focus_pane_left = ["ctrl+left", "prefix+h"] + focus_pane_down = ["ctrl+down", "prefix+j"] + focus_pane_up = ["ctrl+up", "prefix+k"] + focus_pane_right = ["ctrl+right", "prefix+l"] + swap_pane_left = ["ctrl+shift+left", "prefix+shift+h"] + swap_pane_down = ["ctrl+shift+down", "prefix+shift+j"] + swap_pane_up = ["ctrl+shift+up", "prefix+shift+k"] + swap_pane_right = ["ctrl+shift+right", "prefix+shift+l"] + + close_pane = ["ctrl+w", "prefix+x"] + split_vertical = ["ctrl+backslash", "prefix+v"] + split_horizontal = ["ctrl+shift+backslash", "prefix+minus"] + zoom = ["ctrl+shift+z", "prefix+z"] + resize_mode = "prefix+r" + HERDRCONF + mv -fT -- "$config_tmp" "$HOME/.config/herdr/config.toml" || exit 1 + config_tmp="" + ); then + return 1 + fi + fi +} + +install_herdr() { + if command -v herdr &>/dev/null && [ -f "$HOME/.config/herdr/config.toml" ]; then + echo "Herdr already installed and configured, skipping." + return 0 + fi + run_with_spinner "Installing Herdr..." _install_herdr_inner +} + for mux in $(echo "$mux_list" | tr ',' ' '); do case "$mux" in tmux) install_tmux || echo "Warning: tmux installation failed." ;; zellij) install_zellij || echo "Warning: Zellij installation failed." ;; + herdr) install_herdr || echo "Warning: Herdr installation failed." ;; esac done fi # should_run multiplexers From 11658fc78caed76c4b24c685e0c5643b3182d9eb Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Fri, 14 Aug 2026 08:25:16 +1000 Subject: [PATCH 4/4] Validate generated Herdr configuration --- setup.sh | 7 ++++++- tests/test-provision-state.sh | 30 ++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/setup.sh b/setup.sh index 5897b37..2770b2b 100755 --- a/setup.sh +++ b/setup.sh @@ -1823,9 +1823,14 @@ _install_herdr_inner() { split_horizontal = ["ctrl+alt+backslash", "prefix+minus"] zoom = ["ctrl+alt+z", "prefix+z"] resize_mode = "prefix+r" - HERDRCONF + HERDRCONF mv -fT -- "$config_tmp" "$config_path" || exit 1 config_tmp="" + if ! herdr config check >/dev/null 2>&1; then + echo "ERROR: Generated Herdr config failed validation: $config_path" >&2 + rm -f -- "$config_path" + exit 1 + fi ); then return 1 fi diff --git a/tests/test-provision-state.sh b/tests/test-provision-state.sh index 0d3730e..d978e1d 100755 --- a/tests/test-provision-state.sh +++ b/tests/test-provision-state.sh @@ -258,16 +258,38 @@ rm -f "$ZELLIJ_CONF" # Herdr uses the shared immediate-app keyboard language for a fresh Managed # home, while an existing config remains user-controlled. -printf '#!/usr/bin/env bash\nexit 0\n' > "$BIN/herdr" +cat > "$BIN/herdr" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [ "${1:-}" = config ] && [ "${2:-}" = check ]; then + config="$HOME/.config/herdr/config.toml" + grep -Fqx 'detach = ["ctrl+alt+q", "prefix+q"]' "$config" + grep -Fqx 'new_workspace = ["ctrl+alt+n", "prefix+shift+n"]' "$config" + grep -Fqx 'split_horizontal = ["ctrl+alt+backslash", "prefix+minus"]' "$config" + grep -Fqx 'zoom = ["ctrl+alt+z", "prefix+z"]' "$config" + [ "${HERDR_CHECK_FAIL:-0}" -eq 0 ] + printf 'config check\n' >> "$HERDR_TEST_LOG" +fi +EOF chmod +x "$BIN/herdr" printf 'herdr\n' > "$STATE/multiplexer" rm -rf "$HOME_DIR/.config/herdr" -HOME="$HOME_DIR" SQUAREBOX_STATE_DIR="$STATE" \ +HERDR_TEST_LOG="$TMP/herdr-check.log" HOME="$HOME_DIR" SQUAREBOX_STATE_DIR="$STATE" \ SQUAREBOX_TOOL_LIB="$FIXTURE_LIB" SQUAREBOX_TOOLS_YAML=/dev/null \ PATH="$BIN:$PATH" bash "$ROOT/setup.sh" --rerun multiplexers >"$TMP/herdr-fresh.out" 2>"$TMP/herdr-fresh.err" HERDR_CONF="$HOME_DIR/.config/herdr/config.toml" -assert_true "grep -Fqx 'prefix = \"f12\"' '$HERDR_CONF' && grep -Fqx 'new_tab = [\"ctrl+t\", \"prefix+c\"]' '$HERDR_CONF' && grep -Fqx 'new_workspace = [\"ctrl+alt+n\", \"prefix+shift+n\"]' '$HERDR_CONF' && grep -Fqx 'focus_pane_left = [\"ctrl+left\", \"prefix+h\"]' '$HERDR_CONF'" \ - "fresh Herdr config uses terminal-safe direct chords with F12 fallbacks" +assert_true "grep -Fqx 'config check' '$TMP/herdr-check.log' && grep -Fqx 'prefix = \"f12\"' '$HERDR_CONF' && grep -Fqx 'new_tab = [\"ctrl+t\", \"prefix+c\"]' '$HERDR_CONF' && grep -Fqx 'focus_pane_left = [\"ctrl+left\", \"prefix+h\"]' '$HERDR_CONF'" \ + "fresh Herdr config passes validation with terminal-safe direct chords and F12 fallbacks" +rm -f "$HERDR_CONF" +if HERDR_CHECK_FAIL=1 HERDR_TEST_LOG="$TMP/herdr-check-fail.log" HOME="$HOME_DIR" SQUAREBOX_STATE_DIR="$STATE" \ + SQUAREBOX_TOOL_LIB="$FIXTURE_LIB" SQUAREBOX_TOOLS_YAML=/dev/null \ + PATH="$BIN:$PATH" bash "$ROOT/setup.sh" --rerun multiplexers >"$TMP/herdr-invalid.out" 2>"$TMP/herdr-invalid.err"; then + HERDR_INVALID_RC=0 +else + HERDR_INVALID_RC=$? +fi +assert_true "[ \"$HERDR_INVALID_RC\" -ne 0 ] && [ ! -e '$HERDR_CONF' ] && grep -Fq 'Generated Herdr config failed validation' '$TMP/herdr-invalid.err'" \ + "Herdr setup removes a generated config that fails validation" printf '# user-owned Herdr config\n' > "$HERDR_CONF" HOME="$HOME_DIR" SQUAREBOX_STATE_DIR="$STATE" \ SQUAREBOX_TOOL_LIB="$FIXTURE_LIB" SQUAREBOX_TOOLS_YAML=/dev/null \