| | #!/bin/zsh |
| | # ghostty-agent-restore: remember what every Ghostty pane was doing, and put it back. |
| | # |
| | # snapshot record every pane: tty, terminal id, tab position, cwd, how to resume it (launchd: every 2 min) |
| | # list show the last snapshot |
| | # restart snapshot, quit Ghostty and relaunch it; every pane resumes its session |
| | # arm restore the last snapshot again: panes whose shell starts in the next 15 min take their entry |
| | # claim (run by the .zshrc hook) learn this pane's terminal id; print what to run in it |
| | # hook print the zsh code the .zshrc line evals |
| | # install launchd timer + .zshrc hook |
| | # |
| | # Restore starts by itself whenever the running Ghostty is not the one the last snapshot was |
| | # taken under: after a reboot, a quit, a crash or an update. When Ghostty restores its |
| | # windows (window-save-state = always), each terminal keeps its id and gets back the session |
| | # saved under it. When it doesn't, each new tab you open takes the next session, in the |
| | # original tab order. omp, Claude Code and Codex sessions are resumed by session id and run; |
| | # any other command is typed onto the prompt for you to run. |
| | |
| | set -u |
| | emulate -L zsh |
| | setopt extended_glob |
| | |
| | DIR="${GHOSTTY_AGENT_RESTORE_DIR:-$HOME/.config/ghostty/agent-restore}" |
| | CURRENT="$DIR/current.tsv" # tty, terminal id, position, cwd, mode (run | type | empty for a shell), command |
| | RESTORE="$DIR/restore.tsv" # entries still waiting for their pane |
| | INSTANCE="$DIR/instance" # "<pid> <start time>" of the Ghostty that current.tsv describes |
| | LAYOUT="$DIR/layout.tsv" # terminal id → "window.tab.split" position, as of the last pane to open |
| | PANES="$DIR/panes" # <tty> → "<terminal id><TAB><pane shell pid>", written by each pane's claim |
| | LOCK="$DIR/.claim.lock" |
| | FRESH_SECONDS=900 |
| | GHOSTTY_BIN='/Applications/Ghostty.app/Contents/MacOS/ghostty' |
| | LABEL="com.$(whoami).ghostty-agent-restore" |
| | OMP_DIR="${PI_CODING_AGENT_DIR:-$HOME/.omp/agent}" |
| | CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" |
| | CODEX_DIR="${CODEX_HOME:-$HOME/.codex}" |
| | | | | # Per agent: options that take a value, options a resume must not repeat, and subcommands | | | # that are not an interactive session. From --help of omp 18.10, Claude Code 2.1.283 and | | | # Codex 0.153. | | | OMP_TAKES='--model --smol --slow --plan --goal --prewalk-into --plan-yolo-into --provider --api-key --system-prompt --system-prompt-template --append-system-prompt --profile --alias --cwd --mode --config --add-dir -r --resume --session-dir --models --tools --thinking --service-tier --hook -e --extension --skills --export --max-time --approval-mode --plugin-dir' | | | OMP_DROP='-r --resume -c --continue --goal --plan-yolo --plan-yolo-into' | | | CLAUDE_TAKES='--add-dir --agent --agents --allowedTools --allowed-tools --append-system-prompt --append-system-prompt-file --autocompact --betas --client-data-url --debug-file --disallowedTools --disallowed-tools --effort --environment --fallback-model --file --input-format --json-schema --max-budget-usd --mcp-config --model -n --name --output-format --permission-mode --permission-prompts --plugin-dir --plugin-url --remote-control-session-name-prefix --setting-sources --settings --system-prompt --system-prompt-file --system-prompt-snapshot --tools -r --resume --session-id --from-pr --teleport' | | | CLAUDE_DROP='-r --resume -c --continue --session-id --fork-session --from-pr --teleport' | | | CLAUDE_COMMANDS='agents attach auth auto-mode doctor gateway import install logs mcp plugin plugins purge respawn rm setup-token stop kill ultrareview update upgrade' |
| | CODEX_TAKES='-c --config --enable --disable --remote --remote-auth-token-env -i --image -m --model --local-provider -p --profile -s --sandbox -C --cd --add-dir -a --ask-for-approval' |
| | CODEX_DROP='--last --all --include-non-interactive' |
| | CODEX_COMMANDS='exec e review login logout mcp mcp-server app-server remote-control app completion update doctor sandbox debug apply a cloud exec-server features help archive delete unarchive migrate-rollouts agents plugin queue' | | | | | | mkdir -p "$DIR" "$PANES" | | | |
| | ghostty_pid() { |
| | /bin/ps -axo pid=,comm= \| /usr/bin/awk -v b="$GHOSTTY_BIN" '$2==b {print $1; exit}' |
| | } | | | |
| | ghostty_instance() { # "<pid> <start time>" of the running Ghostty, if any |
| | local pid=$(ghostty_pid) |
| | [[ -n $pid ]] \|\| return 0 |
| | local s=$(LC_ALL=C /bin/ps -o lstart= -p "$pid") |
| | print -r -- "$pid ${(j: :)${=s}}" |
| | } | | | |
| | proc_start() { # $1 = pid → its start time in epoch seconds |
| | local s=$(LC_ALL=C /bin/ps -o lstart= -p "$1" 2>/dev/null) |
| | [[ -n $s ]] && LC_ALL=C /bin/date -j -f '%a %b %d %T %Y' "${(j: :)${=s}}" +%s 2>/dev/null |
| | } |
| | |
| | # The interactive shell of a pane is the child of Ghostty's login process. |
| | pane_shells() { |
| | local gpid=$(ghostty_pid) |
| | [[ -z $gpid ]] && return 1 |
| | /bin/ps -axo pid=,ppid= \| /usr/bin/awk -v g="$gpid" '$2==g {print $1}' \| while read -r login; do |
| | local sh=$(/bin/ps -axo pid=,ppid= \| /usr/bin/awk -v p="$login" '$2==p {print $1; exit}') |
| | print -r -- "${sh:-$login}" |
| | done | | | } | | | |
| | foreground_job() { # $1 = shell pid → "pid argv" of its foreground job, if any |
| | /bin/ps -axo pid=,ppid=,stat=,command= \| /usr/bin/awk -v s="$1" '$2==s && $3 ~ /\+/ {p=$1; sub(/^ *[0-9]+ +[0-9]+ +[^ ]+ +/, ""); print p " " $0; exit}' |
| | } | | | | | | # Splits an agent's argv into the options a resume keeps (with their values), the first | | | # positional word (a subcommand, or the start of a prompt or an old session id; positionals | | | # are never kept, so a prompt is not sent again) and every option seen. |
| | typeset -ga KEPT; typeset -g FIRST='' SEEN=' ' |
| | parse_agent() { # $1 = options that take a value, $2 = options to drop, rest = argv |
| | local takes=" $1 " drops=" $2 " a name; integer value; shift 2 |
| | KEPT=() FIRST='' SEEN=' ' |
| | while (( $# )); do |
| | a=$1; shift |
| | if [[ $a == -?* ]]; then |
| | name=${a%%=*}; SEEN+="$name "; value=0 |
| | [[ $a != *=* && $takes == *" $name "* && $# -gt 0 && $1 != -* ]] && value=1 |
| | if [[ $drops == *" $name "* ]]; then |
| | (( value )) && shift |
| | else | | | KEPT+=("$a"); (( value )) && { KEPT+=("$1"); shift } | | | fi | | | elif [[ -z $FIRST ]]; then | | | FIRST=$a | | | fi | | | done | | | } | | | seen() { local f; for f; do [[ $SEEN == " $f " ]] && return 0; done; return 1 } | | | | | | # omp keeps a registry per terminal: line 1 the session's cwd, line 2 its session file. |
| | resume_omp() { # $1 = tty, $2 = cwd, rest = argv after `omp` → "cwd<TAB>command", or nothing |
| | local cwd=$2 reg="$OMP_DIR/terminal-sessions/$1"; shift 2 |
| | parse_agent "$OMP_TAKES" "$OMP_DROP" "$@" |
| | seen -p --print --export --no-session --mode -h --help && return 0 |
| | [[ -r $reg ]] \|\| return 0 |
| | local sess_cwd=$(/usr/bin/sed -n 1p "$reg") |
| | local id=$(/usr/bin/sed -n 2p "$reg" \| /usr/bin/sed -n 's#.*_$[0-9a-f-]\{36\}$\.jsonl$#\1#p') |
| | [[ -n $id ]] \|\| return 0 |
| | local -a cmd; cmd=(omp "${(@q)KEPT}" --resume "$id") |
| | print -r -- "${sess_cwd:-$cwd}"$'\t'"${(j: :)cmd}" |
| | } | | | | | | # Claude Code 2.1 keeps ~/.claude/sessions/<pid>.json with the live session id of each running | | | # claude; it follows /clear and /resume. Older versions: the newest transcript in the folder | | | # written since that claude started. |
| | resume_claude() { # $1 = cwd, $2 = pid, rest = argv after `claude` → "cwd<TAB>command", or nothing |
| | local cwd=$1 pid=$2 state="$CLAUDE_DIR/sessions/$2.json" id='' start; local -a t; shift 2 |
| | parse_agent "$CLAUDE_TAKES" "$CLAUDE_DROP" "$@" |
| | if [[ -r $state ]]; then |
| | local kind=$(/usr/bin/sed -n 's/.*"kind":"$[^"]*$".*/\1/p' "$state") |
| | [[ -z $kind \|\| $kind == interactive ]] \|\| return 0 |
| | id=$(/usr/bin/sed -n 's/.*"sessionId":"$[0-9a-f-]\{36\}$".*/\1/p' "$state") |
| | [[ -n $id ]] && t=($CLAUDE_DIR/projects/*/$id.jsonl(N)) |
| | (( $#t )) \|\| id='' # nothing said in it yet: start fresh |
| | else |
| | seen -p --print --bg --background -h --help -v --version && return 0 |
| | [[ -n $FIRST && " $CLAUDE_COMMANDS " == *" $FIRST "* ]] && return 0 |
| | start=$(proc_start "$pid") |
| | t=($CLAUDE_DIR/projects/${${cwd:A}//[^A-Za-z0-9]/-}/*.jsonl(Nom[1])) |
| | (( $#t )) && (( $(/usr/bin/stat -f %m "$t[1]") >= ${start:-0} )) && id=${${t[1]:t}%.jsonl} |
| | fi |
| | local -a cmd; cmd=(claude "${(@q)KEPT}"); [[ -n $id ]] && cmd+=(--resume "$id") |
| | print -r -- "$cwd"$'\t'"${(j: :)cmd}" |
| | } | | | | | | # Codex writes each session to ~/.codex/sessions/.../rollout-<time>-<id>.jsonl and indexes | | | # it in the threads table of ~/.codex/state_<n>.sqlite. | | | codex_session() { # $1 = codex pid, $2 = cwd → id of the session it is writing, if found | | | local pids=$1 child id |
| | for child in $(/usr/bin/pgrep -P "$1"); do pids+=",$child"; done |
| | id=$(/usr/sbin/lsof -p "$pids" -Fn 2>/dev/null \| /usr/bin/sed -n 's#^n.*/rollout-.*-$[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}$\.jsonl$#\1#p' \| /usr/bin/head -1) |
| | if [[ -z $id ]]; then # not held open: the thread in this folder updated since this codex started |
| | local -a db; db=($CODEX_DIR/state_*.sqlite(Nom[1])) |
| | local start=$(proc_start "$1") a=${2//"'"/"''"} b=${${2:A}//"'"/"''"} |
| | (( $#db )) && [[ -n $start ]] && id=$(/usr/bin/sqlite3 -readonly "$db[1]" \ |
| | "select id from threads where cwd in ('$a', '$b') and updated_at_ms >= ${start}000 and archived = 0 order by updated_at_ms desc limit 1" 2>/dev/null) |
| | fi | | | print -r -- "$id" | | | } | | | |
| | resume_codex() { # $1 = cwd, $2 = pid, rest = argv after `codex` → "cwd<TAB>command", or nothing |
| | local cwd=$1 pid=$2; shift 2 |
| | parse_agent "$CODEX_TAKES" "$CODEX_DROP" "$@" |
| | seen -h --help -V --version && return 0 |
| | [[ -n $FIRST && " $CODEX_COMMANDS " == *" $FIRST "* ]] && return 0 |
| | local id=$(codex_session "$pid" "$cwd") |
| | local -a cmd; cmd=(codex); [[ -n $id ]] && cmd+=(resume "$id"); cmd+=("${(@q)KEPT}") |
| | print -r -- "$cwd"$'\t'"${(j: :)cmd}" |
| | } | | | | | | # How to bring a pane back. An agent session becomes a resume command that runs by itself; | | | # any other foreground command (ssh, vim, a dev server) is typed onto the prompt, not run. |
| | resumable_command() { # $1 = tty, $2 = cwd, $3 = foreground pid, $4 = its argv → "cwd<TAB>mode<TAB>command" |
| | local tty=$1 cwd=$2 pid=$3 fg=$4 out='' |
| | [[ $fg == *ghostty-agent-restore* ]] && fg='' # never record this tool itself: `restart` would loop |
| | [[ -n $fg ]] \|\| { print -r -- "$cwd"$'\t\t'; return } |
| | local -a argv; argv=(${(s: :)fg}) # ps joins argv with spaces and keeps no quoting |
| | [[ ${argv[1]:t} == (node\|bun) && ${${argv[2]:-}:t} == (omp\|claude\|codex) ]] && shift argv |
| | case ${argv[1]:t} in |
| | omp) out=$(resume_omp "$tty" "$cwd" "${(@)argv[2,-1]}") ;; |
| | claude) out=$(resume_claude "$cwd" "$pid" "${(@)argv[2,-1]}") ;; |
| | codex) out=$(resume_codex "$cwd" "$pid" "${(@)argv[2,-1]}") ;; |
| | esac |
| | if [[ -n $out ]]; then |
| | print -r -- "${out%%$'\t'*}"$'\trun\t'"${out#*$'\t'}" |
| | else | | | print -r -- "$cwd"$'\ttype\t'"$fg" | | | fi | | | } | | | | | | # The terminal id a pane's shell recorded at its first prompt; ignored once that shell is | | | # gone, since a new pane can reuse the tty. |
| | pane_terminal() { # $1 = tty, $2 = pane shell pid |
| | [[ -r $PANES/$1 ]] \|\| return 0 |
| | local -a f; f=("${(@ps:\t:)$(<$PANES/$1)}") |
| | [[ ${f[2]:-} == $2 ]] && print -r -- "${f[1]}" |
| | } | | | | | | lock() { | | | local i | | | for i in {1..50}; do | | | /bin/mkdir "$LOCK" 2>/dev/null && return 0 | | | sleep 0.1 | | | done | | | return 1 | | | } | | | unlock() { /bin/rmdir "$LOCK" 2>/dev/null } | | | | | | # True when current.tsv was taken under a Ghostty that is no longer running: the Mac | | | # rebooted, or Ghostty quit, crashed or updated. |
| | from_previous_ghostty() { # $1 = running Ghostty instance |
| | [[ -n $1 && -s $CURRENT && -r $INSTANCE && $(<$INSTANCE) != $1 ]] |
| | } | | | | | | # Caller holds $LOCK. Arms the previous Ghostty's snapshot, once. | | | arm_previous() { # $1 = running Ghostty instance | | | from_previous_ghostty "$1" || return 0 | | | /bin/cp -f "$CURRENT" "$RESTORE" && print -r -- "$1" > "$INSTANCE" | | | } | | | |
| | cmd_snapshot() { |
| | local inst=$(ghostty_instance) |
| | [[ -n $inst ]] \|\| return 1 # Ghostty not running: keep the last snapshot |
| | local -A position; local i p |
| | [[ -r $LAYOUT ]] && while IFS=$'\t' read -r i p; do position[$i]=$p; done < "$LAYOUT" |
| | local tmp="$CURRENT.tmp.$$" sh tty id cwd job |
| | : > "$tmp" |
| | for sh in $(pane_shells); do |
| | tty=$(/bin/ps -o tty= -p "$sh" \| /usr/bin/tr -d ' ') |
| | id=$(pane_terminal "$tty" "$sh"); p='' |
| | [[ -n $id ]] && p=${position[$id]:-} |
| | cwd=$(/usr/sbin/lsof -a -p "$sh" -d cwd -Fn 2>/dev/null \| /usr/bin/sed -n 's/^n//p' \| /usr/bin/head -1) |
| | job=$(foreground_job "$sh") |
| | print -r -- "$tty"$'\t'"$id"$'\t'"$p"$'\t'"$(resumable_command "$tty" "${cwd:-$HOME}" "${job%% *}" "${job#* }")" >> "$tmp" |
| | done | | | if [[ ! -s $tmp ]]; then | | | /bin/rm -f "$tmp" # no panes (Ghostty starting or quitting): keep the last snapshot | | | return 1 | | | fi | | | /usr/bin/sort -o "$tmp" "$tmp" | | | lock || { /bin/rm -f "$tmp"; return 1 } | | | arm_previous "$inst" # the snapshot about to be replaced belongs to the previous Ghostty | | | /bin/mv -f "$tmp" "$CURRENT" && print -r -- "$inst" > "$INSTANCE" | | | unlock | | | } | | | |
| | cmd_list() { |
| | local file="${1:-$CURRENT}" |
| | [[ -r $file ]] \|\| { print "no snapshot yet; run: ghostty-agent-restore snapshot"; return 1 } |
| | print "${file:t} · $(/bin/date -r "$file" '+%b %d %H:%M') · $(/usr/bin/wc -l < "$file" \| /usr/bin/tr -d ' ') panes" |
| | print " tty window:tab cwd command" |
| | /usr/bin/sort -t $'\t' -k3,3 -k1,1 "$file" \| /usr/bin/awk -F'\t' '{ |
| | split($3, p, "."); where = ($3 == "" ? "-" : (p[1] + 0) ":" (p[2] + 0) (p[3] + 0 > 1 ? "." (p[3] + 0) : "")) |
| | cwd = (index($4, ENVIRON["HOME"]) == 1 ? "~" substr($4, length(ENVIRON["HOME"]) + 1) : $4) |
| | printf " %-8s %-11s %-40s %s%s\n", $1, where, cwd, ($6 == "" ? "(shell)" : $6), ($5 == "type" ? " (typed, not run)" : "")}' |
| | } | | | |
| | cmd_arm() { |
| | [[ -s $CURRENT ]] \|\| { print "nothing to arm"; return 1 } |
| | /bin/rm -rf "$LOCK" | | | /bin/cp -f "$CURRENT" "$RESTORE" | | | print "armed for ${FRESH_SECONDS}s: a pane takes its entry when its shell starts (`exec zsh` in an empty pane, or a new tab):" | | | cmd_list "$RESTORE" | | | } | | | | | | # Ghostty answers AppleScript with each terminal's id, position and title, so a shell finds | | | # its own terminal id by setting a unique title and looking for it. Sets TERMINAL_ID and | | | # LIVE_IDS, saves every terminal's position to $LAYOUT, and records this pane for snapshots. |
| | TERMINAL_ID='' LIVE_IDS=' ' |
| | identify() { # $1 = tty, $2 = pane shell pid |
| | local token="ghostty-agent-restore-$$-$RANDOM" out='' i |
| | print -n -- $'\e]2;'"$token"$'\a' > /dev/tty |
| | for i in {1..10}; do |
| | # Two bulk reads (one Apple event each) instead of one event per property per terminal: | | | # about 0.1 s instead of 0.5 s with a dozen terminals. Ids are read again afterwards; | | | # if a tab opened or closed in between, the rows could pair up wrong, so retry. | | | out=$(/usr/bin/osascript -e 'with timeout of 3 seconds | | | tell application "Ghostty" | | | set ids to id of terminals of tabs of windows | | | set names to name of terminals of tabs of windows | | | set again to id of terminals of tabs of windows | | | end tell | | | end timeout | | | if again is not equal to ids then return "" | | | set out to "" | | | repeat with wi from 1 to count of ids | | | repeat with ti from 1 to count of item wi of ids | | | set tabIds to item ti of item wi of ids | | | set tabNames to item ti of item wi of names | | | repeat with ki from 1 to count of tabIds | | | set out to out & (item ki of tabIds) & (character id 9) & wi & "." & ti & "." & ki & (character id 9) & (item ki of tabNames) & linefeed | | | end repeat | | | end repeat | | | end repeat | | | return out' 2>/dev/null) || break |
| | TERMINAL_ID=${${(M)${(f)out}:#*$'\t'$token}%%$'\t'*} |
| | [[ -n $TERMINAL_ID ]] && break |
| | sleep 0.05 | | | done |
| | print -n -- $'\e]2;'"${PWD/#$HOME/~}"$'\a' > /dev/tty |
| | [[ -n $out ]] \|\| return 0 |
| | local -a ids; ids=(${${(f)out}%%$'\t'*}) # unquoted: inside "..." the lines would be joined first |
| | LIVE_IDS=" ${(j: :)ids} " |
| | print -r -- "$out" \| /usr/bin/awk -F'\t' 'NF >= 2 {split($2, p, "."); printf "%s\t%03d.%03d.%03d\n", $1, p[1], p[2], p[3]}' > "$LAYOUT.tmp.$$" \ |
| | && /bin/mv -f "$LAYOUT.tmp.$$" "$LAYOUT" | | | [[ -n $TERMINAL_ID ]] && print -r -- "$TERMINAL_ID"$'\t'"$2" > "$PANES/$1" | | | } | | | | | | # Take this pane's entry from restore.tsv. A pane without one (Ghostty did not restore the | | | # windows, so this is a new tab) takes the session that came first in the old tab order | | | # among those whose terminal is gone. Prints the entry. | | | take_entry() { | | | lock || return |
| | local n=$(/usr/bin/awk -F'\t' -v me="$TERMINAL_ID" -v live="$LIVE_IDS" ' |
| | me != "" && $2 == me { print NR; mine = 1; exit } |
| | $6 != "" && ($2 == "" \|\| index(live, " " $2 " ") == 0) { |
| | key = ($3 == "" ? "999" : $3) "\t" $1 |
| | if (spare == "" \|\| key < best) { spare = NR; best = key } |
| | } | | | END { if (!mine && spare != "") print spare }' "$RESTORE" 2>/dev/null) | | | local line='' |
| | if [[ -n $n ]]; then |
| | line=$(/usr/bin/sed -n "${n}p" "$RESTORE") |
| | /usr/bin/sed "${n}d" "$RESTORE" > "$RESTORE.tmp" && /bin/mv -f "$RESTORE.tmp" "$RESTORE" | | | [[ -s $RESTORE ]] || /bin/rm -f "$RESTORE" | | | fi | | | unlock | | | print -r -- "$line" | | | } | | | | | | # Run by the .zshrc hook at a pane's first prompt, with the pane shell's pid. Learns this | | | # pane's terminal id, arms the previous Ghostty's snapshot if this is a new Ghostty, and | | | # prints "<mode><TAB><command line>" for this pane, if it gets an entry. |
| | cmd_claim() { |
| | local shell=${1:-$PPID} |
| | local login=$(/bin/ps -o ppid= -p "$shell" \| /usr/bin/tr -d ' ') |
| | [[ $(/bin/ps -o comm= -p "$login") == */login ]] \|\| return 0 # a pane's own shell, not a nested one |
| | identify "$(/bin/ps -o tty= -p "$shell" \| /usr/bin/tr -d ' ')" "$shell" |
| | local inst=$(ghostty_instance) |
| | if from_previous_ghostty "$inst" && lock; then | | | arm_previous "$inst" | | | unlock | | | fi |
| | [[ -r $RESTORE ]] \|\| return 0 |
| | local age=$(( $(/bin/date +%s) - $(/usr/bin/stat -f %m "$RESTORE") )) |
| | if (( age > FRESH_SECONDS )); then |
| | /bin/rm -f "$RESTORE"; return 0 |
| | fi |
| | local line=$(take_entry) |
| | local -a f; f=("${(@ps:\t:)line}") |
| | local cwd=${f[4]:-} mode=${f[5]:-} command=${line#*$'\t'*$'\t'*$'\t'*$'\t'*$'\t'} |
| | [[ -n $line && -n $mode && -n $command ]] \|\| return 0 |
| | # real path, so a directory since moved but still reached through a symlink works |
| | [[ -d $cwd && ${cwd:A} != ${PWD:A} ]] && command="cd -- ${(q)cwd:A} && $command" |
| | print -r -- "$mode"$'\t'"$command" |
| | } | | | | | | # zsh code for .zshrc. The claim runs in precmd, where p10k's instant prompt has stdin on | | | # /dev/null and stdout on a file, so nothing is run there: the command is put on the first | | | # prompt's command line, and submitted only for an agent session. | | | cmd_hook() { | | | /bin/cat <<'EOF' |
| | _ghostty_agent_restore_claim() { |
| | precmd_functions=(${precmd_functions:#_ghostty_agent_restore_claim}) |
| | local out=$(ghostty-agent-restore claim $$) |
| | [[ -n $out ]] \|\| return 0 |
| | typeset -g _ghostty_agent_restore_mode=${out%%$'\t'*} _ghostty_agent_restore_cmd=${out#*$'\t'} |
| | zle -N _ghostty_agent_restore_run | | | autoload -Uz add-zle-hook-widget && add-zle-hook-widget line-init _ghostty_agent_restore_run | | | } |
| | _ghostty_agent_restore_run() { |
| | [[ -n ${_ghostty_agent_restore_cmd:-} ]] \|\| return 0 |
| | BUFFER=$_ghostty_agent_restore_cmd | | | CURSOR=${#BUFFER} | | | unset _ghostty_agent_restore_cmd | | | [[ $_ghostty_agent_restore_mode == run ]] && zle accept-line | | | } | | | precmd_functions+=(_ghostty_agent_restore_claim) | | | EOF | | | } | | | | | | cmd_restart() { | | | cmd_snapshot || { print "Ghostty is not running"; return 1 } | | | cmd_list | | | print "\nQuitting Ghostty (confirm the dialog if it asks). It relaunches and every pane resumes." | | | nohup /bin/zsh -c ' | | | /usr/bin/osascript -e '"'"'tell application "Ghostty" to quit'"'"' >/dev/null 2>&1 | | | for i in {1..120}; do /bin/ps -axo comm= | /usr/bin/grep -qx "'"$GHOSTTY_BIN"'" || break; sleep 0.5; done | | | sleep 1 | | | /usr/bin/open -a Ghostty | | | ' >/dev/null 2>&1 &! | | | } | | | | | | cmd_install() { | | | local plist="$HOME/Library/LaunchAgents/$LABEL.plist" | | | /bin/mkdir -p "$HOME/Library/LaunchAgents" | | | /bin/cat > "$plist" <<EOF | | | <?xml version="1.0" encoding="UTF-8"?> | | | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| | <plist version="1.0"><dict> |
| | <key>Label</key><string>$LABEL</string> |
| | <key>ProgramArguments</key><array><string>/bin/zsh</string><string>$HOME/.local/bin/ghostty-agent-restore</string><string>snapshot</string></array> |
| | <key>StartInterval</key><integer>120</integer> |
| | <key>RunAtLoad</key><true/> |
| | <key>StandardErrorPath</key><string>$DIR/launchd.log</string> |
| | </dict></plist> |
| | EOF | | | /bin/launchctl bootout "gui/$UID/$LABEL" 2>/dev/null | | | /bin/launchctl bootstrap "gui/$UID" "$plist" && print "launchd: snapshot every 2 min ($LABEL)" | | | local hook='command -v ghostty-agent-restore >/dev/null && [[ -n ${GHOSTTY_RESOURCES_DIR:-} ]] && eval "$(ghostty-agent-restore hook)" # ghostty-agent-restore' | | | local rc="$HOME/.zshrc" |
| | if /usr/bin/grep -q '# ghostty-agent-restore$' "$rc" 2>/dev/null; then |
| | /usr/bin/awk -v h="$hook" '/# ghostty-agent-restore$/ {print h; next} {print}' "$rc" > "$rc.tmp.$$" && /bin/cat "$rc.tmp.$$" > "$rc" && /bin/rm -f "$rc.tmp.$$" |
| | print ".zshrc: hook updated" | | | else | | | print -- "\n# Resume what each Ghostty pane was running before Ghostty or the Mac restarted\n$hook" >> "$rc" | | | print ".zshrc: hook added" | | | fi | | | } | | | |
| | case "${1:-}" in |
| | snapshot) cmd_snapshot ;; |
| | list) cmd_list "${2:-}" ;; |
| | restart) cmd_restart ;; |
| | arm) cmd_arm ;; |
| | claim) cmd_claim "${2:-}" ;; |
| | hook) cmd_hook ;; |
| | install) cmd_install ;; |
| | *) /usr/bin/sed -n '2,17p' "$0"; exit 64 ;; |
| | esac |