# NixOS / nix-darwin module: automatic nushell completion indexing # # Indexes completions using three strategies in priority order: # 1. Native completion generators (e.g. CMD completions nushell) # 2. Manpage parsing # 3. --help output parsing # # Produces a directory of .json/.nu files at build time. # The `complete` command reads from this directory as a system overlay. # # This module body only uses options shared by NixOS and nix-darwin # (environment.{variables,systemPackages,pathsToLink,extraSetup}), so the # same file backs both flake outputs. On macOS the indexer scrapes Mach-O # binaries; on Linux, ELF — selected by the inshellah build's target os. # # Usage (NixOS): # { pkgs, ... }: { # imports = [ ./path/to/inshellah-rs/nix/module.nix ]; # programs.inshellah.enable = true; # } # Usage (nix-darwin): identical — import the same file (or the flake's # darwinModules.default) and set programs.inshellah.enable = true. { config, lib, pkgs, ... }: let cfg = config.programs.inshellah; completerSnippet = ./inshellah-completer.nu; dynamicStubCommands = [ "systemctl" "journalctl" "coredumpctl" "loginctl" "machinectl" "networkctl" "hostnamectl" "timedatectl" "localectl" "ssh" "scp" "sftp" "docker" "podman" "kubectl" "git" "jj" "npm" "pnpm" "yarn" "make" "just" "cargo" "pkill" ]; dynamicStubCommandArgs = lib.escapeShellArgs dynamicStubCommands; in { options.programs.inshellah = { enable = lib.mkEnableOption "nushell completion indexing via inshellah"; package = lib.mkOption { type = lib.types.package; description = "package to use for indexing completions"; }; completionsPath = lib.mkOption { type = lib.types.str; default = "/share/inshellah"; description = '' subdirectory within the system profile where completion files are placed. used as --dir for the completer. ''; }; extraDirs = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "/etc/profiles/per-user/alice/share/inshellah" ]; description = '' additional read-only completion directories to search. these are appended (colon-separated) to the --dir path alongside the system completions path. ''; }; ignoreCommands = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "problematic-tool" ]; description = '' list of command names to skip during completion indexing ''; }; helpOnlyCommands = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "nix" ]; description = '' list of command names to skip manpage parsing for, using --help scraping instead ''; }; extraScrapePackages = lib.mkOption { type = lib.types.listOf lib.types.package; default = [ ]; example = lib.literalExpression "[ pkgs.git pkgs.clang ]"; description = '' additional packages to scrape for completions alongside the system profile. each package's store path is passed to `inshellah index` via `--prefix`, so it must contain bin/ and/or share/man/. useful on macOS, where the active developer toolchain (git, clang, …) lives outside the nix system profile behind /usr/bin shims: install the nix equivalents and list them here so their completions get indexed reproducibly, rather than probing the host toolchain. ''; }; timeoutMs = lib.mkOption { type = lib.types.nullOr lib.types.int; default = null; example = 200; description = '' per-subprocess timeout in milliseconds. when null the binary's compiled-in default is used (currently 200ms). ''; }; dynamicTimeoutMs = lib.mkOption { type = lib.types.int; default = 5000; example = 2000; description = '' timeout in milliseconds for live dynamic completions in the nushell shim. this bounds runtime calls such as nix, jj, kubectl, and systemctl. set to 0 to disable the runtime timeout. ''; }; dynamicLimit = lib.mkOption { type = lib.types.int; default = 200; example = 100; description = '' maximum number of results requested from live dynamic completion providers when they expose a native result limit. set to 0 to omit native result-limit flags. ''; }; flagTriggers = lib.mkOption { type = lib.types.str; default = "-"; example = "-+"; description = '' characters that trigger flag (option) completions when a partial token begins with one of them. the default "-" reproduces the original behaviour where only a leading dash surfaces flags. each character is taken literally; whitespace is ignored. exported as INSHELLAH_FLAG_TRIGGERS. ''; }; flagOnEmpty = lib.mkOption { type = lib.types.bool; default = false; example = true; description = '' also surface flag completions when nothing has been typed yet — i.e. right after a space/tab — alongside subcommands. when false (the default) an empty token hands off to file/dynamic completion. exported as INSHELLAH_FLAG_ON_EMPTY. ''; }; maxCompletions = lib.mkOption { type = lib.types.int; default = 0; example = 100; description = '' upper bound on the number of static completion candidates returned, and the nushell `max_results` shown. 0 means no inshellah-imposed cap (nushell's built-in default of 200 still applies). exported as INSHELLAH_MAX_COMPLETIONS. ''; }; completeTimeoutMs = lib.mkOption { type = lib.types.nullOr lib.types.int; default = null; example = 400; description = '' per-subprocess timeout in milliseconds for the on-the-fly --help resolution the completer performs for uncached commands. distinct from `timeoutMs` (build-time indexing) and `dynamicTimeoutMs` (the nushell shim's live providers). null uses the binary's compiled default (currently 200ms). exported as INSHELLAH_TIMEOUT_MS. ''; }; workers = lib.mkOption { type = lib.types.nullOr lib.types.int; default = null; example = 8; description = '' worker thread count for the parallel scrape pool. when null, `std::thread::available_parallelism` is used. ''; }; snippet = lib.mkOption { type = lib.types.str; readOnly = true; default = builtins.readFile completerSnippet; description = '' nushell external completer snippet installed by the module. ''; }; }; config = lib.mkIf cfg.enable { environment.variables.INSHELLAH_DYNAMIC_TIMEOUT_MS = toString cfg.dynamicTimeoutMs; environment.variables.INSHELLAH_DYNAMIC_LIMIT = toString cfg.dynamicLimit; environment.variables.INSHELLAH_FLAG_TRIGGERS = cfg.flagTriggers; environment.variables.INSHELLAH_FLAG_ON_EMPTY = if cfg.flagOnEmpty then "1" else "0"; environment.variables.INSHELLAH_MAX_COMPLETIONS = toString cfg.maxCompletions; environment.variables.INSHELLAH_TIMEOUT_MS = lib.mkIf ( cfg.completeTimeoutMs != null ) (toString cfg.completeTimeoutMs); environment.systemPackages = let systemDir = "/run/current-system/sw${cfg.completionsPath}"; dirPaths = lib.concatStringsSep ":" ([ systemDir ] ++ cfg.extraDirs); wrapped = pkgs.writeShellScriptBin "inshellah" '' case "''${1:-}" in complete|query|dump) exec ${cfg.package}/bin/inshellah "$@" --dir "''${XDG_CACHE_HOME:-$HOME/.cache}/inshellah:${dirPaths}" ;; *) exec ${cfg.package}/bin/inshellah "$@" ;; esac ''; in [ (lib.hiPrio wrapped) cfg.package ]; environment.pathsToLink = [ "/share/nushell/autoload" "/share/nushell/vendor/autoload" ]; environment.extraSetup = let inshellah = "${cfg.package}/bin/inshellah"; destDir = "$out${cfg.completionsPath}"; ignoreFile = pkgs.writeText "inshellah-ignore" (lib.concatStringsSep "\n" cfg.ignoreCommands); ignoreFlag = lib.optionalString (cfg.ignoreCommands != [ ]) " --ignore ${ignoreFile}"; helpOnlyFile = pkgs.writeText "inshellah-help-only" ( lib.concatStringsSep "\n" cfg.helpOnlyCommands ); helpOnlyFlag = lib.optionalString (cfg.helpOnlyCommands != [ ]) " --help-only ${helpOnlyFile}"; timeoutFlag = lib.optionalString (cfg.timeoutMs != null) " --timeout-ms ${toString cfg.timeoutMs}"; workersFlag = lib.optionalString (cfg.workers != null) " --workers ${toString cfg.workers}"; # roll the explicit extra packages up into a single colon-separated # --prefix so they're scraped alongside the system profile. prefixFlag = lib.optionalString (cfg.extraScrapePackages != [ ]) ( " --prefix " + lib.concatStringsSep ":" (map toString cfg.extraScrapePackages) ); snippetFile = pkgs.writeText "inshellah-completer.nu" cfg.snippet; in '' mkdir -p ${destDir} if [ -d "$out/bin" ] && [ -d "$out/share/man" ]; then ${inshellah} index "$out" --dir ${destDir}${ignoreFlag}${helpOnlyFlag}${prefixFlag}${timeoutFlag}${workersFlag} \ 2>/dev/null || true fi find ${destDir} -maxdepth 1 -empty -delete # Install the full nushell completer plus sudo/doas wrapped commands. # Nushell otherwise hardcodes sudo/doas to bypass external completers. mkdir -p $out/share/nushell/vendor/autoload cp ${snippetFile} $out/share/nushell/vendor/autoload/inshellah.nu # Register command names for dynamic backends that are actually present # in the linked profile. The externs keep Nu's command list aware of # these commands while the external completer still supplies arguments. stubFile=$out/share/nushell/vendor/autoload/inshellah-command-stubs.nu : > "$stubFile" for cmd in ${dynamicStubCommandArgs}; do if [ -x "$out/bin/$cmd" ]; then printf '@complete external\nextern "%s" [...args]\n\n' "$cmd" >> "$stubFile" fi done if [ ! -s "$stubFile" ]; then rm -f "$stubFile" fi ''; }; }