#!/usr/bin/env bash CONFIG_FILE_NAME="tmux-sessionizer.conf" CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/tmux-sessionizer" CONFIG_FILE="$CONFIG_DIR/$CONFIG_FILE_NAME" PANE_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/tmux-sessionizer" PANE_CACHE_FILE="$PANE_CACHE_DIR/panes.cache" # config file example # ------------------------ # # file: ~/.config/tmux-sessionizer/tmux-sessionizer.conf # # If set this override the default TS_SEARCH_PATHS (~/ ~/personal ~/personal/dev/env/.config) TS_SEARCH_PATHS=(~/git ~/Documents ~/.config ~/.local) # # If set this add additional search paths to the default TS_SEARCH_PATHS # # The number prefix is the depth for the Path [OPTIONAL] # TS_EXTRA_SEARCH_PATHS=(~/ghq:3 ~/Git:3 ~/.config:2) # # if set this override the TS_MAX_DEPTH (1) # TS_MAX_DEPTH=2 # This is not meant to override .tmux-sessionizer. At first i thought this # would be a good command, but i don't think its ackshually what i want. # # Instead, its a list of commands to run on windows who's index is way outside # of the first 10 windows. This allows you to create as many windows in your # session as you would like without having your workflow interrupted by these # programatic windows # # how to use: # tmux-sessionizer -w 0 will execute the first command in window -t 69. # TS_SESSION_COMMANDS=( ) # # TS_LOG=true # will write logs to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs # TS_LOG_FILE= # will write logs to Defaults to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs # ------------------------ if [[ -f "$CONFIG_FILE" ]]; then source "$CONFIG_FILE" fi if [[ -f "$CONFIG_FILE_NAME" ]]; then source "$CONFIG_FILE_NAME" fi if [[ $TS_LOG != "true" ]]; then if [[ -z $TS_LOG_FILE ]]; then TS_LOG_FILE="$HOME/.local/share/tmux-sessionizer/tmux-sessionizer.logs" fi mkdir -p "$(dirname "$TS_LOG_FILE")" fi log() { if [[ -z $TS_LOG ]]; then return elif [[ $TS_LOG == "echo" ]]; then echo "$*" elif [[ $TS_LOG == "file" ]]; then echo "$*" >>"$TS_LOG_FILE" fi } session_idx="" session_cmd="" user_selected="" split_type="" VERSION="0.1.0" while [[ "$#" -gt 0 ]]; do case "$1" in -h | --help) echo "Usage: tmux-sessionizer [OPTIONS] [SEARCH_PATH]" echo "Options:" echo " -h, --help Display this help message" echo " -s, --session session command index." echo " --vsplit Create vertical split (horizontal layout) for session command" echo " --hsplit Create horizontal split (vertical layout) for session command" exit 0 ;; -s | --session) session_idx="$2" if [[ -z $session_idx ]]; then echo "Session index cannot be empty" exit 1 fi if [[ -z $TS_SESSION_COMMANDS ]]; then echo "TS_SESSION_COMMANDS is not set. Must have a command set to run when switching to a session" exit 1 fi if [[ -z "$session_idx" || "$session_idx" -lt 0 || "$session_idx" -ge "${#TS_SESSION_COMMANDS[@]}" ]]; then echo "Error: Invalid index. Please provide an index between 0 and $((${#TS_SESSION_COMMANDS[@]} - 1))." exit 1 fi session_cmd="${TS_SESSION_COMMANDS[$session_idx]}" shift ;; --vsplit) split_type="vsplit" ;; --hsplit) split_type="hsplit" ;; -v | --version) echo "tmux-sessionizer version $VERSION" exit 0 ;; *) user_selected="$1" ;; esac shift done 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" # Validate split options are only used with session commands if [[ -n "$split_type" && -z "$session_idx" ]]; then echo "Error: --vsplit and --hsplit can only be used with -s/--session option" exit 1 fi sanity_check() { if ! command -v tmux &>/dev/null; then echo "tmux is not installed. Please install it first." exit 1 fi if ! command -v fzf &>/dev/null; then echo "fzf is not installed. Please install it first." exit 1 fi } switch_to() { if [[ -z $TMUX ]]; then log "attaching to session $1" tmux attach-session -t "$1" else log "switching to session $1" tmux switch-client -t "$1" fi } has_session() { tmux list-sessions | grep -q "^$1:" } hydrate() { if [[ ! -z $session_cmd ]]; then log "skipping hydrate for $1 -- using \"$session_cmd\" instead" return elif [ -f "$2/.tmux-sessionizer" ]; then log "sourcing(local) $2/.tmux-sessionizer" tmux send-keys -t "$1" "source $2/.tmux-sessionizer" c-M elif [ -f "$HOME/.tmux-sessionizer" ]; then log "sourcing(global) $HOME/.tmux-sessionizer" tmux send-keys -t "$1" "source $HOME/.tmux-sessionizer" c-M fi } is_tmux_running() { tmux_running=$(pgrep tmux) if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then return 1 fi return 0 } init_pane_cache() { mkdir -p "$PANE_CACHE_DIR" touch "$PANE_CACHE_FILE" } get_pane_id() { local session_idx="$1" local split_type="$2" init_pane_cache grep "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" | cut -d: -f3 } set_pane_id() { local session_idx="$1" local split_type="$2" local pane_id="$3" init_pane_cache # Remove existing entry if it exists grep -v "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" >"${PANE_CACHE_FILE}.tmp" 2>/dev/null || true mv "${PANE_CACHE_FILE}.tmp" "$PANE_CACHE_FILE" # Add new entry echo "${session_idx}:${split_type}:${pane_id}" >>"$PANE_CACHE_FILE" } cleanup_dead_panes() { init_pane_cache local temp_file="${PANE_CACHE_FILE}.tmp" while IFS=: read -r idx split pane_id; do if tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${pane_id}$"; then echo "${idx}:${split}:${pane_id}" >>"$temp_file" fi done <"$PANE_CACHE_FILE" mv "$temp_file" "$PANE_CACHE_FILE" 2>/dev/null || touch "$PANE_CACHE_FILE" } sanity_check # if TS_SEARCH_PATHS is not set use default [[ -n "$TS_SEARCH_PATHS" ]] || TS_SEARCH_PATHS=(~/ ~/personal ~/personal/dev/env/.config) # Add any extra search paths to the TS_SEARCH_PATHS array # e.g : EXTRA_SEARCH_PATHS=("$HOME/extra1:4" "$HOME/extra2") # note : Path can be suffixed with :number to limit or extend the depth of the search for the Path if [[ ${#TS_EXTRA_SEARCH_PATHS[@]} -gt 0 ]]; then TS_SEARCH_PATHS+=("${TS_EXTRA_SEARCH_PATHS[@]}") fi # utility function to find directories find_dirs() { # list TMUX sessions if [[ -n "${TMUX}" ]]; then current_session=$(tmux display-message -p '#S') tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | grep -vFx "[TMUX] $current_session" else tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null fi # note: TS_SEARCH_PATHS is an array of paths to search for directories # if the path ends with :number, it will search for directories with a max depth of number ;) # if there is no number, it will search for directories with a max depth defined by TS_MAX_DEPTH or 1 if not set for entry in "${TS_SEARCH_PATHS[@]}"; do # Check if entry as :number as suffix then adapt the maxdepth parameter if [[ "$entry" =~ ^([^:]+):([0-9]+)$ ]]; then path="${BASH_REMATCH[1]}" depth="${BASH_REMATCH[2]}" else path="$entry" fi [[ -d "$path" ]] && find "$path" -mindepth 1 -maxdepth "${depth:-${TS_MAX_DEPTH:-1}}" -path '*/.git' -prune -o -type d -print done } handle_session_cmd() { log "executing session command $session_cmd with index $session_idx split_type=$split_type" if ! is_tmux_running; then echo "Error: tmux is not running. Please start tmux first before using session commands." exit 1 fi current_session=$(tmux display-message -p '#S') if [[ -n "$split_type" ]]; then handle_split_session_cmd "$current_session" else handle_window_session_cmd "$current_session" fi exit 0 } handle_window_session_cmd() { local current_session="$1" start_index=$((69 + $session_idx)) target="$current_session:$start_index" log "target: $target command $session_cmd has-session=$(tmux has-session -t="$target" 2>/dev/null)" if tmux has-session -t="$target" 2>/dev/null; then switch_to "$target" else log "executing session command: tmux neww -dt $target $session_cmd" tmux neww -dt $target "$session_cmd" hydrate "$target" "$selected" tmux select-window -t $target fi } handle_split_session_cmd() { local current_session="$1" cleanup_dead_panes # Check if pane already exists local existing_pane_id=$(get_pane_id "$session_idx" "$split_type") if [[ -n "$existing_pane_id" ]] && tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${existing_pane_id}$"; then log "switching to existing pane $existing_pane_id" tmux select-pane -t "$existing_pane_id" if [[ -z $TMUX ]]; then tmux attach-session -t "$current_session" else tmux switch-client -t "$current_session" fi else # Create new split local split_flag="" if [[ "$split_type" == "vsplit" ]]; then split_flag="-h" # horizontal layout (vertical split) else split_flag="-v" # vertical layout (horizontal split) fi log "creating new split: tmux split-window $split_flag -c $(pwd) $session_cmd" local new_pane_id=$(tmux split-window $split_flag -c "$(pwd)" -P -F "#{pane_id}" "$session_cmd") if [[ -n "$new_pane_id" ]]; then set_pane_id "$session_idx" "$split_type" "$new_pane_id" log "created pane $new_pane_id for session_idx=$session_idx split_type=$split_type" fi fi } if [[ ! -z $session_cmd ]]; then handle_session_cmd elif [[ ! -z $user_selected ]]; then selected="$user_selected" else selected=$(find_dirs | fzf) fi if [[ -z $selected ]]; then exit 0 fi if [[ "$selected" =~ ^\[TMUX\]\ (.+)$ ]]; then selected="${BASH_REMATCH[1]}" fi selected_name=$(basename "$selected" | tr . _) if ! is_tmux_running; then tmux new-session -ds "$selected_name" -c "$selected" hydrate "$selected_name" "$selected" fi if ! has_session "$selected_name"; then tmux new-session -ds "$selected_name" -c "$selected" hydrate "$selected_name" "$selected" fi switch_to "$selected_name"