Nonintrusive fzf with fish
I’ve been getting into fzf recently. In case what it does isn’t immediately obvious to you (it wasn’t to me), here’s what it does:
- You send a bunch of things to
fzf, one per line. fzflets you pick one (or more!) of the things that was sent to it.fzfprints, to standard output, that thing or those things that you picked.
This has a couple obvious use cases:
- “I want to
cdto a directory, but I only want to type the unique end of the path, not the entire path from here to there.” - “I want to
sshinto this one server again, buthistory | grep ssh | grep horse.exampleis more typinggrepthan I want to do.”
If you install fzf by hand, you’ll get an opportunity to let it install its own things into your shell’s configuration. If you let it, it’ll set up C-t, C-r, and M-c so that they run a “file widget”, “history widget”, and a “cd widget”, respectively. Pretty handy, but:
- This installation process puts a symlink from your fish-config directory (likely
~/.config/fish/) to/usr/local/something/fzf/key-bindings.fish. What’s worse, thatsomethingcould vary depending on your OS. This might not be an issue for most fish users, but I use the samefishconfiguration on both macOS and FreeBSD computers. If thekey-bindings.fishfile is in different places on both macOS and FreeBSD, I was gonna have a bad time. - My primary OS is macOS. While the Meta key may be easily used on every other OS, the only hardware Meta key I have is the Option key, and it’s being used to make curly quotes and ellipses, even in Terminal.app. Sure, I can press Escape and then press C, it’s not nearly as pleasant as pressing Alt-C.
I then tried to see if I could get 95% of what I wanted with 0% of the invasive code in my ~/.config/fish/.
Searching through history
Normally, when I want to search through my history, I run hgrep foo, where hgrep is short for history | grep.
I ended up with this:
function hfzf
set -l to_run (history | fzf)
if test -n "$to_run"
echo "$to_run"
eval "$to_run"
end
end
Note that fish won’t let you just run (history | fzf). It’ll give you an error of fish: Command substitutions not allowed. I’m not sure, but I think this is to keep fish users from shooting themselves in the foot. However, if you explicitly set that command to a variable and then run eval on it, fish will let you do what you want.
The echo line is there because it feels weird to run a command that hasn’t been printed to the console in some form. bash will print out lines that have had command-substitution magic (!!, etc.) applied to them; I wanted the same thing in fish with fzf.
Running it is easy. I just type hfzf, pick the history item from the list, and then hit Enter to run it.
Changing directories
fish already has cdh for “change to a recently-visited directory”, so what about cdf for “fuzzy cd”?
function cdf
set -l whither
if command -sq fd
set whither (fd --type d | fzf)
else
set whither (find . -type d | fzf)
end
test -n "$whither"; and cd "$whither"
end
After declaring a local variable, this tests to see if fd is installed. fd is a Rust-based reimplementation of find. Most importantly, it ignores hidden files by default, so it won’t waste time and disk reads by trawling through .git/objects/ directories looking for directories to change to.
I don’t have fd installed on all my machines, so I have a fallback case that uses find.
The final interesting line tests to make sure $whither actually has something in it before changing directories. If I quit out of fzf without picking anything, I don’t want to run cd with no arguments. That’ll just drag me back to my home directory, which likely isn’t what I want.
Wrapup
Two functions in two files gives me most of what I want out of fzf without deeply integrating it into my shell configuration in weird ways. If you dislike it when your utilities get their hooks into your shell, maybe you’ll find all this useful.