Skip to content

Instantly share code, notes, and snippets.

@kashaziz
Last active March 13, 2026 20:00
Show Gist options
  • Select an option

  • Save kashaziz/efd904dc7f88c05a2be6acf376f5fef8 to your computer and use it in GitHub Desktop.

Select an option

Save kashaziz/efd904dc7f88c05a2be6acf376f5fef8 to your computer and use it in GitHub Desktop.
tmux set up for multi project workflow
set -g mouse on
set -g history-limit 100000
set -g base-index 1
setw -g pane-base-index 1
unbind C-b
unbind C-a
set -g prefix C-Space
bind C-Space send-prefix
setw -g mode-keys vi
# bind | split-window -h
# bind - split-window -v
bind v split-window -h
bind s split-window -v
bind n next-window
bind p previous-window
bind -n C-h select-pane -L
bind -n C-j select-pane -D
bind -n C-k select-pane -U
bind -n C-l select-pane -R
# Alt-based bindings (no prefix) — works in Windows Terminal
bind -n M-1 select-window -t 1
bind -n M-2 select-window -t 2
bind -n M-3 select-window -t 3
bind -n M-4 select-window -t 4
bind -n M-n next-window
bind -n M-p previous-window
bind -n M-d detach-client
bind -n M-v split-window -h -c "#{pane_current_path}"
bind -n M-s split-window -v -c "#{pane_current_path}"
bind -n M-z resize-pane -Z
bind -n M-x confirm-before -p "kill pane? (y/n)" kill-pane
bind r source-file ~/.tmux.conf \; display "tmux reloaded"
set -g focus-events on
set -sg escape-time 0
set -g status-left "#S "
set -g status-right "%Y-%m-%d %H:%M"

tmux Project Launcher — WSL2 / Linux Dev Workflow

One command to launch any project in your ~/work folder with a full tmux session — correct windows, panes, and running processes — every time.

Built for developers juggling multiple projects simultaneously. No more manually activating venvs, starting servers, and opening terminals one by one.

work                        # fuzzy picker — lists all git repos in ~/work
work myproject              # launch by name (partial match works)
work kill myproject         # kill session so it starts fresh next time
work kill                   # fuzzy picker to kill a running session
work list                   # see all running sessions

How it works

  • work scans ~/work for all git repos (any depth up to 3 levels)
  • If a ~/.tmuxp/<project>.yaml exists → loads your custom layout
  • If not → opens a generic 2-window session (shell + git log)
  • Sessions persist when you close your terminal — rerunning work myproject reattaches

Install

1. Install tmux

# Ubuntu/Debian
sudo apt install tmux

# already installed on most Linux distros
tmux -V

2. Install tmuxp

pip install tmuxp

3. Install fzf (no sudo needed)

git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install --bin

4. Install direnv (optional — auto-activates venvs on cd)

curl -sfL https://direnv.net/install.sh | bash

5. Add to ~/.bashrc

# Project launcher
export PATH="$HOME/bin:$PATH"

# fzf
export PATH="$HOME/.fzf/bin:$PATH"
[ -f "$HOME/.fzf/shell/completion.bash" ] && source "$HOME/.fzf/shell/completion.bash"
[ -f "$HOME/.fzf/shell/key-bindings.bash" ] && source "$HOME/.fzf/shell/key-bindings.bash"

# direnv (if installed)
eval "$($HOME/.local/bin/direnv hook bash)"

Then reload:

source ~/.bashrc

6. Install the launcher

mkdir -p ~/bin
# copy the `work` file from this gist to ~/bin/work
chmod +x ~/bin/work

7. Copy tmux config

# copy .tmux.conf from this gist to ~/.tmux.conf

Add a project layout

Copy the example yaml and edit it for your project:

mkdir -p ~/.tmuxp
cp example-project.yaml ~/.tmuxp/myproject.yaml

The session name and yaml filename must match your project's directory name (lowercase, hyphens).

Example: project at ~/work/kashif/my-app~/.tmuxp/my-app.yaml

Projects without a yaml still work — they just get a plain 2-window session.


direnv (optional but recommended)

Add a .envrc to any project to auto-activate its environment on cd:

Python project:

# ~/work/myproject/.envrc
source_env_if_exists .env
[ -d ".venv" ] && source .venv/bin/activate

Node project:

# ~/work/myproject/.envrc
source_env_if_exists .env
PATH_add node_modules/.bin

Allow it once:

direnv allow ~/work/myproject

Keybindings

These work in Windows Terminal + WSL2 (Alt-based, no prefix key needed):

Action Shortcut
Switch to window 1/2/3/4 Alt+1 Alt+2 Alt+3 Alt+4
Next / prev window Alt+n / Alt+p
Detach session Alt+d
Split pane vertically Alt+v
Split pane horizontally Alt+s
Zoom pane (fullscreen toggle) Alt+z
Force kill current pane Alt+x
Move between panes Ctrl+h / Ctrl+j / Ctrl+k / Ctrl+l

Note: Standard Ctrl+b prefix is replaced with Ctrl+Space in the config. Windows Terminal intercepts Ctrl+b and Ctrl+a, so Alt-based bindings are used instead.


Customising a project layout

Everything custom lives in the project's yaml. Some examples:

Start Docker before the server:

panes:
  - shell_command:
      - docker-compose up -d
      - python manage.py runserver

Tail a log file in a dedicated pane:

  - window_name: logs
    panes:
      - shell_command:
          - tail -f logs/app.log

Open a database shell automatically:

      - shell_command:
          - source .venv/bin/activate
          - python manage.py dbshell

File summary

File Location Purpose
work ~/bin/work The launcher script
.tmux.conf ~/.tmux.conf tmux keybindings and settings
example-project.yaml ~/.tmuxp/<project>.yaml Per-project session layout
# tmuxp session layout — copy to ~/.tmuxp/<your-project-name>.yaml
# The filename must match your project directory name (lowercase, hyphens)
# Example: project at ~/work/kashif/my-app -> ~/.tmuxp/my-app.yaml
session_name: my-app
start_directory: ~/work/kashif/my-app
windows:
# Window 1: backend / main server
- window_name: server
layout: main-vertical # large pane left, stack on right
start_directory: ~/work/kashif/my-app
panes:
- shell_command:
- source .venv/bin/activate # remove if not a Python project
- python manage.py runserver # replace with your start command
- shell_command:
- source .venv/bin/activate # idle shell with venv active
- shell_command:
- git log --oneline -10 # quick git overview on launch
# Window 2: frontend (remove this window if frontend is in same repo)
- window_name: frontend
layout: main-vertical
start_directory: ~/work/kashif/my-app-fe
panes:
- shell_command:
- npm run dev
- shell_command:
- echo "Ready"
# Window 3: logs (optional)
- window_name: logs
start_directory: ~/work/kashif/my-app
panes:
- shell_command:
- tail -f logs/app.log 2>/dev/null || echo "No log file yet"
# Window 4: git
- window_name: git
start_directory: ~/work/kashif/my-app
panes:
- shell_command:
- git log --oneline -15
#!/bin/bash
# Project launcher — scans ~/work for git repos and launches tmux sessions
# Usage: work (interactive fzf picker)
# work <name> (direct launch by project name, partial match ok)
# work kill <name> (kill a session so it launches fresh next time)
# work kill (interactive fzf picker to kill a session)
# work list (list all running sessions)
FZF_BIN="${HOME}/.fzf/bin/fzf"
WORK_ROOT="${HOME}/work"
# Gather all git repos under ~/work (up to depth 3)
get_projects() {
find "$WORK_ROOT" -maxdepth 3 -name ".git" -type d 2>/dev/null \
| sed 's|/.git||' \
| sed "s|${WORK_ROOT}/||" \
| sort
}
# --- kill subcommand ---
if [ "$1" = "kill" ]; then
if [ -n "$2" ]; then
SESSION=$(tmux ls 2>/dev/null | cut -d: -f1 | grep -i "$2" | head -1)
else
SESSION=$(tmux ls 2>/dev/null | cut -d: -f1 | "$FZF_BIN" --prompt=" Kill session: " --height=40% --layout=reverse --border)
fi
if [ -z "$SESSION" ]; then
echo "No running session matching '${2:-}' found."
tmux ls 2>/dev/null || echo "No sessions running."
exit 1
fi
tmux kill-session -t "$SESSION"
echo "Killed session: $SESSION"
exit 0
fi
# --- list subcommand ---
if [ "$1" = "list" ]; then
tmux ls 2>/dev/null || echo "No sessions running."
exit 0
fi
# Pick project interactively or by argument
if [ -n "$1" ]; then
PROJECT=$(get_projects | grep -i "$1" | head -1)
if [ -z "$PROJECT" ]; then
echo "No project matching '$1' found."
exit 1
fi
else
if [ ! -x "$FZF_BIN" ]; then
echo "fzf not found at $FZF_BIN"
exit 1
fi
PROJECT=$(get_projects | "$FZF_BIN" \
--prompt=" Launch project: " \
--height=40% \
--layout=reverse \
--border \
--preview="ls ${WORK_ROOT}/{}" \
--preview-window=right:40%)
fi
[ -z "$PROJECT" ] && exit 0
FULL_PATH="${WORK_ROOT}/${PROJECT}"
SESSION=$(basename "$PROJECT" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
TMUXP_CONFIG="${HOME}/.tmuxp/${SESSION}.yaml"
echo "Launching: $PROJECT"
# --- Launch strategy ---
EXISTING_SESSION=false
tmux has-session -t "$SESSION" 2>/dev/null && EXISTING_SESSION=true
if [ -f "$TMUXP_CONFIG" ]; then
if [ "$EXISTING_SESSION" = true ]; then
echo "Session '$SESSION' already running — attaching."
tmux attach -t "$SESSION"
else
tmuxp load "$TMUXP_CONFIG"
fi
else
if [ "$EXISTING_SESSION" = false ]; then
tmux new-session -d -s "$SESSION" -c "$FULL_PATH" -n "shell"
tmux new-window -t "$SESSION" -c "$FULL_PATH" -n "git"
tmux send-keys -t "${SESSION}:git" "git log --oneline -10" Enter
else
echo "Session '$SESSION' already running — attaching."
fi
tmux attach -t "$SESSION"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment