Quick notes search with Fish Shell, fzf, and ripgrep

Switching from ZSH to Fish Shell has had me migrating/rewriting shell functions that I use frequently. Near the top of the list of my most essential shell functions: interactive searching of my notes. I’ve been using a shell function like this for years, but the switch to fish gave me a good excuse to make a few tweaks.

The core idea is simple: search through my notes with ripgrep, filter interactively with fzf, and jump to the matching line in my editor. What makes it nice is the bat preview window showing context around each match as I navigate through results.

The fish version is cleaner than my old ZSH one, which is pretty much how I’d sum-up my entire experience switching to fish: everything is just a little cleaner and more joyful. Fish’s argparse makes handling options actually pleasant (if a teeny-bit verbose…), so I added flags for --direction, file --extension, and --language for syntax highlighting in bat. Probably the biggest change is that I’ve made the function default to not opening the file, instead it assembles the command to open the file in the command buffer so I can see it, but won’t trigger until I hit Enter. This frees me to use the function to pick out a file path, but then tweak the prepared command (say, swap $EDITOR for rm if I actually want to delete the file) before running it if needed.

If I do just want to jump in though, I included an --execute flag that opens the editor immediately instead of just staging the command in my buffer, and I have an alias for this.

Speaking of alias’s, I’ve set up a few for searching the different file-types and directories I’m always flitting through: notes, journal, and manifests. They’re just thin wrappers, but they’re what I’ll be typing dozens of times a day, so brevity is welcome. The full function with its options is there when I need it, but usually I just want to search my notes and these make that seamless.

function sn --wraps search-notes --description "Search notes (short alias)"
    search-notes --directory ~/notes --extension md --language toml $argv
end

function sm --wraps search-notes --description "Search manifests (short alias)"
    search-notes --directory ~/records/manifests --extension toml --language toml $argv
end

function sj --wraps search-notes --description "Search manifests (short alias)"
    search-notes --directory ~/journal --extension toml --language toml $argv
end

Here’s the actual script:

function search-notes --description "Search notes with ripgrep and fzf, open in editor"
    # Default values
    set -l dir ~/notes
    set -l ext md
    set -l lang toml
    set -l execute 0

    # Parse options
    argparse 'd/directory=' 'e/extension=' 'l/language=' x/execute -- $argv
    or return

    if set -q _flag_directory
        set dir $_flag_directory
    end
    if set -q _flag_extension
        set ext $_flag_extension
    end
    if set -q _flag_language
        set lang $_flag_language
    end
    if set -q _flag_execute
        set execute 1
    end

    # Perform the search
    set -l selected (rg --color=always --line-number --no-heading --smart-case --glob "*.$ext" '' $dir \
        | fzf --ansi \
              --exact \
              --delimiter ':' \
              --preview "bat --color=always --language=$lang --highlight-line {2} {1}" \
              --preview-window 'up,60%,border-bottom,+{2}+3/3,~3')

    if test -n "$selected"
        set -l file (echo $selected | cut -d: -f1)
        set -l line (echo $selected | cut -d: -f2)

        if test $execute -eq 1
            # Directly execute the editor command
            eval "e +$line $file"
        else
            # Insert into commandline buffer (default behavior)
            commandline -r "e +$line $file"
        end
    end
end