mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
173 lines
8.2 KiB
Nu
173 lines
8.2 KiB
Nu
# 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 }}" [spans: list<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 result = (do {
|
|
with-env { COMPLETE: nu } { ^$worktrunk_bin -- ...$spans }
|
|
} | 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.
|
|
#
|
|
# Completer wired via `@complete` attribute on an untyped `[...args]` rest.
|
|
# The parameter-level `string@"completer"` form keeps tokens literal, which
|
|
# blocks nushell's normal argument handling for `--flag="value"` (quote
|
|
# stripping) and `~` (path expansion). Untyped rest gets that handling once
|
|
# nushell/nushell#18131 ships in stable nu; until then `--flag="value"` still
|
|
# arrives quoted. See nushell/nushell#18128 for upstream tracking.
|
|
@complete "nu-complete {{ cmd }}"
|
|
export def --env --wrapped {{ cmd }} [...args] {
|
|
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.
|
|
#
|
|
# `try` for the same reason as the binary call above: a body that exits
|
|
# non-zero raises a ShellError that would unwind the whole `def`, skipping
|
|
# cleanup (leaking all three temp files) and discarding the stdout this
|
|
# function still has to return. Catching it also makes the body's exit code
|
|
# a value we can fold in, rather than one the unwind happens to carry out.
|
|
let exec_exit = if ($exec_file | path exists) and (open $exec_file --raw | str trim | is-not-empty) {
|
|
let script = open $exec_file --raw
|
|
try { ^sh -c $script; 0 } catch { $env.LAST_EXIT_CODE }
|
|
} else {
|
|
0
|
|
}
|
|
|
|
# First non-zero wins, wt before the body — the precedence the bash, zsh
|
|
# and fish wrappers apply to their `src_exit`.
|
|
let exit_code = if $exit_code != 0 { $exit_code } else { $exec_exit }
|
|
|
|
let output = (open $stdout_file --raw)
|
|
|
|
# Clean up all three temp files at once, after the last read of any of them.
|
|
#
|
|
# A bare `rm` here is shadowable by the user's own `alias rm = ...`:
|
|
# nushell resolves aliases at parse time, and `config.nu` runs before the
|
|
# vendor autoload dir this file lives in, so the alias is already in scope
|
|
# when this `def` is parsed. A `trash`-style alias then leaks the temp
|
|
# files, and an alias that exits non-zero raises a ShellError that aborts
|
|
# the wrapper before it can return `$output` — swallowing the command's
|
|
# stdout entirely.
|
|
#
|
|
# `^rm` bypasses aliases, but it's external-only, so Windows (where the
|
|
# nushell builtin is the only `rm`) keeps the builtin. `try` covers both
|
|
# branches so cleanup can never abort the wrapper. `hide rm` is not an
|
|
# option: it's a parse-time keyword that leaks out of this function into
|
|
# the user's session, silently unbinding their alias.
|
|
if $nu.os-info.family == "windows" {
|
|
try { rm -f $cd_file $exec_file $stdout_file }
|
|
} else {
|
|
try { ^rm -f $cd_file $exec_file $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
|
|
}
|
|
}
|
|
}
|