Ship herdr alongside tmux (#6406)
* Ship herdr with a config that mirrors our tmux setup Installs herdr through the mise shim, ships the matching config as an Omarchy default, and adds the usual refresh/restart pair. The keybindings map tmux sessions to workspaces, windows to tabs, and keep both the prefix and direct bindings from config/tmux/tmux.conf. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Add herdr versions of the tmux dev layout functions hdl, hds, hdlm, and hsl drive herdr through its socket API instead of tmux. hsl tiles into a real grid since herdr has no select-layout tiled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Namespace the herdr layout helpers so they stay out of the shell Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Create hdlm's tabs in its own workspace instead of the focused one herdr tab create follows the focused workspace without --workspace, so switching workspaces while hdlm loops scatters the new tabs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Lay hsl's grid out in visual order Splitting the first column repeatedly inserted each new column between it and the previous one, so uneven counts put the spare row in a middle column instead of the last. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Report herdr config reload failures instead of swallowing them Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Hide herdr's pane scrollbars to match tmux Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Escape queued herdr layout commands * Install herdr from the omarchy-herdr package instead of mise * Use native herdr resize keybindings for tmux-style pane resizing * Rename the omarchy-herdr package to herdr --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
19572a13c7
commit
dd61d4a75b
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Overwrite the user herdr config with the Omarchy default and reload herdr.
|
||||||
|
|
||||||
|
omarchy-refresh-config herdr/config.toml
|
||||||
|
omarchy-restart-herdr
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Reload herdr if running with the latest configuration
|
||||||
|
|
||||||
|
[[ $(herdr status server --json 2>/dev/null | jq -r '.running') == "true" ]] || exit 0
|
||||||
|
|
||||||
|
reload=$(herdr server reload-config)
|
||||||
|
|
||||||
|
jq -r '.result.diagnostics[]?, (.error.message | select(. != null))' <<<"$reload" >&2
|
||||||
|
|
||||||
|
if [[ $(jq -r '.result.status // "failed"' <<<"$reload") == "failed" ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Mirrors the Omarchy tmux config in config/tmux/tmux.conf
|
||||||
|
# tmux session -> herdr workspace, tmux window -> herdr tab, tmux pane -> herdr pane
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
# tmux ran on the terminal's own palette (bg=default, fg=default, ANSI blue accents)
|
||||||
|
name = "terminal"
|
||||||
|
|
||||||
|
[theme.custom]
|
||||||
|
# The active tab is drawn as panel_bg text on an accent background, so panel_bg
|
||||||
|
# has to be dark for it to read - same colors as status-left's "#[fg=black,bg=blue]"
|
||||||
|
panel_bg = "black"
|
||||||
|
|
||||||
|
[terminal]
|
||||||
|
# Matches -c "#{pane_current_path}" on every split, window, and session
|
||||||
|
new_cwd = "follow"
|
||||||
|
|
||||||
|
[keys]
|
||||||
|
prefix = "ctrl+space"
|
||||||
|
|
||||||
|
# Config and help
|
||||||
|
reload_config = "prefix+q"
|
||||||
|
help = "prefix+?"
|
||||||
|
detach = "prefix+d"
|
||||||
|
|
||||||
|
# Copy mode
|
||||||
|
copy_mode = "prefix+["
|
||||||
|
|
||||||
|
# Panes
|
||||||
|
split_horizontal = ["prefix+h", "alt+enter"]
|
||||||
|
split_vertical = ["prefix+v", "alt+shift+enter"]
|
||||||
|
close_pane = ["prefix+x", "alt+esc"]
|
||||||
|
zoom = "prefix+z"
|
||||||
|
last_pane = "prefix+;"
|
||||||
|
|
||||||
|
focus_pane_left = "ctrl+alt+left"
|
||||||
|
focus_pane_down = "ctrl+alt+down"
|
||||||
|
focus_pane_up = "ctrl+alt+up"
|
||||||
|
focus_pane_right = "ctrl+alt+right"
|
||||||
|
|
||||||
|
resize_mode = ["prefix+ctrl+left", "prefix+ctrl+down", "prefix+ctrl+up", "prefix+ctrl+right"]
|
||||||
|
|
||||||
|
# Like resize-pane on C-M-S-arrows
|
||||||
|
resize_pane_left = "ctrl+alt+shift+left"
|
||||||
|
resize_pane_down = "ctrl+alt+shift+down"
|
||||||
|
resize_pane_up = "ctrl+alt+shift+up"
|
||||||
|
resize_pane_right = "ctrl+alt+shift+right"
|
||||||
|
|
||||||
|
# No tmux equivalent; herdr's default prefix+shift+p is taken by previous session
|
||||||
|
rename_pane = "prefix+shift+o"
|
||||||
|
|
||||||
|
# Windows -> tabs
|
||||||
|
new_tab = "prefix+c"
|
||||||
|
rename_tab = "prefix+r"
|
||||||
|
close_tab = "prefix+k"
|
||||||
|
switch_tab = ["prefix+1..9", "alt+1..9"]
|
||||||
|
previous_tab = ["prefix+p", "alt+left"]
|
||||||
|
next_tab = ["prefix+n", "alt+right"]
|
||||||
|
|
||||||
|
# Sessions -> workspaces
|
||||||
|
new_workspace = "prefix+shift+c"
|
||||||
|
rename_workspace = "prefix+shift+r"
|
||||||
|
close_workspace = "prefix+shift+k"
|
||||||
|
previous_workspace = ["prefix+shift+p", "alt+up"]
|
||||||
|
next_workspace = ["prefix+shift+n", "alt+down"]
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
accent = "blue"
|
||||||
|
|
||||||
|
# tmux drew single-line dividers between adjacent panes
|
||||||
|
pane_gaps = false
|
||||||
|
|
||||||
|
# tmux had no scrollbar column beside its panes
|
||||||
|
pane_scrollbars = false
|
||||||
|
|
||||||
|
# kill-window and kill-session never asked
|
||||||
|
confirm_close = false
|
||||||
|
|
||||||
|
# automatic-rename gave windows a name without prompting
|
||||||
|
prompt_new_tab_name = false
|
||||||
|
|
||||||
|
# set -g mouse on
|
||||||
|
mouse_capture = true
|
||||||
@@ -50,6 +50,7 @@ alias cy='codex -s danger-full-access -a never'
|
|||||||
alias d='docker'
|
alias d='docker'
|
||||||
alias r='rails'
|
alias r='rails'
|
||||||
alias t='tmux attach || tmux new -s Work'
|
alias t='tmux attach || tmux new -s Work'
|
||||||
|
alias h='herdr'
|
||||||
alias ic='tdl c'
|
alias ic='tdl c'
|
||||||
alias ix='tdl cx'
|
alias ix='tdl cx'
|
||||||
alias icx='tdl c cx'
|
alias icx='tdl c cx'
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Echo a split ratio as a float
|
||||||
|
# Usage: _herdr_ratio <numerator> <denominator>
|
||||||
|
_herdr_ratio() {
|
||||||
|
awk -v a="$1" -v b="$2" 'BEGIN { printf "%.4f", a / b }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Split a herdr pane and echo the id of the new pane
|
||||||
|
# Usage: _herdr_split <pane_id> <right|down> <ratio> <cwd>
|
||||||
|
_herdr_split() {
|
||||||
|
herdr pane split "$1" --direction "$2" --ratio "$3" --cwd "$4" --no-focus |
|
||||||
|
jq -r '.result.pane.pane_id'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a Herdr Dev Layout with editor, ai, and terminal
|
||||||
|
# Usage: hdl <c|cx|codex|other_ai> [<second_ai>]
|
||||||
|
hdl() {
|
||||||
|
[[ -z $1 ]] && { echo "Usage: hdl <c|cx|codex|other_ai> [<second_ai>]"; return 1; }
|
||||||
|
[[ -z $HERDR_PANE_ID ]] && { echo "You must start herdr to use hdl."; return 1; }
|
||||||
|
|
||||||
|
local current_dir="${PWD}"
|
||||||
|
local editor_pane ai_pane ai2_pane
|
||||||
|
local ai="$1"
|
||||||
|
local ai2="${2:-}"
|
||||||
|
|
||||||
|
# Use HERDR_PANE_ID for the pane we're running in (stable even if focus moves)
|
||||||
|
editor_pane="$HERDR_PANE_ID"
|
||||||
|
|
||||||
|
# Name the current tab after the base directory name
|
||||||
|
herdr tab rename "$HERDR_TAB_ID" "$(basename "$current_dir")" >/dev/null
|
||||||
|
|
||||||
|
# Split tab vertically - top 85%, bottom 15%
|
||||||
|
_herdr_split "$editor_pane" down 0.85 "$current_dir" >/dev/null
|
||||||
|
|
||||||
|
# Split editor pane horizontally - AI on right 30%
|
||||||
|
ai_pane=$(_herdr_split "$editor_pane" right 0.7 "$current_dir")
|
||||||
|
|
||||||
|
# If second AI provided, split the AI pane vertically
|
||||||
|
if [[ -n $ai2 ]]; then
|
||||||
|
ai2_pane=$(_herdr_split "$ai_pane" down 0.5 "$current_dir")
|
||||||
|
herdr pane run "$ai2_pane" "$ai2" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run ai in the right pane
|
||||||
|
herdr pane run "$ai_pane" "$ai" >/dev/null
|
||||||
|
|
||||||
|
# Run nvim in the left pane
|
||||||
|
herdr pane run "$editor_pane" "$EDITOR ." >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a Herdr Dev Square layout with editor, diff watch, terminal, and opencode
|
||||||
|
# Usage: hds
|
||||||
|
hds() {
|
||||||
|
[[ -n $1 ]] && { echo "Usage: hds"; return 1; }
|
||||||
|
[[ -z $HERDR_PANE_ID ]] && { echo "You must start herdr to use hds."; return 1; }
|
||||||
|
|
||||||
|
local current_dir="${PWD}"
|
||||||
|
local editor_pane diff_pane terminal_pane opencode_pane
|
||||||
|
|
||||||
|
editor_pane="$HERDR_PANE_ID"
|
||||||
|
|
||||||
|
herdr tab rename "$HERDR_TAB_ID" "$(basename "$current_dir")" >/dev/null
|
||||||
|
|
||||||
|
terminal_pane=$(_herdr_split "$editor_pane" down 0.5 "$current_dir")
|
||||||
|
diff_pane=$(_herdr_split "$editor_pane" right 0.5 "$current_dir")
|
||||||
|
opencode_pane=$(_herdr_split "$terminal_pane" right 0.5 "$current_dir")
|
||||||
|
|
||||||
|
herdr pane run "$editor_pane" "nvim ." >/dev/null
|
||||||
|
herdr pane run "$diff_pane" "hunk diff --watch" >/dev/null
|
||||||
|
herdr pane run "$opencode_pane" "opencode" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create multiple hdl tabs with one per subdirectory in the current directory
|
||||||
|
# Usage: hdlm <c|cx|codex|other_ai> [<second_ai>]
|
||||||
|
hdlm() {
|
||||||
|
[[ -z $1 ]] && { echo "Usage: hdlm <c|cx|codex|other_ai> [<second_ai>]"; return 1; }
|
||||||
|
[[ -z $HERDR_PANE_ID ]] && { echo "You must start herdr to use hdlm."; return 1; }
|
||||||
|
|
||||||
|
local ai="$1"
|
||||||
|
local ai2="${2:-}"
|
||||||
|
local base_dir="$PWD"
|
||||||
|
local first=true
|
||||||
|
local hdl_command
|
||||||
|
|
||||||
|
# Rename the workspace to the current directory name
|
||||||
|
herdr workspace rename "$HERDR_WORKSPACE_ID" "$(basename "$base_dir")" >/dev/null
|
||||||
|
|
||||||
|
for dir in "$base_dir"/*/; do
|
||||||
|
[[ -d $dir ]] || continue
|
||||||
|
local dirpath="${dir%/}"
|
||||||
|
|
||||||
|
printf -v hdl_command 'hdl %q' "$ai"
|
||||||
|
[[ -n $ai2 ]] && printf -v hdl_command '%s %q' "$hdl_command" "$ai2"
|
||||||
|
|
||||||
|
if $first; then
|
||||||
|
# Reuse the current tab for the first project
|
||||||
|
printf -v hdl_command 'cd %q && %s' "$dirpath" "$hdl_command"
|
||||||
|
herdr pane run "$HERDR_PANE_ID" "$hdl_command" >/dev/null
|
||||||
|
first=false
|
||||||
|
else
|
||||||
|
local pane_id
|
||||||
|
pane_id=$(herdr tab create --workspace "$HERDR_WORKSPACE_ID" --cwd "$dirpath" --no-focus |
|
||||||
|
jq -r '.result.root_pane.pane_id')
|
||||||
|
herdr pane run "$pane_id" "$hdl_command" >/dev/null
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a multi-pane swarm layout with the same command started in each pane (great for AI)
|
||||||
|
# Usage: hsl <pane_count> <command>
|
||||||
|
hsl() {
|
||||||
|
[[ -z $1 || -z $2 ]] && { echo "Usage: hsl <pane_count> <command>"; return 1; }
|
||||||
|
[[ -z $HERDR_PANE_ID ]] && { echo "You must start herdr to use hsl."; return 1; }
|
||||||
|
|
||||||
|
local count="$1"
|
||||||
|
local cmd="$2"
|
||||||
|
local current_dir="${PWD}"
|
||||||
|
local -a columns panes
|
||||||
|
|
||||||
|
herdr tab rename "$HERDR_TAB_ID" "$(basename "$current_dir")" >/dev/null
|
||||||
|
|
||||||
|
# Tile into a grid: ceil(sqrt(count)) columns, rows spread across them
|
||||||
|
local cols=1
|
||||||
|
while (( cols * cols < count )); do ((cols++)); done
|
||||||
|
|
||||||
|
# Even columns come from splitting the rightmost one off at 1/(n-k+1) each time,
|
||||||
|
# which keeps the array in left-to-right order
|
||||||
|
columns=("$HERDR_PANE_ID")
|
||||||
|
local k
|
||||||
|
for (( k = 1; k < cols; k++ )); do
|
||||||
|
columns+=("$(_herdr_split "${columns[-1]}" right "$(_herdr_ratio 1 $((cols - k + 1)))" "$current_dir")")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Split each column into its share of rows, again evenly and top-to-bottom
|
||||||
|
local col index rows j last
|
||||||
|
for (( index = 0; index < cols; index++ )); do
|
||||||
|
col="${columns[index]}"
|
||||||
|
rows=$(( count / cols ))
|
||||||
|
(( index < count % cols )) && (( rows++ ))
|
||||||
|
panes+=("$col")
|
||||||
|
last="$col"
|
||||||
|
for (( j = 1; j < rows; j++ )); do
|
||||||
|
last=$(_herdr_split "$last" down "$(_herdr_ratio 1 $((rows - j + 1)))" "$current_dir")
|
||||||
|
panes+=("$last")
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
local pane
|
||||||
|
for pane in "${panes[@]}"; do
|
||||||
|
herdr pane run "$pane" "$cmd" >/dev/null
|
||||||
|
done
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ gum
|
|||||||
gvfs-mtp
|
gvfs-mtp
|
||||||
gvfs-nfs
|
gvfs-nfs
|
||||||
gvfs-smb
|
gvfs-smb
|
||||||
|
herdr
|
||||||
hyprland
|
hyprland
|
||||||
hyprland-guiutils
|
hyprland-guiutils
|
||||||
hyprland-preview-share-picker
|
hyprland-preview-share-picker
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
echo "Install herdr from the Omarchy package repo and seed its config"
|
||||||
|
|
||||||
|
# The package was briefly published as omarchy-herdr; herdr replaces it
|
||||||
|
omarchy-pkg-drop omarchy-herdr
|
||||||
|
omarchy-pkg-add herdr
|
||||||
|
|
||||||
|
# An earlier revision of this migration installed herdr through mise. Drop that
|
||||||
|
# install so a stale client can't shadow the packaged /usr/bin/herdr with an
|
||||||
|
# older wire protocol.
|
||||||
|
rm -f "$HOME/.local/bin/herdr"
|
||||||
|
if mise ls herdr 2>/dev/null | grep -q herdr; then
|
||||||
|
mise unuse -g herdr &>/dev/null || true
|
||||||
|
mise uninstall -a herdr &>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -rf "$HOME/.local/share/mise/installs/herdr" "$HOME/.local/share/mise/shims/herdr"
|
||||||
|
|
||||||
|
# Only seed. A user who already has a herdr config keeps it; omarchy-refresh-herdr
|
||||||
|
# is the explicit way to take the shipped defaults.
|
||||||
|
[[ -f "$HOME/.config/herdr/config.toml" ]] || omarchy-refresh-config herdr/config.toml
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(dirname "$0")/base-test.sh"
|
||||||
|
source "$ROOT/default/bash/fns/herdr"
|
||||||
|
|
||||||
|
test_dir=$(mktemp -d)
|
||||||
|
trap 'rm -rf -- "$test_dir"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "$test_dir/first project's files" "$test_dir/second project"
|
||||||
|
log="$test_dir/herdr.log"
|
||||||
|
|
||||||
|
herdr() {
|
||||||
|
if [[ $1 == "workspace" ]]; then
|
||||||
|
return
|
||||||
|
elif [[ $1 == "tab" ]]; then
|
||||||
|
printf '{"result":{"root_pane":{"pane_id":"new-pane"}}}\n'
|
||||||
|
elif [[ $1 == "pane" && $2 == "run" ]]; then
|
||||||
|
printf '%s\n' "$4" >>"$log"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
export HERDR_PANE_ID="current-pane"
|
||||||
|
export HERDR_WORKSPACE_ID="workspace"
|
||||||
|
|
||||||
|
cd "$test_dir"
|
||||||
|
hdlm "codex --flag='value with spaces'" "claude --model sonnet"
|
||||||
|
|
||||||
|
mapfile -t commands <"$log"
|
||||||
|
|
||||||
|
expected_first=$(printf 'cd %q && hdl %q %q' \
|
||||||
|
"$test_dir/first project's files" "codex --flag='value with spaces'" "claude --model sonnet")
|
||||||
|
expected_second=$(printf 'hdl %q %q' "codex --flag='value with spaces'" "claude --model sonnet")
|
||||||
|
|
||||||
|
[[ ${commands[0]} == "$expected_first" ]] || fail "hdlm safely quotes its first queued command"
|
||||||
|
pass "hdlm safely quotes its first queued command"
|
||||||
|
|
||||||
|
[[ ${commands[1]} == "$expected_second" ]] || fail "hdlm preserves agent argument boundaries"
|
||||||
|
pass "hdlm preserves agent argument boundaries"
|
||||||
|
|
||||||
|
: >"$log"
|
||||||
|
hdlm "codex --quiet"
|
||||||
|
mapfile -t commands <"$log"
|
||||||
|
expected_first=$(printf 'cd %q && hdl %q' "$test_dir/first project's files" "codex --quiet")
|
||||||
|
|
||||||
|
[[ ${commands[0]} == "$expected_first" ]] || fail "hdlm omits an absent second agent argument"
|
||||||
|
pass "hdlm omits an absent second agent argument"
|
||||||
Reference in New Issue
Block a user