diff --git a/overlays/default.nix b/overlays/default.nix index 29457b4..9086c35 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -1,3 +1,3 @@ { - addition = final: _: import ../pkgs { pkgs = final; }; + addition = final: _: import ../pkgs/default.nix { pkgs = final; }; } diff --git a/pkgs/nix-search-fzf/default.nix b/pkgs/nix-search-fzf/default.nix new file mode 100644 index 0000000..63e4a37 --- /dev/null +++ b/pkgs/nix-search-fzf/default.nix @@ -0,0 +1,50 @@ +{ + createScript, + replaceVars, + gnused, + jq, + fzf, + nix, + coreutils, + bash, + nix-search-fzf, + writeShellScript, +}: + +let + previewText = createScript "fzf-preview" ./fzf-preview.sh { }; + src = replaceVars ./nix-search-fzf.sh { + previewText = "${previewText}/bin/fzf-preview"; + }; +in +createScript "nix-search-fzf" src { + dependencies = [ + gnused + jq + fzf + nix + coreutils + bash + ]; + + # Enter a 'nix shell' with packages selected by this script + passthru.zsh-shell-widget = writeShellScript "nix-search-fzf-shell-widget" '' + nix-search-fzf-shell-widget() { + setopt localoptions pipefail no_aliases 2> /dev/null + local cmd="$(eval "${nix-search-fzf}/bin/nix-search-fzf -c")" + if [[ -z "$cmd" ]]; then + zle redisplay + return 0 + fi + zle push-line + BUFFER="''${cmd}" + zle accept-line + local ret=$? + unset cmd + zle reset-prompt + return $ret + } + ''; + + meta.description = "a wrapper around 'nix {run,shell,edit}' with autocomplete using fzf"; +} diff --git a/pkgs/nix-search-fzf/fzf-preview.sh b/pkgs/nix-search-fzf/fzf-preview.sh new file mode 100644 index 0000000..fabdc12 --- /dev/null +++ b/pkgs/nix-search-fzf/fzf-preview.sh @@ -0,0 +1,73 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i bash -p +# shellcheck shell=bash + +set -euo pipefail + +PKG_NAME="$1" +FLAKE="$2" +FLAKE_PATH="${FLAKE}#${PKG_NAME}" + +removeQuotes() { + local flag="$*" + flag="${flag%\"}" + echo "${flag#\"}" +} + +newlinesToCommaSeperated() { + echo "$@" | sed ':a;N;$!ba;s/\n/, /g' +} + +evalAttr() { + local attr data + attr="$1" + data="$(nix eval "$FLAKE_PATH"."$attr" 2>/dev/null)" + [[ $data != "null" && $data != "false" && -n $data ]] && removeQuotes "$data" +} + +evalJsonAttr() { + local attr jqArgs data + attr="$1" + jqArgs="$2" + data="$(nix eval --json "$FLAKE_PATH"."$attr" 2>/dev/null | jq -r "$jqArgs")" + [[ $data != "null" && -n $data ]] && echo "$data" +} + +evalNixpkgsLib() { + local function data + function="$1" + # Impure is needed to import the flake reference + data="$(nix eval --raw --impure --expr "let pkgs = (builtins.getFlake \"flake:$FLAKE\"); in pkgs.lib.$function pkgs.$PKG_NAME" 2>/dev/null)" + [[ $data != "null" && -n $data ]] && echo "$data" +} + +maybeEcho() { + local -r prefix="$1" + local flag="$2" + local -r commaSeperated="${3:-false}" + [[ $commaSeperated == "true" ]] && flag="$(newlinesToCommaSeperated "$flag")" + test -n "$flag" && echo "$prefix $flag" +} + +test -n "$(evalAttr "meta.broken")" && echo "broken: true" +test -n "$(evalAttr "meta.insecure")" && echo "insecure: true" + +version="$(evalAttr "version")" +# Derive the version from "name" using 'lib.getVersion' if it's not set +test -z "$version" && version="$(evalNixpkgsLib "getVersion")" +maybeEcho "version:" "$version" + +homepage="$(evalAttr "meta.homepage")" +maybeEcho "homepage:" "$homepage" + +description="$(evalAttr "meta.description")" +maybeEcho "description:" "$description" + +license="$(evalJsonAttr "meta.license" 'if type=="array" then .[].fullName else .fullName end')" +maybeEcho "license:" "$license" true + +maintainers="$(evalJsonAttr "meta.maintainers" '.[].github')" +maybeEcho "maintainers:" "$maintainers" true + +platforms="$(evalJsonAttr "meta.platforms" 'if type=="array" then .[] else . end')" +maybeEcho "platforms:" "$platforms" true diff --git a/pkgs/nix-search-fzf/nix-search-fzf.sh b/pkgs/nix-search-fzf/nix-search-fzf.sh new file mode 100644 index 0000000..b7a3036 --- /dev/null +++ b/pkgs/nix-search-fzf/nix-search-fzf.sh @@ -0,0 +1,161 @@ +#!/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)"