diff --git a/README.md b/README.md
index f137ad8b..0f7ea02e 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,10 @@ Automatic restoring and continuous saving of tmux env is also possible with
- `prefix + Ctrl-s` - save
- `prefix + Ctrl-r` - restore
+On tmux `3.2+`, these bindings open a popup by default so save/restore progress
+can stream directly in the popup with verbose logs. On older tmux versions, the
+plugin falls back to the status-line messages automatically.
+
### About
This plugin goes to great lengths to save and restore all the details from your
@@ -90,6 +94,11 @@ You should now be able to use the plugin.
**Configuration**
- [Changing the default key bindings](docs/custom_key_bindings.md).
+- Popup UI options:
+ `@resurrect-popup 'auto'|'on'|'off'`,
+ `@resurrect-popup-width`,
+ `@resurrect-popup-height`,
+ `@resurrect-popup-close-delay`.
- [Setting up hooks on save & restore](docs/hooks.md).
- Only a conservative list of programs is restored by default:
`vi vim nvim emacs man less more tail top htop irssi weechat mutt`.
diff --git a/docs/custom_key_bindings.md b/docs/custom_key_bindings.md
index 99bfc2c6..0cf1db4d 100644
--- a/docs/custom_key_bindings.md
+++ b/docs/custom_key_bindings.md
@@ -9,3 +9,15 @@ To change these, add to `.tmux.conf`:
set -g @resurrect-save 'S'
set -g @resurrect-restore 'R'
+
+Popup UI is enabled automatically on tmux `3.2+`. To force the old status-line
+behavior instead, add:
+
+ set -g @resurrect-popup 'off'
+
+To force popup mode or tune the popup size / close delay:
+
+ set -g @resurrect-popup 'on'
+ set -g @resurrect-popup-width '70%'
+ set -g @resurrect-popup-height '70%'
+ set -g @resurrect-popup-close-delay '2'
diff --git a/resurrect.tmux b/resurrect.tmux
index 21fed7e1..c8d4d474 100755
--- a/resurrect.tmux
+++ b/resurrect.tmux
@@ -9,7 +9,7 @@ set_save_bindings() {
local key_bindings=$(get_tmux_option "$save_option" "$default_save_key")
local key
for key in $key_bindings; do
- tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/save.sh"
+ tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/run_with_popup.sh save"
done
}
@@ -17,7 +17,7 @@ set_restore_bindings() {
local key_bindings=$(get_tmux_option "$restore_option" "$default_restore_key")
local key
for key in $key_bindings; do
- tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/restore.sh"
+ tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/run_with_popup.sh restore"
done
}
diff --git a/scripts/check_tmux_version.sh b/scripts/check_tmux_version.sh
index b0aedece..6bdd11ad 100755
--- a/scripts/check_tmux_version.sh
+++ b/scripts/check_tmux_version.sh
@@ -19,6 +19,11 @@ get_tmux_option() {
display_message() {
local message="$1"
+ if [ "${RESURRECT_OUTPUT_MODE:-}" = "popup" ]; then
+ printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$message"
+ return
+ fi
+
# display_duration defaults to 5 seconds, if not passed as an argument
if [ "$#" -eq 2 ]; then
local display_duration="$2"
diff --git a/scripts/helpers.sh b/scripts/helpers.sh
index 20d87dcd..26059e96 100644
--- a/scripts/helpers.sh
+++ b/scripts/helpers.sh
@@ -25,11 +25,105 @@ get_tmux_option() {
fi
}
+get_digits_from_string() {
+ local string="$1"
+ local only_digits="$(echo "$string" | tr -dC '[:digit:]')"
+ echo "$only_digits"
+}
+
+tmux_version_int() {
+ local tmux_version_string="$(tmux -V)"
+ echo "$(get_digits_from_string "$tmux_version_string")"
+}
+
+tmux_version_at_least() {
+ local minimum_version="$1"
+ local current_version_int="$(tmux_version_int)"
+ local minimum_version_int="$(get_digits_from_string "$minimum_version")"
+ [ "$current_version_int" -ge "$minimum_version_int" ]
+}
+
+tmux_popup_supported() {
+ tmux_version_at_least "3.2"
+}
+
+should_use_popup() {
+ local popup_mode="$(get_tmux_option "$popup_mode_option" "$default_popup_mode")"
+
+ case "$popup_mode" in
+ on|auto)
+ tmux_popup_supported
+ ;;
+ off)
+ return 1
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+}
+
+is_popup_output() {
+ [ "${SCRIPT_OUTPUT:-}" = "popup" ]
+}
+
+popup_log() {
+ local message="$1"
+
+ if is_popup_output; then
+ printf '%s\n' "$message" | ts '[%F %.T]'
+ fi
+}
+
+popup_close_delay() {
+ get_tmux_option "$popup_close_delay_option" "$default_popup_close_delay"
+}
+
+wait_for_popup_close() {
+ if is_popup_output; then
+ local delay="$(popup_close_delay)"
+ if [ -n "$delay" ] && [ "$delay" != "0" ]; then
+ sleep "$delay"
+ fi
+ fi
+}
+
+shell_quote() {
+ printf '%q' "$1"
+}
+
+popup_icon() {
+ local operation="$1"
+
+ case "$operation" in
+ save)
+ printf "\\Uf10e9"
+ ;;
+ restore)
+ printf "\\Uf10ed"
+ ;;
+ esac
+}
+
+popup_header() {
+ local operation="$1"
+ local label="$2"
+
+ if is_popup_output; then
+ printf '%s %s\n' "$(popup_icon "$operation")" "$label" | ts '[%F %.T]'
+ fi
+}
+
# Ensures a message is displayed for 5 seconds in tmux prompt.
# Does not override the 'display-time' tmux option.
display_message() {
local message="$1"
+ if is_popup_output; then
+ popup_log "$message"
+ return
+ fi
+
# display_duration defaults to 5 seconds, if not passed as an argument
if [ "$#" -eq 2 ]; then
local display_duration="$2"
@@ -52,7 +146,7 @@ display_message() {
supported_tmux_version_ok() {
- $CURRENT_DIR/check_tmux_version.sh "$SUPPORTED_VERSION"
+ RESURRECT_OUTPUT_MODE="${SCRIPT_OUTPUT:-}" "$CURRENT_DIR/check_tmux_version.sh" "$SUPPORTED_VERSION"
}
remove_first_char() {
diff --git a/scripts/restore.sh b/scripts/restore.sh
index 1a5e3f98..a3a14b89 100755
--- a/scripts/restore.sh
+++ b/scripts/restore.sh
@@ -249,6 +249,7 @@ detect_if_restoring_from_scratch() {
local total_number_of_panes="$(tmux list-panes -a | wc -l | sed 's/ //g')"
if [ "$total_number_of_panes" -eq 1 ]; then
restore_from_scratch_true
+ popup_log "Only one pane is currently open; restore will reuse it"
fi
}
@@ -256,6 +257,7 @@ detect_if_restoring_pane_contents() {
if capture_pane_contents_option_on; then
cache_tmux_default_command
restore_pane_contents_true
+ popup_log "Restoring saved pane contents"
fi
}
@@ -265,6 +267,7 @@ restore_all_panes() {
detect_if_restoring_from_scratch # sets a global variable
detect_if_restoring_pane_contents # sets a global variable
if is_restoring_pane_contents; then
+ popup_log "Extracting archived pane contents"
pane_content_files_restore_from_archive
fi
while read line; do
@@ -364,24 +367,48 @@ cleanup_restored_pane_contents() {
}
main() {
- if supported_tmux_version_ok && check_saved_session_exists; then
- start_spinner "Restoring..." "Tmux restore complete!"
- execute_hook "pre-restore-all"
- restore_all_panes
- handle_session_0
- restore_window_properties >/dev/null 2>&1
- execute_hook "pre-restore-pane-processes"
- restore_all_pane_processes
- # below functions restore exact cursor positions
- restore_active_pane_for_each_window
- restore_zoomed_windows
- restore_grouped_sessions # also restores active and alt windows for grouped sessions
- restore_active_and_alternate_windows
- restore_active_and_alternate_sessions
- cleanup_restored_pane_contents
- execute_hook "post-restore-all"
+ if ! supported_tmux_version_ok; then
+ wait_for_popup_close
+ return 1
+ fi
+
+ if ! check_saved_session_exists; then
+ wait_for_popup_close
+ return 1
+ fi
+
+ if [ "${SCRIPT_OUTPUT:-}" != "quiet" ]; then
+ popup_header "restore" "loading..."
+ start_spinner "restoring..." "Tmux restore complete!"
+ fi
+ popup_log "Loading restore data from $(last_resurrect_file)"
+
+ popup_log "Running pre-restore-all hook"
+ execute_hook "pre-restore-all"
+ popup_log "Restoring panes and sessions"
+ restore_all_panes
+ handle_session_0
+ popup_log "Restoring window layouts and names"
+ restore_window_properties >/dev/null 2>&1
+ popup_log "Running pre-restore-pane-processes hook"
+ execute_hook "pre-restore-pane-processes"
+ popup_log "Restoring pane processes"
+ restore_all_pane_processes
+ popup_log "Restoring active panes, zoom state, grouped sessions, and client state"
+ # below functions restore exact cursor positions
+ restore_active_pane_for_each_window
+ restore_zoomed_windows
+ restore_grouped_sessions # also restores active and alt windows for grouped sessions
+ restore_active_and_alternate_windows
+ restore_active_and_alternate_sessions
+ popup_log "Cleaning temporary restore artifacts"
+ cleanup_restored_pane_contents
+ popup_log "Running post-restore-all hook"
+ execute_hook "post-restore-all"
+ if [ "${SCRIPT_OUTPUT:-}" != "quiet" ]; then
stop_spinner
display_message "Tmux restore complete!"
fi
+ wait_for_popup_close
}
main
diff --git a/scripts/run_with_popup.sh b/scripts/run_with_popup.sh
new file mode 100755
index 00000000..a32ee027
--- /dev/null
+++ b/scripts/run_with_popup.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+
+CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+source "$CURRENT_DIR/variables.sh"
+source "$CURRENT_DIR/helpers.sh"
+
+action="$1"
+script_path=""
+title=""
+
+case "$action" in
+ save)
+ script_path="$CURRENT_DIR/save.sh"
+ title="tmux-resurrect save"
+ ;;
+ restore)
+ script_path="$CURRENT_DIR/restore.sh"
+ title="tmux-resurrect restore"
+ ;;
+ *)
+ exit 1
+ ;;
+esac
+
+run_command() {
+ "$script_path"
+}
+
+run_popup() {
+ local popup_width="$(get_tmux_option "$popup_width_option" "$default_popup_width")"
+ local popup_height="$(get_tmux_option "$popup_height_option" "$default_popup_height")"
+ local popup_command="$(shell_quote "$script_path") popup"
+
+ tmux display-popup -T "$title" -w "$popup_width" -h "$popup_height" -E "$popup_command"
+}
+
+main() {
+ if should_use_popup; then
+ run_popup
+ else
+ run_command
+ fi
+}
+main
diff --git a/scripts/save.sh b/scripts/save.sh
index 01edcdeb..3dbfbcb8 100755
--- a/scripts/save.sh
+++ b/scripts/save.sh
@@ -12,6 +12,7 @@ delimiter=$'\t'
# if "quiet" script produces no output
SCRIPT_OUTPUT="$1"
+SAVE_RESULT_DETAIL=""
grouped_sessions_format() {
local format
@@ -238,41 +239,64 @@ remove_old_backups() {
save_all() {
local resurrect_file_path="$(resurrect_file_path)"
local last_resurrect_file="$(last_resurrect_file)"
+ popup_log "Writing tmux snapshot to $resurrect_file_path"
mkdir -p "$(resurrect_dir)"
+ popup_log "dumping grouped sessions..."
fetch_and_dump_grouped_sessions > "$resurrect_file_path"
+ popup_log "dumping panes..."
dump_panes >> "$resurrect_file_path"
+ popup_log "dumping windows..."
dump_windows >> "$resurrect_file_path"
+ popup_log "dumping state..."
dump_state >> "$resurrect_file_path"
+ popup_log "running post-save-layout hook..."
execute_hook "post-save-layout" "$resurrect_file_path"
if files_differ "$resurrect_file_path" "$last_resurrect_file"; then
ln -fs "$(basename "$resurrect_file_path")" "$last_resurrect_file"
+ SAVE_RESULT_DETAIL="Save written to $resurrect_file_path"
else
rm "$resurrect_file_path"
+ SAVE_RESULT_DETAIL="No changes detected; existing save was kept"
fi
if capture_pane_contents_option_on; then
+ popup_log "Capturing pane contents"
mkdir -p "$(pane_contents_dir "save")"
dump_pane_contents
pane_contents_create_archive
rm "$(pane_contents_dir "save")"/*
fi
+ popup_log "Pruning expired backup files"
remove_old_backups
+ popup_log "Running post-save-all hook"
execute_hook "post-save-all"
}
-show_output() {
- [ "$SCRIPT_OUTPUT" != "quiet" ]
+is_quiet() {
+ [ "$SCRIPT_OUTPUT" = "quiet" ]
+}
+
+show_any_output() {
+ ! is_quiet
}
main() {
- if supported_tmux_version_ok; then
- if show_output; then
- start_spinner "Saving..." "Tmux environment saved!"
- fi
- save_all
- if show_output; then
- stop_spinner
- display_message "Tmux environment saved!"
- fi
+ if ! supported_tmux_version_ok; then
+ wait_for_popup_close
+ return 1
+ fi
+
+ if show_any_output; then
+ popup_header "save" "saving..."
+ start_spinner "saving..." "Tmux environment saved!"
+ fi
+ save_all
+ if is_popup_output && [ -n "$SAVE_RESULT_DETAIL" ]; then
+ popup_log "$SAVE_RESULT_DETAIL"
+ fi
+ if show_any_output; then
+ stop_spinner
+ display_message "Tmux environment saved!"
fi
+ wait_for_popup_close
}
main
diff --git a/scripts/spinner_helpers.sh b/scripts/spinner_helpers.sh
index fe73cd70..ae46f8b6 100644
--- a/scripts/spinner_helpers.sh
+++ b/scripts/spinner_helpers.sh
@@ -1,8 +1,93 @@
start_spinner() {
+ if is_popup_output; then
+ return
+ fi
$CURRENT_DIR/tmux_spinner.sh "$1" "$2" &
export SPINNER_PID=$!
}
stop_spinner() {
+ if is_popup_output; then
+ return
+ fi
kill $SPINNER_PID
}
+
+spinner_wave_label() {
+ local message="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
+
+ case "$message" in
+ "saving..."|"restoring...")
+ echo "$message"
+ ;;
+ *)
+ echo ""
+ ;;
+ esac
+}
+
+spinner_wave_message() {
+ local message="$1"
+ local frame="$2"
+ local rendered=""
+ local message_length="${#message}"
+ local index=0
+ local char=""
+
+ if [ -z "$message" ]; then
+ return
+ fi
+
+ while [ "$index" -lt "${#message}" ]; do
+ char="${message:$index:1}"
+ if [ "$char" = " " ]; then
+ rendered="${rendered}${char}"
+ else
+ local color="$(spinner_wave_color "$frame" "$index" "$message_length")"
+ rendered="${rendered}#[fg=colour${color}]${char}"
+ fi
+ index=$((index + 1))
+ done
+
+ printf '%s#[default]' "$rendered"
+}
+
+spinner_wave_color() {
+ local frame="$1"
+ local index="$2"
+ local message_length="$3"
+ local min_color=240
+ local max_color=253
+ local gradient_width=5
+ local pause_frames=10
+ local half_width=$((gradient_width / 2))
+ local pass_frames=$((message_length + gradient_width))
+ local cycle_frames=$((pass_frames + pause_frames))
+ local active_frame=0
+ local center=0
+ local distance=0
+
+ if [ -z "$message_length" ] || [ "$message_length" -le 0 ]; then
+ echo "$max_color"
+ return
+ fi
+
+ active_frame=$((frame % cycle_frames))
+ if [ "$active_frame" -ge "$pass_frames" ]; then
+ echo "$max_color"
+ return
+ fi
+
+ center=$((active_frame - half_width))
+ distance=$((index - center))
+ if [ "$distance" -lt 0 ]; then
+ distance=$(( -distance ))
+ fi
+
+ if [ "$distance" -gt "$half_width" ]; then
+ echo "$max_color"
+ return
+ fi
+
+ echo $((min_color + (distance * (max_color - min_color) / half_width)))
+}
diff --git a/scripts/tmux_spinner.sh b/scripts/tmux_spinner.sh
index 9b1b9792..a4e06919 100755
--- a/scripts/tmux_spinner.sh
+++ b/scripts/tmux_spinner.sh
@@ -12,17 +12,133 @@
# ..
# kill $SPINNER_PID # Stops spinner and displays 'End message!'
+CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+source "$CURRENT_DIR/helpers.sh"
+source "$CURRENT_DIR/spinner_helpers.sh"
+source "$CURRENT_DIR/variables.sh"
+
MESSAGE="$1"
END_MESSAGE="$2"
-SPIN='-\|/'
+DEFAULT_SPIN_CHARS='-\|/'
+SPIN_CHARS=$( get_tmux_option "$spinner_chars_option" "$DEFAULT_SPIN_CHARS" )
+SPIN_CHARS_LENGTH=$( echo -n "$SPIN_CHARS" | wc -m )
+WAVE_LABEL="$(spinner_wave_label "$MESSAGE")"
+CONTROL_MODE_FIFO=""
+CONTROL_MODE_DIR=""
+CONTROL_MODE_PID=""
+CONTROL_MODE_READY="false"
+TARGET_CLIENT_TTY=""
+
+tmux_quote() {
+ printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
+}
+
+tmux_control_target_client() {
+ tmux display-message -p -F "#{client_tty}" 2>/dev/null
+}
+
+tmux_control_start() {
+ TARGET_CLIENT_TTY="$(tmux_control_target_client)"
+
+ if [ -z "$TARGET_CLIENT_TTY" ]; then
+ return 1
+ fi
+
+ CONTROL_MODE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/tmux-resurrect-spinner.XXXXXX" 2>/dev/null)"
+ if [ -z "$CONTROL_MODE_DIR" ]; then
+ return 1
+ fi
+
+ CONTROL_MODE_FIFO="$CONTROL_MODE_DIR/control-mode"
+ if ! mkfifo "$CONTROL_MODE_FIFO"; then
+ rmdir "$CONTROL_MODE_DIR"
+ CONTROL_MODE_DIR=""
+ return 1
+ fi
+
+ tmux -C <"$CONTROL_MODE_FIFO" >/dev/null 2>&1 &
+ CONTROL_MODE_PID="$!"
+
+ if ! exec 3>"$CONTROL_MODE_FIFO"; then
+ kill "$CONTROL_MODE_PID" 2>/dev/null
+ wait "$CONTROL_MODE_PID" 2>/dev/null
+ rm -f "$CONTROL_MODE_FIFO"
+ rmdir "$CONTROL_MODE_DIR"
+ CONTROL_MODE_FIFO=""
+ CONTROL_MODE_DIR=""
+ CONTROL_MODE_PID=""
+ return 1
+ fi
+
+ CONTROL_MODE_READY="true"
+}
+
+tmux_control_send() {
+ local command="$1"
+
+ if [ "$CONTROL_MODE_READY" = "true" ] && [ -n "$CONTROL_MODE_PID" ]; then
+ printf '%s\n' "$command" >&3 2>/dev/null || return 1
+ return 0
+ fi
+
+ return 1
+}
+
+tmux_send_display_message() {
+ local message="$1"
+ local quoted_message="$(tmux_quote "$message")"
+
+ if tmux_control_send "display-message -c $(tmux_quote "$TARGET_CLIENT_TTY") $quoted_message"; then
+ return 0
+ fi
+
+ tmux display-message "$message"
+}
+
+cleanup() {
+ local final_message="$1"
+
+ if [ -n "$final_message" ]; then
+ tmux_send_display_message "$final_message"
+ fi
+
+ if [ "$CONTROL_MODE_READY" = "true" ]; then
+ exec 3>&-
+ CONTROL_MODE_READY="false"
+ fi
+
+ if [ -n "$CONTROL_MODE_PID" ]; then
+ wait "$CONTROL_MODE_PID" 2>/dev/null
+ fi
+
+ if [ -n "$CONTROL_MODE_FIFO" ]; then
+ rm -f "$CONTROL_MODE_FIFO"
+ fi
+
+ if [ -n "$CONTROL_MODE_DIR" ]; then
+ rmdir "$CONTROL_MODE_DIR" 2>/dev/null
+ fi
+}
+
+handle_exit() {
+ cleanup "$END_MESSAGE"
+ exit
+}
-trap "tmux display-message '$END_MESSAGE'; exit" SIGINT SIGTERM
+trap "handle_exit" SIGINT SIGTERM
main() {
local i=0
+ tmux_control_start
while true; do
- i=$(( (i+1) %4 ))
- tmux display-message " ${SPIN:$i:1} $MESSAGE"
+ if [ -n "$WAVE_LABEL" ]; then
+ tmux_send_display_message "$(spinner_wave_message "$WAVE_LABEL" "$i")"
+ i=$((i + 1))
+ else
+ i=$(( (i+1) % $SPIN_CHARS_LENGTH ))
+ tmux_send_display_message " ${SPIN_CHARS:$i:1} $MESSAGE"
+ fi
sleep 0.1
done
}
diff --git a/scripts/variables.sh b/scripts/variables.sh
index 9d42e02a..df1f03be 100644
--- a/scripts/variables.sh
+++ b/scripts/variables.sh
@@ -46,3 +46,14 @@ hook_prefix="@resurrect-hook-"
delete_backup_after_option="@resurrect-delete-backup-after"
default_delete_backup_after="30" # days
+
+spinner_chars_option="@resurrect-spinner-chars"
+
+popup_mode_option="@resurrect-popup"
+default_popup_mode="auto"
+popup_height_option="@resurrect-popup-height"
+default_popup_height="80%"
+popup_width_option="@resurrect-popup-width"
+default_popup_width="80%"
+popup_close_delay_option="@resurrect-popup-close-delay"
+default_popup_close_delay="1"