all repos

tmux-stare @ b9948f1539909a7b2a4d411a03b6322b8116225b

session manager, but my session manager

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

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