all repos

tmux-stare @ 29a70cd

session manager, but my session manager

tmux-stare/scripts/sessions.sh (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix session restore, 1 month ago
1
#!/usr/bin/env bash
2
CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3
source "$CURRENT_DIR/helpers.sh"
4
5
declare S=$'\t'
6
7
# === common
8
get_current_session_name() {
9
  if [ "$(tmux display-message -p "#{session_grouped}")" = 0 ]; then
10
    tmux display-message -p "#{session_name}" 2>/dev/null || true
11
  else
12
    tmux display-message -p "#{session_group}" 2>/dev/null || true
13
  fi
14
}
15
16
session_id_map_file() { echo "$(get_opt_dir)/.session_ids" }
17
sync_session_id_map() {
18
  local map_file="$(session_id_map_file)"
19
  tmux list-sessions -F "#{session_id}${S}#{session_name}" >"$map_file"
20
}
21
22
get_session_id_by_name() {
23
  local session_name="$1"
24
  tmux display-message -p -t "$session_name" "#{session_id}" 2>/dev/null
25
}
26
27
get_session_name_by_id() {
28
  local session_id="$1"
29
  local map_file="$(session_id_map_file)"
30
  [[ -f "$map_file" ]] || return 1
31
  awk -F"$S" -v session_id="$session_id" '$1 == session_id { print $2; exit }' "$map_file"
32
}
33
34
upsert_session_id_entry() {
35
  local session_id="$1"
36
  local session_name="$2"
37
  local map_file="$(session_id_map_file)"
38
  local tmp_file="${map_file}.tmp"
39
40
  [[ -z "$session_id" || -z "$session_name" ]] && return 1
41
  [[ -f "$map_file" ]] || : >"$map_file"
42
43
  awk -F"$S" -v OFS="$S" -v session_id="$session_id" -v session_name="$session_name" '
44
    $1 == session_id {
45
      $2 = session_name
46
      found = 1
47
    }
48
    { print }
49
    END {
50
      if (!found) print session_id, session_name
51
    }
52
  ' "$map_file" >"$tmp_file" && mv "$tmp_file" "$map_file"
53
}
54
55
remember_session_identity() {
56
  local session_name="$1"
57
  local session_id
58
  session_id="$(get_session_id_by_name "$session_name")"
59
  [[ -n "$session_id" ]] && upsert_session_id_entry "$session_id" "$session_name"
60
}
61
62
rename_session_files() {
63
  local old="$1"
64
  local new="$2"
65
  local dir="$(get_opt_dir)"
66
  local old_last="${dir}/${old}_last"
67
  local new_last="${dir}/${new}_last"
68
  local global_last="${dir}/last"
69
70
  [[ -z "$old" || -z "$new" || "$old" == "$new" ]] && return 1
71
  [[ -e "$old_last" ]] || return 0
72
  [[ -e "$new_last" ]] && return 1
73
74
  if [[ -L "$old_last" ]]; then
75
    local actual="$(readlink "$old_last")"
76
    local actual_name
77
    local new_actual
78
    [[ "$actual" != /* ]] && actual="${dir}/${actual}"
79
80
    actual_name="$(basename "$actual")"
81
    if [[ "$actual_name" == "${old}_"* && -e "$actual" ]]; then
82
      new_actual="${dir}/${new}_${actual_name#${old}_}"
83
      mv "$actual" "$new_actual"
84
      ln -sf "$new_actual" "$new_last"
85
      rm -f "$old_last"
86
    else
87
      mv "$old_last" "$new_last"
88
    fi
89
  else
90
    mv "$old_last" "$new_last"
91
  fi
92
93
  if [[ -L "$global_last" ]]; then
94
    local global_target="$(readlink "$global_last")"
95
    local global_target_name="$(basename "$global_target")"
96
    if [[ "$global_target_name" == "${old}_last" ]]; then
97
      ln -sf "$new_last" "$global_last"
98
    fi
99
  fi
100
}
101
102
apply_session_rename() {
103
  local session_id="$1"
104
  local new_name="$2"
105
  local old_name="${3:-}"
106
107
  [[ -z "$session_id" || -z "$new_name" ]] && return 1
108
  [[ -n "$old_name" ]] || old_name="$(get_session_name_by_id "$session_id" || true)"
109
110
  if [[ -n "$old_name" && "$old_name" != "$new_name" ]]; then
111
    rename_session_files "$old_name" "$new_name"
112
  fi
113
114
  upsert_session_id_entry "$session_id" "$new_name"
115
}
116
117
rename_session() {
118
  local old="$1"
119
  local new="$2"
120
  local session_id
121
122
  [[ -z "$new" || "$old" == "$new" ]] && return 1
123
  [[ -e "$(get_opt_dir)/${new}_last" ]] && return 1
124
125
  tmux has-session -t "$new" 2>/dev/null && return 1
126
  session_id="$(get_session_id_by_name "$old")"
127
  [[ -n "$session_id" ]] || return 1
128
  tmux rename-session -t "$old" "$new" 2>/dev/null || return 1
129
  apply_session_rename "$session_id" "$new" "$old"
130
}
131
132
handle_session_renamed_hook() {
133
  local session_id="$1"
134
  local new_name="$2"
135
136
  [[ -z "$session_id" || -z "$new_name" ]] && return 1
137
  apply_session_rename "$session_id" "$new_name"
138
}
139
140
# === save
141
save_cwd() {
142
  local session_name="$1"
143
  local save_file="$2"
144
  tmux display-message -p -t "$session_name" -F "#{session_path}" >>"$save_file"
145
}
146
147
save_windows() {
148
  local session_name="$1"
149
  local save_file="$2"
150
  local format="window$S#{window_index}$S#{window_name}$S#{window_layout}$S#{window_active}"
151
  tmux list-windows -t "$session_name" -F "$format" >>"$save_file"
152
153
}
154
155
get_pane_child_pids() {
156
  local pane_pid="$1"
157
  ps -ao ppid=,pid= | awk -v pane_pid="$pane_pid" '$1 == pane_pid { print $2 }'
158
}
159
160
format_process_command() {
161
  local pid="$1"
162
  [[ -r "/proc/${pid}/cmdline" ]] || return 1
163
  xargs -0 bash -c 'printf "%q " "$0" "$@"' <"/proc/${pid}/cmdline" 2>/dev/null | sed 's/[[:space:]]*$//'
164
}
165
166
build_command_from_pids() {
167
  local pids="$1"
168
  local pid
169
  local formatted
170
  local command=""
171
  for pid in $pids; do
172
    formatted="$(format_process_command "$pid")"
173
    [[ -z "$formatted" ]] && continue
174
    [[ -n "$command" ]] && command+=" | "
175
    command+="$formatted"
176
  done
177
  printf "%s" "$command"
178
}
179
180
collect_process_names() {
181
  local pids="$1"
182
  local pid
183
  local first_arg
184
  local name
185
  for pid in $pids; do
186
    [[ -r "/proc/${pid}/cmdline" ]] || continue
187
    first_arg="$(tr '\0' '\n' <"/proc/${pid}/cmdline" 2>/dev/null | head -n1)"
188
    [[ -z "$first_arg" ]] && continue
189
    name="$(basename "$first_arg")"
190
    [[ -n "$name" ]] && printf "%s\n" "$name"
191
  done | sort -u
192
}
193
194
strip_prompt_prefix() {
195
  local line="$1"
196
  if [[ "$line" =~ [\$\#\%\>][[:space:]]+.+$ ]]; then
197
    sed -E 's/^.*[#$%>][[:space:]]+//' <<<"$line"
198
  fi
199
}
200
201
find_command_from_pane_history() {
202
  local pane_target="$1"
203
  local process_names="$2"
204
  local line
205
  local candidate
206
  local name
207
  while IFS= read -r line; do
208
    [[ -z "$line" ]] && continue
209
    candidate="$(strip_prompt_prefix "$line")"
210
    [[ -z "$candidate" ]] && continue
211
    while IFS= read -r name; do
212
      [[ -z "$name" ]] && continue
213
      if [[ "$candidate" == *"$name"* ]]; then
214
        printf "%s" "$candidate"
215
        return 0
216
      fi
217
    done <<<"$process_names"
218
  done < <(tmux capture-pane -pJ -S -200 -t "$pane_target" 2>/dev/null | tac)
219
}
220
221
save_panes() {
222
  local session_name="$1"
223
  local save_file="$2"
224
  local format="pane$S#{pane_index}$S#{pane_current_path}$S#{pane_active}$S#{window_index}$S#{pane_pid}"
225
  tmux list-panes -s -t "$session_name" -F "$format" |
226
    while IFS="$S" read -r line; do
227
      IFS="$S" read -r _ pane_index _ _ window_index pane_pid <<<"$line"
228
      pids="$(get_pane_child_pids "$pane_pid")"
229
      command="$(build_command_from_pids "$pids")"
230
231
      if [[ -n "$command" ]]; then
232
        process_names="$(collect_process_names "$pids")"
233
        pane_target="${session_name}:${window_index}.${pane_index}"
234
        pane_command="$(find_command_from_pane_history "$pane_target" "$process_names")"
235
        [[ -n "$pane_command" ]] && command="$pane_command"
236
      fi
237
238
      awk -v command="$command" \
239
        'BEGIN {FS=OFS="\t"} {$6=command; print}' \
240
        <<<"$line" >>"$save_file"
241
    done
242
}
243
244
link_session_last() {
245
  local save_file="$1"
246
  local last_file="$2"
247
  if ! cmp -s "$save_file" "$last_file"; then
248
    ln -sf "$save_file" "$last_file"
249
  else
250
    rm "$save_file"
251
  fi
252
}
253
254
link_last() {
255
  local save_file="$1"
256
  local save_dir="$2"
257
  ln -sf "$save_file" "$save_dir"/last
258
}
259
260
save_session() {
261
  local session_name="$1"
262
  local save_dir="$(get_opt_dir)"
263
  local save_file="${save_dir}/${session_name}_$(get_time)"
264
  local last_file="${save_dir}/${session_name}_last"
265
266
  save_cwd "$session_name" "$save_file"
267
  save_windows "$session_name" "$save_file"
268
  save_panes "$session_name" "$save_file"
269
  link_session_last "$save_file" "$last_file"
270
  link_last "$last_file" "$save_dir"
271
  remember_session_identity "$session_name"
272
}
273
274
save_all_sessions() {
275
  tmux list-sessions -F "#{session_name}" | while read -r session; do
276
    save_session "$session"
277
  done
278
279
  local current_session="$(get_current_session_name)"
280
  if [[ -n "$current_session" ]]; then
281
    link_last "$(get_opt_dir)/${current_session}_last" "$(get_opt_dir)"
282
  fi
283
}
284
285
unload_session() {
286
  local session_name="$1"
287
  save_session "$session_name"
288
  tmux kill-session -t "$session_name"
289
}
290
291
# === restore
292
restore_pane_processes_enabled() {
293
  local processes="$(get_opt_processes)"
294
  [[ "$processes" != "false" ]]
295
}
296
297
restore_all_processes() {
298
  local processes="$(get_opt_processes)"
299
  [[ "$processes" == ":all:" ]]
300
}
301
302
restore_list() {
303
  get_opt_processes
304
}
305
306
get_proc_match_element() {
307
  local proc="$1"
308
  printf "%s" "${proc%%->*}"
309
}
310
311
proc_matches_command() {
312
  local command="$1"
313
  local match="$2"
314
  if [[ "${match:0:1}" == "~" ]]; then
315
    local relaxed="${match#~}"
316
    [[ "$command" == *"$relaxed"* ]]
317
  else
318
    [[ "$command" == "$match" || "$command" == "$match "* ]]
319
  fi
320
}
321
322
command_on_restore_list() {
323
  local command="$1"
324
  local proc
325
  local match
326
  local restore_list_value
327
  restore_list_value="$(restore_list)"
328
  # shellcheck disable=SC2086
329
  eval "set -- $restore_list_value"
330
  for proc in "$@"; do
331
    match="$(get_proc_match_element "$proc")"
332
    if proc_matches_command "$command" "$match"; then
333
      return 0
334
    fi
335
  done
336
  return 1
337
}
338
339
should_restore_command() {
340
  local command="$1"
341
  [[ -z "$command" ]] && return 1
342
  restore_pane_processes_enabled || return 1
343
  restore_all_processes && return 0
344
  command_on_restore_list "$command"
345
}
346
347
restore_session_from_file() {
348
  local session_file="$1"
349
  local session_name=$(basename "$session_file" | sed 's/_last$//')
350
  exec <"$session_file"
351
352
  start_spinner "Restoring session $session_name"
353
354
  local session_path="$(head -n1)"
355
  [[ -n "$session_path" ]] || session_path="$HOME"
356
  tmux new-session -ds "$session_name" -c "$session_path"
357
358
  declare -A window_layouts
359
  declare active_window
360
  while read -r line; do
361
    case $line in
362
    window*)
363
      IFS=$S read -r _ window_index window_name window_layout window_active <<<"$line"
364
      window_id="$session_name:$window_index"
365
      tmux new-window -k -t "$window_id" -n "$window_name"
366
      window_layouts["$window_id"]="$window_layout"
367
      if [[ "$window_active" == "1" ]]; then
368
        active_window="$window_id"
369
      fi
370
      ;;
371
372
    pane*)
373
      IFS=$S read -r _ pane_index pane_current_path pane_active window_index command <<<"$line"
374
      if [[ "$pane_index" == "$(get_tmux_option base-index 0)" ]]; then
375
        tmux send-keys -t "$session_name:$window_index" "cd \"$pane_current_path\"" Enter "clear" Enter
376
      else
377
        tmux split-window -d -t "$session_name:$window_index" -c "$pane_current_path"
378
      fi
379
      if [[ "$pane_active" == "1" ]]; then
380
        tmux select-pane -t "$session_name:$window_index.$pane_index"
381
      fi
382
      if should_restore_command "$command"; then
383
        tmux send-keys -t "$session_name:$window_index.$pane_index" "$command" Enter
384
      fi
385
      ;;
386
    esac
387
  done
388
389
  for window in "${!window_layouts[@]}"; do
390
    tmux select-layout -t "$window" "${window_layouts[$window]}"
391
  done
392
393
  tmux select-window -t "$active_window"
394
  tmux switch-client -t "$session_name"
395
  stop_spinner "Session restored"
396
}
397
398
restore_session() {
399
  local session_name="$1"
400
  if tmux has-session -t "$session_name" 2>/dev/null; then
401
    tmux switch-client -t "$session_name"
402
    return 0
403
  fi
404
405
  local session_file="$(get_opt_dir)/${session_name}_last"
406
  if [[ ! -f "$session_file" ]]; then
407
    tmux display-message "No saved session found for: $session_name"
408
    return 1
409
  fi
410
411
  restore_session_from_file "$session_file"
412
}
413
414
restore_last() {
415
  local last_file="$(get_opt_dir)/last"
416
  if [[ ! -e "$last_file" ]]; then
417
    tmux display-message "No last session saved"
418
    return 1
419
  fi
420
421
  local session_name=$(basename "$(readlink "$last_file")" | sed 's/_last$//')
422
  restore_session "$session_name"
423
}