Skip to content

Instantly share code, notes, and snippets.

@fangwangme
Last active January 24, 2026 09:09
Show Gist options
  • Select an option

  • Save fangwangme/201313a95b265266f1c97d682b833d12 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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