restore.sh 12.5 KB
Newer Older
Bruno Sutic's avatar
Bruno Sutic committed
1
2
3
4
#!/usr/bin/env bash

CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

Bruno Sutic's avatar
Bruno Sutic committed
5
source "$CURRENT_DIR/variables.sh"
Bruno Sutic's avatar
Bruno Sutic committed
6
source "$CURRENT_DIR/helpers.sh"
7
source "$CURRENT_DIR/process_restore_helpers.sh"
8
source "$CURRENT_DIR/spinner_helpers.sh"
Bruno Sutic's avatar
Bruno Sutic committed
9

10
11
12
# delimiter
d=$'\t'

13
# Global variable.
14
# Used during the restore: if a pane already exists from before, it is
15
16
17
18
# saved in the array in this variable. Later, process running in existing pane
# is also not restored. That makes the restoration process more idempotent.
EXISTING_PANES_VAR=""

19
20
RESTORING_FROM_SCRATCH="false"

21
22
RESTORE_PANE_CONTENTS="false"

23
24
25
26
27
28
29
is_line_type() {
	local line_type="$1"
	local line="$2"
	echo "$line" |
		\grep -q "^$line_type"
}

30
check_saved_session_exists() {
31
32
33
	local resurrect_file="$(last_resurrect_file)"
	if [ ! -f $resurrect_file ]; then
		display_message "Tmux resurrect file not found!"
34
		return 1
35
36
37
	fi
}

38
39
40
41
42
43
44
45
pane_exists() {
	local session_name="$1"
	local window_number="$2"
	local pane_index="$3"
	tmux list-panes -t "${session_name}:${window_number}" -F "#{pane_index}" 2>/dev/null |
		\grep -q "^$pane_index$"
}

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
register_existing_pane() {
	local session_name="$1"
	local window_number="$2"
	local pane_index="$3"
	local pane_custom_id="${session_name}:${window_number}:${pane_index}"
	local delimiter=$'\t'
	EXISTING_PANES_VAR="${EXISTING_PANES_VAR}${delimiter}${pane_custom_id}"
}

is_pane_registered_as_existing() {
	local session_name="$1"
	local window_number="$2"
	local pane_index="$3"
	local pane_custom_id="${session_name}:${window_number}:${pane_index}"
	[[ "$EXISTING_PANES_VAR" =~ "$pane_custom_id" ]]
}

63
64
65
66
67
68
69
70
restore_from_scratch_true() {
	RESTORING_FROM_SCRATCH="true"
}

is_restoring_from_scratch() {
	[ "$RESTORING_FROM_SCRATCH" == "true" ]
}

71
72
73
74
75
76
77
78
restore_pane_contents_true() {
	RESTORE_PANE_CONTENTS="true"
}

is_restoring_pane_contents() {
	[ "$RESTORE_PANE_CONTENTS" == "true" ]
}

79
80
81
82
83
84
85
86
restored_session_0_true() {
	RESTORED_SESSION_0="true"
}

has_restored_session_0() {
	[ "$RESTORED_SESSION_0" == "true" ]
}

Bruno Sutic's avatar
Bruno Sutic committed
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
window_exists() {
	local session_name="$1"
	local window_number="$2"
	tmux list-windows -t "$session_name" -F "#{window_index}" 2>/dev/null |
		\grep -q "^$window_number$"
}

session_exists() {
	local session_name="$1"
	tmux has-session -t "$session_name" 2>/dev/null
}

first_window_num() {
	tmux show -gv base-index
}

tmux_socket() {
	echo $TMUX | cut -d',' -f1
}

107
108
109
110
# Tmux option stored in a global variable so that we don't have to "ask"
# tmux server each time.
cache_tmux_default_command() {
	local default_shell="$(get_tmux_option "default-shell" "")"
111
112
113
114
115
	local opt=""
	if [ "$(basename "$default_shell")" == "bash" ]; then
		opt="-l "
	fi
	export TMUX_DEFAULT_COMMAND="$(get_tmux_option "default-command" "$opt$default_shell")"
116
117
118
119
120
121
122
}

tmux_default_command() {
	echo "$TMUX_DEFAULT_COMMAND"
}

pane_creation_command() {
123
	echo "cat '$(pane_contents_file "restore" "${1}:${2}.${3}")'; exec $(tmux_default_command)"
124
125
}

Bruno Sutic's avatar
Bruno Sutic committed
126
127
128
129
130
new_window() {
	local session_name="$1"
	local window_number="$2"
	local window_name="$3"
	local dir="$4"
131
	local pane_index="$5"
132
133
	local pane_id="${session_name}:${window_number}.${pane_index}"
	if is_restoring_pane_contents && pane_contents_file_exists "$pane_id"; then
134
135
136
137
138
		local pane_creation_command="$(pane_creation_command "$session_name" "$window_number" "$pane_index")"
		tmux new-window -d -t "${session_name}:${window_number}" -n "$window_name" -c "$dir" "$pane_creation_command"
	else
		tmux new-window -d -t "${session_name}:${window_number}" -n "$window_name" -c "$dir"
	fi
Bruno Sutic's avatar
Bruno Sutic committed
139
140
141
142
143
144
145
}

new_session() {
	local session_name="$1"
	local window_number="$2"
	local window_name="$3"
	local dir="$4"
146
	local pane_index="$5"
147
148
	local pane_id="${session_name}:${window_number}.${pane_index}"
	if is_restoring_pane_contents && pane_contents_file_exists "$pane_id"; then
149
150
151
152
153
		local pane_creation_command="$(pane_creation_command "$session_name" "$window_number" "$pane_index")"
		TMUX="" tmux -S "$(tmux_socket)" new-session -d -s "$session_name" -n "$window_name" -c "$dir" "$pane_creation_command"
	else
		TMUX="" tmux -S "$(tmux_socket)" new-session -d -s "$session_name" -n "$window_name" -c "$dir"
	fi
Bruno Sutic's avatar
Bruno Sutic committed
154
155
156
157
158
159
160
161
162
163
164
165
	# change first window number if necessary
	local created_window_num="$(first_window_num)"
	if [ $created_window_num -ne $window_number ]; then
		tmux move-window -s "${session_name}:${created_window_num}" -t "${session_name}:${window_number}"
	fi
}

new_pane() {
	local session_name="$1"
	local window_number="$2"
	local window_name="$3"
	local dir="$4"
166
	local pane_index="$5"
167
168
	local pane_id="${session_name}:${window_number}.${pane_index}"
	if is_restoring_pane_contents && pane_contents_file_exists "$pane_id"; then
169
170
171
172
173
		local pane_creation_command="$(pane_creation_command "$session_name" "$window_number" "$pane_index")"
		tmux split-window -t "${session_name}:${window_number}" -c "$dir" "$pane_creation_command"
	else
		tmux split-window -t "${session_name}:${window_number}" -c "$dir"
	fi
174
	tmux rename-window -t "${session_name}:${window_number}" "$window_name"
175
176
	# minimize window so more panes can fit
	tmux resize-pane  -t "${session_name}:${window_number}" -U "999"
Bruno Sutic's avatar
Bruno Sutic committed
177
178
179
180
}

restore_pane() {
	local pane="$1"
181
	while IFS=$d read line_type session_name window_number window_name window_active window_flags pane_index dir pane_active pane_command pane_full_command; do
182
		dir="$(remove_first_char "$dir")"
Bruno Sutic's avatar
Bruno Sutic committed
183
184
		window_name="$(remove_first_char "$window_name")"
		pane_full_command="$(remove_first_char "$pane_full_command")"
185
186
187
		if [ "$session_name" == "0" ]; then
			restored_session_0_true
		fi
188
		if pane_exists "$session_name" "$window_number" "$pane_index"; then
189
			tmux rename-window -t "$window_number" "$window_name"
190
191
192
193
			if is_restoring_from_scratch; then
				# overwrite the pane
				# happens only for the first pane if it's the only registered pane for the whole tmux server
				local pane_id="$(tmux display-message -p -F "#{pane_id}" -t "$session_name:$window_number")"
194
				new_pane "$session_name" "$window_number" "$window_name" "$dir" "$pane_index"
195
196
197
198
199
200
				tmux kill-pane -t "$pane_id"
			else
				# Pane exists, no need to create it!
				# Pane existence is registered. Later, its process also won't be restored.
				register_existing_pane "$session_name" "$window_number" "$pane_index"
			fi
201
		elif window_exists "$session_name" "$window_number"; then
202
			tmux rename-window -t "$window_number" "$window_name"
203
			new_pane "$session_name" "$window_number" "$window_name" "$dir" "$pane_index"
Bruno Sutic's avatar
Bruno Sutic committed
204
		elif session_exists "$session_name"; then
205
			new_window "$session_name" "$window_number" "$window_name" "$dir" "$pane_index"
Bruno Sutic's avatar
Bruno Sutic committed
206
		else
207
			new_session "$session_name" "$window_number" "$window_name" "$dir" "$pane_index"
Bruno Sutic's avatar
Bruno Sutic committed
208
		fi
209
	done < <(echo "$pane")
Bruno Sutic's avatar
Bruno Sutic committed
210
211
}

212
213
214
restore_state() {
	local state="$1"
	echo "$state" |
215
	while IFS=$d read line_type client_session client_last_session; do
216
217
218
219
220
		tmux switch-client -t "$client_last_session"
		tmux switch-client -t "$client_session"
	done
}

221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
restore_grouped_session() {
	local grouped_session="$1"
	echo "$grouped_session" |
	while IFS=$d read line_type grouped_session original_session alternate_window active_window; do
		TMUX="" tmux -S "$(tmux_socket)" new-session -d -s "$grouped_session" -t "$original_session"
	done
}

restore_active_and_alternate_windows_for_grouped_sessions() {
	local grouped_session="$1"
	echo "$grouped_session" |
	while IFS=$d read line_type grouped_session original_session alternate_window_index active_window_index; do
		alternate_window_index="$(remove_first_char "$alternate_window_index")"
		active_window_index="$(remove_first_char "$active_window_index")"
		if [ -n "$alternate_window_index" ]; then
			tmux switch-client -t "${grouped_session}:${alternate_window_index}"
		fi
		if [ -n "$active_window_index" ]; then
			tmux switch-client -t "${grouped_session}:${active_window_index}"
		fi
	done
}

244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
never_ever_overwrite() {
	local overwrite_option_value="$(get_tmux_option "$overwrite_option" "")"
	[ -n "$overwrite_option_value" ]
}

detect_if_restoring_from_scratch() {
	if never_ever_overwrite; then
		return
	fi
	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
	fi
}

259
260
261
262
263
264
265
detect_if_restoring_pane_contents() {
	if capture_pane_contents_option_on; then
		cache_tmux_default_command
		restore_pane_contents_true
	fi
}

266
267
# functions called from main (ordered)

268
restore_all_panes() {
269
270
	detect_if_restoring_from_scratch   # sets a global variable
	detect_if_restoring_pane_contents  # sets a global variable
271
272
273
	if is_restoring_pane_contents; then
		pane_content_files_restore_from_archive
	fi
Bruno Sutic's avatar
Bruno Sutic committed
274
	while read line; do
275
276
		if is_line_type "pane" "$line"; then
			restore_pane "$line"
Bruno Sutic's avatar
Bruno Sutic committed
277
		fi
278
	done < $(last_resurrect_file)
279
	if is_restoring_pane_contents; then
280
		rm "$(pane_contents_dir "restore")"/*
281
	fi
Bruno Sutic's avatar
Bruno Sutic committed
282
283
}

284
285
286
287
288
289
290
291
292
293
handle_session_0() {
	if is_restoring_from_scratch && ! has_restored_session_0; then
		local current_session="$(tmux display -p "#{client_session}")"
		if [ "$current_session" == "0" ]; then
			tmux switch-client -n
		fi
		tmux kill-session -t "0"
	fi
}

294
295
296
297
298
299
300
restore_pane_layout_for_each_window() {
	\grep '^window' $(last_resurrect_file) |
		while IFS=$d read line_type session_name window_number window_active window_flags window_layout; do
			tmux select-layout -t "${session_name}:${window_number}" "$window_layout"
		done
}

301
302
restore_shell_history() {
	awk 'BEGIN { FS="\t"; OFS="\t" } /^pane/ { print $2, $3, $7, $10; }' $(last_resurrect_file) |
303
		while IFS=$d read session_name window_number pane_index pane_command; do
quentin's avatar
quentin committed
304
			if ! is_pane_registered_as_existing "$session_name" "$window_number" "$pane_index"; then
305
306
307
308
309
				local pane_id="$session_name:$window_number.$pane_index"
				local history_file="$(resurrect_history_file "$pane_id" "$pane_command")"

				if [ "$pane_command" = "bash" ]; then
					local read_command="history -r '$history_file'"
310
					tmux send-keys -t "$pane_id" "$read_command" C-m
311
312
313
314
				elif [ "$pane_command" = "zsh" ]; then
					local accept_line="$(expr "$(zsh -i -c bindkey | grep -m1 '\saccept-line$')" : '^"\(.*\)".*')"
					local read_command="fc -R '$history_file'; clear"
					tmux send-keys -t "$pane_id" "$read_command" "$accept_line"
315
316
317
318
319
				fi
			fi
		done
}

Bruno Sutic's avatar
Bruno Sutic committed
320
restore_all_pane_processes() {
321
322
	if restore_pane_processes_enabled; then
		local pane_full_command
323
		awk 'BEGIN { FS="\t"; OFS="\t" } /^pane/ && $11 !~ "^:$" { print $2, $3, $7, $8, $11; }' $(last_resurrect_file) |
324
			while IFS=$d read -r session_name window_number pane_index dir pane_full_command; do
325
				dir="$(remove_first_char "$dir")"
326
				pane_full_command="$(remove_first_char "$pane_full_command")"
327
				restore_pane_process "$pane_full_command" "$session_name" "$window_number" "$pane_index" "$dir"
328
329
			done
	fi
Bruno Sutic's avatar
Bruno Sutic committed
330
331
}

Bruno Sutic's avatar
Bruno Sutic committed
332
restore_active_pane_for_each_window() {
333
	awk 'BEGIN { FS="\t"; OFS="\t" } /^pane/ && $9 == 1 { print $2, $3, $7; }' $(last_resurrect_file) |
334
		while IFS=$d read session_name window_number active_pane; do
Bruno Sutic's avatar
Bruno Sutic committed
335
336
337
338
339
			tmux switch-client -t "${session_name}:${window_number}"
			tmux select-pane -t "$active_pane"
		done
}

340
341
342
343
344
345
346
restore_zoomed_windows() {
	awk 'BEGIN { FS="\t"; OFS="\t" } /^pane/ && $6 ~ /Z/ && $9 == 1 { print $2, $3; }' $(last_resurrect_file) |
		while IFS=$d read session_name window_number; do
			tmux resize-pane -t "${session_name}:${window_number}" -Z
		done
}

Bruno Sutic's avatar
Bruno Sutic committed
347
348
349
350
restore_grouped_sessions() {
	while read line; do
		if is_line_type "grouped_session" "$line"; then
			restore_grouped_session "$line"
351
			restore_active_and_alternate_windows_for_grouped_sessions "$line"
Bruno Sutic's avatar
Bruno Sutic committed
352
353
354
355
		fi
	done < $(last_resurrect_file)
}

356
restore_active_and_alternate_windows() {
357
	awk 'BEGIN { FS="\t"; OFS="\t" } /^window/ && $5 ~ /[*-]/ { print $2, $4, $3; }' $(last_resurrect_file) |
358
		sort -u |
359
		while IFS=$d read session_name active_window window_number; do
360
361
362
363
			tmux switch-client -t "${session_name}:${window_number}"
		done
}

Bruno Sutic's avatar
Bruno Sutic committed
364
365
366
restore_active_and_alternate_sessions() {
	while read line; do
		if is_line_type "state" "$line"; then
367
368
			restore_state "$line"
		fi
369
	done < $(last_resurrect_file)
Bruno Sutic's avatar
Bruno Sutic committed
370
371
372
}

main() {
373
	if supported_tmux_version_ok && check_saved_session_exists; then
374
		start_spinner "Restoring..." "Tmux restore complete!"
Ash Berlin-Taylor's avatar
Ash Berlin-Taylor committed
375
		execute_hook "pre-restore-all"
376
		restore_all_panes
377
		handle_session_0
Bruno Sutic's avatar
Bruno Sutic committed
378
		restore_pane_layout_for_each_window >/dev/null 2>&1
Ash Berlin-Taylor's avatar
Ash Berlin-Taylor committed
379
		execute_hook "pre-restore-history"
380
		if save_shell_history_option_on; then
381
382
			restore_shell_history
		fi
Ash Berlin-Taylor's avatar
Ash Berlin-Taylor committed
383
		execute_hook "pre-restore-pane-processes"
384
		restore_all_pane_processes
Bruno Sutic's avatar
Bruno Sutic committed
385
		# below functions restore exact cursor positions
Bruno Sutic's avatar
Bruno Sutic committed
386
		restore_active_pane_for_each_window
Bruno Sutic's avatar
Bruno Sutic committed
387
		restore_zoomed_windows
388
		restore_grouped_sessions  # also restores active and alt windows for grouped sessions
389
		restore_active_and_alternate_windows
Bruno Sutic's avatar
Bruno Sutic committed
390
		restore_active_and_alternate_sessions
Ash Berlin-Taylor's avatar
Ash Berlin-Taylor committed
391
		execute_hook "post-restore-all"
392
		stop_spinner
393
		display_message "Tmux restore complete!"
394
	fi
Bruno Sutic's avatar
Bruno Sutic committed
395
396
}
main