fcd: A fuzzy project switcher

By (3 minutes read)

As I often switch between various projects, I got tired of constantly typing cd ~/projects/... to find the project I needed to switch to and decided to come up with a better way. The tool I came up with is a zsh plugin I call fcd.

Why do this?

A basic use case for this can also be solved with a TextExpander shortcut, but unfortunately that doesn’t scale very well when you have more complex directory structures. For example, Go projects will by definition be stored in a path similar to $GOPATH/src/github.com/ignoreme/projectname and when doing client work you might want to separate sites (~/projects/ignoreme/sites/ignoreme) and server configuration (~/projects/ignoreme/sites/ignoreme).

So, what I really wanted was the ability to type a command followed by a couple of characters for the project and then automatically switching to it regardless of where it might be stored.

I actually wanted to go even further than this, and have it use autocompletion as well. This way I could use either fuzzy search (typing some part of the name) or tab completion to find the result I want to switch to.

What did I create?

As I use zsh as my shell, using Oh My Zsh, I figured the easiest way to do this was by building a plugin for that. It has good native support for autocompletion and creating a custom plugin for Oh My Zsh is a standard functionality for it.

While it took a lot of trial and error, the end result is actually fairly simple and compact. The code can be found on my fork of Oh My Zsh and I created a pull request for it as well, although I’m unsure if it will ever be merged.

How does it work?

Saying it’s simple is easy, but let’s have a look at how it works. The method fcd() is the basis of it all, but before that can be used we need to define $FCD_BASEDIR in our .zshrc config file.

FCD_BASEDIR=($HOME/projects/*/sites $HOME/projects/*/servers $GOPATH/src/github.com/arjenschwarz)

This configuration gives me 3 sets of possible sources for my projects to be located. And as you can see, wildcards are accepted.

fcd() {
    DIRS=("${(@f)$(find ${FCD_BASEDIR} -mindepth 1 -maxdepth 1 -type d -path "*$1*" ! -iname ".*")}")
    if (( ${#DIRS} == 1 )); then
        cd ${DIRS[1]}
    else
        echo "Multiple results found:"
        print -C 1 $DIRS
        cd ${DIRS[1]}
    fi
}

fcd() will now use find to look at the children of the $FCD_BASEDIR paths. Each (non-hidden) directory in there will be matched against the first argument. It will always switch to the first result, but if multiple results are found it will also show a list of all matches.

Next is the autocompletion.

compdef _fcd fcd

Using the zsh method compdef we first define that when we run the command fcd this will trigger the autocompletion defined in _fcd().

_fcd() {
   compadd _fcd_get_list
}

_fcd() in turn will add the results of _fcd_get_list() to the the autocompletion using compadd.

_fcd_get_list() {
    DIRS=("${(@f)$(find ${FCD_BASEDIR} -mindepth 1 -maxdepth 1 -type d -path "*$1*" ! -iname ".*")}")
    print -C 1 $DIRS | awk '{gsub(/\/.*\//,"",$1); print}'
}

And finally, there is _fcd_get_list() itself, which simply prints a list of the same result we would get when running the command itself. The only difference being that it will only print the final directory name instead of the whole path.

What does it look like?

The below gif is a short demonstration of fcd, but you’re of course welcome to copy the code for your own use.

fcd in action

comments powered by Disqus