all repos

tmux-stare @ 5d25dc0

session manager, but my session manager

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

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