From b8c37da28739692c0dfbb30bc2484ed4b1fbf02f Mon Sep 17 00:00:00 2001 From: M~Johansson Date: Sun, 4 Oct 2026 13:59:05 +0200 Subject: [PATCH 1/2] feat: tmux sessions survive reboots (resurrect + continuum) - scripts/tmux.sh: dual-mode helper (sourced by setup.sh or standalone) that clones TPM, installs plugins headlessly, and wires persistent shell history into ~/.bashrc or ~/.zshrc (resurrect dropped history saving upstream, so the rc wiring provides up-arrow recall instead) - .tmux.conf: tpm + tmux-resurrect + tmux-continuum; autosave every 15 min, auto-restore on server start, pane contents captured, nvim restored via the 'session' strategy - nvim: VimLeavePre autocmd writes Session.vim when inside tmux - git: global gitignore (core.excludesfile) keeps Session.vim out of git status everywhere Co-Authored-By: Claude Fable 5 --- README.md | 13 ++- git/.gitconfig | 1 + git/gitignore_global | 3 + nvim/lua/config/autocmds.lua | 13 +++ scripts/tmux.sh | 171 +++++++++++++++++++++++++++++++++++ setup.sh | 10 ++ tmux/.tmux.conf | 16 +++- 7 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 git/gitignore_global create mode 100644 nvim/lua/config/autocmds.lua create mode 100755 scripts/tmux.sh diff --git a/README.md b/README.md index baaee38..30bd30c 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,19 @@ brew install neovim tmux starship ripgrep fd - `tmux.lua` — `vim-tmux-navigator` for Ctrl-h/j/k/l pane navigation (paired with `tmux/.tmux.conf`). - `himalaya.lua` — `himalaya-vim` email client. - `fzf.lua` — `junegunn/fzf` + `fzf.vim`. -- `tmux/.tmux.conf` — terminal defaults, mouse on, and the tmux side of seamless Vim/tmux Ctrl-h/j/k/l navigation. +- `tmux/.tmux.conf` — terminal defaults (mouse off), the tmux side of seamless Vim/tmux Ctrl-h/j/k/l navigation, and TPM-managed session persistence (tmux-resurrect + tmux-continuum). - `starship/starship.toml` — Starship prompt config. Reloads automatically on the next prompt render — no command needed. +- `scripts/` — source-or-standalone helpers used by `setup.sh`: `starship.sh` (binary install + shell wiring) and `tmux.sh` (TPM clone, headless plugin install, persistent-history wiring). +- `git/gitignore_global` — global gitignore (symlinked to `~/.gitignore_global`, wired via `core.excludesfile`); keeps `Session.vim` out of `git status` everywhere. + +## Session persistence + +tmux sessions survive reboots. `setup.sh` clones [TPM](https://github.com/tmux-plugins/tpm) and installs [tmux-resurrect](https://github.com/tmux-plugins/tmux-resurrect) + [tmux-continuum](https://github.com/tmux-plugins/tmux-continuum) headlessly (no `prefix + I` needed, though it still works). + +- **Automatic**: continuum autosaves every 15 minutes and restores the last saved state when the tmux server starts after a reboot — windows, panes, working directories, and visible pane text included. +- **Manual**: save with `prefix + Ctrl-s`, restore with `prefix + Ctrl-r`. +- **Neovim**: panes that were running nvim reopen it from a `Session.vim` written automatically on exit (only when inside tmux). `Session.vim` files are globally gitignored via `git/gitignore_global`. +- **Shell history**: restored panes keep up-arrow history. `setup.sh` wires persistent history into `~/.bashrc` (`histappend` + `history -a` on every prompt) or `~/.zshrc` (`INC_APPEND_HISTORY`), depending on `$SHELL` — tmux-resurrect itself no longer supports history saving. Re-runnable standalone for other shells: `./scripts/tmux.sh --no-config`. ## Updating plugins diff --git a/git/.gitconfig b/git/.gitconfig index 41d0b4d..9f67498 100644 --- a/git/.gitconfig +++ b/git/.gitconfig @@ -3,6 +3,7 @@ [core] editor = nvim + excludesfile = ~/.gitignore_global [init] defaultBranch = main diff --git a/git/gitignore_global b/git/gitignore_global new file mode 100644 index 0000000..5453d36 --- /dev/null +++ b/git/gitignore_global @@ -0,0 +1,3 @@ +# Ignored in every repo (core.excludesfile) +# Session files written by nvim for tmux-resurrect restore +Session.vim diff --git a/nvim/lua/config/autocmds.lua b/nvim/lua/config/autocmds.lua new file mode 100644 index 0000000..cebbc00 --- /dev/null +++ b/nvim/lua/config/autocmds.lua @@ -0,0 +1,13 @@ +-- Autocmds are automatically loaded on the VeryLazy event +-- Default autocmds that are always set: https://www.lazyvim.org/configuration/general#auto-commands + +-- Write Session.vim on exit (only inside tmux) so tmux-resurrect's +-- nvim 'session' strategy can restore the editor after a reboot. +vim.api.nvim_create_autocmd("VimLeavePre", { + group = vim.api.nvim_create_augroup("tmux_resurrect_session", { clear = true }), + callback = function() + if vim.env.TMUX then + pcall(vim.cmd, "mksession! Session.vim") + end + end, +}) diff --git a/scripts/tmux.sh b/scripts/tmux.sh new file mode 100755 index 0000000..36567db --- /dev/null +++ b/scripts/tmux.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# tmux session persistence: install TPM, install plugins headlessly, +# wire persistent shell history (what tmux-resurrect no longer does upstream). +# +# Can be sourced by setup.sh (uses caller's helpers) or run standalone: +# ./scripts/tmux.sh # link config + TPM + plugins + history wiring +# ./scripts/tmux.sh --no-config # skip the config symlink +set -euo pipefail + +_TMUX_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# --- Helpers (only defined when running standalone) --- +if ! declare -F log >/dev/null 2>&1; then + if [[ -t 1 ]]; then + C_OK=$'\033[1;32m'; C_WARN=$'\033[1;33m'; C_INFO=$'\033[1;34m'; C_RST=$'\033[0m' + else + C_OK=''; C_WARN=''; C_INFO=''; C_RST='' + fi + log() { printf '%s==>%s %s\n' "$C_INFO" "$C_RST" "$*"; } + ok() { printf '%s ok%s %s\n' "$C_OK" "$C_RST" "$*"; } + warn() { printf '%s !!%s %s\n' "$C_WARN" "$C_RST" "$*" >&2; } + dep_warn() { warn "$*"; } +fi + +# --- Install TPM (tmux plugin manager) --- +install_tpm() { + local tpm_dir="$HOME/.tmux/plugins/tpm" + + if [[ -d "$tpm_dir/.git" ]]; then + ok "TPM already installed: $tpm_dir" + return 0 + fi + + log "Installing TPM" + if ! git clone --depth 1 https://github.com/tmux-plugins/tpm "$tpm_dir" 2>/dev/null; then + dep_warn "TPM clone failed — install manually:" + printf ' git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm\n' + return 0 + fi + ok "TPM installed: $tpm_dir" +} + +# --- Install declared plugins without needing prefix+I --- +install_tmux_plugins() { + local installer="$HOME/.tmux/plugins/tpm/bin/install_plugins" + + if [[ ! -f "$HOME/.tmux.conf" ]]; then + dep_warn "~/.tmux.conf not found — link it first, then re-run plugin install." + return 0 + fi + if [[ ! -x "$installer" ]]; then + dep_warn "TPM not present — plugins not installed (prefix+I inside tmux also works)." + return 0 + fi + + export TMUX_PLUGIN_MANAGER_PATH="$HOME/.tmux/plugins" + if ! "$installer" >/dev/null 2>&1; then + dep_warn "TPM headless plugin install failed — press prefix+I inside tmux instead." + return 0 + fi + + local plugin + for plugin in tmux-resurrect tmux-continuum; do + if [[ -d "$HOME/.tmux/plugins/$plugin" ]]; then + ok "tmux plugin installed: $plugin" + else + dep_warn "tmux plugin missing after install: $plugin" + fi + done +} + +# --- Wire persistent shell history into the shell rc --- +# Restored panes get up-arrow recall: every command is flushed to the history +# file immediately, so history survives reboots alongside resurrect's layout. +wire_history_persistence() { + local shell_name="${SHELL##*/}" + local rcfile + + case "$shell_name" in + zsh) rcfile="$HOME/.zshrc" ;; + bash) rcfile="$HOME/.bashrc" ;; + fish) + ok "fish persists history by default — nothing to wire" + return 0 + ;; + *) + dep_warn "Unknown shell ($SHELL) — wire up persistent history manually." + return 0 + ;; + esac + + if [[ -f "$rcfile" ]] && grep -q '# Persistent shell history' "$rcfile" 2>/dev/null; then + ok "Persistent history already in $rcfile" + return 0 + fi + + mkdir -p "$(dirname "$rcfile")" + if [[ "$shell_name" == "bash" ]]; then + cat >> "$rcfile" <<'EOF' + +# Persistent shell history +shopt -s histappend +HISTSIZE=100000 +HISTFILESIZE=200000 +PROMPT_COMMAND="history -a${PROMPT_COMMAND:+; $PROMPT_COMMAND}" +EOF + else + cat >> "$rcfile" <<'EOF' + +# Persistent shell history +HISTFILE="$HOME/.zsh_history" +HISTSIZE=100000 +SAVEHIST=200000 +setopt INC_APPEND_HISTORY +EOF + fi + ok "Persistent history added to $rcfile" +} + +# --- Symlink tmux config --- +link_tmux_config() { + local src="$_TMUX_SCRIPT_DIR/../tmux/.tmux.conf" + local dst="$HOME/.tmux.conf" + + # Resolve to absolute path + src="$(cd "$(dirname "$src")" && pwd)/$(basename "$src")" + + if [[ ! -f "$src" ]]; then + warn "tmux config not found: $src" + return 1 + fi + + # If setup.sh's link_path is available, use it (handles conflicts properly) + if declare -F link_path >/dev/null 2>&1; then + link_path "$src" "$dst" "tmux config" + return + fi + + # Standalone: simple symlink logic + if [[ -L "$dst" && "$(readlink "$dst")" == "$src" ]]; then + ok "tmux config already linked: $dst -> $src" + return + fi + + if [[ -e "$dst" ]]; then + warn "tmux config: $dst already exists. Back it up, then re-run." + return 1 + fi + + ln -s "$src" "$dst" + ok "tmux config linked: $dst -> $src" +} + +# --- Main (only when run directly, not sourced) --- +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + link_config=1 + for arg in "$@"; do + case "$arg" in + --no-config) link_config=0 ;; + esac + done + + log "Setting up tmux session persistence" + if (( link_config )); then + link_tmux_config + fi + install_tpm + install_tmux_plugins + wire_history_persistence + log "Done." +fi diff --git a/setup.sh b/setup.sh index 527b64f..a19ea6e 100755 --- a/setup.sh +++ b/setup.sh @@ -11,6 +11,8 @@ STARSHIP_SRC="$SCRIPT_DIR/starship/starship.toml" STARSHIP_DST="$HOME/.config/starship.toml" GIT_SRC="$SCRIPT_DIR/git/.gitconfig" GIT_DST="$HOME/.gitconfig" +GITIGNORE_SRC="$SCRIPT_DIR/git/gitignore_global" +GITIGNORE_DST="$HOME/.gitignore_global" if [[ -t 1 ]]; then C_OK=$'\033[1;32m'; C_WARN=$'\033[1;33m'; C_INFO=$'\033[1;34m'; C_RST=$'\033[0m' @@ -125,6 +127,10 @@ check_nerd_font() { # shellcheck source=scripts/starship.sh source "$SCRIPT_DIR/scripts/starship.sh" +# tmux persistence: TPM, plugins, history wiring (shared with scripts/tmux.sh) +# shellcheck source=scripts/tmux.sh +source "$SCRIPT_DIR/scripts/tmux.sh" + macos_terminal_reminder() { [[ "$OSTYPE" == darwin* ]] || return 0 printf ' macOS reminder: installing a Nerd Font does NOT change your terminal'\''s font.\n' @@ -280,6 +286,7 @@ check_deps() { macos_terminal_reminder check_login_sources_bashrc wire_starship_shell_init + wire_history_persistence printf ' Optional: install the himalaya CLI if you use the himalaya-vim plugin.\n' } @@ -290,6 +297,7 @@ main() { link_path "$STARSHIP_SRC" "$STARSHIP_DST" "starship config" link_path "$GIT_SRC" "$GIT_DST" "git config" + link_path "$GITIGNORE_SRC" "$GITIGNORE_DST" "global gitignore" # Remind about per-machine identity if not yet configured if [[ ! -f "$HOME/.gitconfig.local" ]]; then @@ -298,6 +306,8 @@ main() { fi install_starship + install_tpm + install_tmux_plugins check_deps if (( LINK_CONFLICTS > 0 )); then diff --git a/tmux/.tmux.conf b/tmux/.tmux.conf index 2bd6874..2c579fa 100644 --- a/tmux/.tmux.conf +++ b/tmux/.tmux.conf @@ -33,4 +33,18 @@ bind L resize-pane -R 5 bind r source-file ~/.tmux.conf \; display "Tmux configuration reloaded!" # --- Status Bar --- -set -g status-right '#h [#W] (#S) %Y-%m-%d %H:%M' +set -g status-right '#h [#W] (#S) %Y-%m-%d %H:%M' + +# --- Session Persistence (TPM + resurrect + continuum) --- +set -g @plugin 'tmux-plugins/tpm' +set -g @plugin 'tmux-plugins/tmux-resurrect' +set -g @plugin 'tmux-plugins/tmux-continuum' + +# Restore visible pane text and relaunch nvim from Session.vim +set -g @resurrect-capture-pane-contents 'on' +set -g @resurrect-strategy-nvim 'session' +# Autosave every 15 min (continuum default) and restore on server start +set -g @continuum-restore 'on' + +# Initialize TPM — keep this line at the very bottom of .tmux.conf +run '~/.tmux/plugins/tpm/tpm' From 3a689b8b486023f4d15724f6b4cecd77e623fed9 Mon Sep 17 00:00:00 2001 From: M~Johansson Date: Sun, 4 Oct 2026 14:10:46 +0200 Subject: [PATCH 2/2] ci: bump auto-assign-issue to v2 so PR assignment works The v1 tag resolves to a release that only accepts repo-token and assignees and predates pull_request support, so the workflow failed on every PR opened. Co-Authored-By: Claude Fable 5 --- .github/workflows/auto-assign.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index 1cd656c..0bffae9 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write steps: - name: 'Auto-assign issue' - uses: pozil/auto-assign-issue@v1 + uses: pozil/auto-assign-issue@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} assignees: mathjoha