use std::collections::BTreeMap;
use std::ffi::OsStr;
use clap_complete::engine::{CompletionCandidate, PathCompleter, ValueCompleter as _};
use crate::model::PackageSystem;
const HELP_LIMIT: usize = 72;
pub(crate) fn package_candidates_from(
prefix: &str,
packages: impl Iterator<Item = (String, String)>,
) -> Vec<CompletionCandidate> {
let prefix = prefix.to_lowercase();
let mut matched: BTreeMap<String, String> = BTreeMap::new();
for (name, summary) in packages {
if !name.to_lowercase().starts_with(&prefix) {
continue;
}
matched.entry(name).or_insert(summary);
}
matched
.into_iter()
.map(|(name, summary)| -> CompletionCandidate {
let mut candidate = CompletionCandidate::new(name);
if !summary.is_empty() {
let help = if summary.chars().count() > HELP_LIMIT {
format!("{}…", summary.chars().take(HELP_LIMIT).collect::<String>())
} else {
summary
};
candidate = candidate.help(Some(help.into()));
}
candidate
})
.collect()
}
pub(crate) fn package_completer(current: &OsStr) -> Vec<CompletionCandidate> {
package_candidates_from(¤t.to_string_lossy(), installed_packages())
}
pub(crate) fn search_pattern_completer(current: &OsStr) -> Vec<CompletionCandidate> {
if current.to_string_lossy().starts_with('/') {
return PathCompleter::any().complete(current);
}
package_completer(current)
}
pub(crate) fn cache_target_completer(current: &OsStr) -> Vec<CompletionCandidate> {
["all", "index", "repos", "contents"]
.into_iter()
.filter(|t| t.starts_with(current.to_string_lossy().as_ref()))
.map(CompletionCandidate::new)
.collect()
}
fn installed_packages() -> impl Iterator<Item = (String, String)> {
type BuildFn = fn() -> Vec<(String, String)>;
let (source, build): (Option<std::path::PathBuf>, BuildFn) =
match crate::engine::support::detect_system() {
Some(PackageSystem::Deb) => (
Some(std::path::PathBuf::from("/var/lib/dpkg/status")),
build_deb_names,
),
Some(PackageSystem::Rpm) => {
(crate::backend::rpm::local::rpm_db_path(), build_rpm_names)
}
None => (None, Vec::new as BuildFn),
};
let Some(source) = source else {
return Vec::new().into_iter();
};
let source_mtime = std::fs::metadata(&source)
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
let cache_path = crate::cache::completion_names_cache_path();
if let Some(cached) = crate::cache::CompletionNamesCache::load(&cache_path, source_mtime) {
return cached.packages.into_iter();
}
let packages = build();
if !packages.is_empty() {
let _ =
crate::cache::CompletionNamesCache::save(&cache_path, source_mtime, packages.clone());
}
packages.into_iter()
}
fn build_deb_names() -> Vec<(String, String)> {
crate::backend::deb::local::DebLocal::load()
.map(|l| {
l.packages
.iter()
.map(|p| (p.name.clone(), p.summary.clone()))
.collect()
})
.unwrap_or_default()
}
fn build_rpm_names() -> Vec<(String, String)> {
crate::backend::rpm::local::RpmLocal::load()
.map(|l| {
l.packages
.iter()
.map(|p| (p.name.clone(), p.summary.clone()))
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn names(cands: &[CompletionCandidate]) -> Vec<String> {
cands
.iter()
.map(|c| c.get_value().to_string_lossy().into_owned())
.collect()
}
#[test]
fn test_package_candidates_filter_dedup_sort() {
let pkgs = [
("libgnutls-dane0".to_string(), "DANE library".to_string()),
("LibGnuTLS30".to_string(), "TLS".to_string()),
("libgnutls-dane0".to_string(), "dup".to_string()),
("bash".to_string(), String::new()),
];
let cands = package_candidates_from("libgnutls", pkgs.iter().cloned());
assert_eq!(names(&cands), ["LibGnuTLS30", "libgnutls-dane0"]);
assert_eq!(
cands[0].get_help().map(|h| h.to_string()),
Some("TLS".into())
);
assert_eq!(
package_candidates_from("bash", pkgs.iter().cloned())[0].get_help(),
None
);
}
#[test]
fn test_summary_truncated() {
let long = "x".repeat(100);
let cands = package_candidates_from("", [("a".to_string(), long)].into_iter());
let help = cands[0].get_help().unwrap().to_string();
assert_eq!(help.chars().count(), HELP_LIMIT + 1);
assert!(help.ends_with('…'));
}
#[test]
fn test_cache_target_completer() {
assert_eq!(names(&cache_target_completer(OsStr::new("in"))), ["index"]);
assert_eq!(names(&cache_target_completer(OsStr::new(""))).len(), 4);
assert!(cache_target_completer(OsStr::new("xyz")).is_empty());
}
}