all repos

tmux-stare @ a51b0355fd1644e52d873af0384bd7a20c919ee6

session manager, but my session manager

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

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