# worktrunk shell integration for nushell
# Tab completions: calls binary with COMPLETE=nu to get candidates.
# Note: nushell's completion engine bypasses custom completers when the current
# token starts with `-`, so flag completions (e.g. `wt switch --<TAB>`) don't
# appear. Subcommand and value completions work. (nushell/nushell#14504)
export def "nu-complete {{ cmd }}" [context: string] {
let worktrunk_bin = if ($env.WORKTRUNK_BIN? | is-not-empty) {
$env.WORKTRUNK_BIN
} else {
let external = (which -a {{ cmd }} | where type == "external")
if ($external | is-empty) { return [] }
($external | get 0.path)
}
let tokens = ($context | split row " " | where {|t| $t != "" })
let tokens = if ($context | str ends-with " ") {
$tokens | append ""
} else {
$tokens
}
let result = (do {
with-env { COMPLETE: nu } { ^$worktrunk_bin -- ...$tokens }
} | complete)
if $result.exit_code != 0 { return [] }
$result.stdout | lines | each {|line|
let parts = ($line | split row "\t")
if ($parts | length) >= 2 {
{ value: ($parts | get 0), description: ($parts | get 1) }
} else {
{ value: ($parts | get 0) }
}
}
}
# Override {{ cmd }} command with split directive passing.
# Creates two temp files: one for cd (raw path) and one for exec (shell).
# WORKTRUNK_BIN can override the binary path (for testing dev builds).
#
# Nushell pipeline workaround (nushell/nushell#12643):
#
# In POSIX shells, pipes are file-descriptor plumbing — stdout flows through
# the pipe while the shell continues executing post-command logic (directive
# processing, exit codes). In nushell, a function's "output" is its return
# value (the last expression), not bytes written to fd1. When an external
# command isn't the last expression, its stdout goes to the terminal as a
# side effect but doesn't flow through `|`.
#
# This means we can't both stream stdout through a pipe AND do post-processing
# (directives, cleanup) in the same function. We handle this by splitting into
# two paths based on the subcommand:
#
# - `list`: Direct passthrough. The binary is the last expression, so stdout
# streams to the terminal (progressive rendering) and flows through pipes
# (`{{ cmd }} list --format json | from json`). No directive processing — `list`
# never emits directives. No directive env vars are set.
#
# - Everything else: Stdout is redirected to a temp file via `o>`, then
# returned as the function's last expression after directive processing.
# Stderr flows to the terminal in real-time. The binary sees non-TTY stdout
# and uses buffered mode, but commands other than `list` don't benefit from
# progressive rendering anyway.
export def --env --wrapped {{ cmd }} [...args: string@"nu-complete {{ cmd }}"] {
let worktrunk_bin = if ($env.WORKTRUNK_BIN? | is-not-empty) {
$env.WORKTRUNK_BIN
} else {
# Find the external binary, not the custom function
# `which -a` is needed because `which` alone only returns the custom command
let external = (which -a {{ cmd }} | where type == "external")
if ($external | is-empty) {
error make {
msg: "{{ cmd }} binary not found in PATH. Install with 'cargo install --path .' or set $env.WORKTRUNK_BIN"
}
}
($external | get 0.path)
}
# `list` is the only command that benefits from streaming stdout (progressive
# table rendering). It never emits directives, so we skip directive processing
# and let the binary be the last expression — stdout flows through pipes.
# Note: global flags before `list` (e.g. `wt -C /path list`) miss this check
# and fall through to the buffered path. Output is still correct, just not
# progressively rendered.
if (not ($args | is-empty)) and ($args | first) == "list" {
# Direct passthrough: binary is the last expression of this branch,
# which is the last expression of the function. Stdout streams to the
# terminal and flows through nushell pipelines.
^$worktrunk_bin ...$args
} else {
let cd_file = (mktemp --tmpdir)
let exec_file = (mktemp --tmpdir)
let stdout_file = (mktemp --tmpdir)
# Capture stdout to file for pipeline passthrough; stderr flows to terminal.
# Nushell 0.98+ throws ShellError on non-zero exit (like bash `set -e`).
# `try` catches it so directive processing and temp file cleanup still run.
let exit_code = (try {
with-env { WORKTRUNK_DIRECTIVE_CD_FILE: $cd_file, WORKTRUNK_DIRECTIVE_EXEC_FILE: $exec_file } {
^$worktrunk_bin ...$args o> $stdout_file
}
0
} catch {
$env.LAST_EXIT_CODE
})
# cd file holds a raw path — no shell parsing needed
if ($cd_file | path exists) and (open $cd_file --raw | str trim | is-not-empty) {
let target_dir = open $cd_file --raw | str trim
cd $target_dir
}
# exec file holds arbitrary shell (e.g. from --execute).
# Execute the whole file as one sh invocation so multi-line payloads
# share a single shell session (matching bash/zsh/fish `source` semantics:
# variables persist, `cd` affects later commands, etc.).
# Env changes (export) won't persist in the nushell session, but no
# worktrunk code emits export directives.
if ($exec_file | path exists) and (open $exec_file --raw | str trim | is-not-empty) {
let script = open $exec_file --raw
^sh -c $script
}
rm -f $cd_file $exec_file
let output = (open $stdout_file --raw)
rm -f $stdout_file
# Return stdout or propagate failure as the function's last expression.
# Using a failing external command (not `error make`) so nushell treats it
# identically to the original non-zero exit — minimal display in scripts,
# no verbose source trace.
if $exit_code != 0 {
if ($output | is-not-empty) { print -n $output }
^sh -c $"exit ($exit_code)"
} else if ($output | is-not-empty) {
$output
}
}
}