An Over-Engineered Workflow To Organizing Projects With tmuxp And Shell Autocompletes

Organizing projects is something that affects every developer. Some people work on a few projects, some many, and everyone has their own way of storing all those 1’s and 0’s . Recently I had a discussion with some friends about it and it was so interesting to hear how they worked that I thought it might be fun to share my work flow. This is by no means the “best” way, it’s more of a conversation starter. Feel free to steal the ideas and code and implement them on your own.

Part 1: Folder Layout

~/Work
├── organization1
│   ├── project1
│   │   ├── code
│   │   └── extra
│   └── project2
│       └── code
├── organization2
│   └── project3
│       ├── code
│       └── extra
└── mine
    ├── project4
    │   └── code
    └── project5
        └── code
        └── extra

Figure 1: Sample Folder Layout

Nothing fancy but everything going forward builds on this structure:

  • Level 1 (~/Work): Everything is in here.
  • Level 2 (Organizations): Organizations are companies or groups that I am doing work for. There is also a folder called mine where code for my personal projects is stored.
  • Level 3 (Project): This folder contains the project, usually named after the github repo. (E.g thornycrackers/docker-neovim would be docker-neovim).
  • Level 4 (Files): code is where the actual code of the project lives. I usually have a folder called extra where I store files related to the project.

I find this structure simple and robust enough that everything has a place. The first function I’ll share is for cloning repositories, since now I store them in the code folder I want it autocreated when I clone a repository:

gfcc () {
    # This function assumes urls of one of the following formats. All others
    # will not work:
    #
    # git@github.com:user/repo.git
    # https://github.com/user/repo
    PROTOCOL=$(echo "$1" | cut -c1-3)
    if [[ "$PROTOCOL" == 'git' ]]; then
        REPO=$(echo "$1" | cut -d'/' -f2 | cut -d'.' -f1) 
    elif [[ "$PROTOCOL" == 'htt' ]]; then
        REPO=$(echo "$1" | cut -d'/' -f5 )
    fi
    git clone "$1" "$REPO"/code
}

Figure 2: Git clone function

After you add the function to your ~/.bashrc, ~/.zshrc or wherever you store functions you can use the function as such:

$ gfcc https://github.com/thornycrackers/docker-neovim
$ gfcc git@github.com:thornycrackers/docker-neovim.git

Figure 3: Clone examples

Part 2: tmux Templating and Usage

I use tmuxp templating but there are many alternatives like tmuxomatic and tmuxinator. Choose your poison. My templates are stored in my tmux github repo with my project template stored here. Here is a current version of the template:

session_name: "${PROJECT}"
start_directory: "${DIR}"
windows:
  - window_name: code
    layout: even-vertical
    focus: true
    panes:
      - shell_command:
        - nvim
        focus: true
  - window_name: docker
    layout: even-vertical
    panes:
      - pane
      - pane
      - pane

Figure 4: tmuxp template

Which corresponds to a setup like:

Window 1      Window 2

111111111     222222222
111111111     222222222
111111111     222222222
111111111     333333333
111111111     333333333
111111111     333333333
111111111     444444444
111111111     444444444
111111111     444444444

Figure 5: tmux Window Setup

A sample command to load up a project looks like:

PROJECT="docker-neovim" DIR="/home/thorny/Work/mine/docker-neovim" tmuxp load ~/.tmux/templates/template.yaml

Figure 6: tmuxp command example

This is obviously too long to type out each time we want to boot up a project so we will add another function to help automate this since we have a folder structure that is predictable:

tlo() { # tmux templating
    if [[ -z "$1" ]]; then
        return
    fi
    # Search in all folders under 'Work'
    local PROJS=($(find "$HOME"/Work/* -mindepth 1 -maxdepth 1 -type d))
    local PROJ_NAME=''
    local PROJ_DIR=''
    # Find if we have a match
    for dir in "${PROJS[@]}"; do
        if [[ $(basename "$dir") == "$1" ]]; then
            PROJ_NAME=$(basename "$dir")
            PROJ_DIR="$dir/code"
        fi
    done
    if [[ -z "$PROJ_NAME" ]]; then
        echo 'Project not found'
        return
    fi
    cd "$PROJ_DIR"
    PROJECT="$PROJ_NAME" DIR="$PROJ_DIR" tmuxp load ~/.tmux/templates/template.yaml
    cd -
}

Figure 6: tmux template bash command

The tlo command will stop if:

  1. No argument is provided
  2. A project cannot be found

Referring to Figure 1 we can now execute a tmux sesion for a project with tlo projec1 or tlo project4. We don’t have to worry which organization the code is under we just need to know the project name. If there are dupicated names across organizations I will just namespace the folders organization_proj_name. Refering to Figure 5, I usually don’t like to have multiple panes open at one time and I used window 2 as a buffer of extra panes. I used tmux-drawer to quickly create a small window at the bottom of the screen and the following tmux command to switch that pane with other panes in window 2.

bind C-e command-prompt -p destination 'swap-pane -s 1.2 -t 2.%1'

Figure 7: tmux pane swap command

So I can hit ^B^M and type 3<CR> which will swap the bottom pane for the 3rd pane on the second window.

Part 3: Autocompletions for Shell commands

This part is extra but when I type tlo <tab> I would like to have the projects suggest as autocompletion. I’ll give examples of how to do auto complete in bash as well as zsh.

Part 3.1: ZSH

ZSH will look for custom autocompletes by looking through the $fpath variable and looking for a file based on the command we are using (E.g. tlo autocompletes should be stored on the $fpath in a file called _tlo). A minimal working example for ~/.zshrc would look like the following:

tlo (){
    ...
}

# Custom autocompletes
fpath=($HOME/autocompletes $fpath)

# Initialize the autocompletion
autoload -Uz compinit && compinit -i

Figure 8: zshrc example

Now we will create our custom autocomplete at ~/autocompletes/_tlo with the following content.

#compdef tlo

local -a options
options=$(find "$HOME"/Work/* -mindepth 1 -maxdepth 1 -type d | xargs -n1 basename)
_alternative "args:projects:($options)"

Figure 9: tlo zsh autocomplete file

After reloading your zsh shell you should now be able to autocomplete project names for the tlo command.

Part 3.2: Bash

The quickest way to create your own bash autocompletions is to just put them in your ~/.bashrc file or wherever you store your tlo function.

tlo() {
    ...
}
_tlo()
{
    local cur
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    opts=$(find "$HOME"/Work/* -mindepth 1 -maxdepth 1 -type d | xargs -n1 basename)


    COMPREPLY=($(compgen -W "${opts}" $cur))
}
complete -F _tlo tlo

Figure 10: tlo bash autocompletion

Final Thoughts

The main “issues” that I’ve tried to solve within my own workflow with this setup are:

  • Launch a project to work on with a templated tmux session. Because everything is repeatable it’s quick to get up and running after a system restart.
  • Work on multiple projects simultaneously with easy switching. This is great for anyone who’s constantly juggling projects.
  • Project related files are not cluttering up home or downloads folder.
  • Autocompletes in case you are having troubles remembering a projects name.

Hopefully this post sparked some ideas to make your dev work flow easier.