4 files changed,
86 insertions(+),
196 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-05-31 11:27:52 +0300
Authored at:
2026-04-14 21:46:29 +0300
Change ID:
kpmnnzuvyysxlurwwqymnnmoztzuqqxn
Parent:
29a70cd
jump to
| D | scripts/_session_renamed.sh |
| M | scripts/pick.sh |
| M | scripts/sessions.sh |
| M | stare.tmux |
D
scripts/_session_renamed.sh
··· 1 -#!/usr/bin/env bash 2 -CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 3 -source "$CURRENT_DIR/helpers.sh" 4 -source "$CURRENT_DIR/sessions.sh" 5 - 6 -main() { 7 - if [[ "${1:-}" == "sync" ]]; then 8 - sync_session_id_map 9 - return 0 10 - fi 11 - 12 - local session_id="$1" 13 - local new_name="$2" 14 - handle_session_renamed_hook "$session_id" "$new_name" 15 -} 16 -main "$@"
M
scripts/pick.sh
··· 73 73 local session_file="$save_dir/${session_name}_last" 74 74 75 75 local result 76 - result=$(printf "Cancel\nDelete" | fzf --header="Delete saved session '$session_name'?") 76 + result=$(printf "Delete\nCancel" | fzf --header="Delete saved session '$session_name'?") 77 77 if [[ "$result" == "Delete" ]]; then 78 78 rm -f "$session_file" 79 79 fi
M
scripts/sessions.sh
··· 13 13 fi 14 14 } 15 15 16 -session_id_map_file() { echo "$(get_opt_dir)/.session_ids" } 17 -sync_session_id_map() { 18 - local map_file="$(session_id_map_file)" 19 - tmux list-sessions -F "#{session_id}${S}#{session_name}" >"$map_file" 20 -} 21 - 22 -get_session_id_by_name() { 23 - local session_name="$1" 24 - tmux display-message -p -t "$session_name" "#{session_id}" 2>/dev/null 25 -} 26 - 27 -get_session_name_by_id() { 28 - local session_id="$1" 29 - local map_file="$(session_id_map_file)" 30 - [[ -f "$map_file" ]] || return 1 31 - awk -F"$S" -v session_id="$session_id" '$1 == session_id { print $2; exit }' "$map_file" 32 -} 33 - 34 -upsert_session_id_entry() { 35 - local session_id="$1" 36 - local session_name="$2" 37 - local map_file="$(session_id_map_file)" 38 - local tmp_file="${map_file}.tmp" 39 - 40 - [[ -z "$session_id" || -z "$session_name" ]] && return 1 41 - [[ -f "$map_file" ]] || : >"$map_file" 42 - 43 - awk -F"$S" -v OFS="$S" -v session_id="$session_id" -v session_name="$session_name" ' 44 - $1 == session_id { 45 - $2 = session_name 46 - found = 1 47 - } 48 - { print } 49 - END { 50 - if (!found) print session_id, session_name 51 - } 52 - ' "$map_file" >"$tmp_file" && mv "$tmp_file" "$map_file" 53 -} 54 - 55 -remember_session_identity() { 56 - local session_name="$1" 57 - local session_id 58 - session_id="$(get_session_id_by_name "$session_name")" 59 - [[ -n "$session_id" ]] && upsert_session_id_entry "$session_id" "$session_name" 60 -} 61 - 62 -rename_session_files() { 16 +rename_session() { 63 17 local old="$1" 64 18 local new="$2" 65 19 local dir="$(get_opt_dir)" 66 - local old_last="${dir}/${old}_last" 67 - local new_last="${dir}/${new}_last" 68 - local global_last="${dir}/last" 69 20 70 21 [[ -z "$old" || -z "$new" || "$old" == "$new" ]] && return 1 71 - [[ -e "$old_last" ]] || return 0 72 - [[ -e "$new_last" ]] && return 1 73 - 74 - if [[ -L "$old_last" ]]; then 75 - local actual="$(readlink "$old_last")" 76 - local actual_name 77 - local new_actual 78 - [[ "$actual" != /* ]] && actual="${dir}/${actual}" 79 - 80 - actual_name="$(basename "$actual")" 81 - if [[ "$actual_name" == "${old}_"* && -e "$actual" ]]; then 82 - new_actual="${dir}/${new}_${actual_name#${old}_}" 83 - mv "$actual" "$new_actual" 84 - ln -sf "$new_actual" "$new_last" 85 - rm -f "$old_last" 86 - else 87 - mv "$old_last" "$new_last" 88 - fi 89 - else 90 - mv "$old_last" "$new_last" 91 - fi 92 - 93 - if [[ -L "$global_last" ]]; then 94 - local global_target="$(readlink "$global_last")" 95 - local global_target_name="$(basename "$global_target")" 96 - if [[ "$global_target_name" == "${old}_last" ]]; then 97 - ln -sf "$new_last" "$global_last" 98 - fi 99 - fi 100 -} 101 - 102 -apply_session_rename() { 103 - local session_id="$1" 104 - local new_name="$2" 105 - local old_name="${3:-}" 106 - 107 - [[ -z "$session_id" || -z "$new_name" ]] && return 1 108 - [[ -n "$old_name" ]] || old_name="$(get_session_name_by_id "$session_id" || true)" 109 - 110 - if [[ -n "$old_name" && "$old_name" != "$new_name" ]]; then 111 - rename_session_files "$old_name" "$new_name" 112 - fi 113 - 114 - upsert_session_id_entry "$session_id" "$new_name" 115 -} 116 - 117 -rename_session() { 118 - local old="$1" 119 - local new="$2" 120 - local session_id 121 - 122 - [[ -z "$new" || "$old" == "$new" ]] && return 1 123 22 [[ -e "$(get_opt_dir)/${new}_last" ]] && return 1 124 23 125 24 tmux has-session -t "$new" 2>/dev/null && return 1 126 - session_id="$(get_session_id_by_name "$old")" 127 - [[ -n "$session_id" ]] || return 1 128 - tmux rename-session -t "$old" "$new" 2>/dev/null || return 1 129 - apply_session_rename "$session_id" "$new" "$old" 130 -} 25 + tmux rename-session -t "$old" "$new" 2>/dev/null 131 26 132 -handle_session_renamed_hook() { 133 - local session_id="$1" 134 - local new_name="$2" 135 - 136 - [[ -z "$session_id" || -z "$new_name" ]] && return 1 137 - apply_session_rename "$session_id" "$new_name" 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 + } 138 35 } 139 36 140 37 # === save ··· 152 49 153 50 } 154 51 155 -get_pane_child_pids() { 156 - local pane_pid="$1" 157 - ps -ao ppid=,pid= | awk -v pane_pid="$pane_pid" '$1 == pane_pid { print $2 }' 158 -} 159 - 160 52 format_process_command() { 161 53 local pid="$1" 162 54 [[ -r "/proc/${pid}/cmdline" ]] || return 1 163 55 xargs -0 bash -c 'printf "%q " "$0" "$@"' <"/proc/${pid}/cmdline" 2>/dev/null | sed 's/[[:space:]]*$//' 164 56 } 165 57 166 -build_command_from_pids() { 167 - local pids="$1" 168 - local pid 169 - local formatted 170 - local command="" 171 - for pid in $pids; do 172 - formatted="$(format_process_command "$pid")" 173 - [[ -z "$formatted" ]] && continue 174 - [[ -n "$command" ]] && command+=" | " 175 - command+="$formatted" 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# }" 176 86 done 177 - printf "%s" "$command" 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" 178 96 } 179 97 180 -collect_process_names() { 181 - local pids="$1" 98 +select_active_command_pid() { 99 + local pane_pid="$1" 100 + local pane_target="$2" 101 + local pane_current_command 102 + local pids 182 103 local pid 183 - local first_arg 184 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 + 185 112 for pid in $pids; do 186 - [[ -r "/proc/${pid}/cmdline" ]] || continue 187 - first_arg="$(tr '\0' '\n' <"/proc/${pid}/cmdline" 2>/dev/null | head -n1)" 188 - [[ -z "$first_arg" ]] && continue 189 - name="$(basename "$first_arg")" 190 - [[ -n "$name" ]] && printf "%s\n" "$name" 191 - done | sort -u 192 -} 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 193 119 194 -strip_prompt_prefix() { 195 - local line="$1" 196 - if [[ "$line" =~ [\$\#\%\>][[:space:]]+.+$ ]]; then 197 - sed -E 's/^.*[#$%>][[:space:]]+//' <<<"$line" 120 + if [[ -n "$matching_pid" ]]; then 121 + printf "%s" "$matching_pid" 122 + elif [[ "$descendant_count" == "1" ]]; then 123 + printf "%s" "$fallback_pid" 198 124 fi 199 125 } 200 126 201 -find_command_from_pane_history() { 202 - local pane_target="$1" 203 - local process_names="$2" 204 - local line 205 - local candidate 206 - local name 207 - while IFS= read -r line; do 208 - [[ -z "$line" ]] && continue 209 - candidate="$(strip_prompt_prefix "$line")" 210 - [[ -z "$candidate" ]] && continue 211 - while IFS= read -r name; do 212 - [[ -z "$name" ]] && continue 213 - if [[ "$candidate" == *"$name"* ]]; then 214 - printf "%s" "$candidate" 215 - return 0 216 - fi 217 - done <<<"$process_names" 218 - done < <(tmux capture-pane -pJ -S -200 -t "$pane_target" 2>/dev/null | tac) 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" 219 135 } 220 136 221 137 save_panes() { ··· 224 140 local format="pane$S#{pane_index}$S#{pane_current_path}$S#{pane_active}$S#{window_index}$S#{pane_pid}" 225 141 tmux list-panes -s -t "$session_name" -F "$format" | 226 142 while IFS="$S" read -r line; do 143 + local pane_target 144 + local command 227 145 IFS="$S" read -r _ pane_index _ _ window_index pane_pid <<<"$line" 228 - pids="$(get_pane_child_pids "$pane_pid")" 229 - command="$(build_command_from_pids "$pids")" 230 - 231 - if [[ -n "$command" ]]; then 232 - process_names="$(collect_process_names "$pids")" 233 - pane_target="${session_name}:${window_index}.${pane_index}" 234 - pane_command="$(find_command_from_pane_history "$pane_target" "$process_names")" 235 - [[ -n "$pane_command" ]] && command="$pane_command" 236 - fi 146 + pane_target="${session_name}:${window_index}.${pane_index}" 147 + command="$(capture_running_command "$pane_pid" "$pane_target")" 237 148 238 149 awk -v command="$command" \ 239 150 'BEGIN {FS=OFS="\t"} {$6=command; print}' \ ··· 268 179 save_panes "$session_name" "$save_file" 269 180 link_session_last "$save_file" "$last_file" 270 181 link_last "$last_file" "$save_dir" 271 - remember_session_identity "$session_name" 272 182 } 273 183 274 184 save_all_sessions() {
M
stare.tmux
··· 15 15 16 16 main() { 17 17 local pick_key="$(get_opt_pick)" 18 - [[ -n "$pick_key" ]] && tmux bind-key "$pick_key" run-shell "bash \"$CURRENT_DIR/scripts/_pick.sh\"" 18 + [[ -n "$pick_key" ]] && tmux bind-key "$pick_key" run-shell "$CURRENT_DIR/scripts/_pick.sh" 19 19 20 20 local save_key="$(get_opt_save)" 21 - [[ -n "$save_key" ]] && tmux bind-key "$save_key" run-shell "bash \"$CURRENT_DIR/scripts/_save.sh\"" 21 + [[ -n "$save_key" ]] && tmux bind-key "$save_key" run-shell "$CURRENT_DIR/scripts/_save.sh" 22 22 23 23 local start_action="$(get_opt_start)" 24 24 if [[ "$(get_opt_initialized)" == "0" ]]; then 25 25 if [[ "$start_action" == "last" ]]; then 26 - tmux run-shell -b "sleep 0.1 && bash \"$CURRENT_DIR/scripts/_restore.sh\"" 26 + tmux run-shell -b "sleep 0.1 && '$CURRENT_DIR/scripts/_restore.sh'" 27 27 elif [[ "$start_action" == "pick" ]]; then 28 - tmux run-shell -b "sleep 0.1 && bash \"$CURRENT_DIR/scripts/_pick.sh\"" 28 + tmux run-shell -b "sleep 0.1 && '$CURRENT_DIR/scripts/_pick.sh'" 29 29 fi 30 30 31 31 set_opt_initialized "1" 32 32 fi 33 - 34 - # rename hook 35 - tmux set-hook -g session-renamed "run-shell \"bash '$CURRENT_DIR/scripts/_session_renamed.sh' #{q:hook_session} #{q:hook_session_name}\"" 36 - tmux run-shell "bash '$CURRENT_DIR/scripts/_session_renamed.sh' sync" 37 33 38 34 add_save_interpolation 39 35 }