diff --git a/doc/nixos.md b/doc/nixos.md index d50ada6..6b0a46e 100644 --- a/doc/nixos.md +++ b/doc/nixos.md @@ -73,6 +73,14 @@ programs.inshellah = { # default of 200ms) timeoutMs = null; + # timeout in ms for live dynamic completions at tab-completion time + # set to 0 to disable the runtime timeout + dynamicTimeoutMs = 5000; + + # result cap requested from live providers that support native limits + # set to 0 to omit native result-limit flags + dynamicLimit = 200; + # worker-thread count for the parallel scrape workers = null; }; @@ -109,6 +117,14 @@ $env.config.completions.external = { ... } the snippet provides both static lookups against the system index and runtime fallbacks for cases the static index can't cover: +runtime fallbacks have a default 5s timeout, controlled by +`programs.inshellah.dynamicTimeoutMs` or `INSHELLAH_DYNAMIC_TIMEOUT_MS` +when sourcing the snippet manually. providers with native result caps use +`programs.inshellah.dynamicLimit` or `INSHELLAH_DYNAMIC_LIMIT`, defaulting +to 200. set either value to 0 to disable that guard. on timeout the +completer returns `null` so nushell can fall back to its normal completion +behavior. + | command | dynamic source | |---|---| | `nix` | flake refs via `NIX_GET_COMPLETIONS`, with optional `meta.description` | @@ -120,6 +136,7 @@ runtime fallbacks for cases the static index can't cover: | `docker` / `podman` | containers + image refs by subcommand | | `kubectl` | resource names from the live cluster | | `git` | refs + worktree paths | +| `jj` | revisions, operations, bookmarks, remotes, files, and workspaces | | `npm` / `pnpm` / `yarn` | scripts from package.json | | `make` / `just` | targets / recipes | | `cargo` | workspace targets behind `--bin` / `--example` / etc. | diff --git a/flake.nix b/flake.nix index dd20c6b..b5f9b19 100644 --- a/flake.nix +++ b/flake.nix @@ -68,6 +68,9 @@ fakeNix = pkgs.writeShellScriptBin "nix" '' if [ "''${1:-}" = eval ]; then printf 'raw package description\n' + elif [ "''${1:-}" = slow ]; then + sleep 1 + printf 'header\nslow-package\n' else printf 'header\nbuild\nflake#pkg\n' fi @@ -101,6 +104,9 @@ printf 'origin\nupstream\n' ;; for-each-ref) + if [ -n "''${INSHELLAH_GIT_ARGS_FILE:-}" ]; then + printf '%s\n' "$*" > "$INSHELLAH_GIT_ARGS_FILE" + fi case "$*" in *"refs/heads refs/remotes refs/tags"*) printf 'main\tcommit\tMain branch\norigin/main\tcommit\tRemote main\nv1.0\tcommit\tRelease 1\n' @@ -210,9 +216,30 @@ echo "running nushell shim checks" export PATH="${fakeCompletionBackends}/bin:$PATH" export KUBECTL_ARGS_FILE="$TMPDIR/kubectl.args" + export INSHELLAH_GIT_ARGS_FILE="$TMPDIR/git.args" export INSHELLAH_STATIC_FILE="$TMPDIR/inshellah-static.json" + export INSHELLAH_DYNAMIC_TIMEOUT_MS=50 : > "$INSHELLAH_STATIC_FILE" nu --no-config-file -c 'source ${./nix/inshellah-completer.nu}; source ${./tests/nushell-completer.nu}' + INSHELLAH_DYNAMIC_TIMEOUT_MS=0 nu --no-config-file -c ' + source ${./nix/inshellah-completer.nu} + "" | save --force $env.INSHELLAH_STATIC_FILE + let completer = $env.config.completions.external.completer + let slow = do $completer [nix slow ""] + if (($slow | get 0.value) != "slow-package") { + error make {msg: "dynamic timeout 0 should disable timeout"} + } + ' + INSHELLAH_DYNAMIC_LIMIT=0 nu --no-config-file -c ' + source ${./nix/inshellah-completer.nu} + "[]" | save --force $env.INSHELLAH_STATIC_FILE + "" | save --force $env.INSHELLAH_GIT_ARGS_FILE + let completer = $env.config.completions.external.completer + do $completer [git fetch origin ""] + if ((open $env.INSHELLAH_GIT_ARGS_FILE) | str contains "--count") { + error make {msg: "dynamic limit 0 should omit provider limit flags"} + } + ' cat > "$TMPDIR/config-load.nu" <<'EOF' source ${./nix/inshellah-completer.nu} diff --git a/nix/inshellah-completer.nu b/nix/inshellah-completer.nu index bee5363..432a64e 100644 --- a/nix/inshellah-completer.nu +++ b/nix/inshellah-completer.nu @@ -13,6 +13,47 @@ let inshellah_nonempty = { |items| if ($result | is-empty) { null } else { $result } } +let inshellah_default_dynamic_timeout_ms = 5000 + +let inshellah_dynamic_timeout_ms = do { + let raw = (try { + $env.INSHELLAH_DYNAMIC_TIMEOUT_MS? | default $inshellah_default_dynamic_timeout_ms | into int + } catch { $inshellah_default_dynamic_timeout_ms }) + if $raw >= 0 { $raw } else { $inshellah_default_dynamic_timeout_ms } +} + +let inshellah_dynamic_timeout = ($inshellah_dynamic_timeout_ms * 1ms) + +let inshellah_default_dynamic_limit = 200 + +let inshellah_dynamic_limit = do { + let raw = (try { + $env.INSHELLAH_DYNAMIC_LIMIT? | default $inshellah_default_dynamic_limit | into int + } catch { $inshellah_default_dynamic_limit }) + if $raw >= 0 { $raw } else { $inshellah_default_dynamic_limit } +} + +let inshellah_limit_args = { |flag| + if $inshellah_dynamic_limit == 0 { [] } else { [$flag $inshellah_dynamic_limit] } +} + +let inshellah_with_timeout = { |body| + if $inshellah_dynamic_timeout_ms == 0 { + try { do $body } catch { null } + } else { + let tag = random int 1..2147483647 + let job_id = job spawn { + do $body | job send --tag $tag 0 + } + try { + job recv --tag $tag --timeout $inshellah_dynamic_timeout + } catch { + try { job kill $job_id } catch { null } + null + } + } +} + let inshellah_fuzzy_score = { |needle, haystack| let needle = $needle | default "" | into string let haystack = $haystack | default "" | into string @@ -189,7 +230,7 @@ let inshellah_kubectl_names = { |kind, spans| let inshellah_git_refs = { || try { - ^git for-each-ref --format='%(refname:short)%09%(objecttype)%09%(contents:subject)' refs/heads refs/remotes refs/tags + ^git for-each-ref ...(do $inshellah_limit_args "--count") --format='%(refname:short)%09%(objecttype)%09%(contents:subject)' refs/heads refs/remotes refs/tags | lines | each { |l| let p = $l | split row "\t" @@ -200,7 +241,7 @@ let inshellah_git_refs = { || let inshellah_git_branches = { || try { - ^git for-each-ref --format='%(refname:short)%09%(contents:subject)' refs/heads + ^git for-each-ref ...(do $inshellah_limit_args "--count") --format='%(refname:short)%09%(contents:subject)' refs/heads | lines | each { |l| let p = $l | split row "\t" @@ -211,7 +252,7 @@ let inshellah_git_branches = { || let inshellah_git_tags = { || try { - ^git for-each-ref --format='%(refname:short)%09%(contents:subject)' refs/tags + ^git for-each-ref ...(do $inshellah_limit_args "--count") --format='%(refname:short)%09%(contents:subject)' refs/tags | lines | each { |l| let p = $l | split row "\t" @@ -232,7 +273,7 @@ let inshellah_git_remotes = { || let inshellah_git_stashes = { || try { - ^git stash list + ^git stash list ...(do $inshellah_limit_args "-n") | lines | each { |l| let m = $l | parse -r '^(?Pstash@\{[0-9]+\}):\s*(?P.*)$' @@ -289,7 +330,7 @@ let inshellah_git_worktrees = { || let inshellah_jj_revs = { || try { - ^jj log --ignore-working-copy --no-graph -r 'all()' -T 'change_id.shortest() ++ "\t" ++ description.first_line() ++ "\n"' err> /dev/null + ^jj log --ignore-working-copy --no-graph ...(do $inshellah_limit_args "-n") -r 'all()' -T 'change_id.shortest() ++ "\t" ++ description.first_line() ++ "\n"' err> /dev/null | lines | each { |l| let p = $l | split row "\t" @@ -331,7 +372,7 @@ let inshellah_jj_remotes = { || let inshellah_jj_ops = { || try { - ^jj op log --ignore-working-copy --no-graph -T 'id.short() ++ "\t" ++ description.first_line() ++ "\n"' err> /dev/null + ^jj op log --ignore-working-copy --no-graph ...(do $inshellah_limit_args "-n") -T 'id.short() ++ "\t" ++ description.first_line() ++ "\n"' err> /dev/null | lines | each { |l| let p = $l | split row "\t" @@ -368,6 +409,7 @@ let inshellah_complete = { |spans| let sub = if $span_len >= 2 { $spans | get 1 } else { "" } let additional = if ($completions == null and $span_len > 0) { + do $inshellah_with_timeout { match $spans.0 { "nix" => { if $span_len < 2 { @@ -425,7 +467,7 @@ let inshellah_complete = { |spans| if (($sub in $unit_verbs) and $span_len >= 3) { let units = (do $inshellah_unit_candidates [] $last_span | default []) let pids = (try { - ^coredumpctl list --no-pager --no-legend + ^coredumpctl list ...(do $inshellah_limit_args "-n") --no-pager --no-legend | lines | each { |l| let p = $l | split row -r '\s+' @@ -502,7 +544,7 @@ let inshellah_complete = { |spans| let need_image = ["run" "rmi" "tag" "push" "pull" "history" "save" "create"] if ($sub in $need_container) { try { - ^($spans.0) ps -a --format '{{.Names}}\t{{.Image}}' + ^($spans.0) ps ...(do $inshellah_limit_args "--last") --format '{{.Names}}\t{{.Image}}' | lines | each { |l| let p = $l | split row "\t" if ($p | length) >= 2 { {value: $p.0, description: $p.1} } @@ -801,6 +843,7 @@ let inshellah_complete = { |spans| } _ => { null } } + } } else { null } if $completions == null { diff --git a/nix/module.nix b/nix/module.nix index 95289f8..940e816 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -110,6 +110,28 @@ in ''; }; + 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. + ''; + }; + workers = lib.mkOption { type = lib.types.nullOr lib.types.int; default = null; @@ -131,6 +153,9 @@ in }; config = lib.mkIf cfg.enable { + environment.variables.INSHELLAH_DYNAMIC_TIMEOUT_MS = toString cfg.dynamicTimeoutMs; + environment.variables.INSHELLAH_DYNAMIC_LIMIT = toString cfg.dynamicLimit; + environment.systemPackages = let systemDir = "/run/current-system/sw${cfg.completionsPath}"; diff --git a/tests/nushell-completer.nu b/tests/nushell-completer.nu index 0913c36..547b8c9 100644 --- a/tests/nushell-completer.nu +++ b/tests/nushell-completer.nu @@ -42,6 +42,8 @@ let nix_commands = do $completer [nix ""] assert-eq ($nix_commands | get 0.value) "build" "nix command completion uses NIX_GET_COMPLETIONS" let nix_pkg = do $completer [nix "flake#pkg"] assert-eq ($nix_pkg | get 0.description) "raw package description" "nix descriptions are raw strings" +let nix_slow = do $completer [nix slow ""] +assert-eq $nix_slow null "slow dynamic completions time out" let systemctl_empty = do $completer [systemctl daemon-reload ""] assert-eq $systemctl_empty null "systemctl does not offer units for non-unit verbs"