all repos

tmux-stare @ 4443c84cd1bb7bdfb96953a1b820145f10553e20

session manager, but my session manager

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
make easier to delete sessions, 6 days 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 new_actual="${dir}/${new}_$(basename "$actual" | cut -d_ -f2-)"
31
    mv "$actual" "$new_actual"
32
    ln -sf "$new_actual" "${dir}/${new}_last"
33
    rm "$old_last"
34
  }
35
}
36
37
# === save
38
save_cwd() {
39
  local session_name="$1"
40
  local save_file="$2"
41
  tmux display-message -p -t "$session_name" -F "#{session_path}" >>"$save_file"
42
}
43
44
save_windows() {
45
  local session_name="$1"
46
  local save_file="$2"
47
  local format="window$S#{window_index}$S#{window_name}$S#{window_layout}$S#{window_active}"
48
  tmux list-windows -t "$session_name" -F "$format" >>"$save_file"
49
50
}
51
52
format_process_command() {
53
  local pid="$1"
54
  [[ -r "/proc/${pid}/cmdline" ]] || return 1
55
  xargs -0 bash -c 'printf "%q " "$0" "$@"' <"/proc/${pid}/cmdline" 2>/dev/null | sed 's/[[:space:]]*$//'
56
}
57
58
get_descendant_pids() {
59
  local root_pid="$1"
60
  local proc_table
61
  local -A seen
62
  local frontier
63
64
  [[ -n "$root_pid" ]] || return 1
65
  proc_table="$(ps -ao ppid=,pid= --sort=pid)"
66
  frontier="$root_pid"
67
  seen["$root_pid"]="1"
68
69
  while [[ -n "$frontier" ]]; do
70
    local next_frontier=""
71
    local ppid
72
    local pid
73
74
    while read -r ppid pid; do
75
      local parent
76
      for parent in $frontier; do
77
        [[ "$ppid" == "$parent" ]] || continue
78
        [[ -n "${seen[$pid]:-}" ]] && continue
79
        seen["$pid"]="1"
80
        printf "%s\n" "$pid"
81
        next_frontier+=" $pid"
82
      done
83
    done <<<"$proc_table"
84
85
    frontier="${next_frontier# }"
86
  done
87
}
88
89
get_process_name() {
90
  local pid="$1"
91
  local first_arg
92
  [[ -r "/proc/${pid}/cmdline" ]] || return 1
93
  first_arg="$(tr '\0' '\n' <"/proc/${pid}/cmdline" 2>/dev/null | head -n1)"
94
  [[ -n "$first_arg" ]] || return 1
95
  basename "$first_arg"
96
}
97
98
select_active_command_pid() {
99
  local pane_pid="$1"
100
  local pane_target="$2"
101
  local pane_current_command
102
  local pids
103
  local pid
104
  local name
105
  local matching_pid=""
106
  local fallback_pid=""
107
  local descendant_count=0
108
109
  pane_current_command="$(tmux display-message -p -t "$pane_target" "#{pane_current_command}" 2>/dev/null)"
110
  pids="$(get_descendant_pids "$pane_pid")"
111
112
  for pid in $pids; do
113
    descendant_count=$((descendant_count + 1))
114
    fallback_pid="$pid"
115
    [[ -n "$pane_current_command" ]] || continue
116
    name="$(get_process_name "$pid")" || continue
117
    [[ "$name" == "$pane_current_command" ]] && matching_pid="$pid"
118
  done
119
120
  if [[ -n "$matching_pid" ]]; then
121
    printf "%s" "$matching_pid"
122
  elif [[ "$descendant_count" == "1" ]]; then
123
    printf "%s" "$fallback_pid"
124
  fi
125
}
126
127
capture_running_command() {
128
  local pane_pid="$1"
129
  local pane_target="$2"
130
  local command_pid
131
132
  command_pid="$(select_active_command_pid "$pane_pid" "$pane_target")"
133
  [[ -n "$command_pid" ]] || return 1
134
  format_process_command "$command_pid"
135
}
136
137
save_panes() {
138
  local session_name="$1"
139
  local save_file="$2"
140
  local format="pane$S#{pane_index}$S#{pane_current_path}$S#{pane_active}$S#{window_index}$S#{pane_pid}"
141
  tmux list-panes -s -t "$session_name" -F "$format" |
142
    while IFS="$S" read -r line; do
143
      local pane_target
144
      local command
145
      IFS="$S" read -r _ pane_index _ _ window_index pane_pid <<<"$line"
146
      pane_target="${session_name}:${window_index}.${pane_index}"
147
      command="$(capture_running_command "$pane_pid" "$pane_target")"
148
149
      awk -v command="$command" \
150
        'BEGIN {FS=OFS="\t"} {$6=command; print}' \
151
        <<<"$line" >>"$save_file"
152
    done
153
}
154
155
link_session_last() {
156
  local save_file="$1"
157
  local last_file="$2"
158
  if ! cmp -s "$save_file" "$last_file"; then
159
    ln -sf "$save_file" "$last_file"
160
  else
161
    rm "$save_file"
162
  fi
163
}
164
165
link_last() {
166
  local save_file="$1"
167
  local save_dir="$2"
168
  ln -sf "$save_file" "$save_dir"/last
169
}
170
171
save_session() {
172
  local session_name="$1"
173
  local save_dir="$(get_opt_dir)"
174
  local save_file="${save_dir}/${session_name}_$(get_time)"
175
  local last_file="${save_dir}/${session_name}_last"
176
177
  save_cwd "$session_name" "$save_file"
178
  save_windows "$session_name" "$save_file"
179
  save_panes "$session_name" "$save_file"
180
  link_session_last "$save_file" "$last_file"
181
  link_last "$last_file" "$save_dir"
182
}
183
184
save_all_sessions() {
185
  tmux list-sessions -F "#{session_name}" | while read -r session; do
186
    save_session "$session"
187
  done
188
189
  local current_session="$(get_current_session_name)"
190
  if [[ -n "$current_session" ]]; then
191
    link_last "$(get_opt_dir)/${current_session}_last" "$(get_opt_dir)"
192
  fi
193
}
194
195
unload_session() {
196
  local session_name="$1"
197
  save_session "$session_name"
198
  tmux kill-session -t "$session_name"
199
}
200
201
# === restore
202
restore_pane_processes_enabled() {
203
  local processes="$(get_opt_processes)"
204
  [[ "$processes" != "false" ]]
205
}
206
207
restore_all_processes() {
208
  local processes="$(get_opt_processes)"
209
  [[ "$processes" == ":all:" ]]
210
}
211
212
restore_list() {
213
  get_opt_processes
214
}
215
216
get_proc_match_element() {
217
  local proc="$1"
218
  printf "%s" "${proc%%->*}"
219
}
220
221
proc_matches_command() {
222
  local command="$1"
223
  local match="$2"
224
  if [[ "${match:0:1}" == "~" ]]; then
225
    local relaxed="${match#~}"
226
    [[ "$command" == *"$relaxed"* ]]
227
  else
228
    [[ "$command" == "$match" || "$command" == "$match "* ]]
229
  fi
230
}
231
232
command_on_restore_list() {
233
  local command="$1"
234
  local proc
235
  local match
236
  local restore_list_value
237
  restore_list_value="$(restore_list)"
238
  # shellcheck disable=SC2086
239
  eval "set -- $restore_list_value"
240
  for proc in "$@"; do
241
    match="$(get_proc_match_element "$proc")"
242
    if proc_matches_command "$command" "$match"; then
243
      return 0
244
    fi
245
  done
246
  return 1
247
}
248
249
should_restore_command() {
250
  local command="$1"
251
  [[ -z "$command" ]] && return 1
252
  restore_pane_processes_enabled || return 1
253
  restore_all_processes && return 0
254
  command_on_restore_list "$command"
255
}
256
257
restore_session_from_file() {
258
  local session_file="$1"
259
  local session_name=$(basename "$session_file" | sed 's/_last$//')
260
  exec <"$session_file"
261
262
  start_spinner "Restoring session $session_name"
263
264
  local session_path="$(head -n1)"
265
  [[ -n "$session_path" ]] || session_path="$HOME"
266
  tmux new-session -ds "$session_name" -c "$session_path"
267
268
  declare -A window_layouts
269
  declare active_window
270
  while read -r line; do
271
    case $line in
272
    window*)
273
      IFS=$S read -r _ window_index window_name window_layout window_active <<<"$line"
274
      window_id="$session_name:$window_index"
275
      tmux new-window -k -t "$window_id" -n "$window_name"
276
      window_layouts["$window_id"]="$window_layout"
277
      if [[ "$window_active" == "1" ]]; then
278
        active_window="$window_id"
279
      fi
280
      ;;
281
282
    pane*)
283
      IFS=$S read -r _ pane_index pane_current_path pane_active window_index command <<<"$line"
284
      if [[ "$pane_index" == "$(get_tmux_option base-index 0)" ]]; then
285
        tmux send-keys -t "$session_name:$window_index" "cd \"$pane_current_path\"" Enter "clear" Enter
286
      else
287
        tmux split-window -d -t "$session_name:$window_index" -c "$pane_current_path"
288
      fi
289
      if [[ "$pane_active" == "1" ]]; then
290
        tmux select-pane -t "$session_name:$window_index.$pane_index"
291
      fi
292
      if should_restore_command "$command"; then
293
        tmux send-keys -t "$session_name:$window_index.$pane_index" "$command" Enter
294
      fi
295
      ;;
296
    esac
297
  done
298
299
  for window in "${!window_layouts[@]}"; do
300
    tmux select-layout -t "$window" "${window_layouts[$window]}"
301
  done
302
303
  tmux select-window -t "$active_window"
304
  tmux switch-client -t "$session_name"
305
  stop_spinner "Session restored"
306
}
307
308
restore_session() {
309
  local session_name="$1"
310
  if tmux has-session -t "$session_name" 2>/dev/null; then
311
    tmux switch-client -t "$session_name"
312
    return 0
313
  fi
314
315
  local session_file="$(get_opt_dir)/${session_name}_last"
316
  if [[ ! -f "$session_file" ]]; then
317
    tmux display-message "No saved session found for: $session_name"
318
    return 1
319
  fi
320
321
  restore_session_from_file "$session_file"
322
}
323
324
restore_last() {
325
  local last_file="$(get_opt_dir)/last"
326
  if [[ ! -e "$last_file" ]]; then
327
    tmux display-message "No last session saved"
328
    return 1
329
  fi
330
331
  local session_name=$(basename "$(readlink "$last_file")" | sed 's/_last$//')
332
  restore_session "$session_name"
333
}