From b632368e31d64174377c646a2cf80828c9fef296 Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Tue, 4 Aug 2026 19:10:40 +1000 Subject: [PATCH 1/2] align Herdr keyboard shortcuts --- README.md | 27 ++++++++++++++-- setup.sh | 60 +++++++++++++++++++++++++++++++++-- tests/test-provision-state.sh | 19 +++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ddbb7fd..89c8035 100644 --- a/README.md +++ b/README.md @@ -386,9 +386,30 @@ Aliases | `claude-yolo` | `claude --dangerously-skip-permissions` | Claude without prompts | | `opencode-yolo` | `opencode --dangerously-skip-permissions` | OpenCode without prompts | -### Multiplexer Keybindings (Experimental) - -Both tmux and zellij ship with Omarchy-inspired defaults and matching keybindings: +### Shared Keyboard Language (Experimental) + +Squarebox builds on Omarchy's defaults with one portable rule: `Super` belongs +to the OS/window manager, while `Ctrl` acts immediately in the focused app. +Adding `Shift` modifies or moves the target. Herdr follows this language +directly; its `F12` prefix remains available as a compatibility fallback. + +Herdr's generated default uses familiar app shortcuts: + +| 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` | + +These bindings intentionally take precedence over matching shell or nested-app +shortcuts while Herdr is focused. Existing `~/.config/herdr/config.toml` files +remain user-managed and are never replaced by setup. + +Tmux and Zellij retain their earlier Omarchy-inspired matching defaults: | Feature | Tmux | Zellij | |---------|------|--------| diff --git a/setup.sh b/setup.sh index 6dabe22..e73642a 100755 --- a/setup.sh +++ b/setup.sh @@ -1762,11 +1762,67 @@ install_zellij() { _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; then - echo "Herdr already installed, skipping." + 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 diff --git a/tests/test-provision-state.sh b/tests/test-provision-state.sh index 32a53a6..aab8704 100755 --- a/tests/test-provision-state.sh +++ b/tests/test-provision-state.sh @@ -249,6 +249,25 @@ assert_true "[ -L '$ZELLIJ_CONF' ] && grep -Fqx 'on_force_close \"quit\"' '$ZELL "Zellij migration does not rewrite a symlinked config" 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" +chmod +x "$BIN/herdr" +printf 'herdr\n' > "$STATE/multiplexer" +rm -rf "$HOME_DIR/.config/herdr" +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 'focus_pane_left = [\"ctrl+left\", \"prefix+h\"]' '$HERDR_CONF'" \ + "fresh Herdr config uses immediate Ctrl actions with F12 fallbacks" +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 \ + PATH="$BIN:$PATH" bash "$ROOT/setup.sh" --rerun multiplexers >"$TMP/herdr-user.out" 2>"$TMP/herdr-user.err" +assert_true "grep -Fqx '# user-owned Herdr config' '$HERDR_CONF'" \ + "Herdr setup preserves an existing user config" + # A gum cancellation and a confirmed empty multi-select are different state # transitions: cancel preserves prior files; empty intentionally clears them. if command -v script >/dev/null 2>&1; then From 2c165a23904be779dad6eb3bca4c5bb5fec047d1 Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Tue, 4 Aug 2026 19:45:58 +1000 Subject: [PATCH 2/2] Reject invalid Herdr config paths --- setup.sh | 9 +++++++-- tests/test-provision-state.sh | 12 ++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/setup.sh b/setup.sh index e73642a..29e1900 100755 --- a/setup.sh +++ b/setup.sh @@ -1760,10 +1760,15 @@ install_zellij() { } _install_herdr_inner() { + local config_path="$HOME/.config/herdr/config.toml" 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 [ -e "$config_path" ] && [ ! -L "$config_path" ] && [ ! -f "$config_path" ]; then + echo "ERROR: Herdr config path is not a regular file: $config_path" >&2 + return 1 + fi + if [ ! -e "$config_path" ] && [ ! -L "$config_path" ]; then if ! ( config_tmp="" cleanup_herdr_stage() { @@ -1812,7 +1817,7 @@ _install_herdr_inner() { zoom = ["ctrl+shift+z", "prefix+z"] resize_mode = "prefix+r" HERDRCONF - mv -fT -- "$config_tmp" "$HOME/.config/herdr/config.toml" || exit 1 + mv -fT -- "$config_tmp" "$config_path" || exit 1 config_tmp="" ); then return 1 diff --git a/tests/test-provision-state.sh b/tests/test-provision-state.sh index aab8704..13647e4 100755 --- a/tests/test-provision-state.sh +++ b/tests/test-provision-state.sh @@ -268,6 +268,18 @@ HOME="$HOME_DIR" SQUAREBOX_STATE_DIR="$STATE" \ assert_true "grep -Fqx '# user-owned Herdr config' '$HERDR_CONF'" \ "Herdr setup preserves an existing user config" +rm -rf "$HERDR_CONF" +mkdir -p "$HERDR_CONF" +if 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-directory.out" 2>"$TMP/herdr-directory.err"; then + HERDR_DIRECTORY_RC=0 +else + HERDR_DIRECTORY_RC=$? +fi +assert_true "[ \"$HERDR_DIRECTORY_RC\" -ne 0 ] && [ -d '$HERDR_CONF' ] && grep -Fq 'Herdr config path is not a regular file' '$TMP/herdr-directory.err'" \ + "Herdr rejects a directory at the config path" + # A gum cancellation and a confirmed empty multi-select are different state # transitions: cancel preserves prior files; empty intentionally clears them. if command -v script >/dev/null 2>&1; then