use std::io::{IsTerminal, Write};
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::{generate, Shell};
use clap_complete_nushell::Nushell;
#[derive(Parser)]
#[command(name = "cargo-fresh")]
#[command(about = "Check and update globally installed Cargo packages")]
#[command(version)]
pub struct Cli {
#[arg(short, long)]
pub verbose: bool,
#[arg(short, long)]
pub updates_only: bool,
#[arg(long)]
pub no_interactive: bool,
#[arg(long)]
pub include_prerelease: bool,
#[arg(long)]
pub batch: bool,
#[arg(long)]
pub filter: Option<String>,
#[arg(long, action = clap::ArgAction::Append)]
pub exclude: Vec<String>,
#[arg(long)]
pub dry_run: bool,
#[arg(long, value_name = "URL")]
pub registry_url: Option<String>,
#[arg(long, value_enum, default_value_t = OutputFormat::Human, value_name = "FORMAT")]
pub format: OutputFormat,
#[arg(long)]
pub no_cargo_search_fallback: bool,
#[arg(long)]
pub check_prebuilt: bool,
#[arg(long)]
pub debug: bool,
#[arg(short = 'j', long, default_value_t = 4, value_name = "N")]
pub jobs: u32,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
Completion {
#[arg(value_enum)]
shell: ShellType,
#[arg(long)]
cargo_fresh: bool,
#[arg(long)]
install: bool,
#[arg(long)]
yes: bool,
},
Man,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Human,
Json,
}
pub enum InstallOutcome {
Written(std::path::PathBuf),
Skipped(std::path::PathBuf),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum InstallTarget {
TopLevel,
CargoSubcommand,
}
impl InstallTarget {
pub fn is_cargo_subcommand(self) -> bool {
matches!(self, InstallTarget::CargoSubcommand)
}
}
#[derive(Clone, ValueEnum)]
pub enum ShellType {
Bash,
Zsh,
Fish,
Powershell,
Elvish,
Nushell,
}
impl ShellType {
fn display_name(&self) -> &'static str {
match self {
ShellType::Bash => "bash",
ShellType::Zsh => "zsh",
ShellType::Fish => "fish",
ShellType::Powershell => "powershell",
ShellType::Elvish => "elvish",
ShellType::Nushell => "nushell",
}
}
}
impl Cli {
fn render_completion_into(shell: &ShellType, cmd: &mut clap::Command, name: &str, out: &mut dyn Write) {
let shell_type = match shell {
ShellType::Bash => Shell::Bash,
ShellType::Zsh => Shell::Zsh,
ShellType::Fish => Shell::Fish,
ShellType::Powershell => Shell::PowerShell,
ShellType::Elvish => Shell::Elvish,
ShellType::Nushell => return generate(Nushell, cmd, name, out),
};
generate(shell_type, cmd, name, out);
}
fn build_cargo_fresh_command() -> clap::Command {
let fresh = Self::command()
.name("fresh")
.about("Check and update globally installed Cargo packages");
clap::Command::new("cargo")
.about("Rust's package manager")
.subcommand(fresh)
}
pub fn generate_completion(shell: ShellType) {
let mut cmd = Self::command();
Self::render_completion_into(&shell, &mut cmd, "cargo-fresh", &mut std::io::stdout());
}
pub fn generate_cargo_fresh_completion(shell: ShellType) {
let mut cargo_cmd = Self::build_cargo_fresh_command();
Self::render_completion_into(&shell, &mut cargo_cmd, "cargo", &mut std::io::stdout());
}
fn render_completion_to_bytes(shell: &ShellType, target: InstallTarget) -> Vec<u8> {
let mut buf = Vec::new();
if target.is_cargo_subcommand() {
let mut cargo_cmd = Self::build_cargo_fresh_command();
Self::render_completion_into(shell, &mut cargo_cmd, "cargo", &mut buf);
} else {
let mut cmd = Self::command();
Self::render_completion_into(shell, &mut cmd, "cargo-fresh", &mut buf);
}
buf
}
fn config_home() -> Option<std::path::PathBuf> {
std::env::var_os("XDG_CONFIG_HOME")
.map(std::path::PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config")))
}
fn data_home() -> Option<std::path::PathBuf> {
std::env::var_os("XDG_DATA_HOME")
.map(std::path::PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".local/share")))
}
fn home_dir() -> Option<std::path::PathBuf> {
std::env::var_os("HOME").map(std::path::PathBuf::from)
}
pub fn install_target_path(
shell: &ShellType,
target: InstallTarget,
) -> Option<std::path::PathBuf> {
let is_cargo = target.is_cargo_subcommand();
match shell {
ShellType::Fish => {
let base = Self::config_home()?;
if is_cargo {
Some(base.join("fish").join("conf.d").join("cargo-fresh.fish"))
} else {
Some(base.join("fish").join("completions").join("cargo-fresh.fish"))
}
}
ShellType::Bash => {
let base = Self::data_home()?;
let file = if is_cargo { "cargo" } else { "cargo-fresh" };
Some(base.join("bash-completion").join("completions").join(file))
}
ShellType::Zsh => {
let base = Self::home_dir()?;
let file = if is_cargo { "_cargo" } else { "_cargo-fresh" };
Some(base.join(".zfunc").join(file))
}
ShellType::Nushell => {
let base = Self::config_home()?;
let file = if is_cargo { "cargo.nu" } else { "cargo-fresh.nu" };
Some(base.join("nushell").join("completions").join(file))
}
ShellType::Elvish => {
let base = Self::config_home()?;
let file = if is_cargo { "cargo.elv" } else { "cargo-fresh.elv" };
Some(base.join("elvish").join("lib").join(file))
}
ShellType::Powershell => {
let base = Self::config_home()?;
let file = if is_cargo { "cargo.ps1" } else { "cargo-fresh.ps1" };
Some(base.join("powershell").join(file))
}
}
}
pub fn install_post_hint(
shell: &ShellType,
target: InstallTarget,
path: &std::path::Path,
) -> Option<String> {
let path_str = path.display();
match shell {
ShellType::Bash | ShellType::Fish => None,
ShellType::Zsh => Some(
"make sure ~/.zfunc is on $fpath: add `fpath=(~/.zfunc $fpath)` then `autoload -Uz compinit && compinit` to ~/.zshrc"
.to_string(),
),
ShellType::Powershell => Some(format!(
"dot-source from $PROFILE: add `. \"{path_str}\"` to your $PROFILE"
)),
ShellType::Elvish => {
let module = if target.is_cargo_subcommand() { "cargo" } else { "cargo-fresh" };
Some(format!("add `use {module}` to ~/.config/elvish/rc.elv"))
}
ShellType::Nushell => Some(format!(
"add `source \"{path_str}\"` to your config.nu (or `$nu.config-path`)"
)),
}
}
pub fn install_completion_target(
shell: &ShellType,
target: InstallTarget,
language: crate::locale::Language,
force: bool,
) -> anyhow::Result<InstallOutcome> {
let path = Self::install_target_path(shell, target).ok_or_else(|| {
anyhow::anyhow!(
"{}",
language.format_text(
"completion_install_no_home",
&[("shell", shell.display_name())],
)
)
})?;
let bytes = Self::render_completion_to_bytes(shell, target);
if path.exists() && !force {
if !std::io::stderr().is_terminal() {
return Ok(InstallOutcome::Skipped(path));
}
let prompt = language
.get_text("completion_overwrite_prompt")
.replace("{}", &path.display().to_string());
let proceed = dialoguer::Confirm::new()
.with_prompt(prompt)
.default(false)
.show_default(true)
.interact()
.unwrap_or(false);
if !proceed {
return Ok(InstallOutcome::Skipped(path));
}
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, bytes)?;
Ok(InstallOutcome::Written(path))
}
pub fn select_install_targets(
language: crate::locale::Language,
yes: bool,
) -> anyhow::Result<Vec<InstallTarget>> {
let all = vec![InstallTarget::TopLevel, InstallTarget::CargoSubcommand];
if yes || !std::io::stderr().is_terminal() {
return Ok(all);
}
let items = [
language.get_text("completion_target_top"),
language.get_text("completion_target_cargo"),
];
let chosen = dialoguer::MultiSelect::new()
.with_prompt(language.get_text("completion_install_prompt"))
.items(items)
.defaults(&[true, true])
.interact()?;
let selected: Vec<InstallTarget> = chosen
.into_iter()
.filter_map(|idx| match idx {
0 => Some(InstallTarget::TopLevel),
1 => Some(InstallTarget::CargoSubcommand),
_ => None,
})
.collect();
Ok(selected)
}
pub fn generate_man() -> std::io::Result<()> {
let cmd = Self::command();
let man = clap_mangen::Man::new(cmd);
if !std::io::stdout().is_terminal() {
return man.render(&mut std::io::stdout());
}
let mut buf = Vec::new();
man.render(&mut buf)?;
let tmp_dir = std::env::temp_dir();
let pid = std::process::id();
let tmp_path = tmp_dir.join(format!("cargo-fresh-{pid}.1"));
{
let mut f = std::fs::File::create(&tmp_path)?;
f.write_all(&buf)?;
}
let status = std::process::Command::new("man").arg(&tmp_path).status();
let _ = std::fs::remove_file(&tmp_path);
match status {
Ok(s) if s.success() => Ok(()),
Ok(_) | Err(_) => {
std::io::stdout().write_all(&buf)
}
}
}
pub fn maybe_hint_fish_install(shell: &ShellType, cargo_fresh: bool) {
if !matches!(shell, ShellType::Fish) || !cargo_fresh {
return;
}
if !std::io::stderr().is_terminal() {
return;
}
let _ = writeln!(
std::io::stderr(),
"hint: install to ~/.config/fish/conf.d/cargo-fresh.fish (eager-loaded at shell start, \
coexists with cargo's own completion). \
Do NOT save to ~/.config/fish/completions/cargo.fish — that shadows cargo's built-in \
completion and you'll lose `cargo build`/`run`/`test` tab-completion. \
Saving to ~/.config/fish/completions/cargo-fresh.fish will NOT enable `cargo fresh<TAB>` either."
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_command_builds() {
Cli::command().debug_assert();
}
#[test]
fn cli_parses_basic_flags() {
let cli = Cli::try_parse_from([
"cargo-fresh",
"--verbose",
"--batch",
"--filter",
"cargo-*",
"--exclude",
"foo",
"--exclude",
"bar",
])
.expect("parse");
assert!(cli.verbose);
assert!(cli.batch);
assert_eq!(cli.filter.as_deref(), Some("cargo-*"));
assert_eq!(cli.exclude, vec!["foo".to_string(), "bar".to_string()]);
}
#[test]
fn cli_completion_subcommand() {
let cli =
Cli::try_parse_from(["cargo-fresh", "completion", "bash", "--cargo-fresh"]).expect("parse");
match cli.command {
Some(Commands::Completion { cargo_fresh, install, yes, .. }) => {
assert!(cargo_fresh);
assert!(!install);
assert!(!yes);
}
_ => panic!("expected completion subcommand"),
}
}
#[test]
fn cli_completion_install_flag() {
let cli = Cli::try_parse_from(["cargo-fresh", "completion", "fish", "--install"]).expect("parse");
match cli.command {
Some(Commands::Completion { install, cargo_fresh, yes, .. }) => {
assert!(install);
assert!(!cargo_fresh);
assert!(!yes);
}
_ => panic!("expected completion subcommand"),
}
}
#[test]
fn cli_completion_install_yes_flag() {
let cli = Cli::try_parse_from(["cargo-fresh", "completion", "fish", "--install", "--yes"])
.expect("parse");
match cli.command {
Some(Commands::Completion { install, yes, .. }) => {
assert!(install);
assert!(yes);
}
_ => panic!("expected completion subcommand"),
}
}
#[test]
fn install_target_path_fish_top_level() {
let path = Cli::install_target_path(&ShellType::Fish, InstallTarget::TopLevel)
.expect("fish supported");
assert!(path.ends_with("fish/completions/cargo-fresh.fish"));
}
#[test]
fn install_target_path_fish_cargo_subcommand() {
let path = Cli::install_target_path(&ShellType::Fish, InstallTarget::CargoSubcommand)
.expect("fish supported");
assert!(path.ends_with("fish/conf.d/cargo-fresh.fish"));
}
#[test]
fn install_target_path_bash_paths() {
let top = Cli::install_target_path(&ShellType::Bash, InstallTarget::TopLevel).unwrap();
assert!(top.ends_with("bash-completion/completions/cargo-fresh"));
let cargo = Cli::install_target_path(&ShellType::Bash, InstallTarget::CargoSubcommand).unwrap();
assert!(cargo.ends_with("bash-completion/completions/cargo"));
}
#[test]
fn install_target_path_zsh_paths() {
let top = Cli::install_target_path(&ShellType::Zsh, InstallTarget::TopLevel).unwrap();
assert!(top.ends_with(".zfunc/_cargo-fresh"));
let cargo = Cli::install_target_path(&ShellType::Zsh, InstallTarget::CargoSubcommand).unwrap();
assert!(cargo.ends_with(".zfunc/_cargo"));
}
#[test]
fn install_target_path_nushell_paths() {
let top = Cli::install_target_path(&ShellType::Nushell, InstallTarget::TopLevel).unwrap();
assert!(top.ends_with("nushell/completions/cargo-fresh.nu"));
}
#[test]
fn install_target_path_elvish_powershell_paths() {
let elv = Cli::install_target_path(&ShellType::Elvish, InstallTarget::TopLevel).unwrap();
assert!(elv.ends_with("elvish/lib/cargo-fresh.elv"));
let ps = Cli::install_target_path(&ShellType::Powershell, InstallTarget::CargoSubcommand).unwrap();
assert!(ps.ends_with("powershell/cargo.ps1"));
}
#[test]
fn install_post_hint_fish_and_bash_none() {
let path = std::path::PathBuf::from("/tmp/x");
assert!(Cli::install_post_hint(&ShellType::Fish, InstallTarget::TopLevel, &path).is_none());
assert!(Cli::install_post_hint(&ShellType::Bash, InstallTarget::TopLevel, &path).is_none());
}
#[test]
fn install_post_hint_zsh_mentions_fpath() {
let path = std::path::PathBuf::from("/tmp/_cargo-fresh");
let hint = Cli::install_post_hint(&ShellType::Zsh, InstallTarget::TopLevel, &path).unwrap();
assert!(hint.contains("fpath"));
}
#[test]
fn parses_jobs_long() {
let cli = Cli::try_parse_from(["cargo-fresh", "--jobs", "8"]).unwrap();
assert_eq!(cli.jobs, 8);
}
#[test]
fn parses_jobs_short() {
let cli = Cli::try_parse_from(["cargo-fresh", "-j", "2"]).unwrap();
assert_eq!(cli.jobs, 2);
}
#[test]
fn jobs_default_is_four() {
let cli = Cli::try_parse_from(["cargo-fresh"]).unwrap();
assert_eq!(cli.jobs, 4);
}
#[test]
fn jobs_zero_is_accepted() {
let cli = Cli::try_parse_from(["cargo-fresh", "-j", "0"]).unwrap();
assert_eq!(cli.jobs, 0);
}
#[test]
fn parses_check_prebuilt() {
let cli = Cli::try_parse_from(["cargo-fresh", "--check-prebuilt"]).unwrap();
assert!(cli.check_prebuilt);
}
#[test]
fn parses_debug_flag() {
let cli = Cli::try_parse_from(["cargo-fresh", "--debug"]).unwrap();
assert!(cli.debug);
let cli = Cli::try_parse_from(["cargo-fresh"]).unwrap();
assert!(!cli.debug);
}
}