I spend most of my working day in the terminal. Over the years, I've experimented with different setups—multiple terminal windows, tabs, tiling window managers; but I always come back to tmux. It's reliable, scriptable, and works seamlessly over SSH connections, which makes it invaluable when working on remote machines.
The real turning point came when I realized that while tmux is powerful, managing sessions and windows manually was still too much friction. Every time I switched projects, I found myself recreating the same window layout: four panes arranged in a grid, each positioned in a specific directory. For a full-stack project, that might be the frontend repo in the top-left, the backend in the top-right, and database or log monitoring in the bottom panes.
Doing this manually dozens of times a week was tedious. I wanted to reduce context switching to a single command, so I built a small script to automate it.
The Solution: A Custom Dev Script
The idea is simple: define project environments as configuration files, then use
a script to load them on demand. The script takes a single argument—either the
name of a predefined environment or a dot (.) to create a new environment in
the current directory.
Here's the complete script:
#!/bin/sh
#
# Setup tmux
#
SESSION="dev"
PARAM=${1:-latest}
if [ "${PARAM}" != "." ]; then
. "${HOME}/.config/tmux-dev/dev/${PARAM}.zsh"
if [ "${PARAM}" != "latest" ]; then
rm "${HOME}/.config/tmux-dev/dev/latest.zsh"
ln -s "${HOME}/.config/tmux-dev/dev/${PARAM}.zsh" "${HOME}/.config/tmux-dev/dev/latest.zsh"
fi
else
NAME=$(basename "${PWD}")
PANE1="${PWD}"
PANE2="${PWD}"
PANE3="${PWD}"
PANE4="${PWD}"
fi
if ! tmux has -t $SESSION >/dev/null; then
echo "creating <$SESSION> session"
tmux new-session -d -s "$SESSION" -n "HOME"
fi
if tmux lsw -F "#W" | rg -q "^${NAME}$"; then
tmux selectw -t "$SESSION:$NAME"
else
tmux new-window -n "$NAME" -c "$BASE"
tmux selectw -t "$SESSION:$NAME"
tmux send-keys "cd $PANE1; clear" C-m
tmux split-window -v
tmux send-keys "cd $PANE3; clear" C-m
tmux split-window -h
tmux send-keys "cd $PANE4; clear" C-m
tmux select-pane -U
tmux split-window -h
tmux send-keys "cd $PANE2; clear" C-m
tmux select-pane -L
fi
if [ -n "$TMUX" ]; then
echo "already at <$SESSION>"
else
echo "attaching <$SESSION> session"
tmux attach -t $SESSION
fi
How It Works
The script operates in two modes depending on the argument you pass:
Predefined Environments
When you call dev my-project, the script sources a configuration file from
~/.config/tmux-dev/dev/my-project.zsh. This file defines environment variables
that specify the window name, base path, and directory for each pane:
# ~/.config/tmux-dev/dev/my-project.zsh
NAME="my-project"
BASE="${HOME}/code/my-project"
PANE1="${BASE}/frontend"
PANE2="${BASE}/backend"
PANE3="${BASE}/backend"
PANE4="${BASE}"
In this example, the top-left pane opens in the frontend directory, both top-right and bottom-left panes open in the backend (useful for running servers and tests simultaneously), and the bottom-right pane opens in the project root for git operations or other tasks.
The script also maintains a symlink to latest.zsh, which points to the most
recently used environment. This means running dev with no arguments (which
defaults to latest) will reopen your last project—a small detail that eliminates
even more friction.
Ad-Hoc Environments
When you call dev . from any directory, the script creates a new environment on
the fly. It uses the current directory name as the window name and opens all four
panes in that same location. This is perfect for quick experiments or one-off tasks
where you don't need a predefined layout.
The Pane Layout
The script creates a consistent four-quadrant layout:
┌─────────┬─────────┐
│ PANE1 │ PANE2 │
├─────────┼─────────┤
│ PANE3 │ PANE4 │
└─────────┴─────────┘
This layout has become second nature. I typically use the top panes for code editing and running dev servers, and the bottom panes for logs, database consoles, or git operations. The predictability means I never have to think about where things are—it just works.
Integration with Dotfiles
All of this—the script itself and the environment configurations—lives in my dotfiles repository, which I wrote about in a previous post. This makes the setup portable: on a new machine, I clone my dotfiles, and all my project environments are immediately available.
Real-World Usage
Here's what a typical workflow looks like:
# Start working on a specific project
$ dev my-project
# Later, switch to another project
$ dev other-project
# Need to work on a new or temporal project?
$ cd /tmp/debug && dev .
# Resume the last project you were working on
$ dev
Each invocation takes a fraction of a second. If the tmux session doesn't exist, it creates one. If the window already exists, it switches to it. If you're already inside a tmux session, it just switches windows without creating nested sessions.
The result is that context switching becomes nearly instantaneous. I don't think about tmux anymore; I think about the project I want to work on, and the script handles the rest.
Limitations and Trade-offs
This setup works well for my workflow, but it's not without constraints. The four-pane layout is hardcoded, which means if you need a different arrangement, you have to modify the script. For more complex setups, tools like tmuxinator or tmuxp offer more flexibility and configurability.
I also chose to keep environment files as simple shell scripts that just set variables. This makes them easy to read and modify, but it means they can't handle more advanced scenarios like running commands automatically in each pane or setting pane-specific environment variables.
For my needs, though, this balance is perfect. The script is about 40 lines of shell code that I fully understand and can tweak in seconds. It does one thing well: eliminate the friction of switching between projects.
Conclusion
This script has been part of my daily workflow for years now, and it's one of those small improvements that compounds over time. The few minutes I spent writing it have saved countless hours of manual window management.
If you spend a lot of time working across multiple projects in the terminal, I recommend building something similar. It doesn't have to be elaborate—just enough to remove the repetitive parts of your workflow. The code is simple, portable, and once it's in your dotfiles, it follows you everywhere.