From e3d3135e43e211387088432da6b62427d8709239 Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 27 Aug 2022 12:23:17 +0000 Subject: [PATCH 1/2] Added search-by directive for AUR --- src/args.rs | 5 +++ src/internal/rpc.rs | 7 ++-- src/main.rs | 2 +- src/operations/mod.rs | 2 +- src/operations/search.rs | 69 ++++++++++++++++++++++++++++++++++++++-- 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/args.rs b/src/args.rs index a21df05..be063c4 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,5 +1,6 @@ #![allow(clippy::module_name_repetitions)] +use crate::operations::SearchBy; use clap::{Parser, Subcommand, ValueHint}; #[derive(Debug, Clone, Parser)] @@ -105,6 +106,10 @@ pub struct SearchArgs { /// The string the package must match in the search #[clap(required = true)] pub search: Vec, + + /// Sets the search-by directive for searching the AUR only + #[clap(long, short, possible_values = SearchBy::variants())] + pub by: Option, } #[derive(Default, Debug, Clone, Parser)] diff --git a/src/internal/rpc.rs b/src/internal/rpc.rs index 81ca890..ebc5764 100644 --- a/src/internal/rpc.rs +++ b/src/internal/rpc.rs @@ -1,3 +1,4 @@ +use crate::operations::SearchBy; use std::sync::Arc; #[derive(serde::Deserialize, Debug, Clone)] @@ -75,7 +76,7 @@ pub fn rpcinfo(pkg: &str) -> InfoResults { } /// Return a struct of type [`SearchResults`] from the AUR. -pub fn rpcsearch(pkg: &str) -> SearchResults { +pub fn rpcsearch(pkg: &str, by: SearchBy) -> SearchResults { // Initialise TLS connector let tls_connector = Arc::new(native_tls::TlsConnector::new().unwrap()); @@ -87,8 +88,8 @@ pub fn rpcsearch(pkg: &str) -> SearchResults { // Send request and parse results into json agent .get(&format!( - "https://aur.archlinux.org/rpc/?v=5&type=search&arg={}", - pkg + "https://aur.archlinux.org/rpc/?v=5&type=search&by={}&arg={}", + by, pkg )) .call() .unwrap() diff --git a/src/main.rs b/src/main.rs index 5cc42d7..4386ec2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -214,7 +214,7 @@ fn cmd_search(args: &SearchArgs, options: Options) { let asp = spinner!("Searching AUR for {}", query_string); // Search AUR - let ret = operations::aur_search(&query_string, options); + let ret = operations::aur_search(&query_string, options, args.by.unwrap_or_default()); asp.stop_bold("AUR search complete"); ret diff --git a/src/operations/mod.rs b/src/operations/mod.rs index ef348bb..0843999 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, SearchBy}; pub use uninstall::*; pub use upgrade::*; diff --git a/src/operations/search.rs b/src/operations/search.rs index d1f9dee..35865be 100644 --- a/src/operations/search.rs +++ b/src/operations/search.rs @@ -2,17 +2,82 @@ use chrono::{Local, TimeZone}; use colored::Colorize; use textwrap::wrap; +use std::fmt::Display; +use std::str::FromStr; + use crate::internal::commands::ShellCommand; use crate::internal::error::SilentUnwrap; use crate::internal::exit_code::AppExitCode; use crate::internal::rpc::rpcsearch; use crate::{log, Options}; +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone, Copy)] +pub enum SearchBy { + Name, + NameDesc, + Maintainer, + Depends, + MakeDepends, + OptDepends, + CheckDepends, +} + +impl FromStr for SearchBy { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "name" => Ok(Self::Name), + "name-desc" => Ok(Self::NameDesc), + "maintainer" => Ok(Self::Maintainer), + "depends" => Ok(Self::Depends), + "makedepends" => Ok(Self::MakeDepends), + "optdepends" => Ok(Self::OptDepends), + "checkdepends" => Ok(Self::CheckDepends), + _ => Err(format!("Invalid search-by directive \"{}\"", s)), + } + } +} + +impl Display for SearchBy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Name => write!(f, "name"), + Self::NameDesc => write!(f, "name-desc"), + Self::Maintainer => write!(f, "maintainer"), + Self::Depends => write!(f, "depends"), + Self::MakeDepends => write!(f, "makedepends"), + Self::OptDepends => write!(f, "optdepends"), + Self::CheckDepends => write!(f, "checkdepends"), + } + } +} + +impl Default for SearchBy { + fn default() -> Self { + Self::NameDesc + } +} + +impl SearchBy { + pub fn variants() -> Vec<&'static str> { + vec![ + "name", + "name-desc", + "maintainer", + "depends", + "makedepends", + "optdepends", + "checkdepends", + ] + } +} + #[allow(clippy::module_name_repetitions)] /// Searches for packages from the AUR and returns wrapped results -pub fn aur_search(query: &str, options: Options) -> String { +pub fn aur_search(query: &str, options: Options, by: SearchBy) -> String { // Query AUR for package info - let res = rpcsearch(query); + let res = rpcsearch(query, by); // Get verbosity let verbosity = options.verbosity; From b484e2b26873f7b1d979915e031a4dd4ce50a653 Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 27 Aug 2022 13:36:03 +0000 Subject: [PATCH 2/2] Initial implementation of sorting aur and repo results together --- Cargo.lock | 1 + Cargo.toml | 3 +- src/args.rs | 7 +- src/internal/rpc.rs | 7 +- src/main.rs | 40 ++++++-- src/operations/mod.rs | 2 +- src/operations/search.rs | 204 ++++++++++++++------------------------- 7 files changed, 111 insertions(+), 153 deletions(-) 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 be063c4..241d96c 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,6 +1,5 @@ #![allow(clippy::module_name_repetitions)] -use crate::operations::SearchBy; use clap::{Parser, Subcommand, ValueHint}; #[derive(Debug, Clone, Parser)] @@ -105,11 +104,7 @@ pub struct SearchArgs { /// The string the package must match in the search #[clap(required = true)] - pub search: Vec, - - /// Sets the search-by directive for searching the AUR only - #[clap(long, short, possible_values = SearchBy::variants())] - pub by: Option, + pub search: String, } #[derive(Default, Debug, Clone, Parser)] diff --git a/src/internal/rpc.rs b/src/internal/rpc.rs index ebc5764..81ca890 100644 --- a/src/internal/rpc.rs +++ b/src/internal/rpc.rs @@ -1,4 +1,3 @@ -use crate::operations::SearchBy; use std::sync::Arc; #[derive(serde::Deserialize, Debug, Clone)] @@ -76,7 +75,7 @@ pub fn rpcinfo(pkg: &str) -> InfoResults { } /// Return a struct of type [`SearchResults`] from the AUR. -pub fn rpcsearch(pkg: &str, by: SearchBy) -> SearchResults { +pub fn rpcsearch(pkg: &str) -> SearchResults { // Initialise TLS connector let tls_connector = Arc::new(native_tls::TlsConnector::new().unwrap()); @@ -88,8 +87,8 @@ pub fn rpcsearch(pkg: &str, by: SearchBy) -> SearchResults { // Send request and parse results into json agent .get(&format!( - "https://aur.archlinux.org/rpc/?v=5&type=search&by={}&arg={}", - by, pkg + "https://aur.archlinux.org/rpc/?v=5&type=search&arg={}", + pkg )) .call() .unwrap() diff --git a/src/main.rs b/src/main.rs index 4386ec2..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 @@ -214,30 +218,46 @@ fn cmd_search(args: &SearchArgs, options: Options) { let asp = spinner!("Searching AUR for {}", query_string); // Search AUR - let ret = operations::aur_search(&query_string, options, args.by.unwrap_or_default()); + let ret = operations::aur_search(&query_string, options); asp.stop_bold("AUR search complete"); 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 0843999..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, SearchBy}; +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 35865be..b20bf2f 100644 --- a/src/operations/search.rs +++ b/src/operations/search.rs @@ -3,7 +3,6 @@ use colored::Colorize; use textwrap::wrap; use std::fmt::Display; -use std::str::FromStr; use crate::internal::commands::ShellCommand; use crate::internal::error::SilentUnwrap; @@ -12,72 +11,69 @@ use crate::internal::rpc::rpcsearch; use crate::{log, Options}; #[allow(clippy::module_name_repetitions)] -#[derive(Debug, Clone, Copy)] -pub enum SearchBy { - Name, - NameDesc, - Maintainer, - Depends, - MakeDepends, - OptDepends, - CheckDepends, +#[derive(Debug, Clone)] +pub struct SearchResult { + pub repo: String, + pub name: String, + pub version: String, + pub ood: Option, + pub description: String, } -impl FromStr for SearchBy { - type Err = String; - fn from_str(s: &str) -> Result { - match s { - "name" => Ok(Self::Name), - "name-desc" => Ok(Self::NameDesc), - "maintainer" => Ok(Self::Maintainer), - "depends" => Ok(Self::Depends), - "makedepends" => Ok(Self::MakeDepends), - "optdepends" => Ok(Self::OptDepends), - "checkdepends" => Ok(Self::CheckDepends), - _ => Err(format!("Invalid search-by directive \"{}\"", s)), - } - } -} - -impl Display for SearchBy { +impl Display for SearchResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Name => write!(f, "name"), - Self::NameDesc => write!(f, "name-desc"), - Self::Maintainer => write!(f, "maintainer"), - Self::Depends => write!(f, "depends"), - Self::MakeDepends => write!(f, "makedepends"), - Self::OptDepends => write!(f, "optdepends"), - Self::CheckDepends => write!(f, "checkdepends"), - } + let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 4) + .subsequent_indent(" "); + let description = wrap(&self.description, opts).join("\n"); + write!( + f, + "{}{} {} {}\n {}", + 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(self.ood.unwrap().try_into().unwrap(), 0) + .date_naive() + ) + .bold() + .red() + } else { + "".bold() + }, + description + ) } } -impl Default for SearchBy { - fn default() -> Self { - Self::NameDesc +pub struct ResultsVec(pub Vec); + +impl From> for ResultsVec { + fn from(v: Vec) -> Self { + Self(v) } } -impl SearchBy { - pub fn variants() -> Vec<&'static str> { - vec![ - "name", - "name-desc", - "maintainer", - "depends", - "makedepends", - "optdepends", - "checkdepends", - ] +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, by: SearchBy) -> String { +pub fn aur_search(query: &str, options: Options) -> Vec { // Query AUR for package info - let res = rpcsearch(query, by); + let res = rpcsearch(query); // Get verbosity let verbosity = options.verbosity; @@ -85,36 +81,17 @@ pub fn aur_search(query: &str, options: Options, by: SearchBy) -> String { // Format output let mut results_vec = vec![]; for package in &res.results { - // Define wrapping options - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 4) - .subsequent_indent(" "); - - let result = format!( - "{}{} {} {}\n {}", - "aur/".cyan().bold(), - package.name.bold(), - package.version.green().bold(), - if package.out_of_date.is_some() { - format!( - "[out of date: since {}]", - Local - .timestamp(package.out_of_date.unwrap().try_into().unwrap(), 0) - .date_naive() - ) - .red() - .bold() - } else { - "".bold() - }, - wrap( - package - .description - .as_ref() - .unwrap_or(&"No description".to_string()), - opts, - ) - .join("\n"), - ); + 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); } @@ -126,19 +103,12 @@ pub fn aur_search(query: &str, options: Options, by: SearchBy) -> 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; @@ -156,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 { @@ -182,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 }