This commit is contained in:
atagen 2026-05-19 23:32:51 +10:00
parent da4bc139eb
commit 7567f202e8
49 changed files with 8338 additions and 5483 deletions

437
nix/inshellah-completer.nu Normal file
View file

@ -0,0 +1,437 @@
@complete external
def --wrapped sudo [...args] {
^sudo ...$args
}
@complete external
def --wrapped doas [...args] {
^doas ...$args
}
let inshellah_static_complete = { |spans|
try {
let completed = (^inshellah complete ...$spans | complete)
if $completed.exit_code != 0 {
null
} else {
let parsed = (try { $completed.stdout | from json } catch { null })
let parsed_type = ($parsed | describe)
if $parsed == null {
null
} else if (($parsed_type | str starts-with "list") or ($parsed_type | str starts-with "table")) {
$parsed
} else {
null
}
}
} catch {
null
}
}
let inshellah_nonempty = { |items|
let result = ($items | default [] | compact)
if ($result | is-empty) { null } else { $result }
}
let inshellah_unit_candidates = { |scope, prefix|
try {
^systemctl ...$scope list-units --all --no-pager --plain --full --no-legend $"($prefix)*"
| lines
| each { |l|
let parsed = $l | parse -r '(?P<unit>\S+)\s+\S+\s+\S+\s+\S+\s+(?P<desc>.*)'
if ($parsed | length) > 0 {
{value: $parsed.0.unit, description: ($parsed.0.desc | str trim)}
}
} | compact
} catch { null }
}
let inshellah_kubectl_scope = { |spans|
let all_namespaces = ("-A" in $spans) or ("--all-namespaces" in $spans)
let namespace_eq = ($spans | where { |s| $s =~ '^--namespace=' } | get 0? | default "")
let namespace_arg = (
$spans
| enumerate
| where { |it| $it.item == "-n" or $it.item == "--namespace" }
| reverse
| get 0?
| default null
)
let namespace = if not ($namespace_eq | is-empty) {
$namespace_eq | str replace --regex '^--namespace=' ''
} else if $namespace_arg != null and (($namespace_arg.index + 1) < ($spans | length)) {
$spans | get ($namespace_arg.index + 1)
} else {
""
}
if $all_namespaces {
{args: [--all-namespaces], all: true}
} else if not ($namespace | is-empty) {
{args: [-n $namespace], all: false}
} else {
{args: [], all: false}
}
}
let inshellah_kubectl_names = { |kind, spans|
if ($kind | is-empty) or ($kind | str starts-with "-") {
null
} else {
let scope = do $inshellah_kubectl_scope $spans
let columns = if $scope.all {
"custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name"
} else {
"custom-columns=NAME:.metadata.name"
}
try {
let rows = (
^kubectl get $kind ...$scope.args --no-headers -o $columns
| lines
| str trim
| where { |n| not ($n | is-empty) }
)
if $scope.all {
$rows | each { |row|
let parts = $row | split row -r '\s+'
if ($parts | length) >= 2 {
{value: ($parts | get 1), description: $"($kind) in ($parts | get 0)"}
}
} | compact
} else {
$rows | each { |n| {value: $n, description: $kind} }
}
} catch { null }
}
}
let inshellah_git_refs = { ||
try {
^git for-each-ref --format='%(refname:short)%09%(objecttype)%09%(contents:subject)' refs/heads refs/remotes refs/tags
| lines
| each { |l|
let p = $l | split row "\t"
if ($p | length) >= 3 { {value: $p.0, description: $p.2} }
} | compact
} catch { null }
}
let inshellah_git_branches = { ||
try {
^git for-each-ref --format='%(refname:short)%09%(contents:subject)' refs/heads
| lines
| each { |l|
let p = $l | split row "\t"
if ($p | length) >= 2 { {value: $p.0, description: $p.1} }
} | compact
} catch { null }
}
let inshellah_git_remotes = { ||
try {
^git remote
| lines
| str trim
| where { |r| not ($r | is-empty) }
| each { |r| {value: $r, description: "remote"} }
} catch { null }
}
let inshellah_git_worktrees = { ||
try {
^git worktree list --porcelain
| lines
| each { |l|
let m = $l | parse -r '^worktree\s+(?P<p>.+)$'
if ($m | length) > 0 { {value: $m.0.p, description: ""} }
} | compact
} catch { null }
}
let inshellah_complete = { |spans|
let completions = do $inshellah_static_complete $spans
let span_len = ($spans | length)
let last_span = if $span_len > 0 { $spans | last } else { "" }
let prev_span = if $span_len >= 2 { $spans | get ($span_len - 2) } else { "" }
let sub = if $span_len >= 2 { $spans | get 1 } else { "" }
let additional = if ($completions == null and $span_len > 0) {
match $spans.0 {
"nix" => {
if $span_len < 2 {
null
} else {
try {
let nix_output = (
with-env { NIX_GET_COMPLETIONS: ($span_len - 1) } {
$spans | run-external $in
}
| split row -r '\n'
| str trim
| skip 1
| where { |e| not ($e | is-empty) }
)
if (($nix_output | length) < 6 and
$last_span =~ "[a-zA-Z][a-zA-Z0-9_-]*#[a-zA-Z][a-zA-Z0-9_-]*") {
with-env { NIX_ALLOW_UNFREE: "1" NIX_ALLOW_BROKEN: "1" } {
$nix_output | par-each { |e|
try {
{value: $e, description: (^nix eval --raw --impure $e --apply "f: f.meta.description" err> /dev/null)}
} catch {
{value: $e, description: ""}
}
}
}
} else {
$nix_output | each { |e| {value: $e, description: ""} }
}
} catch { null }
}
}
"systemctl" => {
let unit_verbs = [
"status" "show" "cat" "help" "start" "stop" "restart" "reload" "try-restart"
"reload-or-restart" "reload-or-try-restart" "isolate" "kill" "reset-failed"
"enable" "disable" "reenable" "preset" "mask" "unmask" "is-active" "is-failed"
"is-enabled" "edit"
]
let args = $spans | skip 1 | where { |s| not ($s | str starts-with "-") }
let verb = $args | get 0? | default ""
if (($verb in $unit_verbs) and $span_len >= 3) {
let scope = if ("--user" in $spans) { [--user] } else { [] }
do $inshellah_unit_candidates $scope $last_span
} else { null }
}
"journalctl" => {
if ($prev_span == "--unit" or $prev_span == "-u") {
let scope = if ("--user-unit" in $spans or "--user" in $spans) { [--user] } else { [] }
do $inshellah_unit_candidates $scope $last_span
} else { null }
}
"coredumpctl" => {
let unit_verbs = ["dump" "info" "debug" "list"]
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
| lines
| each { |l|
let p = $l | split row -r '\s+'
if ($p | length) >= 5 { {value: $p.4, description: $"PID ($p.4) ($p | get 9? | default "")"} }
} | compact
} catch { [] })
$units | append $pids
} else { null }
}
"loginctl" => {
let user_verbs = ["user-status" "show-user" "enable-linger" "disable-linger" "kill-user" "terminate-user"]
let session_verbs = ["session-status" "show-session" "activate" "lock-session" "unlock-session" "terminate-session" "kill-session"]
if (($sub in $user_verbs) and $span_len >= 3) {
try {
^loginctl list-users --no-pager --no-legend
| lines | each { |l|
let p = $l | str trim | split row -r '\s+'
if ($p | length) >= 2 { {value: $p.1, description: $"UID ($p.0)"} }
} | compact
} catch { null }
} else if (($sub in $session_verbs) and $span_len >= 3) {
try {
^loginctl list-sessions --no-pager --no-legend
| lines | each { |l|
let p = $l | str trim | split row -r '\s+'
if ($p | length) >= 3 { {value: $p.0, description: $"user ($p.2)"} }
} | compact
} catch { null }
} else { null }
}
"machinectl" => {
let machine_verbs = ["status" "show" "start" "login" "shell" "enable" "disable" "poweroff" "reboot" "terminate" "kill" "bind" "copy-to" "copy-from"]
if (($sub in $machine_verbs) and $span_len >= 3) {
try {
^machinectl list --no-pager --no-legend
| lines | each { |l|
let p = $l | str trim | split row -r '\s+'
if ($p | length) >= 1 { {value: $p.0, description: ($p | get 1? | default "")} }
} | compact
} catch { null }
} else { null }
}
"networkctl" => {
let link_verbs = ["status" "show" "up" "down" "renew" "forcerenew" "reconfigure" "delete"]
if (($sub in $link_verbs) and $span_len >= 3) {
try {
^networkctl list --no-pager --no-legend
| lines | each { |l|
let p = $l | str trim | split row -r '\s+'
if ($p | length) >= 4 { {value: $p.1, description: $"($p.2) ($p.3)"} }
} | compact
} catch { null }
} else { null }
}
"hostnamectl" | "timedatectl" | "localectl" => {
null
}
"ssh" | "scp" | "sftp" => {
let cfg_hosts = (try {
open ~/.ssh/config | lines | each { |l|
let m = $l | parse -r '(?i)^\s*Host\s+(?P<h>.+)$'
if ($m | length) > 0 { $m.0.h | split row -r '\s+' } else { [] }
} | flatten | where { |h| not ($h | str contains '*') and not ($h | is-empty) }
} catch { [] })
let known = (try {
open ~/.ssh/known_hosts | lines | each { |l|
($l | split row -r '\s+' | get 0? | default "") | split row ','
} | flatten | where { |h| (not ($h | is-empty)) and (not ($h | str starts-with '|')) and (not ($h | str starts-with '[')) }
} catch { [] })
$cfg_hosts | append $known | uniq | each { |h| {value: $h, description: ""} }
}
"docker" | "podman" => {
let need_container = ["exec" "logs" "inspect" "start" "stop" "restart" "rm" "kill" "attach" "cp" "top" "wait" "pause" "unpause" "port" "commit" "diff" "export"]
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}}'
| lines | each { |l|
let p = $l | split row "\t"
if ($p | length) >= 2 { {value: $p.0, description: $p.1} }
} | compact
} catch { null }
} else if ($sub in $need_image) {
try {
^($spans.0) images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}'
| lines | each { |l|
let p = $l | split row "\t"
if (($p | length) >= 2) and (not ($p.0 | str ends-with ':<none>')) {
{value: $p.0, description: $p.1}
}
} | compact
} catch { null }
} else { null }
}
"kubectl" => {
let resource_verbs = ["get" "describe" "delete" "edit" "scale" "annotate" "label"]
if (($sub in $resource_verbs) and $span_len >= 4) {
let kind = $spans | get 2? | default ""
do $inshellah_kubectl_names $kind $spans
} else if (($sub == "logs" or $sub == "exec" or $sub == "port-forward") and $span_len >= 3) {
do $inshellah_kubectl_names "pods" $spans
} else if ($sub == "rollout" and $span_len >= 5) {
let action = $spans | get 2? | default ""
let kind = $spans | get 3? | default ""
if ($action in ["history" "pause" "restart" "resume" "status" "undo"]) {
do $inshellah_kubectl_names $kind $spans
} else { null }
} else { null }
}
"git" => {
let ref_verbs = ["checkout" "merge" "rebase" "log" "diff" "show" "reset" "cherry-pick" "revert" "tag" "blame" "bisect"]
let branch_verbs = ["switch" "branch"]
if ($sub == "worktree") {
let worktree_verb = $spans | get 2? | default ""
if ($worktree_verb in ["remove" "move" "lock" "unlock" "repair"]) {
do $inshellah_git_worktrees
} else if ($worktree_verb == "add" and $span_len >= 5) {
do $inshellah_git_refs
} else { null }
} else if (($sub == "push" or $sub == "pull") and $span_len >= 3) {
if $span_len <= 3 {
do $inshellah_git_remotes
} else {
do $inshellah_git_refs
}
} else if (($sub in $branch_verbs) and $span_len >= 3) {
do $inshellah_git_branches
} else if (($sub in $ref_verbs) and $span_len >= 3) {
do $inshellah_git_refs
} else { null }
}
"npm" | "pnpm" | "yarn" => {
let wants = (
(($spans.0 == "yarn") and $span_len == 2)
or (($sub == "run" or $sub == "run-script") and $span_len == 3)
)
if $wants {
try {
open package.json | get scripts? | default {} | transpose name cmd
| each { |row| {value: $row.name, description: $row.cmd} }
} catch { null }
} else { null }
}
"make" => {
if $span_len <= 2 {
try {
open Makefile | lines
| each { |l|
let m = $l | parse -r '^(?P<t>[A-Za-z0-9_./-]+)\s*:'
if (($m | length) > 0) and (not ($m.0.t | str starts-with '.')) {
{value: $m.0.t, description: ""}
}
} | compact | uniq-by value
} catch { null }
} else { null }
}
"just" => {
if $span_len <= 2 {
try {
^just --list --unsorted
| lines | skip 1
| each { |l|
let m = $l | parse -r '^\s+(?P<t>[A-Za-z0-9_-]+)(?:\s+\S.*)?(?:\s*#\s*(?P<d>.*))?$'
if ($m | length) > 0 {
{value: $m.0.t, description: ($m.0.d? | default "")}
}
} | compact
} catch { null }
} else { null }
}
"cargo" => {
let target_flags = ["--bin" "--example" "--test" "--bench"]
if ($prev_span == "-p" or $prev_span == "--package") {
try {
^cargo metadata --no-deps --format-version 1
| from json
| get packages
| each { |pkg| {value: $pkg.name, description: ($pkg.version? | default "")} }
| uniq-by value
} catch { null }
} else if ($prev_span in $target_flags) {
let kind = $prev_span | str replace "--" ""
try {
^cargo metadata --no-deps --format-version 1
| from json
| get packages
| each { |pkg|
$pkg.targets
| where { |t| $kind in $t.kind }
| each { |t| {value: $t.name, description: ($t.kind | str join ",")} }
}
| flatten
| uniq-by value
} catch { null }
} else { null }
}
"kill" | "pkill" => {
try {
^ps -eo pid,comm --no-headers
| lines
| each { |l|
let parts = $l | str trim | split row -r '\s+'
if ($parts | length) >= 2 {
let pid = $parts | get 0
let comm = $parts | skip 1 | str join " "
if ($spans.0 == "kill") { {value: $pid, description: $comm} }
else { {value: $comm, description: $pid} }
}
} | compact
} catch { null }
}
_ => { null }
}
} else { null }
let result = ($completions | default []) | append ($additional | default []) | compact
if ($result | is-empty) { null } else { $result }
}
$env.config.completions.external = {enable: true, max_results: 200, completer: $inshellah_complete}

View file

@ -10,7 +10,7 @@
#
# Usage:
# { pkgs, ... }: {
# imports = [ ./path/to/inshellah/nix/module.nix ];
# imports = [ ./path/to/inshellah-rs/nix/module.nix ];
# programs.inshellah.enable = true;
# }
@ -23,6 +23,7 @@
let
cfg = config.programs.inshellah;
completerSnippet = ./inshellah-completer.nu;
in
{
options.programs.inshellah = {
@ -72,9 +73,33 @@ in
'';
};
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).
'';
};
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.
'';
};
};
@ -98,7 +123,10 @@ in
(lib.hiPrio wrapped)
cfg.package
];
environment.pathsToLink = [ "/share/nushell/autoload" ];
environment.pathsToLink = [
"/share/nushell/autoload"
"/share/nushell/vendor/autoload"
];
environment.extraSetup =
let
inshellah = "${cfg.package}/bin/inshellah";
@ -109,30 +137,24 @@ in
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}";
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} \
${inshellah} index "$out" --dir ${destDir}${ignoreFlag}${helpOnlyFlag}${timeoutFlag}${workersFlag} \
2>/dev/null || true
fi
find ${destDir} -maxdepth 1 -empty -delete
# nushell hardcodes sudo and doas to bypass the external completer,
# returning command-name completion instead of calling inshellah.
# these @complete external stubs override that so inshellah handles
# their flags and elevation stripping. placed in the nushell autoload
# dir so they are sourced automatically at shell startup.
# 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
cat > $out/share/nushell/vendor/autoload/inshellah-elevation.nu << 'NUSHELL'
@complete external
extern "sudo" []
@complete external
extern "doas" []
NUSHELL
cp ${snippetFile} $out/share/nushell/vendor/autoload/inshellah.nu
'';
};
}