use crate::aur::{AurClient, AurPackage};
use crate::cli::{Options, SortBy};
use crate::localdb::LocalDb;
use crate::menu::parse_selection;
use crate::output;
use crate::pacman::{RepoPackage, Toolchain};
use anyhow::Result;
use std::cmp::Ordering;
use std::io;
#[derive(Debug, Clone)]
pub struct SearchHit {
pub source: String,
pub name: String,
pub version: String,
pub desc: Option<String>,
pub votes: i64,
pub popularity: f64,
pub last_modified: u64,
pub installed: Option<String>,
}
pub async fn print_search(
options: &Options,
tools: &Toolchain,
aur: &AurClient,
terms: &[String],
) -> Result<i32> {
let hits = search(options, tools, aur, terms).await?;
print_hits(options, &hits, true);
Ok(hits.is_empty() as i32)
}
pub async fn interactive_select(
options: &Options,
tools: &Toolchain,
aur: &AurClient,
terms: &[String],
) -> Result<Vec<String>> {
let hits = search(options, tools, aur, terms).await?;
if hits.is_empty() {
return Ok(Vec::new());
}
print_hits(options, &hits, false);
if options.no_confirm {
return Ok(vec![hits[0].name.clone()]);
}
output::prompt("Packages to install (eg: 1 2, 1-3, ^4, name): ")?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let names = hits.iter().map(|hit| hit.name.clone()).collect::<Vec<_>>();
Ok(parse_selection(&input, hits.len(), &names)
.into_iter()
.map(|index| hits[index].name.clone())
.collect())
}
async fn search(
options: &Options,
tools: &Toolchain,
aur: &AurClient,
terms: &[String],
) -> Result<Vec<SearchHit>> {
let repo_future = async {
if options.mode.repo() {
tools.search_repo(terms).await
} else {
Ok(Vec::new())
}
};
let aur_future = async {
if options.mode.aur() {
aur.search_narrowed(terms, options.search_by).await
} else {
Ok(Vec::new())
}
};
let (repo, aur_pkgs) = tokio::try_join!(repo_future, aur_future)?;
let mut hits = Vec::with_capacity(repo.len() + aur_pkgs.len());
hits.extend(repo.into_iter().map(SearchHit::from_repo));
if !aur_pkgs.is_empty() {
let local = LocalDb::load_default().unwrap_or_else(|_| LocalDb::empty());
hits.extend(
aur_pkgs
.into_iter()
.map(|pkg| SearchHit::from_aur(pkg, &local)),
);
}
sort_hits(&mut hits, terms, options.sort_by);
if let Some(limit) = options.limit {
hits.truncate(limit);
}
Ok(hits)
}
impl SearchHit {
fn from_repo(pkg: RepoPackage) -> Self {
Self {
source: pkg.repo,
name: pkg.name,
version: pkg.version,
desc: pkg.desc,
votes: -1,
popularity: -1.0,
last_modified: 0,
installed: pkg.installed,
}
}
fn from_aur(pkg: AurPackage, local: &LocalDb) -> Self {
let installed = local.get(&pkg.name).map(|local| {
if local.version == pkg.version {
"installed".to_string()
} else {
format!("installed: {}", local.version)
}
});
Self {
source: "aur".to_string(),
name: pkg.name,
version: pkg.version,
desc: pkg.description,
votes: pkg.num_votes,
popularity: pkg.popularity,
last_modified: pkg.last_modified,
installed,
}
}
}
fn sort_hits(hits: &mut [SearchHit], terms: &[String], sort_by: SortBy) {
let terms = terms
.iter()
.map(|term| term.to_lowercase())
.collect::<Vec<_>>();
hits.sort_by(|left, right| match sort_by {
SortBy::Relevance => compare_relevance(left, right, &terms),
SortBy::Votes => right
.votes
.cmp(&left.votes)
.then_with(|| left.name.cmp(&right.name)),
SortBy::Popularity => right
.popularity
.partial_cmp(&left.popularity)
.unwrap_or(Ordering::Equal)
.then_with(|| left.name.cmp(&right.name)),
SortBy::Name => left.name.cmp(&right.name),
SortBy::Modified => right
.last_modified
.cmp(&left.last_modified)
.then_with(|| left.name.cmp(&right.name)),
});
}
fn compare_relevance(left: &SearchHit, right: &SearchHit, terms: &[String]) -> Ordering {
relevance(left, terms)
.cmp(&relevance(right, terms))
.then_with(|| source_rank(&left.source).cmp(&source_rank(&right.source)))
.then_with(|| {
right
.popularity
.partial_cmp(&left.popularity)
.unwrap_or(Ordering::Equal)
})
.then_with(|| left.name.cmp(&right.name))
}
fn relevance(hit: &SearchHit, terms: &[String]) -> u8 {
if terms.is_empty() {
return 3;
}
let name = hit.name.to_lowercase();
if terms.contains(&name) {
0
} else if terms.iter().all(|term| name.starts_with(term)) {
1
} else if terms.iter().all(|term| name.contains(term)) {
2
} else {
let Some(desc) = hit.desc.as_deref() else {
return 5;
};
let desc = desc.to_lowercase();
if terms.iter().all(|term| desc.contains(term)) {
4
} else {
5
}
}
}
fn source_rank(source: &str) -> u8 {
if source == "aur" {
1
} else {
0
}
}
fn print_hits(options: &Options, hits: &[SearchHit], omit_numbers: bool) {
for (index, hit) in hits.iter().enumerate() {
if options.quiet {
output::inline_line(&hit.name);
continue;
}
let mut line = String::new();
if !omit_numbers {
line.push_str(&format!("{:>3} ", index + 1));
}
line.push_str(&format!(
"{}/{} {}",
output::inline(&hit.source),
output::inline(&hit.name),
output::inline(&hit.version)
));
if hit.votes >= 0 {
line.push_str(&format!(" (+{} ~{:.2})", hit.votes, hit.popularity));
}
if let Some(installed) = &hit.installed {
line.push_str(&format!(" [{}]", output::inline(installed)));
}
output::line(line);
if let Some(desc) = &hit.desc {
output::line(format!(" {}", output::inline(desc)));
}
}
}