zipzap logo

zipzap is a tool for tracking and jumping to frequently-used directories.

It's easy to install and to integrate with your shell:

cargo install --locked zipzap   # installs the binary
zipzap install                  # installs shell integrations
zipzap db import                # imports an existing `z` database
# restart your shell to load shell integrations, then use `z` to jump around

More details are in the README; keep scrolling for the narrative backstory.


Still with me? Great!

This all started with – earlier this week – I made the mistake of running brew upgrade.

Every time I do, it's a roll of the dice as to what will break: maybe my carefully constructed Python environment will stop working, or my stack of neovim plugin will fail due to API changes

This time, it was my shell: fish had changed how it handled colors, so I got a spew of warnings every time I opened a new shell.

Years ago, I had set up oh-my-fish, so there was an extra layer of indirection here: my configuration loaded the omf code, which did something with colors. I decided to tear it all out, copied the prompt into my own dotfiles, deleted omf, and called it a day.


Later that week, I typed z fidget into the shell. z is a utility for jumping into directories based on "frecency" (a combination of frequency and recency), so I expected this to jump to ~/code/fidget, where I spend a lot of my time.

➜  ~ z
fish: Unknown command: z

It turns out that I had been using omf's plugin-z integration, and now it was gone.

I could just copy those plugin files into my own dotfiles, but my curiosity had been piqued: how did z actually work? Here's z.fish:

function z -d "jump around"
  set -lx Z_SCRIPT_PATH (dirname (status -f))/../z/z.sh

  # Start a Bash process, source z, run the _z function, and capture the working directory and exit status.
  bash -c '
    source $Z_SCRIPT_PATH
    _z "$@" 2>&1
    Z_STATUS=$?
    echo "$PWD" >&2
    exit $Z_STATUS
  ' bash $argv 2>| read -l Z_PWD
  set -l Z_STATUS $status

  # If z changed directories, reflect that in the current process.
  if test $Z_PWD != $PWD
    builtin cd $Z_PWD
  end

  return $Z_STATUS
end

Okay, so the fish function spawned a bash shell to invoke the _z function.

Let's keep going down the stack: _z turns out to be 267 lines of bash, with the heavy lifting delegated to an awk script.

Here's a chunk of the code, for flavor:

local echo fnd last list opt typ
while [ "$1" ]; do case "$1" in
    --) while [ "$1" ]; do shift; fnd="$fnd${fnd:+ }$1";done;;
    -*) opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in
            c) fnd="^$PWD $fnd";;
            e) echo=1;;
            h) echo "${_Z_CMD:-z} [-cehlrtx] args" >&2; return;;
            l) list=1;;
            r) typ="rank";;
            t) typ="recent";;
            x) \sed -i -e "\:^${PWD}|.*:d" "$datafile";;
        esac; opt=${opt:1}; done;;
     *) fnd="$fnd${fnd:+ }$1";;
esac; last=$1; [ "$#" -gt 0 ] && shift; done
[ "$fnd" -a "$fnd" != "^$PWD " ] || list=1

None of this is bad, per se, but I can't parse it easily, and at this point my foot was firmly stuck down the rabbit hole.

I spent the weekend poking around, and emerged with zipzap, a single-binary replacement. zipzap isn't particularly novel! There's z-rs, zoxide, a native fish port of z, autojump, and presumably dozens of others – it's a particularly appealing rabbit hole, since you can knock out a customized solution in a day or two.

Still, it works for my use case, falling into the category of "home-cooked apps" from Robin Sloan's essay. It also gave me the chance to play with rusqlite, and another opportunity to build a lovely hand-crafted CI suite.

All in all, time well spent.