inshellah/nix/module.nix
2026-05-24 18:15:32 +10:00

310 lines
10 KiB
Nix

# 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|purge)
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
'';
};
}