use std::ffi::{OsStr, OsString};
use std::io::{self, Write as _};
use std::path::Path;
use std::process::ExitCode;
use clap::{CommandFactory, ValueEnum};
use clap_complete::CompleteEnv;
use clap_complete::env::{Bash, EnvCompleter, Fish, Shells, Zsh};
use crate::formatdoc;
use crate::shell::{Script, Shell};
pub const VAR: &str = "COMPLETE";
const SHELLS: Shells<'static> = Shells(&[&Bash, &Zsh, &Fish]);
impl Shell {
pub fn script(self, bin: &str) -> io::Result<Script> {
let mut buffer = Vec::new();
self.completer()
.write_registration(VAR, bin, bin, bin, &mut buffer)?;
let mut text = String::from_utf8(buffer)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
match self {
Self::Bash | Self::Fish => {}
Self::Zsh => text.push_str(&zsh_autoload(bin)),
}
Ok(Script { shell: self, text })
}
fn completer(self) -> &'static dyn EnvCompleter {
match self {
Self::Bash => &Bash,
Self::Zsh => &Zsh,
Self::Fish => &Fish,
}
}
}
fn zsh_autoload(bin: &str) -> String {
formatdoc! {r#"
if [[ ${{funcstack[1]}} == _{bin} ]]; then
_clap_dynamic_completer_{name} "$@"
fi
"#,
name = bin.replace('-', "_"),
}
}
#[derive(Debug, Clone, PartialEq, Eq, clap::Subcommand)]
pub enum CompletionCommand {
Completion(CompletionArgs),
}
impl CompletionCommand {
pub fn script(&self, bin: &str) -> io::Result<Script> {
match self {
Self::Completion(args) => args.shell.script(bin),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, clap::Args)]
#[non_exhaustive]
pub struct CompletionArgs {
#[arg(value_enum)]
pub shell: Shell,
}
pub(crate) fn complete<C: CommandFactory>(bin: &str, args: &[OsString]) -> Option<ExitCode> {
let requested = std::env::var_os(VAR).filter(|value| !value.is_empty() && value != "0")?;
let words = args
.iter()
.skip(1)
.skip_while(|arg| arg.as_os_str() != "--")
.nth(1)
.is_some();
Some(if words {
candidates::<C>(bin, args)
} else {
registration(bin, &requested)
})
}
fn registration(bin: &str, requested: &OsStr) -> ExitCode {
let name = Path::new(requested)
.file_stem()
.unwrap_or(requested)
.to_string_lossy();
let Ok(shell) = <Shell as ValueEnum>::from_str(&name, false) else {
let expected = Shell::value_variants()
.iter()
.filter_map(ValueEnum::to_possible_value)
.map(|value| value.get_name().to_owned())
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(
io::stderr().lock(),
"{bin}: error: unknown shell `{name}` in {VAR}, expected one of {expected}"
);
return ExitCode::from(2);
};
shell
.script(bin)
.and_then(|script| crate::view::write_stdout(script.text.as_bytes()))
.map_or(ExitCode::FAILURE, |()| ExitCode::SUCCESS)
}
fn candidates<C: CommandFactory>(bin: &str, args: &[OsString]) -> ExitCode {
let current_dir = std::env::current_dir().ok();
match CompleteEnv::with_factory(|| crate::parser::with_help(C::command()))
.var(VAR)
.bin(bin)
.completer(bin)
.shells(SHELLS)
.try_complete(args.iter().cloned(), current_dir.as_deref())
{
Ok(_) => ExitCode::SUCCESS,
Err(error) => {
let _ = error.print();
u8::try_from(error.exit_code()).map_or(ExitCode::FAILURE, ExitCode::from)
}
}
}