From 44afaf393660a0f60c7d4ed24bd9c86b9069d1c8 Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Tue, 3 Nov 2026 14:22:55 +0100 Subject: [PATCH] pinwheel: Restore `claude` and `emacs` panes with tmux-resurrect --- hosts/pinwheel/modules/ai/default.nix | 10 +++ hosts/pinwheel/modules/emacs/config.org | 35 ++++++++++ hosts/pinwheel/modules/emacs/default.nix | 2 +- hosts/pinwheel/modules/tmux/default.nix | 13 ++++ hosts/pinwheel/modules/tmux/panes.sh | 81 ++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 hosts/pinwheel/modules/tmux/panes.sh diff --git a/hosts/pinwheel/modules/ai/default.nix b/hosts/pinwheel/modules/ai/default.nix index a038b11..39ed9f1 100644 --- a/hosts/pinwheel/modules/ai/default.nix +++ b/hosts/pinwheel/modules/ai/default.nix @@ -34,6 +34,16 @@ let pr = ""; }; hooks = { + SessionStart = [ + { + hooks = [ + { + type = "command"; + command = "tmux-panes claude-session"; + } + ]; + } + ]; PostToolUse = [ { matcher = "Write|Edit"; diff --git a/hosts/pinwheel/modules/emacs/config.org b/hosts/pinwheel/modules/emacs/config.org index 54df649..1bb09e6 100644 --- a/hosts/pinwheel/modules/emacs/config.org +++ b/hosts/pinwheel/modules/emacs/config.org @@ -332,6 +332,41 @@ https://github.com/justbur/emacs-which-key (setq history-length 25) (savehist-mode 1) #+END_SRC +** Remember open buffers per tmux pane +Used by `tmux-panes` to reopen buffers when tmux-resurrect restores a pane +#+BEGIN_SRC emacs-lisp + (defun tmux-panes--state-file () + (when-let* ((pane (getenv "TMUX_PANE"))) + (expand-file-name (concat "tmux-panes/" (substring pane 1) "/emacs") + (or (getenv "XDG_STATE_HOME") "~/.local/state")))) + + (defun tmux-panes-save-buffers () + (when-let* ((file (tmux-panes--state-file))) + (make-directory (file-name-directory file) t) + (let ((buffers (delq nil (mapcar (lambda (buffer) + (when-let* ((name (buffer-file-name buffer))) + (cons name (with-current-buffer buffer (point))))) + (buffer-list))))) + (with-temp-file file + (prin1 buffers (current-buffer)))))) + + (defun tmux-panes-restore-buffers () + (when-let* ((file (getenv "TMUX_PANES_RESTORE"))) + (setenv "TMUX_PANES_RESTORE" nil) + (when (file-readable-p file) + (let ((buffers (with-temp-buffer + (insert-file-contents file) + (read (current-buffer))))) + (dolist (entry (reverse buffers)) + (when (file-exists-p (car entry)) + (find-file (car entry)) + (goto-char (cdr entry))))) + (delete-file file))) + (tmux-panes-save-buffers)) + + (add-hook 'emacs-startup-hook #'tmux-panes-restore-buffers) + (run-with-idle-timer 30 t #'tmux-panes-save-buffers) +#+END_SRC ** Vertico #+BEGIN_SRC emacs-lisp (use-package vertico diff --git a/hosts/pinwheel/modules/emacs/default.nix b/hosts/pinwheel/modules/emacs/default.nix index ecaaf38..86cbaf0 100644 --- a/hosts/pinwheel/modules/emacs/default.nix +++ b/hosts/pinwheel/modules/emacs/default.nix @@ -18,7 +18,7 @@ let }; e = pkgs.writeShellScriptBin "e" '' - ${emacs}/bin/emacs -nw $@ + exec ${emacs}/bin/emacs -nw "$@" ''; ec = pkgs.writeShellScriptBin "ec" '' diff --git a/hosts/pinwheel/modules/tmux/default.nix b/hosts/pinwheel/modules/tmux/default.nix index 9645dbb..434e0fe 100644 --- a/hosts/pinwheel/modules/tmux/default.nix +++ b/hosts/pinwheel/modules/tmux/default.nix @@ -2,9 +2,20 @@ let colors = config.lib.colors.terminal; dim = builtins.elemAt colors.bright 0; + + tmuxPanes = pkgs.writeShellApplication { + name = "tmux-panes"; + runtimeInputs = [ + pkgs.jq + pkgs.tmux + ]; + text = builtins.readFile ./panes.sh; + }; in { home-manager.users.alex = { + home.packages = [ tmuxPanes ]; + programs.tmux = { enable = true; @@ -20,6 +31,8 @@ in plugin = pkgs.tmuxPlugins.resurrect; extraConfig = '' set -g @resurrect-capture-pane-contents 'on' + set -g @resurrect-processes '"~bin/claude->${tmuxPanes}/bin/tmux-panes restore claude" "~bin/emacs -nw->${tmuxPanes}/bin/tmux-panes restore emacs"' + set -g @resurrect-hook-post-save-all '${tmuxPanes}/bin/tmux-panes save' ''; } { diff --git a/hosts/pinwheel/modules/tmux/panes.sh b/hosts/pinwheel/modules/tmux/panes.sh new file mode 100644 index 0000000..eff5918 --- /dev/null +++ b/hosts/pinwheel/modules/tmux/panes.sh @@ -0,0 +1,81 @@ +state_root="${XDG_STATE_HOME:-$HOME/.local/state}/tmux-panes" +snapshot_root="${XDG_DATA_HOME:-$HOME/.local/share}/tmux/resurrect/panes" + +pane_state() { + echo "$state_root/${1#%}" +} + +pane_snapshot() { + echo "$snapshot_root/${1//\//_}" +} + +case "${1:-}" in + claude-session) + [ -n "${TMUX_PANE:-}" ] || exit 0 + session_id="$(jq -r '.session_id')" + dir="$(pane_state "$TMUX_PANE")" + mkdir -p "$dir" + printf '%s\n%s\n' "${CLAUDE_CONFIG_DIR:-}" "$session_id" > "$dir/claude" + ;; + + save) + mkdir -p "$(dirname "$snapshot_root")" "$state_root" + snapshot="$(mktemp -d "$snapshot_root.XXXXXX")" + live=() + + while IFS=$'\t' read -r id pos cmd; do + live+=("${id#%}") + case "$cmd" in + claude | emacs) ;; + *) continue ;; + esac + src="$(pane_state "$id")/$cmd" + [ -f "$src" ] || continue + dst="$snapshot/${pos//\//_}" + mkdir -p "$dst" + cp "$src" "$dst/$cmd" + done < <(tmux list-panes -a -F '#{pane_id} #S:#I.#P #{pane_current_command}') + + rm -rf "$snapshot_root" + mv "$snapshot" "$snapshot_root" + + for dir in "$state_root"/*/; do + [ -d "$dir" ] || continue + name="$(basename "$dir")" + [[ " ${live[*]} " == *" $name "* ]] || rm -rf "$dir" + done + ;; + + restore) + pos="$(tmux display-message -p -t "$TMUX_PANE" '#S:#I.#P')" + file="$(pane_snapshot "$pos")/${2:-}" + + case "${2:-}" in + claude) + if [ ! -f "$file" ]; then + echo "tmux-panes: no saved claude session for $pos" >&2 + exit 1 + fi + { read -r config_dir; read -r session_id; } < "$file" + CLAUDE_CONFIG_DIR="$config_dir" exec claude --resume "$session_id" + ;; + emacs) + if [ -f "$file" ]; then + buffers="$(mktemp)" + cp "$file" "$buffers" + TMUX_PANES_RESTORE="$buffers" exec e + fi + exec e + ;; + *) + echo "usage: tmux-panes restore claude|emacs" >&2 + exit 1 + ;; + esac + ;; + + *) + echo "usage: tmux-panes claude-session|save|restore " >&2 + exit 1 + ;; +esac