use crate::cli::Cli;
use eyre::Result;
use strum::EnumString;
#[derive(Debug, usage_rs::Args)]
#[usage(aliases = ["complete", "completions"], verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub(crate) struct Completion {
#[usage(required_unless = "shell_type", value_enum)]
shell: Option<Shell>,
#[usage(long = "shell", short = 's', hide = true, value_enum)]
shell_type: Option<Shell>,
#[usage(long, verbatim_doc_comment)]
include_bash_completion_lib: bool,
#[usage(long, verbatim_doc_comment, hide = true)]
usage: bool,
#[usage(long, verbatim_doc_comment, effect = "write")]
install: bool,
#[usage(long, requires = "--install", effect = "write")]
force: bool,
}
impl Completion {
pub(crate) async fn run(self) -> Result<()> {
let shell = self.shell.or(self.shell_type).unwrap();
if self.install {
return self.install_script(shell.into());
}
let script = Cli::completion_script(shell.into());
miseprintln!("{}", script.trim());
Ok(())
}
fn install_script(&self, shell: usage_rs::complete::Shell) -> Result<()> {
use usage_rs::install::{self, OnForeign, Wrote};
let on_foreign = if self.force {
OnForeign::Overwrite
} else {
OnForeign::Refuse
};
let done = Cli::install_completion(shell, &install::Env::from_process(), on_foreign)
.map_err(|err| match &err {
install::Error::Foreign { .. } => eyre::eyre!(
"{err}\n\nPass --force to replace it, or redirect the script yourself."
),
_ => eyre::Report::new(err),
})?;
eprintln!("installing to {}", done.plan.path.display());
if done.wrote == Wrote::Unchanged {
eprintln!("already up to date");
}
if let Some(line) = done.plan.loading.instruction() {
let file = match &done.plan.loading {
install::Loading::Manual { file, .. } => file.as_str(),
_ => "your shell's startup file",
};
eprintln!("\nadd this to {file}, once:\n\n{line}\n");
}
if let Some(note) = done.plan.note {
eprintln!("note: {note}");
}
Ok(())
}
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
# put it where the shell looks, and print any one-time line it still needs
$ <bold>mise completion zsh --install</bold>
# or choose the path yourself
$ <bold>mise completion bash > ~/.local/share/bash-completion/completions/mise</bold>
$ <bold>mise completion zsh > /usr/local/share/zsh/site-functions/_mise</bold>
$ <bold>mise completion fish > ~/.config/fish/completions/mise.fish</bold>
$ <bold>mise completion powershell >> $PROFILE</bold>
"#
);
#[derive(Debug, Clone, Copy, EnumString, strum::Display, usage_rs::ValueEnum)]
#[strum(serialize_all = "snake_case")]
#[usage(rename_all = "snake_case")]
enum Shell {
Bash,
Fish,
#[strum(serialize = "powershell")]
#[usage(name = "powershell", visible_alias = "pwsh")]
Powershell,
Zsh,
}
impl From<Shell> for usage_rs::complete::Shell {
fn from(shell: Shell) -> Self {
match shell {
Shell::Bash => Self::Bash,
Shell::Fish => Self::Fish,
Shell::Powershell => Self::PowerShell,
Shell::Zsh => Self::Zsh,
}
}
}
#[cfg(test)]
mod shell_name_tests {
use super::*;
use usage_rs::spec::ValueEnum;
#[test]
fn pwsh_is_accepted_as_powershell() {
assert!(matches!(
<Shell as ValueEnum>::from_choice("pwsh"),
Some(Shell::Powershell)
));
assert!(matches!(
<Shell as ValueEnum>::from_choice("powershell"),
Some(Shell::Powershell)
));
}
#[test]
fn the_primary_names_are_unchanged() {
let listed: Vec<&str> = Shell::DETAILS.iter().map(|choice| choice.value).collect();
assert_eq!(listed, ["bash", "fish", "powershell", "zsh"]);
}
#[test]
fn completion_script_calls_back_into_mise() {
let script = Cli::completion_script(usage_rs::complete::Shell::Bash);
assert!(script.contains("mise' __complete_word__"), "{script}");
assert!(!script.contains("command usage"), "{script}");
}
}