ProductivityPaolo Castro

Streamlining Development Workflow with Tmux

Context switching between projects costs time and mental energy. Learn how a simple shell script can automate tmux session management, making it effortless to jump between different development environments.

#tmux#terminal#workflow#automation#shell

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:

hljs bash
#!/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:

hljs bash
# ~/.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:

text
┌─────────┬─────────┐
│ 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:

hljs bash
# 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.

Want to read more?

View All Stories