use clap::{Parser, ValueEnum, ValueHint}; use clap_complete::engine::{ArgValueCompleter, CompletionCandidate}; use std::ffi::OsStr; #[derive(Parser, Debug, PartialEq)] #[clap(author, version, about, long_about = None)] #[command(arg_required_else_help(true))] pub struct Args { #[arg( short = 'I', long, value_delimiter = ',', num_args = 1.., add = ArgValueCompleter::new(ignore_completer), value_parser = ["dupes", "same-named", "tmpfiles"], long_help = "\ Comma-separated list of things to ignore: dupes, same-named, tmpfiles dupes: ignore duplicate files same-named: ignore same-named files tmpfiles: ignore temporary files " )] pub ignore: Vec, #[arg(short = 'v', long = "verbose", help = "Verbose operation?")] pub verbose: bool, #[arg(short = 'L', long = "correct-le", help = "Correct line endings")] pub correct_le: bool, #[arg(short = 'C', long = "correct-perms", help = "Correct permissions")] pub correct_perms: bool, #[arg(long = "no-colors", help = "Don't display messages in color")] pub no_colors: bool, #[arg(long = "urlcheck", help = "Check URLs found in README files")] pub urlcheck: bool, #[arg(long = "warnings-no-errors", help = "Don't treat warnings as errors")] pub warnings_no_errors: bool, #[arg(short = 'T', long = "tds-zip", help = "tds zip archive", group = "tds", value_hint = ValueHint::FilePath)] pub tds_zip: Option, #[arg( short = 'e', long = "explain", help = "Explain error or warning message", group = "only_one" )] pub explain: Option, #[arg( long = "explain-all", help = "Explains all error or warning messages", group = "only_one" )] pub explain_all: bool, /// Print completion setup instructions for the given shell. /// For nushell, generates a static completion script to stdout. /// /// Examples: /// pkgcheck --completion fish > ~/.config/fish/completions/pkgcheck.fish /// pkgcheck --completion nushell > ~/.config/nushell/completions/pkgcheck.nu #[arg(long = "generate-completion", value_name = "SHELL")] pub generate_completion: Option, // #[arg(long = "generate-completion", group = "only_one", value_enum)] // pub generator: Option, #[arg( long = "show-temp-endings", help = "Show file endings for temporary files", group = "only_one" )] pub show_tmp_endings: bool, #[arg(short = 'd', long = "package-dir", help = "Package directory", value_hint = ValueHint::DirPath)] pub pkg_dir: Option, #[arg(long = "config-file", help = "Specify config file to use", value_hint = ValueHint::FilePath)] pub config_file: Option, } // ── Ignore values ───────────────────────────────────────────────────────────── const IGNORE_VALUES: &[&str] = &["dupes", "same-named", "tmpfiles"]; pub fn ignore_completer(current: &OsStr) -> Vec { let current = current.to_str().unwrap_or(""); let (prefix, fragment) = match current.rfind(',') { Some(pos) => (¤t[..=pos], ¤t[pos + 1..]), None => ("", current), }; let already: Vec<&str> = prefix .split(',') .map(|s| s.trim_end_matches(',')) .filter(|s| !s.is_empty()) .collect(); IGNORE_VALUES .iter() .filter(|&&v| v.starts_with(fragment) && !already.contains(&v)) .map(|&v| CompletionCandidate::new(format!("{prefix}{v}"))) .collect() } // ── Shell enum ──────────────────────────────────────────────────────────────── #[derive(Debug, Clone, ValueEnum, PartialEq)] pub enum Shell { Fish, Bash, Zsh, Nushell, } impl Shell { fn setup_line(&self, bin: &str) -> Option { match self { Shell::Fish => Some(format!("COMPLETE=fish {bin} | source")), Shell::Bash => Some(format!("source <(COMPLETE=bash {bin})")), Shell::Zsh => Some(format!("source <(COMPLETE=zsh {bin})")), Shell::Nushell => None, } } fn config_file(&self, bin: &str) -> String { match self { Shell::Fish => format!("~/.config/fish/completions/{bin}.fish"), Shell::Bash => "~/.bashrc".to_string(), Shell::Zsh => "~/.zshrc".to_string(), Shell::Nushell => format!("~/.config/nushell/completions/{bin}.nu"), } } pub fn usage(&self, bin: &str) -> String { let file = self.config_file(bin); match self.setup_line(bin) { Some(line) => format!("Add the following line to {file}:\n\n {line}"), None => format!( "Generate static completion file:\n\n \ {bin} --completion nushell > {file}\n\n\ Then add the following line to ~/.config/nushell/config.nu:\n\n \ source {file}" ), } } } impl Args { pub fn ignore_dupes(&self) -> bool { self.ignore.iter().any(|v| v == "dupes") } pub fn ignore_same_named(&self) -> bool { self.ignore.iter().any(|v| v == "same-named") } pub fn ignore_tmpfiles(&self) -> bool { self.ignore.iter().any(|v| v == "tmpfiles") } } // ── Nushell completion ──────────────────────────────────────────────────────── /// Generate the Nushell static completion script for pkgcheck. pub fn nushell_completion() -> String { let lines: &[&str] = &[ r#"def "nu-complete pkgcheck ignore" [context: string] {"#, r#" let current = ($context | split row " " | last)"#, r#" let prefix = if ($current | str contains ",") {"#, r#" $current | split row "," | drop 1 | str join "," | $"($in),""#, r#" } else {"#, r#" """#, r#" }"#, r#" let already = ($prefix | split row "," | each { str trim } | where { |v| $v != "" })"#, r#" let options = ["dupes" "same-named" "tmpfiles"]"#, r#" $options"#, r#" | where { |v| not ($already | any { |a| $a == $v }) }"#, r#" | each { |v| $"($prefix)($v)" }"#, r#"}"#, r#""#, r#"def "nu-complete pkgcheck completion" [] {"#, r#" ["fish" "bash" "zsh" "nushell"]"#, r#"}"#, r#""#, r#"export extern main ["#, r#" --ignore: string@"nu-complete pkgcheck ignore" # Comma-separated: dupes, same-named, tmpfiles"#, r#" --completion: string@"nu-complete pkgcheck completion" # Shell to generate completion for"#, r#" --help(-h) # Print help"#, r#"]"#, ]; lines.join("\n") + "\n" }