Notes

Save project specific tmux layouts with tmuxp

Edit on GitHub

Workflow
6 minutes

Built-in tmux layouts

tmux has 5 built-in layouts, mapped to <prefix> Alt+1..5 in this order:

  1. even-horizontal — panes stacked side-by-side, equal width

    ┌───────┬───────┬───────┬───────┐
    │       │       │       │       │
    │   1   │   2   │   3   │   4   │
    │       │       │       │       │
    └───────┴───────┴───────┴───────┘
    
  2. even-vertical — panes stacked top-to-bottom, equal height

    ┌───────────────────────────────┐
    │               1               │
    ├───────────────────────────────┤
    │               2               │
    ├───────────────────────────────┤
    │               3               │
    ├───────────────────────────────┤
    │               4               │
    └───────────────────────────────┘
    
  3. main-horizontal — one large pane on top, smaller ones across the bottom

    ┌───────────────────────────────┐
    │                               │
    │               1               │
    │                               │
    ├─────────┬──────────┬──────────┤
    │    2    │    3     │    4     │
    └─────────┴──────────┴──────────┘
    
  4. main-vertical — one large pane on the left, smaller ones stacked on the right

    ┌──────────────────┬────────────┐
    │                  │     2      │
    │                  ├────────────┤
    │        1         │     3      │
    │                  ├────────────┤
    │                  │     4      │
    └──────────────────┴────────────┘
    
  5. tiled — roughly equal-sized grid

    ┌───────────────┬───────────────┐
    │       1       │       2       │
    ├───────────────┼───────────────┤
    │       3       │       4       │
    └───────────────┴───────────────┘
    

You can cycle through these with <prefix> space

Note that tmux’s “horizontal” / “vertical” refers to the orientation of the divider line, not the arrangement of panes. So even-horizontal has a horizontal row of panes (with vertical dividers between them), and main-horizontal has the main pane separated from the rest by a horizontal divider.

Run tmux list-commands | grep select-layout or see man tmux under select-layout for the authoritative list.

Defining a custom layout

1curl -LsSf https://astral.sh/uv/install.sh | sh    # install uv
2uv tool install tmuxp                              # install tmuxp
3tmuxp load .tmuxp.yaml                             # load layout
  • if you want a completely custom layout, you can use tmuxp.
  • the layout is saved to a YAML file and loaded by name.
  • in order to get the layout string, build the layout once by hand and then use the tmux list-windows -F '#{window_layout}' command to get the string. You will get something like c2a1,203x50,0,0{...}.

For example:

1# 3 vertical panes of equal size. last vertical pane is split once horizontally
2# ┌─────────┬─────────┬─────────┐
3# │         │         │    3    │
4# │    1    │    2    ├─────────┤
5# │         │         │    4    │
6# └─────────┴─────────┴─────────┘     
7983f,305x53,0,0{112x53,0,0,1,106x53,113,0,2,85x53,220,0[85x26,220,0,3,85x26,220,27,4]}

Drop that string into a tmuxp.yaml as the layout key. Panes are assigned to the layout slots in order (left-to-right, top-to-bottom):

1session_name: work
2windows:
3  - window_name: main
4    layout: 983f,305x53,0,0{112x53,0,0,1,106x53,113,0,2,85x53,220,0[85x26,220,0,3,85x26,220,27,4]} # custom layout
5    panes:
6      - shell_command: nvim
7      - shell_command: npm run dev
8      - shell_command: htop
9      - shell_command: tail -f log

Here is the default example

 1session_name: 4-pane-split
 2windows:
 3  - window_name: dev window
 4    layout: tiled # built-in tmux layout
 5    shell_command_before:
 6      - cd ~/ # run as a first command in all panes
 7    panes:
 8      - shell_command: # pane no. 1
 9          - cd /var/log # run multiple commands in this pane
10          - ls -al | grep \.log
11      - echo second pane # pane no. 2
12      - echo third pane # pane no. 3
13      - echo fourth pane # pane no. 4

The examples above are all running a command in each pane. If you don’t want to run any commands, then add an empty item with -. The total amount of panes defined with - need to match with the amount of panes defined in the layout string.

You can define multiple commands to run in a pane. e.g.

1- shell-command:
2  - export NODE_ENV=development
3  - nvm use lts
4  - npm run dev

You can decide which pane gets focus with focus: true. e.g.

1- shell_command: claude
2  focus: true

To run a command in all panes:

1shell_command_before:
2  - cd ~/ # run as a first command in all panes

Other values you can define: session_name, start_directory

Creating a bash command to load the right tmux layout

If you live in tmux, you’ve probably wished tmux would just know which layout to start. tmuxp already handles the layout part — YAML configs that describe your windows and panes. What’s missing is a single command that picks the right config for wherever you are.

Here’s a small bash function, t, that does exactly that. It grew out of a few rounds of refinement, and each step is worth understanding on its own.

Step 1: Load a project layout if one exists

The simplest version: if there’s a tmuxp.yaml in the current directory, load it. Otherwise, fall back to plain tmux.

1t() {
2  if [[ -f tmuxp.yaml ]]; then
3    tmuxp load tmuxp.yaml
4  else
5    tmux
6  fi
7}

Drop this in your ~/.bashrc or ~/.zshrc, source it, and any project folder with a tmuxp.yaml now has a one-keystroke launcher.

Step 2: Fall back to a default layout

Most projects don’t have a custom layout — but you probably still want a consistent starting shape (say: an editor pane, a shell pane, a log tailer). Save that once in ~/.tmuxp/default.yaml and have t fall back to it:

 1t() {
 2  local default="$HOME/.tmuxp/default.yaml"
 3  if [[ -f tmuxp.yaml ]]; then
 4    tmuxp load tmuxp.yaml
 5  elif [[ -f $default ]]; then
 6    tmuxp load "$default"
 7  else
 8    tmux
 9  fi
10}

Now t has three modes: project layout, personal default, bare tmux.

A minimal ~/.tmuxp/default.yaml might look like:

1windows:
2  - window_name: dev
3    focus: true
4    panes:
5      - shell_command: vim
6      - shell_command: htop
7        focus: true

focus: true is how you tell tmuxp which pane (and window) to land on when the session attaches.

Step 3: Name the session after the directory

By default, tmuxp uses whatever session_name is in the YAML. That means every project you open via the default config ends up with the same session name — annoying if you keep a few open at once.

The fix: pass -s <name> to tmuxp load (it overrides the YAML’s session_name), and use ${PWD##*/} to grab just the current folder name.

 1t() {
 2  local default="$HOME/.tmuxp/default.yaml"
 3  local name="${PWD##*/}"
 4  name="${name//[.:]/_}"   # tmux forbids . and : in names
 5
 6  if [[ -f tmuxp.yaml ]]; then
 7    tmuxp load -s "$name" tmuxp.yaml
 8  elif [[ -f $default ]]; then
 9    tmuxp load -s "$name" "$default"
10  else
11    tmux new -s "$name"
12  fi
13}

Now each folder gets its own session, named after itself.

Step 4: Respect an explicit session_name in the YAML

Overriding is fine for the default layout, but if a project’s tmuxp.yaml deliberately sets session_name, we should honor it. Only supply -s when the file doesn’t already name the session:

 1t() {
 2  local default="$HOME/.tmuxp/default.yaml"
 3  local name="${PWD##*/}"
 4  name="${name//[.:]/_}"
 5
 6  _tmuxp_load() {
 7    local file=$1
 8    if grep -qE '^\s*session_name\s*:' "$file"; then
 9      tmuxp load "$file"
10    else
11      tmuxp load -s "$name" "$file"
12    fi
13  }
14
15  if [[ -f tmuxp.yaml ]]; then
16    _tmuxp_load tmuxp.yaml
17  elif [[ -f $default ]]; then
18    _tmuxp_load "$default"
19  else
20    tmux new -s "$name"
21  fi
22}

That’s the full function. A quick grep for a top-level session_name: decides whether to pass -s or not.

Why this is nice

  • One command, everywhere. t works in any directory.
  • Zero config for most projects. Put a default layout in ~/.tmuxp/ and forget about it.
  • Per-project overrides are trivial. Drop a tmuxp.yaml in the repo root.
  • Sessions stay distinct. Each folder becomes its own session by default, but projects can still claim a fixed name when they want one.

Bonus: named layouts via tmuxp’s own discovery

tmuxp auto-discovers any YAML in ~/.tmuxp/, so alternates are a tmuxp load <name> away. ~/.tmuxp/work.yamltmuxp load work. Pair that with t for the common case and you’ve got the best of both worlds.