Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bin/omarchy-refresh-herdr
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions bin/omarchy-restart-herdr
Original file line number Diff line number Diff line change
@@ -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
82 changes: 82 additions & 0 deletions config/herdr/config.toml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions default/bash/aliases
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ alias cy='codex -s danger-full-access -a never'
alias d='docker'
alias r='rails'
alias t='tmux attach || tmux new -s Work'
alias h='herdr'
alias ic='tdl c'
alias ix='tdl cx'
alias icx='tdl c cx'
Expand Down
151 changes: 151 additions & 0 deletions default/bash/fns/herdr
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions install/omarchy-base.packages
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ gum
gvfs-mtp
gvfs-nfs
gvfs-smb
herdr
hyprland
hyprland-guiutils
hyprland-preview-share-picker
Expand Down
19 changes: 19 additions & 0 deletions migrations/1786273938.sh
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions test/shell.d/herdr-functions-test.sh
Original file line number Diff line number Diff line change
@@ -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"