|
#!/usr/bin/env zsh |
|
set -euo pipefail |
|
|
|
usage() { |
|
cat <<'EOF' |
|
durable: tmux-backed durable sessions |
|
|
|
Usage: |
|
durable Enter a durable shell for the current context |
|
durable <command...> Enter a durable session for that command |
|
durable <session-name> Rejoin an existing durable tmux session |
|
durable --name Print computed session name |
|
durable --list List tmux sessions |
|
durable --kill Kill the durable session for the current context |
|
durable --kill <role> Kill the durable session for a role in current context |
|
|
|
Examples: |
|
durable |
|
durable opencode |
|
durable opencode -c |
|
durable --kill opencode |
|
|
|
Detach without killing the session: |
|
Ctrl-b then d |
|
|
|
Exit/finish the session: |
|
exit |
|
EOF |
|
} |
|
|
|
die() { |
|
print -u2 -- "durable: $*" |
|
exit 1 |
|
} |
|
|
|
need() { |
|
command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1" |
|
} |
|
|
|
sanitize() { |
|
local value="$1" |
|
value="${value//[^A-Za-z0-9_.-]/_}" |
|
value="${value##[_-.]}" |
|
value="${value%%[_-.]}" |
|
print -r -- "${value:-x}" |
|
} |
|
|
|
display_session_name() { |
|
local durable_role="$1" |
|
local dir branch folder |
|
|
|
dir="$(context_dir)" |
|
branch="$(context_branch "$dir")" |
|
folder="$(basename "$dir")" |
|
|
|
if [[ "$branch" == "nogit" ]]; then |
|
print -r -- "$(sanitize "$folder") · $(sanitize "$durable_role")" |
|
else |
|
print -r -- "$(sanitize "$folder") · $(sanitize "$branch") · $(sanitize "$durable_role")" |
|
fi |
|
} |
|
|
|
context_dir() { |
|
git rev-parse --show-toplevel 2>/dev/null || pwd -P |
|
} |
|
|
|
context_branch() { |
|
local dir="$1" |
|
local branch |
|
|
|
branch="$(git -C "$dir" branch --show-current 2>/dev/null || true)" |
|
if [[ -n "$branch" ]]; then |
|
print -r -- "$branch" |
|
return |
|
fi |
|
|
|
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
|
branch="$(git -C "$dir" rev-parse --short HEAD 2>/dev/null || true)" |
|
print -r -- "${branch:-detached}" |
|
else |
|
print -r -- "nogit" |
|
fi |
|
} |
|
|
|
session_prefix() { |
|
local durable_role="$1" |
|
local dir branch folder |
|
|
|
dir="$(context_dir)" |
|
branch="$(context_branch "$dir")" |
|
folder="$(basename "$dir")" |
|
|
|
print -r -- "durable_$(sanitize "$folder")_$(sanitize "$branch")_$(sanitize "$durable_role")" |
|
} |
|
|
|
timestamp_suffix() { |
|
date '+%Y%m%d-%H%M%S' |
|
} |
|
|
|
session_name() { |
|
local durable_role="$1" |
|
print -r -- "$(session_prefix "$durable_role")_$(timestamp_suffix)" |
|
} |
|
|
|
durable_sessions() { |
|
tmux list-sessions -F '#{session_created} #{session_name}' 2>/dev/null | awk '$2 ~ /^durable_/ { print $0 }' |
|
} |
|
|
|
matching_sessions_for_prefix() { |
|
local durable_prefix="$1" |
|
durable_sessions | awk -v prefix="$durable_prefix" 'index($2, prefix "_") == 1 { print $0 }' | sort -rn | awk '{ print $2 }' |
|
} |
|
|
|
existing_session_for() { |
|
local durable_role="$1" |
|
local dir="$2" |
|
local durable_prefix candidate fallback context |
|
|
|
durable_prefix="$(session_prefix "$durable_role")" |
|
fallback="" |
|
|
|
while IFS= read -r candidate; do |
|
[[ -n "$candidate" ]] || continue |
|
if [[ -z "$fallback" ]]; then |
|
fallback="$candidate" |
|
fi |
|
|
|
context="$(tmux show-option -qv -t "$candidate" @durable_context_dir 2>/dev/null || true)" |
|
if [[ "$context" == "$dir" ]]; then |
|
print -r -- "$candidate" |
|
return 0 |
|
fi |
|
done < <(matching_sessions_for_prefix "$durable_prefix") |
|
|
|
if [[ -n "$fallback" ]]; then |
|
print -r -- "$fallback" |
|
fi |
|
} |
|
|
|
role_for_args() { |
|
if [[ "$#" -eq 0 ]]; then |
|
print -r -- "shell" |
|
else |
|
print -r -- "$1" |
|
fi |
|
} |
|
|
|
startup_command_for_args() { |
|
if [[ "$#" -eq 0 ]]; then |
|
return 0 |
|
else |
|
local quoted |
|
quoted="${(j: :)${(q)@}}" |
|
print -r -- "$quoted" |
|
fi |
|
} |
|
|
|
attach_or_switch() { |
|
local durable_session_name="$1" |
|
|
|
if [[ -n "${TMUX:-}" ]]; then |
|
tmux switch-client -t "$durable_session_name" |
|
else |
|
tmux attach-session -t "$durable_session_name" |
|
fi |
|
} |
|
|
|
main() { |
|
need tmux |
|
|
|
case "${1:-}" in |
|
-h|--help) |
|
usage |
|
return 0 |
|
;; |
|
--list|list|ls) |
|
tmux list-sessions |
|
return 0 |
|
;; |
|
--name|name) |
|
shift |
|
local name_role name_existing |
|
name_role="$(role_for_args "$@")" |
|
name_existing="$(existing_session_for "$name_role" "$(context_dir)")" |
|
print -r -- "${name_existing:-$(session_name "$name_role")}" |
|
return 0 |
|
;; |
|
--kill|kill) |
|
shift |
|
local kill_role kill_session |
|
if [[ "${1:-}" == durable_* ]]; then |
|
kill_session="$1" |
|
else |
|
kill_role="$(role_for_args "$@")" |
|
kill_session="$(existing_session_for "$kill_role" "$(context_dir)")" |
|
fi |
|
[[ -n "$kill_session" ]] || die "no matching durable session" |
|
tmux kill-session -t "$kill_session" |
|
return 0 |
|
;; |
|
esac |
|
|
|
local dir durable_role durable_session_name startup_command |
|
dir="$(context_dir)" |
|
|
|
if [[ "${1:-}" == durable_* ]] && tmux has-session -t "$1" 2>/dev/null; then |
|
attach_or_switch "$1" |
|
return 0 |
|
fi |
|
if [[ "${1:-}" == durable_* ]]; then |
|
die "no such durable session: $1" |
|
fi |
|
|
|
durable_role="$(role_for_args "$@")" |
|
durable_session_name="$(existing_session_for "$durable_role" "$dir")" |
|
if [[ -z "$durable_session_name" ]]; then |
|
durable_session_name="$(session_name "$durable_role")" |
|
fi |
|
startup_command="$(startup_command_for_args "$@")" |
|
|
|
if ! tmux has-session -t "$durable_session_name" 2>/dev/null; then |
|
local shell_path tmux_command display_name window_name startup_script |
|
shell_path="${SHELL:-/bin/zsh}" |
|
if [[ -n "$startup_command" ]]; then |
|
startup_script="${startup_command}"$'\n''exec "${SHELL:-/bin/zsh}" -l' |
|
tmux_command="${(q)shell_path} -lc ${(q)startup_script}" |
|
else |
|
tmux_command="${(q)shell_path} -l" |
|
fi |
|
display_name="$(display_session_name "$durable_role")" |
|
window_name="$(sanitize "$durable_role")" |
|
tmux new-session -d -s "$durable_session_name" -n "$window_name" -c "$dir" "$tmux_command" |
|
tmux set-option -t "$durable_session_name" @durable_display_name "$display_name" |
|
tmux set-option -t "$durable_session_name" @durable_context_dir "$dir" |
|
fi |
|
|
|
attach_or_switch "$durable_session_name" |
|
} |
|
|
|
main "$@" |