Scripting a Dev Environment with Tmux

I've been working through a lot of exercises on exercism.io recently. If you're not familiar with the site, it's basically a place to download and upload small programming exercises. It has been a great tool for me to learn Clojure, and I highly recommend it.

But there's one aspect of working with these exercises that's been a bit of a nuisance: when I complete an exercise, I have to change directories and set up my development environment all over again for the next exercise.

My development environment isn't very complicated (it's just three tmux panes doing different things), but I'm a programmer, so regularly re-issuing the same series of commands just feels wrong.

My Environment

These days, my typical Clojure development environment is a tmux session with three panes: my editor, my tests running automatically, and a REPL. For Exercism, I use Lein-Auto to automatically run my tests as I make changes. It looks like this:

My terminal

When I start a new exercise, I have to create a new tmux session, split the screen twice, start lein auto test in one pane, lein repl in another, an vim in the the last. Lastly, I resize the panes so vim has more room and I'm finally ready to code.

Scripting Tmux

Fortunately, this is Linux, so everything can be scripted! To automate this setup, I'm using two scripts: one global script that kicks things off and a project-local configuration script to handle the particulars for that project.

First, the global* script

*by "global" I mean that it's in my $PATH

I need to be able to have multiple tmux sessions going at once so that I can be working on more than one project at a time. These sessions should be named after the project they're for so I can detach and reattach easily. If a session already exists for a given project, I should connect to that session, and if no session exists for that project, one should be created.

#!/usr/bin/env zsh

local session_name  
session_name="$(pwd | rev | cut -d '/' -f1 | rev)"

if ! $(tmux has-session -t "$session_name" 2> /dev/null); then  
  tmux -2 new-session -d -s "$session_name"
fi

tmux -2 attach-session -t "$session_name"  

This uses the name of the current directory to name a tmux session, then either creates or attaches to it. Now we just have to configure the tmux session for each specific project.

Project-local configuration

I work in several languages with varying tech stacks. Not every project has the same development environment. For a Ruby project, for example, I usually run my tests manually since I don't have to wait for the JVM to load. And on Clojure projects, I might use a different test runner, like Midje, or even use Boot instead of Leiningen to run tasks. So projects need a way to specify their particular setup.

For now, I'm doing this simply by looking for special config files to source. I call these files .tconf, and there can be one per project. They should contain the tmux commands needed to set up a new environment for their project. The .tconf file for my Clojure Exercism project looks like this:

# lower-left pane
tmux split-window -v  
tmux resize-pane -D 10  
tmux send-keys "lein auto test" C-m

# lower-right pane
tmux split-window -h  
tmux send-keys "lein repl" C-m

tmux select-pane -U  
tmux send-keys "vim" C-m  

This splits the window into top and bottom, shrinks the bottom pane, starts the test runner, then splits the window again and starts the REPL, then moves the cursor to the top pane and opens vim. That's everything I need to start a new Clojure exercise.

Normally this file would live in the project's root directory, but since each exercise in Exercism has its own directory, I put this file in the parent directory for all the Clojure exercises. Now we just need to make the global script source this file when creating new sessions.

The complete global script:
#!/usr/bin/env zsh

local session_name  
session_name="$(pwd | rev | cut -d '/' -f1 | rev)"

if ! $(tmux has-session -t "$session_name" 2> /dev/null); then  
  tmux -2 new-session -d -s "$session_name"

  if [ -f .tconf ]; then       # normally the config will be in the project root
    tconf=".tconf"
  elif [ -f ../.tconf ]; then  # if it's not, check the parent directory too
    tconf="../.tconf"
  fi
  [ "$tconf" ] && source "$tconf"
fi

tmux -2 attach-session -t "$session_name"  

With this saved somewhere in your path and appropriate project config files in place, you're just one command away from your development environment!

What's next?

As nice as this is, I could imagine a lot of copy/pasting of the project-local config files from one project to the next. It would be nice to have a simpler and more declarative way to define the project environment. It might also be nice to have some default configurations for a given tech stack that could be overridden as necessary. But these are problems for another time. If you'd be interested in such features (or implementing them yourself!), let me know!