Last active
January 24, 2026 09:09
-
-
Save fangwangme/201313a95b265266f1c97d682b833d12 to your computer and use it in GitHub Desktop.
Tmux Worktree & Layout Manager: Automates session creation with a 3-column layout (AI/Shell, Neovim/Shell, LF), Git Worktree support, and Lazygit integration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================== | |
| # Script Name: tmw (Tmux Worktree Manager) | |
| # Layout: | |
| # - Col 1 (Left 100%): Opencode (Manual Start) | |
| # - Col 2 (Mid) : Neovim (Top) / Worktree Shell (Bot) | |
| # - Col 3 (Right) : LF File Manager | |
| # | |
| # Arguments: | |
| # -o Override (Recreate session) | |
| # -d Delete session | |
| # -l List sessions | |
| # -h Help | |
| # ============================================================================== | |
| set -e | |
| # ========================================== | |
| # 0. Helper: Usage & List | |
| # ========================================== | |
| usage() { | |
| echo "Usage: $(basename "$0") [OPTIONS] [PATH]" | |
| echo "" | |
| echo "Options:" | |
| echo " -o, --override Force recreate the session (Kill & New)" | |
| echo " -d, --delete Kill the session for the target path" | |
| echo " -l, --list List all active tmux sessions" | |
| echo " -h, --help Show this help message" | |
| echo "" | |
| echo "Examples:" | |
| echo " tmw # Open/Attach current dir" | |
| echo " tmw ~/my-repo # Open/Attach specific dir" | |
| echo " tmw -o # Re-open current dir (Refresh layout)" | |
| echo " tmw -d ~/old # Kill session for ~/old" | |
| } | |
| # ========================================== | |
| # 1. Argument Parsing | |
| # ========================================== | |
| FORCE_RECREATE=0 | |
| DELETE_SESSION=0 | |
| TARGET_PATH="" | |
| while [[ "$#" -gt 0 ]]; do | |
| case $1 in | |
| -o|--override) FORCE_RECREATE=1 ;; | |
| -d|--delete) DELETE_SESSION=1 ;; | |
| -l|--list) tmux list-sessions 2>/dev/null || echo "No active sessions."; exit 0 ;; | |
| -h|--help) usage; exit 0 ;; | |
| -*) echo "Unknown option: $1"; usage; exit 1 ;; | |
| *) TARGET_PATH="$1" ;; # Capture positional arg (path) | |
| esac | |
| shift | |
| done | |
| # Handle Path | |
| if [ -n "$TARGET_PATH" ]; then | |
| if [ -d "$TARGET_PATH" ]; then | |
| cd "$TARGET_PATH" || exit 1 | |
| else | |
| echo "❌ Error: Directory '$TARGET_PATH' not found." | |
| exit 1 | |
| fi | |
| fi | |
| # Generate Session Name | |
| session_name="$(basename "$PWD")" | |
| session_name="${session_name//./_}" # Replace dots with underscores | |
| # ========================================== | |
| # 2. Handle -d (Delete) | |
| # ========================================== | |
| if [ "$DELETE_SESSION" -eq 1 ]; then | |
| if tmux has-session -t "$session_name" 2>/dev/null; then | |
| tmux kill-session -t "$session_name" | |
| echo "🗑️ Session '$session_name' deleted." | |
| else | |
| echo "⚠️ Session '$session_name' not found." | |
| fi | |
| exit 0 | |
| fi | |
| # ========================================== | |
| # 3. Set Window Title | |
| # ========================================== | |
| printf "\033]0;%s\007" "$session_name" | |
| # ========================================== | |
| # 4. Logic Dispatch (Override / Attach) | |
| # ========================================== | |
| if [ "$FORCE_RECREATE" -eq 1 ] && tmux has-session -t "$session_name" 2>/dev/null; then | |
| echo "🔥 Override flag detected. Killing existing session '$session_name'..." | |
| tmux kill-session -t "$session_name" | |
| fi | |
| if tmux has-session -t "$session_name" 2>/dev/null; then | |
| echo "⚡️ Session '$session_name' already exists. Attaching..." | |
| if [ -n "$TMUX" ]; then | |
| tmux switch-client -t "$session_name" | |
| else | |
| tmux attach -t "$session_name" | |
| fi | |
| exit 0 | |
| fi | |
| echo "🔨 Creating new session: $session_name" | |
| if [ -n "$TMUX" ]; then | |
| echo "[ERROR] Inside tmux. To create a NEW session, please detach first." | |
| exit 1 | |
| fi | |
| # ========================================== | |
| # 5. Configuration Generation (Safe Mode) | |
| # ========================================== | |
| LF_CONFIG="/tmp/lfrc_tmw_${session_name}" | |
| cat > "$LF_CONFIG" <<'EOF' | |
| set hidden true | |
| set icons true | |
| set number false | |
| set drawbox false | |
| set preview false | |
| set ratios 1 | |
| # Smart Open Logic | |
| cmd smart_open ${{ | |
| if [ -d "$f" ]; then | |
| lf -remote "send $id open" | |
| else | |
| # 1. Ensure Neovim is in Normal mode | |
| tmux send-keys -t "$NVIM_ID" Escape | |
| # 2. Handle paths | |
| file="$f" | |
| if [ "${file#/}" = "$file" ]; then | |
| file="$PWD/$file" | |
| fi | |
| # 3. Escape single quotes for Vim | |
| safe_f=$(printf '%s' "$file" | sed "s/'/''/g") | |
| # 4. Construct Vim command: :call execute('tab drop ...') | |
| vim_cmd="call execute('tab drop ' .. fnameescape('$safe_f'))" | |
| # 5. Send to Neovim | |
| tmux send-keys -t "$NVIM_ID" ":" "$vim_cmd" Enter | |
| fi | |
| }} | |
| map <enter> smart_open | |
| map l smart_open | |
| EOF | |
| # ========================================== | |
| # 6. Preparation | |
| # ========================================== | |
| TERM_WIDTH=$(tput cols) | |
| TERM_HEIGHT=$(tput lines) | |
| worktrees=() | |
| if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then | |
| while IFS= read -r line; do | |
| if [[ "$line" == *"(bare)"* ]]; then | |
| continue | |
| fi | |
| path="${line%% *}" | |
| [[ -n "$path" ]] && worktrees+=("$path") | |
| done <<<"$(git worktree list 2>/dev/null)" | |
| fi | |
| if [ ${#worktrees[@]} -eq 0 ]; then | |
| worktrees+=("$PWD") | |
| fi | |
| # ========================================== | |
| # 7. Helper Functions | |
| # ========================================== | |
| setup_split_pane() { | |
| local pane_id=$1 | |
| local cmd=$2 | |
| local dir=$3 | |
| tmux select-pane -t "$pane_id" | |
| # Split bottom shell (25%) | |
| local shell_id | |
| shell_id=$(tmux split-window -v -p 25 -c "$dir" -P -F "#{pane_id}") | |
| # Clean up bottom shell | |
| tmux send-keys -t "$shell_id" "clear" C-m | |
| # Setup top pane command | |
| if [[ -n "$cmd" ]]; then | |
| tmux send-keys -t "$pane_id" "$cmd" C-m | |
| fi | |
| } | |
| setup_layout() { | |
| local wt_path="$1" | |
| local target="$2" | |
| # 1. Get Left ID (Left Column - 100% Opencode) | |
| local left_id | |
| left_id=$(tmux list-panes -t "$target" -F "#{pane_id}" | head -n1) | |
| # 2. Setup Columns | |
| # Split Right (LF) | |
| local right_id | |
| right_id=$(tmux split-window -t "$left_id" -h -l 30 -c "$wt_path" -P -F "#{pane_id}") | |
| # Split Middle (Neovim area) | |
| local mid_id | |
| mid_id=$(tmux split-window -t "$left_id" -h -p 50 -c "$wt_path" -P -F "#{pane_id}") | |
| # 3. Configure Left Column (Opencode ONLY) | |
| # Zsh Safe: read line | |
| local opencode_cmd="echo '💡 Opencode Ready. Press <Enter>...'; read line && opencode" | |
| tmux send-keys -t "$left_id" "$opencode_cmd" C-m | |
| # 4. Configure Middle Column (Neovim + Worktree Shell) | |
| local nvim_cmd="echo '🚀 Neovim Ready. Press <Enter>...'; read line && nvim" | |
| setup_split_pane "$mid_id" "$nvim_cmd" "$wt_path" | |
| # 5. Start LF (Right) | |
| local lf_cmd="echo 'brew install lf'" | |
| if command -v lf >/dev/null 2>&1; then | |
| lf_cmd="export NVIM_ID=$mid_id; lf -config $LF_CONFIG" | |
| fi | |
| tmux send-keys -t "$right_id" "$lf_cmd" C-m | |
| } | |
| # ========================================== | |
| # 8. Execution | |
| # ========================================== | |
| first_wt="${worktrees[0]}" | |
| first_name="$(basename "$first_wt")" | |
| # Create Session | |
| tmux new-session -d -x "$TERM_WIDTH" -y "$TERM_HEIGHT" -s "$session_name" -n "$first_name" -c "$first_wt" | |
| tmux set -g mouse on 2>/dev/null || true | |
| # Tmux Title | |
| tmux set-option -t "$session_name" set-titles on | |
| tmux set-option -t "$session_name" set-titles-string "#S" | |
| # Lazygit Binding | |
| if command -v lazygit >/dev/null 2>&1; then | |
| tmux bind-key g display-popup -w 90% -h 90% -d '#{pane_current_path}' -E "lazygit" | |
| else | |
| tmux bind-key g display-message "❌ Lazygit not found." | |
| fi | |
| first_window_id=$(tmux list-windows -t "$session_name" -F "#{window_id}" | head -n1) | |
| setup_layout "$first_wt" "$first_window_id" | |
| if [ ${#worktrees[@]} -gt 1 ]; then | |
| for ((i=1; i<${#worktrees[@]}; i++)); do | |
| wt_path="${worktrees[$i]}" | |
| wt_name="$(basename "$wt_path")" | |
| new_win_id=$(tmux new-window -a -t "$session_name" -n "$wt_name" -c "$wt_path" -P -F "#{window_id}") | |
| setup_layout "$wt_path" "$new_win_id" | |
| done | |
| fi | |
| tmux select-window -t "${session_name}:${first_name}" | |
| tmux select-pane -t "${session_name}:${first_name}" | |
| tmux select-pane -R | |
| tmux attach -t "$session_name" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment