typenil

Matt White

developer

typenil

developer

A Terminal Case of Workspace Management

Who is this for?

  • You work in the terminal a lot
  • You have to jump between projects
  • You think easy context switching might make you curse less
  • (Bonus) you use terminal-based editors

Lately I’ve found myself with an D-tier superpower - near-painless workspace switching.

Normalizing your workflow

I regularly work with a dozen different projects - none of which have consistent command usage. For example, to run the unit test suite for any given project:

# These are all equivalent, depending on the project
pytest tests
poetry run pytest tests
docker-compose run whatever-project pytest tests
docker-compose run whatever-project python manage.py test
docker-compose -f ./compose/docker-compose-test.yml run whatever-project pytest tests
...

This is cognitive overhead - and usually involves a few screwups each time I context switch. I wanted a setup where I could simply type test in each project and not have to think about it.

Enter desk

desk is a lightweight workspace manager - essentially just a wrapper for shell scripts that define each workspace.

I maintain a deskfile (really just a .sh file) for each project I work with regularly. By default, deskfiles live at ~/.desk/desks/ - a great folder to keep under version control.

So what goes into a desk workspace? Whatever fits your workflow. For me, the vast majority falls under:

  • run - to run the project
  • lint - for any linting tools
  • test - for the test runner
  • build - to build the project and/or container
  • format - for code formatting
  • interact - to enter a project’s docker container
  • typecheck - to run static type analysis

These are simply aliases in shell files; you can do whatever makes your workflow easier - as sparse or comprehensive as you want. Here’s how a couple of mine look.

A project based on Python Poetry

#!/bin/bash
#
# Description: video editing library
#

basedir=~/Workspaces/typenil/ergot

cd $basedir || exit

# code formatting
alias format="poetry run black ."

# run unit tests
alias test="poetry run pytest tests/"

# build the project
alias build="poetry build"

# static type checking
alias typecheck="poetry run mypy ."

# run flake8
alias flake8="poetry run flake8 --max-complexity 10 ."

# run pylint
alias pylint="poetry run pylint tests/ ergot/"

# run linting
alias lint="flake8 && pylint"

# run bandit security check
alias bandit="poetry run bandit -r ergot/"

# run code quality
alias quality="test; format; typecheck; bandit; lint"

# run the ergot command line
alias run="poetry run python main.py"

A project with the development flow built in docker compose

#!/bin/bash
#
# Description: scheduled and queue driven jobs
#

basedir=~/Workspaces/typenil/JobSlob

export COMPOSEPREFIX="docker-compose -f ${basedir}/docker-compose.yaml"
export PYTHONPATH=$basedir
cd $basedir || exit

# close running instances
alias down="$(echo "${COMPOSEPREFIX}") down --remove-orphans"

# code formatting
alias format="$(echo "${COMPOSEPREFIX}") run jobslob formatting"

# run unit tests
alias test="$(echo "${COMPOSEPREFIX}") run jobslob test"

# build the project
alias build="$(echo "${COMPOSEPREFIX}") build"

# static type checking
alias typecheck="$(echo "${COMPOSEPREFIX}") run jobslob typecheck"

# run linting
alias lint="$(echo "${COMPOSEPREFIX}") run jobslob lint"

# run code quality
alias quality="test; format; typecheck; lint"

# run bash inside the app container
alias interact="$(echo "${COMPOSEPREFIX}") run --entrypoint bash jobslob"

I’d welcome a better solution than my weird echo "${COMPOSEPREFIX}" nonsense. Email me, shell experts.

Obviously some projects will need more or fewer aliases, but these workspaces are much more uniform than before. It doesn’t matter which workspace I’m in - test runs the test suite. Whatever you want.

If you need to know what’s available, desk will give you a nice rundown of commands.

Persisting and switching between your workspaces

Desk is great for rapidly switching between terminal workspaces, but we can still do better. This is where terminal multiplexers come in.

To borrow from tmux’s wiki (which says this far better than I could), terminal multiplexers:

[let] you switch easily between several programs in one terminal, detach them (they keep running in the background) and reattach them to a different terminal.

With tmux, we can easily keep many workspaces running simultaneously and rapidly switch between them.

I’ll focus on integrating desk with tmux, but I’m sure the same thing can be accomplished with other multiplexers like screen.

New to tmux?

If you need a quickstart, tmux has a very solid getting started guide and there are countless tutorials out there that I can’t beat.

tmux cheat sheet

For our purposes, the following should be enough.

  • tmux uses ctrl+b as its prefix hotkey by default. After entering ctrl+b, you can either type a hotkey sequence or use : and enter a command.
  • to detach from a running tmux session: ctrl+b, d
  • to list and switch between running sessions: ctrl+b, s
  • (Optional) to create a new named session from within tmux: ctrl+b, :new -s new-session-name

Integrating with desk

The real magic happens when you add the following to your .bashrc/.zshrc/.whatevershellrc file:

tsession=$([[ -n "${TMUX+set}" ]] && tmux display-message -p "#S")

# strip suffixes so that you can make
# multiple instances of the same desk
# by using deskname-1, deskname-2, etc
desk=$(echo "$tsession" | sed -E "s/-[0-9]+$//g")

if [ "x$tsession" != "x" ] && [ "$tsession" != "$WITHIN_DESK" ]
then
	export WITHIN_DESK="$tsession"
	desk . "$desk"
fi;

This snippet sets up your shell to detect whether you’re running in a tmux session. If you are, the shell will attempt to activate a desk with the same name as the session.

At this stage, if you have a desk called whatevs, you can run tmux new-session -s whatevs to create a new tmux session that automatically activates your whatevs desk. If it’s already running, you could use tmux attach -t whatevs to attach to it.

But again, we can make it easier.

t () {
  tmux attach -t $1 || tmux new-session -s $1
}

Adding this to your .bashrc/.zshrc/.whatevershellrc file, you can just run t whatevs and not worry about its state. tmux will drop you into your workspace.

Take advantage of tmux session switching

This is how you switch between open workspaces quickly. By default, tmux will give you a list of sessions when you enter ctrl+b, s. You can navigate the list with your arrow keys or use the numbers displayed alongside each session.

It’s so easy to jump between projects - and even more useful if you rely on terminal-based editors. I’ve never had such a fluid workflow.

Deleted scenes

Alternatives

direnv

If desk doesn’t suite your purposes (to be fair, it hasn’t had much development lately), I’ve found that direnv can meet many of the same needs.

For me, the advantage goes to desk for the following reasons:

  • desk stores workspaces in a single folder that’s trivial to put in version control
  • direnv would require a completely different method to integrate with tmux in a similar way

tmuxinator

tmuxinator seems like it could be quite useful for workspace management and customization, but I’ve never personally used it.

Desired future improvements

  • Cleanly integrate pass or some other credential manager with this setup
  • Get rid of the not found warning starting tmux with a name that has no matching desk
Join me in becoming a little less terrible every day.