tmux-sessionizer (10234B)
1 #!/usr/bin/env bash 2 CONFIG_FILE_NAME="tmux-sessionizer.conf" 3 CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/tmux-sessionizer" 4 CONFIG_FILE="$CONFIG_DIR/$CONFIG_FILE_NAME" 5 PANE_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/tmux-sessionizer" 6 PANE_CACHE_FILE="$PANE_CACHE_DIR/panes.cache" 7 8 # config file example 9 # ------------------------ 10 # # file: ~/.config/tmux-sessionizer/tmux-sessionizer.conf 11 # # If set this override the default TS_SEARCH_PATHS (~/ ~/personal ~/personal/dev/env/.config) 12 # TS_SEARCH_PATHS=(~/git ~/Documents ~/.config ~/.local) 13 # # If set this add additional search paths to the default TS_SEARCH_PATHS 14 # # The number prefix is the depth for the Path [OPTIONAL] 15 # TS_EXTRA_SEARCH_PATHS=(~/ghq:3 ~/Git:3 ~/.config:2) 16 TS_EXTRA_SEARCH_PATHS=(~/git ~/Documents ~/.config ~/.local) 17 # # if set this override the TS_MAX_DEPTH (1) 18 # TS_MAX_DEPTH=2 19 # This is not meant to override .tmux-sessionizer. At first i thought this 20 # would be a good command, but i don't think its ackshually what i want. 21 # 22 # Instead, its a list of commands to run on windows who's index is way outside 23 # of the first 10 windows. This allows you to create as many windows in your 24 # session as you would like without having your workflow interrupted by these 25 # programatic windows 26 # 27 # how to use: 28 # tmux-sessionizer -w 0 will execute the first command in window -t 69. 29 # TS_SESSION_COMMANDS=(<cmd1> <cmd2>) 30 # 31 # TS_LOG=true # will write logs to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs 32 # TS_LOG_FILE=<file> # will write logs to <file> Defaults to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs 33 # ------------------------ 34 35 if [[ -f "$CONFIG_FILE" ]]; then 36 source "$CONFIG_FILE" 37 fi 38 39 if [[ -f "$CONFIG_FILE_NAME" ]]; then 40 source "$CONFIG_FILE_NAME" 41 fi 42 43 if [[ $TS_LOG != "true" ]]; then 44 if [[ -z $TS_LOG_FILE ]]; then 45 TS_LOG_FILE="$HOME/.local/share/tmux-sessionizer/tmux-sessionizer.logs" 46 fi 47 48 mkdir -p "$(dirname "$TS_LOG_FILE")" 49 fi 50 51 log() { 52 if [[ -z $TS_LOG ]]; then 53 return 54 elif [[ $TS_LOG == "echo" ]]; then 55 echo "$*" 56 elif [[ $TS_LOG == "file" ]]; then 57 echo "$*" >>"$TS_LOG_FILE" 58 fi 59 } 60 61 session_idx="" 62 session_cmd="" 63 user_selected="" 64 split_type="" 65 VERSION="0.1.0" 66 67 while [[ "$#" -gt 0 ]]; do 68 case "$1" in 69 -h | --help) 70 echo "Usage: tmux-sessionizer [OPTIONS] [SEARCH_PATH]" 71 echo "Options:" 72 echo " -h, --help Display this help message" 73 echo " -s, --session <name> session command index." 74 echo " --vsplit Create vertical split (horizontal layout) for session command" 75 echo " --hsplit Create horizontal split (vertical layout) for session command" 76 exit 0 77 ;; 78 -s | --session) 79 session_idx="$2" 80 if [[ -z $session_idx ]]; then 81 echo "Session index cannot be empty" 82 exit 1 83 fi 84 85 if [[ -z $TS_SESSION_COMMANDS ]]; then 86 echo "TS_SESSION_COMMANDS is not set. Must have a command set to run when switching to a session" 87 exit 1 88 fi 89 90 if [[ -z "$session_idx" || "$session_idx" -lt 0 || "$session_idx" -ge "${#TS_SESSION_COMMANDS[@]}" ]]; then 91 echo "Error: Invalid index. Please provide an index between 0 and $((${#TS_SESSION_COMMANDS[@]} - 1))." 92 exit 1 93 fi 94 95 session_cmd="${TS_SESSION_COMMANDS[$session_idx]}" 96 97 shift 98 ;; 99 --vsplit) 100 split_type="vsplit" 101 ;; 102 --hsplit) 103 split_type="hsplit" 104 ;; 105 -v | --version) 106 echo "tmux-sessionizer version $VERSION" 107 exit 0 108 ;; 109 *) 110 user_selected="$1" 111 ;; 112 esac 113 shift 114 done 115 116 log "tmux-sessionizer($VERSION): idx=$session_idx cmd=$session_cmd user_selected=$user_selected split_type=$split_type log=$TS_LOG log_file=$TS_LOG_FILE" 117 118 # Validate split options are only used with session commands 119 if [[ -n "$split_type" && -z "$session_idx" ]]; then 120 echo "Error: --vsplit and --hsplit can only be used with -s/--session option" 121 exit 1 122 fi 123 124 sanity_check() { 125 if ! command -v tmux &>/dev/null; then 126 echo "tmux is not installed. Please install it first." 127 exit 1 128 fi 129 130 if ! command -v fzf &>/dev/null; then 131 echo "fzf is not installed. Please install it first." 132 exit 1 133 fi 134 } 135 136 switch_to() { 137 if [[ -z $TMUX ]]; then 138 log "attaching to session $1" 139 tmux attach-session -t "$1" 140 else 141 log "switching to session $1" 142 tmux switch-client -t "$1" 143 fi 144 } 145 146 has_session() { 147 tmux list-sessions | grep -q "^$1:" 148 } 149 150 hydrate() { 151 if [[ ! -z $session_cmd ]]; then 152 log "skipping hydrate for $1 -- using \"$session_cmd\" instead" 153 return 154 elif [ -f "$2/.tmux-sessionizer" ]; then 155 log "sourcing(local) $2/.tmux-sessionizer" 156 tmux send-keys -t "$1" "source $2/.tmux-sessionizer" c-M 157 elif [ -f "$HOME/.tmux-sessionizer" ]; then 158 log "sourcing(global) $HOME/.tmux-sessionizer" 159 tmux send-keys -t "$1" "source $HOME/.tmux-sessionizer" c-M 160 fi 161 } 162 163 is_tmux_running() { 164 tmux_running=$(pgrep tmux) 165 166 if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then 167 return 1 168 fi 169 return 0 170 } 171 172 init_pane_cache() { 173 mkdir -p "$PANE_CACHE_DIR" 174 touch "$PANE_CACHE_FILE" 175 } 176 177 get_pane_id() { 178 local session_idx="$1" 179 local split_type="$2" 180 init_pane_cache 181 grep "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" | cut -d: -f3 182 } 183 184 set_pane_id() { 185 local session_idx="$1" 186 local split_type="$2" 187 local pane_id="$3" 188 init_pane_cache 189 190 # Remove existing entry if it exists 191 grep -v "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" >"${PANE_CACHE_FILE}.tmp" 2>/dev/null || true 192 mv "${PANE_CACHE_FILE}.tmp" "$PANE_CACHE_FILE" 193 194 # Add new entry 195 echo "${session_idx}:${split_type}:${pane_id}" >>"$PANE_CACHE_FILE" 196 } 197 198 cleanup_dead_panes() { 199 init_pane_cache 200 local temp_file="${PANE_CACHE_FILE}.tmp" 201 202 while IFS=: read -r idx split pane_id; do 203 if tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${pane_id}$"; then 204 echo "${idx}:${split}:${pane_id}" >>"$temp_file" 205 fi 206 done <"$PANE_CACHE_FILE" 207 208 mv "$temp_file" "$PANE_CACHE_FILE" 2>/dev/null || touch "$PANE_CACHE_FILE" 209 } 210 211 sanity_check 212 213 # if TS_SEARCH_PATHS is not set use default 214 [[ -n "$TS_SEARCH_PATHS" ]] || TS_SEARCH_PATHS=(~/ ~/personal ~/personal/dev/env/.config) 215 216 # Add any extra search paths to the TS_SEARCH_PATHS array 217 # e.g : EXTRA_SEARCH_PATHS=("$HOME/extra1:4" "$HOME/extra2") 218 # note : Path can be suffixed with :number to limit or extend the depth of the search for the Path 219 220 if [[ ${#TS_EXTRA_SEARCH_PATHS[@]} -gt 0 ]]; then 221 TS_SEARCH_PATHS+=("${TS_EXTRA_SEARCH_PATHS[@]}") 222 fi 223 224 # utility function to find directories 225 find_dirs() { 226 # list TMUX sessions 227 if [[ -n "${TMUX}" ]]; then 228 current_session=$(tmux display-message -p '#S') 229 tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | grep -vFx "[TMUX] $current_session" 230 else 231 tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null 232 fi 233 234 # note: TS_SEARCH_PATHS is an array of paths to search for directories 235 # if the path ends with :number, it will search for directories with a max depth of number ;) 236 # if there is no number, it will search for directories with a max depth defined by TS_MAX_DEPTH or 1 if not set 237 for entry in "${TS_SEARCH_PATHS[@]}"; do 238 # Check if entry as :number as suffix then adapt the maxdepth parameter 239 if [[ "$entry" =~ ^([^:]+):([0-9]+)$ ]]; then 240 path="${BASH_REMATCH[1]}" 241 depth="${BASH_REMATCH[2]}" 242 else 243 path="$entry" 244 fi 245 246 [[ -d "$path" ]] && find "$path" -mindepth 1 -maxdepth "${depth:-${TS_MAX_DEPTH:-1}}" -path '*/.git' -prune -o -type d -print 247 done 248 } 249 250 handle_session_cmd() { 251 log "executing session command $session_cmd with index $session_idx split_type=$split_type" 252 if ! is_tmux_running; then 253 echo "Error: tmux is not running. Please start tmux first before using session commands." 254 exit 1 255 fi 256 257 current_session=$(tmux display-message -p '#S') 258 259 if [[ -n "$split_type" ]]; then 260 handle_split_session_cmd "$current_session" 261 else 262 handle_window_session_cmd "$current_session" 263 fi 264 exit 0 265 } 266 267 handle_window_session_cmd() { 268 local current_session="$1" 269 start_index=$((69 + $session_idx)) 270 target="$current_session:$start_index" 271 272 log "target: $target command $session_cmd has-session=$(tmux has-session -t="$target" 2>/dev/null)" 273 if tmux has-session -t="$target" 2>/dev/null; then 274 switch_to "$target" 275 else 276 log "executing session command: tmux neww -dt $target $session_cmd" 277 tmux neww -dt $target "$session_cmd" 278 hydrate "$target" "$selected" 279 tmux select-window -t $target 280 fi 281 } 282 283 handle_split_session_cmd() { 284 local current_session="$1" 285 cleanup_dead_panes 286 287 # Check if pane already exists 288 local existing_pane_id=$(get_pane_id "$session_idx" "$split_type") 289 290 if [[ -n "$existing_pane_id" ]] && tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${existing_pane_id}$"; then 291 log "switching to existing pane $existing_pane_id" 292 tmux select-pane -t "$existing_pane_id" 293 if [[ -z $TMUX ]]; then 294 tmux attach-session -t "$current_session" 295 else 296 tmux switch-client -t "$current_session" 297 fi 298 else 299 # Create new split 300 local split_flag="" 301 if [[ "$split_type" == "vsplit" ]]; then 302 split_flag="-h" # horizontal layout (vertical split) 303 else 304 split_flag="-v" # vertical layout (horizontal split) 305 fi 306 307 log "creating new split: tmux split-window $split_flag -c $(pwd) $session_cmd" 308 local new_pane_id=$(tmux split-window $split_flag -c "$(pwd)" -P -F "#{pane_id}" "$session_cmd") 309 310 if [[ -n "$new_pane_id" ]]; then 311 set_pane_id "$session_idx" "$split_type" "$new_pane_id" 312 log "created pane $new_pane_id for session_idx=$session_idx split_type=$split_type" 313 fi 314 fi 315 } 316 317 if [[ ! -z $session_cmd ]]; then 318 handle_session_cmd 319 elif [[ ! -z $user_selected ]]; then 320 selected="$user_selected" 321 else 322 selected=$(find_dirs | fzf) 323 fi 324 325 if [[ -z $selected ]]; then 326 exit 0 327 fi 328 329 if [[ "$selected" =~ ^\[TMUX\]\ (.+)$ ]]; then 330 selected="${BASH_REMATCH[1]}" 331 fi 332 333 selected_name=$(basename "$selected" | tr . _) 334 335 if ! is_tmux_running; then 336 tmux new-session -ds "$selected_name" -c "$selected" 337 hydrate "$selected_name" "$selected" 338 fi 339 340 if ! has_session "$selected_name"; then 341 tmux new-session -ds "$selected_name" -c "$selected" 342 hydrate "$selected_name" "$selected" 343 fi 344 345 switch_to "$selected_name"