diff --git a/Cargo.lock b/Cargo.lock index 1c2440b..4cc7dd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,7 @@ dependencies = [ "rm_rf", "serde", "spinoff", + "strsim", "textwrap", "toml", "ureq", diff --git a/Cargo.toml b/Cargo.toml index d59267f..62e0ff5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,4 +35,5 @@ spinoff = { version = "0.5.2", default-features = false } textwrap = { version = "0.15.0", features = [ "terminal_size", "smawk" ] } chrono = { version = "0.4.22", default-features = false, features = [ "clock", "std", "wasmbind" ] } toml = { version = "0.5.9", default-features = false } -crossterm = { version = "0.25.0", default-features = false } \ No newline at end of file +crossterm = { version = "0.25.0", default-features = false } +strsim = { version = "0.10.0", default-features = false } \ No newline at end of file diff --git a/src/args.rs b/src/args.rs index a21df05..241d96c 100644 --- a/src/args.rs +++ b/src/args.rs @@ -104,7 +104,7 @@ pub struct SearchArgs { /// The string the package must match in the search #[clap(required = true)] - pub search: Vec, + pub search: String, } #[derive(Default, Debug, Clone, Parser)] diff --git a/src/main.rs b/src/main.rs index 5cc42d7..60af43d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,11 @@ use args::Args; use clap::{CommandFactory, Parser}; use clap_complete::{Generator, Shell}; +use strsim::sorensen_dice; + use internal::commands::ShellCommand; use internal::error::SilentUnwrap; + use std::env; use std::fs; use std::path::Path; @@ -17,6 +20,7 @@ use crate::args::{ use crate::internal::exit_code::AppExitCode; use crate::internal::utils::pager; use crate::internal::{detect, init, sort, start_sudoloop, structs::Options}; +use crate::operations::ResultsVec; #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; @@ -186,7 +190,7 @@ fn cmd_remove(args: RemoveArgs, options: Options) { fn cmd_search(args: &SearchArgs, options: Options) { // Initialise variables - let query_string = args.search.join(" "); + let query_string = &args.search; // Logic for searching let repo = args.repo || env::args().collect::>()[1] == "-Ssr"; @@ -198,12 +202,12 @@ fn cmd_search(args: &SearchArgs, options: Options) { let rsp = spinner!("Searching repos for {}", query_string); // Search repos - let ret = operations::search(&query_string, options); + let ret = operations::search(query_string, options); rsp.stop_bold("Repo search complete"); ret } else { - "".to_string() + Vec::new() }; // Start AUR spinner @@ -219,25 +223,41 @@ fn cmd_search(args: &SearchArgs, options: Options) { ret } else { - "".to_string() + Vec::new() }; - let results = repo_results + "\n" + &aur_results; + let mut results = repo_results + .into_iter() + .chain(aur_results) + .collect::>(); + + // Sort results by how closely they match the query + results.sort_by(|a, b| { + let a_score = sorensen_dice(&a.name, query_string); + let b_score = sorensen_dice(&b.name, query_string); + b_score + .partial_cmp(&a_score) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + let results = ResultsVec::from(results); // Print results either way, so that the user can see the results after they exit `less` let text = if internal::uwu_enabled() { - uwu!(results.trim()) + uwu!(results.to_string()) } else { - results.trim().to_string() + results.to_string() }; + let text = text.trim().to_string(); + println!("{}", text); // Check if results are longer than terminal height - if results.lines().count() > crossterm::terminal::size().unwrap().1 as usize { + if results.0.len() > (crossterm::terminal::size().unwrap().1 / 2).into() { // If so, paginate results #[allow(clippy::let_underscore_drop)] - let _ = pager(&results.trim().to_string()); + let _ = pager(&results.to_string()); } } diff --git a/src/operations/mod.rs b/src/operations/mod.rs index ef348bb..936f7c9 100644 --- a/src/operations/mod.rs +++ b/src/operations/mod.rs @@ -1,7 +1,7 @@ pub use aur_install::*; pub use clean::*; pub use install::*; -pub use search::{aur_search, repo_search as search}; +pub use search::{aur_search, repo_search as search, ResultsVec}; pub use uninstall::*; pub use upgrade::*; diff --git a/src/operations/search.rs b/src/operations/search.rs index d1f9dee..b20bf2f 100644 --- a/src/operations/search.rs +++ b/src/operations/search.rs @@ -2,6 +2,8 @@ use chrono::{Local, TimeZone}; use colored::Colorize; use textwrap::wrap; +use std::fmt::Display; + use crate::internal::commands::ShellCommand; use crate::internal::error::SilentUnwrap; use crate::internal::exit_code::AppExitCode; @@ -9,47 +11,87 @@ use crate::internal::rpc::rpcsearch; use crate::{log, Options}; #[allow(clippy::module_name_repetitions)] -/// Searches for packages from the AUR and returns wrapped results -pub fn aur_search(query: &str, options: Options) -> String { - // Query AUR for package info - let res = rpcsearch(query); - - // Get verbosity - let verbosity = options.verbosity; +#[derive(Debug, Clone)] +pub struct SearchResult { + pub repo: String, + pub name: String, + pub version: String, + pub ood: Option, + pub description: String, +} - // Format output - let mut results_vec = vec![]; - for package in &res.results { - // Define wrapping options +impl Display for SearchResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 4) .subsequent_indent(" "); - - let result = format!( + let description = wrap(&self.description, opts).join("\n"); + write!( + f, "{}{} {} {}\n {}", - "aur/".cyan().bold(), - package.name.bold(), - package.version.green().bold(), - if package.out_of_date.is_some() { + if self.repo == "aur" { + (self.repo.clone() + "/").bold().cyan() + } else { + (self.repo.clone() + "/").bold().purple() + }, + self.name.bold(), + self.version.bold().green(), + if self.ood.is_some() { format!( "[out of date: since {}]", Local - .timestamp(package.out_of_date.unwrap().try_into().unwrap(), 0) + .timestamp(self.ood.unwrap().try_into().unwrap(), 0) .date_naive() ) - .red() .bold() + .red() } else { "".bold() }, - wrap( - package - .description - .as_ref() - .unwrap_or(&"No description".to_string()), - opts, - ) - .join("\n"), - ); + description + ) + } +} + +pub struct ResultsVec(pub Vec); + +impl From> for ResultsVec { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl Display for ResultsVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for result in &self.0 { + writeln!(f, "{}", result)?; + } + Ok(()) + } +} + +#[allow(clippy::module_name_repetitions)] +/// Searches for packages from the AUR and returns wrapped results +pub fn aur_search(query: &str, options: Options) -> Vec { + // Query AUR for package info + let res = rpcsearch(query); + + // Get verbosity + let verbosity = options.verbosity; + + // Format output + let mut results_vec = vec![]; + for package in &res.results { + let result = SearchResult { + repo: "aur".to_string(), + name: package.name.to_string(), + version: package.version.to_string(), + ood: package.out_of_date, + description: package + .description + .as_ref() + .unwrap_or(&"No description".to_string()) + .to_string(), + }; results_vec.push(result); } @@ -61,19 +103,12 @@ pub fn aur_search(query: &str, options: Options) -> String { ); } - results_vec.join("\n") -} - -struct SearchResult { - repo: String, - name: String, - version: String, - description: String, + results_vec } #[allow(clippy::module_name_repetitions)] /// Searches for packages from the repos and returns wrapped results -pub fn repo_search(query: &str, options: Options) -> String { +pub fn repo_search(query: &str, options: Options) -> Vec { // Initialise variables let verbosity = options.verbosity; @@ -91,22 +126,19 @@ pub fn repo_search(query: &str, options: Options) -> String { // Initialise results vector let mut results_vec: Vec = vec![]; - let clone = lines.clone().collect::>(); - if clone.len() == 1 && clone[0].is_empty() { - // If no results, return empty string - return "".to_string(); - } - // Iterate over lines for line in lines { - let parts: Vec<&str> = line.split('\\').collect(); - let res = SearchResult { - repo: parts[0].to_string(), - name: parts[1].to_string(), - version: parts[2].to_string(), - description: parts[3].to_string(), - }; - results_vec.push(res); + if line.contains('\\') { + let parts: Vec<&str> = line.split('\\').collect(); + let res = SearchResult { + repo: parts[0].to_string(), + name: parts[1].to_string(), + version: parts[2].to_string(), + ood: None, + description: parts[3].to_string(), + }; + results_vec.push(res); + } } if verbosity >= 1 { @@ -117,30 +149,5 @@ pub fn repo_search(query: &str, options: Options) -> String { ); } - // Format output - let results_vec = results_vec - .into_iter() - .map(|res| { - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 4) - .subsequent_indent(" "); - format!( - "{}{}{} {}\n {}", - res.repo.purple().bold(), - "/".purple().bold(), - res.name.bold(), - res.version.green().bold(), - if res.description.is_empty() { - "No description".to_string() - } else { - wrap(&res.description, opts).join("\n") - }, - ) - }) - .collect::>(); - - if output.trim().is_empty() { - "".to_string() - } else { - results_vec.join("\n") - } + results_vec }