Taking the Hell out of Shell: Making tmux Object-Oriented

Problem

tmux is a powerful productivity boosting command-line tool, but I find its command interface to be awkward. I’ve been using tmux as a sort of DIY IDE for some time now, creating shell scripts in each project’s root directory that bootstraps my development environment with all the services I need running, along with my preferred editor, neovim. For example:

#!/bin/sh

SESSION_NAME="my_proj"

# Test for existing sessions by name so we don't accidentally modify an existing session
# If the session exists already, assume it's already set up correctly, attach and exit.
EXISTING_INSTANCE=$(tmux list-clients -F \#S | grep "$SESSION_NAME")
if [ -n "$EXISTING_INSTANCE" ]; then
  tmux attach -t $SESSION_NAME
  exit 0
fi

# Otherwise, create the session, but don't attach yet
tmux new -s $SESSION_NAME -d

# Set up clipboard integration
tmux bind -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "xclip -i -f -selection primary | xclip -i -selection clipboard"

# Split current window, creating two panes that are aligned horizontally
tmux split-window -h

# Select first pane
tmux select-pane -t 0
# Split it 3 times, creating 4 panes aligned vertically
tmux split-window -v
tmux split-window -v
tmux split-window -v

# Run a command in first pane
tmux select-pane -t 0
tmux send-keys "yarn test-dev" C-m

tmux select-pane -t 1
tmux send-keys "yarn dev" C-m

tmux select-pane -t 2
tmux send-keys "yarn playwright test --ui" C-m

tmux select-pane -t 4
tmux send-keys "nvim" C-m

# Finally, attach to the session
tmux attach -t $SESSION_NAME

The above script creates a development environment that looks something like this:

Recently, when creating such a script for the umpteenth project, I realized that there may be a better way which starts with changing how I think about tmux. Perhaps what these scripts are really doing is interacting with objects stored in the tmux server, via the tmux client. Those objects fall into a few classes:

  • Session
  • Window
  • Pane

And perhaps more, but that’s enough for what I’d like to discuss today. When I create a session using new-session, I’m not just instantiating a Session, but also a Window and a Pane. When I split a Window using split-window, I’m actually creating a new Pane, adding it to a Window and determining its relationship to the existing Panes in that Window.

Despite this domain seeming like a great candidate for sensible object-oriented design, tmux‘s API seems like an instance of the god object anti-pattern: The command handler is directly responsible for everything. It handles every command and holds all the state of the server, including the active session, window and pane that are the default targets for their related commands.

Solution

Since, as far as I know, it’s not possible to create objects or classes in POSIX Shell, at least not ones that a Smalltalker would recognize as such, I attempted to emulate the grammar of Object Oriented languages in the way I named my functions. All functions share a common prefix, denoting that they belong to the same package. The next part of the name denotes the class that the function operates on, followed by the operation itself. In some instances, there are further parts that denote modifiers. All of the parts are joined with an underscore (_). Whether or not the method is static (class-side) or not (instance-side) can be inferred by the user (or at least it seemed fairly obvious to me.) All of the functions that create objects return a UUID natively understood by tmux, so we can store the result in our own script’s state and interact with these objects explicitly.

With this approach, I was able to turn the script above into

#!/bin/sh

# Source the wm_* fuctions, providing an OOP-like interface to tmux
. ./wootmux.sh

SESSION_NAME="zomboban"

# Test for existing sessions by name so we don't accidentally modify an existing session
# If the session exists already, assume it's already set up correctly, attach and exit.
if [ "$(wm_session_exists $SESSION_NAME)" ]; then
  echo "attaching to existing session"
  wm_session_attach $SESSION_NAME
  exit 0
fi

wm_session_new $SESSION_NAME
wm_use_clipboard

left_pane="$(wm_pane_current)"

wm_pane_new_right "$left_pane" nvim

wm_pane_new_below "$left_pane" "yarn test-dev"
wm_pane_new_below "$left_pane" "yarn playwright test --ui"
wm_pane_new_below "$left_pane" "yarn dev"

wm_session_attach $SESSION_NAME

About half as many lines, and way more readable!

Read more about my implementation of this idea (wootmux) on GitHub.

Critique

Simpler and more readable code is great, but this does add an extra dependency, just to improve one script. The real benefit comes when you’re writing lots of tmux scripts, which might live in different projects, in which case you need to decide whether and how to share this dependency across projects. That’s more to think about, and as software developers, we have plenty of that as it is. Like many things in software engineering, it’s a question of pros and cons with no clear answers.

Another possible critique is that Shell is terrible, should we keep writing software for such an unproductive and unforgiving platform? It’s kind of a vicious cycle. Shell is available on all POSIX systems, and this ubiquity is a big plus, encouraging the development of more software that makes use of it. This ever growing selection of software applications, in turn, encourages its spread. At least some of these tools, such as ShellCheck, help smooth the platform’s sharp edges.

Possibly the biggest criticism is that this amounts to “abstraction inversion,” which is an anti-pattern in which a new layer is added to a system that re-implements interfaces that already exist in a lower layer, but are hidden. I’d be willing to bet there is something akin to Session, Window and Pane classes in tmux’s source code (though they won’t be literal classes, because it’s mostly written in C.) But for a proof of concept, this is okay. I’ll dog food this for a bit before I dive into trying to make a contribution to tmux’s source code.

At the very least, even if this isn’t a very useful idea, I learned a lot about tmux!

There’s probably more reasonable critiques that could be made, but I’ll leave it here for now. Feel free to reach out on this site or via GitHub with any thoughts or feedback, and thanks for reading!


Posted

in

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *