all repos

tmux-stare @ fdfe6db

session manager, but my session manager

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

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