#!/usr/bin/env bash # An fzf script with autocomplete from "nix search" which allows for interactive fuzzy searching of derivations. # After the search a nix subcommand is executed on the selected derivation(s), e.g. "nix shell" or "nix run". set -eou pipefail FLAKE="nixpkgs" # The default flake to use. TODO: make this configurable NIX_SUBCOMMAND="shell" # The default nix subcommand to execute MULTIPLE_SELECTION=true # Whether to allow the user to select multiple derivations PRINT_COMMAND=false # Only print the command that would be executed, don't execute it if [ -n "${XDG_CACHE_HOME-}" ]; then CACHE_PATH="$XDG_CACHE_HOME/nix-search-fzf/cache.txt" else CACHE_PATH="$HOME/.cache/nix-search-fzf/cache.txt" fi # Because fzf executes commands from keybindings in a subprocess, we cannot directly change this scripts state. # Instead we can use a temporary file as an IPC mechanism, to change which subcommand to execute. TMP_FILE="$(mktemp --dry-run --suffix "-nix-search-fzf")" trap 'rm -f "$TMP_FILE"' EXIT INT TERM handleArguments() { while (("$#" > 0)); do case "$1" in -s | shell | --shell) NIX_SUBCOMMAND="shell" ;; -b | build | --build) NIX_SUBCOMMAND="build" ;; -r | run | --run) NIX_SUBCOMMAND="run" MULTIPLE_SELECTION=false ;; -e | edit | --edit) NIX_SUBCOMMAND="edit" MULTIPLE_SELECTION=false ;; -c | command | --command) PRINT_COMMAND=true ;; -u | update | --update) manageCache true exit ;; -h | help | --help) echo "Usage: $(basename "$0") [--shell|--build|--run|--edit|--update]" echo " --shell: enter a nix shell with the selected package(s). This is the default" echo " --build: build the selected package(s) with nix build" echo " --run: run the selected package with nix run" echo " --edit: edit the selected package with nix edit" echo " --command: only print the command that would be executed, don't execute it" echo " --update: update the nix search cache, this is done automatically every 10 days" echo " --help: show this help message" exit 0 ;; *) echo "Unknown option '$1'" exit 1 ;; esac shift 1 done } runColored() { printf "\e[32m\$ %s\n\e[0m" "$1" eval "$1" } manageCache() { local doUpdate="${1:-false}" mkdir -p "$(dirname "$CACHE_PATH")" if [ ! -f "$CACHE_PATH" ] || [ ! -s "$CACHE_PATH" ]; then doUpdate="true" echo "attribute path cache does not exist, generating..." >&2 elif (($(date -r "$CACHE_PATH" +%s) < $(date -d "now - 10 days" +%s))); then doUpdate="true" echo "cache file is older than 10 days, updating..." >&2 fi if [ "$doUpdate" == "true" ]; then echo "caching attribute paths..." >&2 # Create a list of all attribute paths with "legacyPackages.$arch" stripped # In the future this could contain metadata as well, doing a "nix-eval" for each is not the fastest nix search "$FLAKE" "^" --quiet --json | jq -r 'keys[]' | cut -d'.' -f3- >"$CACHE_PATH" echo "successfully generated attribute path cache" >&2 fi } fzfBindingFlag() { local tmpFile="$1" local -A bindings=( ["shell"]="ctrl-s" ["build"]="ctrl-b" ["edit"]="ctrl-e" ["run"]="ctrl-r" ) local result="--bind=" for subCommand in "${!bindings[@]}"; do local binding="${bindings[$subCommand]}" # When pressed, write the appropriate command to our temporary IPC file, and change the prompt accordingly result+="$binding:execute-silent(echo $subCommand > $tmpFile)+change-prompt($subCommand > )," done echo "${result%,}" } runFzf() { local multi_flag if [ "$MULTIPLE_SELECTION" == true ]; then multi_flag="--multi" else multi_flag="--no-multi" fi fzf "$multi_flag" \ --height 40% \ --preview-window right,70% \ --border rounded \ --prompt "$NIX_SUBCOMMAND > " \ --preview "bash -c \"@previewText@ {} $FLAKE\"" \ "$(fzfBindingFlag "$TMP_FILE")" <"$CACHE_PATH" } runNix() { local packages selectedPkgs command readarray -t selectedPkgs <<<"$@" ((${#selectedPkgs[@]} == 0)) && exit 0 if [ "$MULTIPLE_SELECTION" == true ] && ((${#selectedPkgs[@]} > 1)); then # Build a brace expansion string local pkg_list="{" for pkg in "${selectedPkgs[@]}"; do pkg_list+="$pkg," done packages="${pkg_list%,}}" else packages="${selectedPkgs[0]}" fi ((${#packages} == 0)) && exit 0 # Update what subcommand to execute, in case it was changed by a keybinding from fzf [ -s "$TMP_FILE" ] && NIX_SUBCOMMAND="$(<"$TMP_FILE")" command="NIXPKGS_ALLOW_UNFREE=1 nix $NIX_SUBCOMMAND $FLAKE#$packages --impure" if [ "$PRINT_COMMAND" == true ]; then echo "$command" exit 0 else runColored "$command" fi } handleArguments "$@" manageCache runNix "$(runFzf)"