use std::io::Read;
use std::process::{Command, Stdio};
use std::time::Duration;
use crate::env;
const TIMEOUT: Duration = Duration::from_secs(3);
const POLL: Duration = Duration::from_millis(5);
const FISH: &str = r#"complete -C (string join -- " " (string escape -- $argv[1..-2]) (string escape --no-quoted -- $argv[-1]))"#;
const BASH: &str = r#"
if ! declare -F _command_offset >/dev/null; then
for f in "${BASH_COMPLETION:-}" /usr/share/bash-completion/bash_completion \
/etc/bash_completion /usr/local/share/bash-completion/bash_completion \
/opt/homebrew/share/bash-completion/bash_completion; do
if [[ -n $f && -r $f ]]; then . "$f"; break; fi
done
fi
declare -F _command_offset >/dev/null || exit 0
COMP_WORDS=()
COMP_LINE=
i=0 prev=
for w in "$@"; do
i=$((i + 1))
if ((i < $#)); then printf -v q '%q' "$w"; else q=$w; fi
if [[ -n $COMP_LINE && $q != = && $prev != = ]]; then COMP_LINE+=' '; fi
COMP_LINE+=$q
rest=$q
while [[ $rest == *=* ]]; do
[[ -n ${rest%%=*} ]] && COMP_WORDS+=("${rest%%=*}")
COMP_WORDS+=(=)
rest=${rest#*=}
done
if [[ -n $rest ]] || ((i == $# && ${#q} == 0)); then COMP_WORDS+=("$rest"); fi
prev=$q
done
COMP_CWORD=$((${#COMP_WORDS[@]} - 1))
COMP_POINT=${#COMP_LINE}
COMP_TYPE=9 COMP_KEY=9
export COMP_LINE COMP_POINT COMP_TYPE COMP_KEY
COMPREPLY=()
_command_offset 0
for c in "${COMPREPLY[@]}"; do printf '%s\n' "${c% }"; done
"#;
const ZSH: &str = r#"
zmodload zsh/zpty || exit 0
zmodload zsh/datetime 2>/dev/null
export __USAGE_DELEGATE_NONCE=__usage_delegate_$$_${RANDOM}${RANDOM}_${EPOCHREALTIME//[^0-9]/}
local -a w=("$@")
export __USAGE_DELEGATE_LINE="${(j: :)${(@q)w[1,-2]}} ${(q)w[-1]}"
export __USAGE_DELEGATE_SETUP='
PROMPT= RPROMPT= PS2= ; unsetopt beep zle_bracketed_paste 2>/dev/null
autoload -Uz compinit
__usage_cache=${XDG_CACHE_HOME:-$HOME/.cache}/usage
if mkdir -p -m 700 -- "$__usage_cache" 2>/dev/null; then
compinit -u -d "$__usage_cache/delegate.zcompdump"
else
compinit -u -D
fi
compadd() {
if [[ ${@[1,(i)(-|--)]} == *-(O|A|D)\ * ]]; then builtin compadd "$@"; return; fi
local -a __h __d __o __p __hp __s __hs
local __t __i
__i=${@[(i)-(|l)d]}
if (( __i < $# )); then
__t=${@[__i+1]}
if [[ $__t == \(* ]]; then eval "__d=$__t"; else __d=( "${(@P)__t}" ); fi
fi
zparseopts -E -a __o P:=__p p:=__hp S:=__s s:=__hs
builtin compadd -A __h -D __d "$@"
for (( __i = 1; __i <= $#__h; __i++ )); do
print -r -- "$__USAGE_DELEGATE_NONCE:HIT:${(Q)IPREFIX}${__p[2]}${__hp[2]}${(Q)__h[__i]}${__hs[2]}${__s[2]}"$'"'"'\t'"'"'"${${__d[__i]#$__h[__i]}##[[:space:]]#(--|:)[[:space:]]#}"
done
}
__usage_delegate() {
BUFFER=$__USAGE_DELEGATE_LINE; CURSOR=$#BUFFER
zle complete-word
print -r -- $'"'"'\n'"'"'"$__USAGE_DELEGATE_NONCE:DONE"; exit
}
zle -N __usage_delegate; bindkey "^G" __usage_delegate
print -r -- "$__USAGE_DELEGATE_NONCE:READY"'
zpty __usage_delegate zsh -f -i
zpty -w __usage_delegate 'eval "$__USAGE_DELEGATE_SETUP"'
local out n=$__USAGE_DELEGATE_NONCE
while zpty -r __usage_delegate out; do
[[ ${${out//$'\r'/}%$'\n'} == "$n:READY" ]] && break
done
zpty -w -n __usage_delegate $'\x07'
while zpty -r __usage_delegate out; do
out=${${out//$'\r'/}%$'\n'}
[[ $out == "$n:DONE" ]] && break
[[ $out == *"$n:HIT:"* ]] && print -r -- ${out#*"$n:HIT:"}
done
zpty -d __usage_delegate
"#;
pub(crate) fn complete(shell: &str, words: &[String]) -> Vec<(String, String)> {
let args: &[&str] = match shell {
"fish" => &["-c", FISH, "--"],
"bash" => &["--norc", "--noprofile", "-c", BASH, "usage"],
"zsh" => &["-f", "-c", ZSH, "usage"],
_ => {
debug!("delegate: completion is not delegated in {shell}");
return vec![];
}
};
let program = env::shell_program_override(shell, |key| std::env::var(key).ok())
.unwrap_or_else(|| shell.to_string());
let mut command = Command::new(program);
#[cfg(unix)]
std::os::unix::process::CommandExt::process_group(&mut command, 0);
command
.args(args)
.args(words)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
let Some(stdout) = run_with_timeout(command) else {
return vec![];
};
stdout
.lines()
.filter(|line| !line.is_empty())
.map(|line| match line.split_once('\t') {
Some((value, description)) => (value.to_string(), description.trim().to_string()),
None => (line.to_string(), String::new()),
})
.collect()
}
fn run_with_timeout(mut command: Command) -> Option<String> {
let mut child = command
.spawn()
.inspect_err(|err| {
let program = command.get_program().to_string_lossy();
debug!("delegate: {program} failed to start: {err}")
})
.ok()?;
let mut stdout = child.stdout.take()?;
let reader = std::thread::spawn(move || {
let mut out = Vec::new();
let _ = stdout.read_to_end(&mut out);
out
});
let deadline = std::time::Instant::now() + TIMEOUT;
while !reader.is_finished() && std::time::Instant::now() < deadline {
std::thread::sleep(POLL);
}
let out = if reader.is_finished() {
reader.join().ok()
} else {
debug!("delegate: gave up after {}s", TIMEOUT.as_secs());
kill_tree(&mut child);
None
};
let _ = child.wait();
String::from_utf8(out?).ok()
}
#[cfg(unix)]
fn kill_tree(child: &mut std::process::Child) {
let Ok(group) = libc::pid_t::try_from(child.id()) else {
let _ = child.kill();
return;
};
if unsafe { libc::killpg(group, libc::SIGKILL) } != 0 {
let _ = child.kill();
}
}
#[cfg(not(unix))]
fn kill_tree(child: &mut std::process::Child) {
let _ = child.kill();
}