From cdae601f4aefd12ff9526c12a440bbcbf5a11885 Mon Sep 17 00:00:00 2001 From: Jonathan Lopez Date: Sun, 9 Aug 2026 09:16:07 -0400 Subject: [PATCH 1/7] moss: Add clap_complete unstable-dynamic feature --- Cargo.lock | 12 ++++++++++++ Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 6e8447bfc..f8575b351 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -515,6 +515,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" dependencies = [ "clap", + "clap_lex", + "is_executable", + "shlex", ] [[package]] @@ -1676,6 +1679,15 @@ dependencies = [ "serde", ] +[[package]] +name = "is_executable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" diff --git a/Cargo.toml b/Cargo.toml index 30c542ea3..1346f98e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ bytes = "1.6.0" camino = "1.1.10" chrono = "0.4.38" clap = { version = "4.5.8", features = ["derive", "string"] } -clap_complete = "4.5.37" +clap_complete = { version = "4.5.37", features = ["unstable-dynamic"] } clap_mangen = "0.2.24" criterion = { version = "0.8.2", features = ["html_reports"] } crossterm = "0.29.0" From ac63825cf6ad18ac5ef9ca1f5576d7adb8f6cba1 Mon Sep 17 00:00:00 2001 From: Jonathan Lopez Date: Sun, 9 Aug 2026 09:31:49 -0400 Subject: [PATCH 2/7] moss: Add completions module --- moss/src/completions.rs | 42 +++++++++++++++++++++++++++++++++++++++++ moss/src/lib.rs | 1 + 2 files changed, 43 insertions(+) create mode 100644 moss/src/completions.rs diff --git a/moss/src/completions.rs b/moss/src/completions.rs new file mode 100644 index 000000000..c59c4f149 --- /dev/null +++ b/moss/src/completions.rs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 AerynOS Developers +// SPDX-License-Identifier: MPL-2.0 + +use clap_complete::CompletionCandidate; +use itertools::Itertools; +use std::path::PathBuf; + +use crate::{Installation, client, package}; + +const MAX_RESULTS: usize = 100; + +fn generate_results(client: &client::Client, flags: package::Flags, prefix: &str) -> Vec { + client + .search_packages(prefix, flags) + .filter(|pkg| pkg.meta.name.as_str().starts_with(prefix)) + .map(|pkg| pkg.meta.name.to_string()) + .unique() + .take(MAX_RESULTS) + .map(CompletionCandidate::new) + .collect() +} + +fn default_client() -> Result { + let root = PathBuf::from("/"); + let installation = Installation::open(root, None)?; + client::Client::new("moss", installation) +} + +pub fn prefix_completer(flags: package::Flags) -> impl Fn(&std::ffi::OsStr) -> Vec { + move |prefix: &std::ffi::OsStr| { + let Some(prefix) = prefix.to_str() else { + return vec![]; + }; + if prefix.is_empty() { + return vec![]; + } + let Ok(client) = default_client() else { + return vec![]; + }; + generate_results(&client, flags, prefix) + } +} diff --git a/moss/src/lib.rs b/moss/src/lib.rs index 8a6efa66c..3e568d70f 100644 --- a/moss/src/lib.rs +++ b/moss/src/lib.rs @@ -12,6 +12,7 @@ pub use self::state::State; pub use self::system_model::SystemModel; pub mod client; +pub mod completions; pub mod db; pub mod dependency; pub mod environment; From 00b409bb8e47a2b19a2dda51cdc1a5aae0b267ed Mon Sep 17 00:00:00 2001 From: Jonathan Lopez Date: Sun, 9 Aug 2026 12:54:28 -0400 Subject: [PATCH 3/7] moss::cli: Add prefix completion to install and remove commands --- moss/src/cli/install.rs | 4 ++++ moss/src/cli/remove.rs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/moss/src/cli/install.rs b/moss/src/cli/install.rs index 08bec59a6..19b49ca12 100644 --- a/moss/src/cli/install.rs +++ b/moss/src/cli/install.rs @@ -4,11 +4,14 @@ use std::path::PathBuf; use clap::{ArgMatches, CommandFactory, FromArgMatches, Parser}; +use clap_complete::ArgValueCompleter; +use moss::completions::prefix_completer; use moss::{Installation, client::Client, environment}; use tracing::instrument; pub use moss::client::Error; +use moss::package; pub fn command() -> clap::Command { Command::command() @@ -23,6 +26,7 @@ pub fn command() -> clap::Command { )] pub struct Command { /// Packages to install + #[arg(add=ArgValueCompleter::new(prefix_completer(package::Flags::default().with_available())))] packages: Vec, /// Simulate the operation (dry-run) diff --git a/moss/src/cli/remove.rs b/moss/src/cli/remove.rs index ba58cdaa0..2d374ff2e 100644 --- a/moss/src/cli/remove.rs +++ b/moss/src/cli/remove.rs @@ -2,6 +2,10 @@ // SPDX-License-Identifier: MPL-2.0 use clap::{ArgMatches, CommandFactory, FromArgMatches, Parser}; +use clap_complete::ArgValueCompleter; + +use moss::completions::prefix_completer; +use moss::package; use moss::{Installation, client::Client, environment}; use tracing::instrument; @@ -21,6 +25,7 @@ pub fn command() -> clap::Command { )] pub struct Command { /// Packages to remove + #[arg(add=ArgValueCompleter::new(prefix_completer(package::Flags::default().with_installed())))] packages: Vec, /// Simulate the operation (dry-run) From b8b623a96e7451147564cfcfd1d44f161c0f5d5e Mon Sep 17 00:00:00 2001 From: Jonathan Lopez Date: Thu, 13 Aug 2026 06:40:16 -0400 Subject: [PATCH 4/7] moss::cli: Update process to generate shell completions --- moss/src/cli/mod.rs | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/moss/src/cli/mod.rs b/moss/src/cli/mod.rs index 18ff22933..31812f831 100644 --- a/moss/src/cli/mod.rs +++ b/moss/src/cli/mod.rs @@ -4,10 +4,11 @@ use std::{env, io, path::Path, path::PathBuf}; use clap::{Arg, ArgAction, Command}; -use clap_complete::{ - generate_to, - shells::{Bash, Fish, Zsh}, -}; +// use clap_complete::{ +// generate_to, +// shells::{Bash, Fish, Zsh}, +// }; +use clap_complete::CompleteEnv; use clap_mangen::Man; use fs_err as fs; use moss::{Installation, installation}; @@ -94,14 +95,6 @@ fn command() -> Command { .value_name("DIR") .hide(true), ) - .arg( - Arg::new("generate-completions") - .long("generate-completions") - .help("Generate shell completions") - .action(ArgAction::Set) - .value_name("DIR") - .hide(true), - ) .arg_required_else_help(true) .subcommand(boot::command()) .subcommand(cache::command()) @@ -147,16 +140,11 @@ fn generate_manpages(cmd: &Command, dir: &Path, prefix: Option<&str>) -> io::Res Ok(()) } -/// Generate shell completions -fn generate_completions(cmd: &mut Command, dir: &Path) -> io::Result<()> { - generate_to(Bash, cmd, "moss", dir)?; - generate_to(Fish, cmd, "moss", dir)?; - generate_to(Zsh, cmd, "moss", dir)?; - Ok(()) -} - /// Process all CLI arguments pub fn process() -> Result<(), Error> { + // Generate shell completions + CompleteEnv::with_factory(command).complete(); + let args = replace_aliases(env::args()); let matches = command().get_matches_from(args); @@ -178,13 +166,6 @@ pub fn process() -> Result<(), Error> { return Ok(()); } - if let Some(dir) = matches.get_one::("generate-completions") { - let dir = Path::new(dir); - fs::create_dir_all(dir)?; - generate_completions(&mut command(), dir)?; - return Ok(()); - } - // Print the version, but not if the user is using the version subcommand if verbose && let Some(command) = matches.subcommand_name() From 74011e01319fe8fc9a338f3346d2be60d2ede8cf Mon Sep 17 00:00:00 2001 From: Jonathan Lopez Date: Thu, 13 Aug 2026 16:36:50 -0400 Subject: [PATCH 5/7] moss: Set up criterion benchmarks --- Cargo.lock | 1 + moss/Cargo.toml | 5 +++++ moss/benches/completions.rs | 40 +++++++++++++++++++++++++++++++++++++ moss/src/completions.rs | 2 +- 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 moss/benches/completions.rs diff --git a/Cargo.lock b/Cargo.lock index f8575b351..6ad8a0c41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1992,6 +1992,7 @@ dependencies = [ "clap_mangen", "config", "container", + "criterion", "dag", "derive_more", "fnmatch", diff --git a/moss/Cargo.toml b/moss/Cargo.toml index 8e06d801d..7446fb271 100644 --- a/moss/Cargo.toml +++ b/moss/Cargo.toml @@ -70,6 +70,11 @@ tempfile.workspace = true moss = { path = ".", features = ["testing"] } tempfile.workspace = true +criterion.workspace = true + +[[bench]] +name = "completions" +harness = false [lints] workspace = true diff --git a/moss/benches/completions.rs b/moss/benches/completions.rs new file mode 100644 index 000000000..d1f5d89ba --- /dev/null +++ b/moss/benches/completions.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2026 AerynOS Developers +// SPDX-License-Identifier: MPL-2.0 + +use std::hint::black_box; + +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use moss::completions; +use moss::package::Flags; +use moss::{Client, Installation}; + +fn criterion_benchmark(c: &mut Criterion) { + // Use actual moss database for benchmarks + let installation = match Installation::open("/", None) { + Ok(installation) => installation, + Err(err) => { + eprintln!("Skipping completions benchmark: {err}"); + return; + } + }; + let client = match Client::new("moss", installation) { + Ok(client) => client, + Err(err) => { + eprintln!("Skipping completions benchmark: {err}"); + return; + } + }; + + let flags = Flags::default().with_available(); + let prefixes = &["a", "g", "l", "lib", "p", "py"]; + let mut group = c.benchmark_group("prefix_completion"); + for prefix in prefixes { + group.bench_with_input(BenchmarkId::new("available", prefix), prefix, |b, &p| { + b.iter(|| completions::generate_results(&client, flags, black_box(p))); + }); + } + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/moss/src/completions.rs b/moss/src/completions.rs index c59c4f149..ccb30fc98 100644 --- a/moss/src/completions.rs +++ b/moss/src/completions.rs @@ -9,7 +9,7 @@ use crate::{Installation, client, package}; const MAX_RESULTS: usize = 100; -fn generate_results(client: &client::Client, flags: package::Flags, prefix: &str) -> Vec { +pub fn generate_results(client: &client::Client, flags: package::Flags, prefix: &str) -> Vec { client .search_packages(prefix, flags) .filter(|pkg| pkg.meta.name.as_str().starts_with(prefix)) From 7a75620a69eb28c5bb144610e07feb3c8e7ea9e0 Mon Sep 17 00:00:00 2001 From: Jonathan Lopez Date: Thu, 13 Aug 2026 16:52:47 -0400 Subject: [PATCH 6/7] moss::client: Add new methods to search by prefix --- moss/src/cli/mod.rs | 4 -- moss/src/client/mod.rs | 10 +++++ moss/src/completions.rs | 5 +-- moss/src/db/meta/mod.rs | 60 ++++++++++++++++++++++++++ moss/src/package/meta.rs | 4 ++ moss/src/registry/mod.rs | 5 +++ moss/src/registry/plugin/active.rs | 15 +++++++ moss/src/registry/plugin/cobble.rs | 12 ++++++ moss/src/registry/plugin/mod.rs | 20 +++++++++ moss/src/registry/plugin/repository.rs | 14 ++++++ 10 files changed, 142 insertions(+), 7 deletions(-) diff --git a/moss/src/cli/mod.rs b/moss/src/cli/mod.rs index 31812f831..45a93bd49 100644 --- a/moss/src/cli/mod.rs +++ b/moss/src/cli/mod.rs @@ -4,10 +4,6 @@ use std::{env, io, path::Path, path::PathBuf}; use clap::{Arg, ArgAction, Command}; -// use clap_complete::{ -// generate_to, -// shells::{Bash, Fish, Zsh}, -// }; use clap_complete::CompleteEnv; use clap_mangen::Man; use fs_err as fs; diff --git a/moss/src/client/mod.rs b/moss/src/client/mod.rs index 880d34993..2c7769317 100644 --- a/moss/src/client/mod.rs +++ b/moss/src/client/mod.rs @@ -335,6 +335,16 @@ impl Client { self.registry.by_keyword(keyword, flags) } + /// Returns the name of all packages with names starting with the + /// provided prefix and match the given flags + pub fn search_packages_by_prefix<'a>( + &'a self, + prefix: &'a str, + flags: package::Flags, + ) -> impl Iterator + 'a { + self.registry.by_prefix(prefix, flags) + } + /// Activates the provided state and runs system triggers once applied. /// /// The current state gets archived.\ diff --git a/moss/src/completions.rs b/moss/src/completions.rs index ccb30fc98..7301c5fdf 100644 --- a/moss/src/completions.rs +++ b/moss/src/completions.rs @@ -11,9 +11,8 @@ const MAX_RESULTS: usize = 100; pub fn generate_results(client: &client::Client, flags: package::Flags, prefix: &str) -> Vec { client - .search_packages(prefix, flags) - .filter(|pkg| pkg.meta.name.as_str().starts_with(prefix)) - .map(|pkg| pkg.meta.name.to_string()) + .search_packages_by_prefix(prefix, flags) + .map(|name| name.to_string()) .unique() .take(MAX_RESULTS) .map(CompletionCandidate::new) diff --git a/moss/src/db/meta/mod.rs b/moss/src/db/meta/mod.rs index 97050f793..ae5899a48 100644 --- a/moss/src/db/meta/mod.rs +++ b/moss/src/db/meta/mod.rs @@ -30,6 +30,7 @@ pub enum Filter<'a> { Dependency(Dependency), Name(package::Name), Keyword(&'a str), + Prefix(&'a str), All, } @@ -76,6 +77,61 @@ impl Database { }) } + pub fn package_names(&self, filter: Filter<'_>) -> Result, Error> { + self.conn.exec(|conn| { + let mut stmt; + let meta_query = match filter { + Filter::Id(id) => { + stmt = conn.prepare("SELECT name FROM meta WHERE package = ?")?; + stmt.query([id.as_str()]) + } + Filter::Provider(provider) => { + stmt = conn.prepare(indoc! {" + SELECT m.name + FROM meta m + INNER JOIN meta_providers mp ON m.package = mp.package + WHERE mp.provider = ?"})?; + stmt.query([provider.to_string()]) + } + Filter::Dependency(dependency) => { + stmt = conn.prepare(indoc! {" + SELECT m.name + FROM meta m + INNER JOIN meta_dependencies md ON m.package = md.package + WHERE md.dependency = ?"})?; + stmt.query([dependency.to_string()]) + } + Filter::Name(name) => { + stmt = conn.prepare("SELECT name FROM meta WHERE name = ?")?; + stmt.query([name.to_string()]) + } + Filter::Keyword(kw) => { + stmt = conn.prepare( + "SELECT name FROM meta WHERE name LIKE concat('%', ?1, '%') OR summary LIKE concat('%', ?1, '%')", + )?; + stmt.query([kw.to_owned()]) + } + Filter::All => { + stmt = conn.prepare("SELECT name FROM meta")?; + stmt.query([]) + } + Filter::Prefix(prefix) => { + stmt = conn.prepare("SELECT name FROM meta WHERE name LIKE concat(?1, '%')")?; + stmt.query([prefix.to_owned()]) + } + }?; + + let entries = meta_query + .mapped(|row| { + let name = package::Name::from(row.get::<_, String>("name")?); + Ok( name) + }) + .collect::, _>>()?; + + Ok(entries) + }) + } + pub fn query(&self, filter: Filter<'_>) -> Result, Error> { self.conn.exec(|conn| { let mut stmt; @@ -114,6 +170,10 @@ impl Database { stmt = conn.prepare("SELECT * FROM meta")?; stmt.query([]) } + Filter::Prefix(prefix) => { + stmt = conn.prepare("SELECT * FROM meta WHERE name LIKE concat(?1, '%')")?; + stmt.query([prefix.to_owned()]) + } }?; let mut entries: BTreeMap = meta_base_query diff --git a/moss/src/package/meta.rs b/moss/src/package/meta.rs index db12aa6e0..e5d06a949 100644 --- a/moss/src/package/meta.rs +++ b/moss/src/package/meta.rs @@ -27,6 +27,10 @@ impl Name { pub fn contains(&self, text: &str) -> bool { self.0.contains(text) } + + pub fn starts_with(&self, prefix: &str) -> bool { + self.0.starts_with(prefix) + } } /// The metadata of a [`super::Package`] diff --git a/moss/src/registry/mod.rs b/moss/src/registry/mod.rs index f1a1d899a..58eaec837 100644 --- a/moss/src/registry/mod.rs +++ b/moss/src/registry/mod.rs @@ -75,6 +75,11 @@ impl Registry { self.query(move |plugin| plugin.query_keyword(keyword, flags)) } + /// Return a sorted stream of `package::Name` for packages whose name starts with `prefix` + pub fn by_prefix<'a>(&'a self, prefix: &'a str, flags: package::Flags) -> impl Iterator + 'a { + self.query(move |plugin| plugin.query_prefix(prefix, flags)) + } + /// Return a sorted stream of [`Package`] matching the given [`Flags`] /// /// [`Flags`]: package::Flags diff --git a/moss/src/registry/plugin/active.rs b/moss/src/registry/plugin/active.rs index d884e12ee..9efce1798 100644 --- a/moss/src/registry/plugin/active.rs +++ b/moss/src/registry/plugin/active.rs @@ -75,6 +75,21 @@ impl Active { self.query(flags, db::meta::Filter::Keyword(keyword)) } + pub fn query_prefix(&self, prefix: &str, flags: package::Flags) -> Vec { + let filter = db::meta::Filter::Prefix(prefix); + if flags.installed || flags == package::Flags::default() { + match self.db.package_names(filter) { + Ok(names) => names, + Err(error) => { + warn!("failed to query repository packages: {error}"); + vec![] + } + } + } else { + vec![] + } + } + /// Query all packages that match the given provider identity pub fn query_provider(&self, provider: &Provider, flags: package::Flags) -> Vec { self.query(flags, db::meta::Filter::Provider(provider.clone())) diff --git a/moss/src/registry/plugin/cobble.rs b/moss/src/registry/plugin/cobble.rs index 659410cbe..12d1fe53e 100644 --- a/moss/src/registry/plugin/cobble.rs +++ b/moss/src/registry/plugin/cobble.rs @@ -76,6 +76,18 @@ impl Cobble { }) } + pub fn query_prefix(&self, prefix: &str, flags: package::Flags) -> Vec { + if flags.available { + self.packages + .iter() + .filter(|(_, state)| state.meta.name.starts_with(prefix)) + .map(|(_, state)| state.meta.name.clone()) + .collect() + } else { + vec![] + } + } + pub fn query_provider(&self, provider: &Provider, flags: package::Flags) -> Vec { self.query(flags, |meta| meta.providers.contains(provider)) } diff --git a/moss/src/registry/plugin/mod.rs b/moss/src/registry/plugin/mod.rs index 648bf6d30..68c9fca2a 100644 --- a/moss/src/registry/plugin/mod.rs +++ b/moss/src/registry/plugin/mod.rs @@ -72,6 +72,17 @@ impl Plugin { }) } + pub fn query_prefix(&self, prefix: &str, flags: package::Flags) -> package::Sorted> { + package::Sorted::new(match self { + Plugin::Active(plugin) => plugin.query_prefix(prefix, flags), + Plugin::Cobble(plugin) => plugin.query_prefix(prefix, flags), + Plugin::Repository(plugin) => plugin.query_prefix(prefix, flags), + + #[cfg(any(test, feature = "testing"))] + Plugin::Test(plugin) => plugin.query_prefix(prefix, flags), + }) + } + /// Returns a list of packages with matching `provider` and `flags` pub fn query_provider(&self, provider: &Provider, flags: package::Flags) -> package::Sorted> { package::Sorted::new(match self { @@ -169,6 +180,15 @@ pub mod test { .collect() } + pub fn query_prefix(&self, prefix: &str, _flags: package::Flags) -> Vec { + let prefix_lower = prefix.to_ascii_lowercase(); + self.packages + .iter() + .filter(|pkg| pkg.meta.name.as_str().to_ascii_lowercase().starts_with(&prefix_lower)) + .map(|pkg| pkg.meta.name.clone()) + .collect() + } + pub fn query_provider(&self, provider: &Provider, flags: package::Flags) -> Vec { self.packages .iter() diff --git a/moss/src/registry/plugin/repository.rs b/moss/src/registry/plugin/repository.rs index 398f428e3..287ab7e92 100644 --- a/moss/src/registry/plugin/repository.rs +++ b/moss/src/registry/plugin/repository.rs @@ -82,6 +82,20 @@ impl Repository { self.query(flags, db::meta::Filter::Keyword(keyword)) } + pub fn query_prefix(&self, prefix: &str, flags: package::Flags) -> Vec { + if flags.available || flags == package::Flags::default() { + match self.active.db.package_names(db::meta::Filter::Prefix(prefix)) { + Ok(names) => names, + Err(error) => { + warn!("failed to query repository packages: {error}"); + vec![] + } + } + } else { + vec![] + } + } + /// Query all packages that match the given provider identity pub fn query_provider(&self, provider: &Provider, flags: package::Flags) -> Vec { self.query(flags, db::meta::Filter::Provider(provider.clone())) From 403bf187fd25a36822debf338f1151a9cf36f083 Mon Sep 17 00:00:00 2001 From: Jonathan Lopez Date: Fri, 21 Aug 2026 18:31:35 -0400 Subject: [PATCH 7/7] moss::completions: Query PackageSummary to add help text to completion script --- moss/src/client/mod.rs | 6 +++--- moss/src/completions.rs | 8 +++----- moss/src/db/meta/mod.rs | 19 ++++++++++--------- moss/src/package/meta.rs | 7 +++++++ moss/src/package/mod.rs | 2 +- moss/src/registry/mod.rs | 10 +++++++--- moss/src/registry/plugin/active.rs | 4 ++-- moss/src/registry/plugin/cobble.rs | 7 +++++-- moss/src/registry/plugin/mod.rs | 25 ++++++++++++++++++------- moss/src/registry/plugin/repository.rs | 4 ++-- 10 files changed, 58 insertions(+), 34 deletions(-) diff --git a/moss/src/client/mod.rs b/moss/src/client/mod.rs index 2c7769317..53cdb423f 100644 --- a/moss/src/client/mod.rs +++ b/moss/src/client/mod.rs @@ -337,12 +337,12 @@ impl Client { /// Returns the name of all packages with names starting with the /// provided prefix and match the given flags - pub fn search_packages_by_prefix<'a>( + pub fn search_package_summaries_by_prefix<'a>( &'a self, prefix: &'a str, flags: package::Flags, - ) -> impl Iterator + 'a { - self.registry.by_prefix(prefix, flags) + ) -> impl Iterator + 'a { + self.registry.package_summaries_by_prefix(prefix, flags) } /// Activates the provided state and runs system triggers once applied. diff --git a/moss/src/completions.rs b/moss/src/completions.rs index 7301c5fdf..4c44cf33d 100644 --- a/moss/src/completions.rs +++ b/moss/src/completions.rs @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: 2026 AerynOS Developers // SPDX-License-Identifier: MPL-2.0 +use clap::builder::StyledStr; use clap_complete::CompletionCandidate; -use itertools::Itertools; use std::path::PathBuf; use crate::{Installation, client, package}; @@ -11,11 +11,9 @@ const MAX_RESULTS: usize = 100; pub fn generate_results(client: &client::Client, flags: package::Flags, prefix: &str) -> Vec { client - .search_packages_by_prefix(prefix, flags) - .map(|name| name.to_string()) - .unique() + .search_package_summaries_by_prefix(prefix, flags) .take(MAX_RESULTS) - .map(CompletionCandidate::new) + .map(|pkg| CompletionCandidate::new(pkg.name.to_string()).help(Some(StyledStr::from(pkg.summary)))) .collect() } diff --git a/moss/src/db/meta/mod.rs b/moss/src/db/meta/mod.rs index ae5899a48..34f715896 100644 --- a/moss/src/db/meta/mod.rs +++ b/moss/src/db/meta/mod.rs @@ -77,17 +77,17 @@ impl Database { }) } - pub fn package_names(&self, filter: Filter<'_>) -> Result, Error> { + pub fn package_summaries(&self, filter: Filter<'_>) -> Result, Error> { self.conn.exec(|conn| { let mut stmt; let meta_query = match filter { Filter::Id(id) => { - stmt = conn.prepare("SELECT name FROM meta WHERE package = ?")?; + stmt = conn.prepare("SELECT name, summary FROM meta WHERE package = ?")?; stmt.query([id.as_str()]) } Filter::Provider(provider) => { stmt = conn.prepare(indoc! {" - SELECT m.name + SELECT m.name, m.summary FROM meta m INNER JOIN meta_providers mp ON m.package = mp.package WHERE mp.provider = ?"})?; @@ -95,28 +95,28 @@ impl Database { } Filter::Dependency(dependency) => { stmt = conn.prepare(indoc! {" - SELECT m.name + SELECT m.name, m.summary FROM meta m INNER JOIN meta_dependencies md ON m.package = md.package WHERE md.dependency = ?"})?; stmt.query([dependency.to_string()]) } Filter::Name(name) => { - stmt = conn.prepare("SELECT name FROM meta WHERE name = ?")?; + stmt = conn.prepare("SELECT name, summary FROM meta WHERE name = ?")?; stmt.query([name.to_string()]) } Filter::Keyword(kw) => { stmt = conn.prepare( - "SELECT name FROM meta WHERE name LIKE concat('%', ?1, '%') OR summary LIKE concat('%', ?1, '%')", + "SELECT name, summary FROM meta WHERE name LIKE concat('%', ?1, '%') OR summary LIKE concat('%', ?1, '%')", )?; stmt.query([kw.to_owned()]) } Filter::All => { - stmt = conn.prepare("SELECT name FROM meta")?; + stmt = conn.prepare("SELECT name, summary FROM meta")?; stmt.query([]) } Filter::Prefix(prefix) => { - stmt = conn.prepare("SELECT name FROM meta WHERE name LIKE concat(?1, '%')")?; + stmt = conn.prepare("SELECT name, summary FROM meta WHERE name LIKE concat(?1, '%')")?; stmt.query([prefix.to_owned()]) } }?; @@ -124,7 +124,8 @@ impl Database { let entries = meta_query .mapped(|row| { let name = package::Name::from(row.get::<_, String>("name")?); - Ok( name) + let summary = row.get::<_, String>("summary")?; + Ok( package::PackageSummary {name, summary}) }) .collect::, _>>()?; diff --git a/moss/src/package/meta.rs b/moss/src/package/meta.rs index e5d06a949..1b0a884bf 100644 --- a/moss/src/package/meta.rs +++ b/moss/src/package/meta.rs @@ -33,6 +33,13 @@ impl Name { } } +/// A short package summary +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub struct PackageSummary { + pub name: Name, + pub summary: String, +} + /// The metadata of a [`super::Package`] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Meta { diff --git a/moss/src/package/mod.rs b/moss/src/package/mod.rs index b30c260cf..e4500d661 100644 --- a/moss/src/package/mod.rs +++ b/moss/src/package/mod.rs @@ -7,7 +7,7 @@ use astr::AStr; use derive_more::{Debug, Display, From, Into}; use itertools::Itertools; -pub use self::meta::{Meta, MissingMetaFieldError, Name}; +pub use self::meta::{Meta, MissingMetaFieldError, Name, PackageSummary}; pub mod meta; pub mod render; diff --git a/moss/src/registry/mod.rs b/moss/src/registry/mod.rs index 58eaec837..3bf32c12d 100644 --- a/moss/src/registry/mod.rs +++ b/moss/src/registry/mod.rs @@ -75,9 +75,13 @@ impl Registry { self.query(move |plugin| plugin.query_keyword(keyword, flags)) } - /// Return a sorted stream of `package::Name` for packages whose name starts with `prefix` - pub fn by_prefix<'a>(&'a self, prefix: &'a str, flags: package::Flags) -> impl Iterator + 'a { - self.query(move |plugin| plugin.query_prefix(prefix, flags)) + /// Return a sorted stream of `(id, name)` for packages whose name starts with `prefix` + pub fn package_summaries_by_prefix<'a>( + &'a self, + prefix: &'a str, + flags: package::Flags, + ) -> impl Iterator + 'a { + self.query(move |plugin| plugin.package_summaries_by_prefix(prefix, flags)) } /// Return a sorted stream of [`Package`] matching the given [`Flags`] diff --git a/moss/src/registry/plugin/active.rs b/moss/src/registry/plugin/active.rs index 9efce1798..718c947cb 100644 --- a/moss/src/registry/plugin/active.rs +++ b/moss/src/registry/plugin/active.rs @@ -75,10 +75,10 @@ impl Active { self.query(flags, db::meta::Filter::Keyword(keyword)) } - pub fn query_prefix(&self, prefix: &str, flags: package::Flags) -> Vec { + pub fn package_summaries_by_prefix(&self, prefix: &str, flags: package::Flags) -> Vec { let filter = db::meta::Filter::Prefix(prefix); if flags.installed || flags == package::Flags::default() { - match self.db.package_names(filter) { + match self.db.package_summaries(filter) { Ok(names) => names, Err(error) => { warn!("failed to query repository packages: {error}"); diff --git a/moss/src/registry/plugin/cobble.rs b/moss/src/registry/plugin/cobble.rs index 12d1fe53e..381b163be 100644 --- a/moss/src/registry/plugin/cobble.rs +++ b/moss/src/registry/plugin/cobble.rs @@ -76,12 +76,15 @@ impl Cobble { }) } - pub fn query_prefix(&self, prefix: &str, flags: package::Flags) -> Vec { + pub fn package_summaries_by_prefix(&self, prefix: &str, flags: package::Flags) -> Vec { if flags.available { self.packages .iter() .filter(|(_, state)| state.meta.name.starts_with(prefix)) - .map(|(_, state)| state.meta.name.clone()) + .map(|(_, state)| package::PackageSummary { + name: state.meta.name.clone(), + summary: state.meta.summary.clone(), + }) .collect() } else { vec![] diff --git a/moss/src/registry/plugin/mod.rs b/moss/src/registry/plugin/mod.rs index 68c9fca2a..719d4b1a0 100644 --- a/moss/src/registry/plugin/mod.rs +++ b/moss/src/registry/plugin/mod.rs @@ -72,14 +72,18 @@ impl Plugin { }) } - pub fn query_prefix(&self, prefix: &str, flags: package::Flags) -> package::Sorted> { + pub fn package_summaries_by_prefix( + &self, + prefix: &str, + flags: package::Flags, + ) -> package::Sorted> { package::Sorted::new(match self { - Plugin::Active(plugin) => plugin.query_prefix(prefix, flags), - Plugin::Cobble(plugin) => plugin.query_prefix(prefix, flags), - Plugin::Repository(plugin) => plugin.query_prefix(prefix, flags), + Plugin::Active(plugin) => plugin.package_summaries_by_prefix(prefix, flags), + Plugin::Cobble(plugin) => plugin.package_summaries_by_prefix(prefix, flags), + Plugin::Repository(plugin) => plugin.package_summaries_by_prefix(prefix, flags), #[cfg(any(test, feature = "testing"))] - Plugin::Test(plugin) => plugin.query_prefix(prefix, flags), + Plugin::Test(plugin) => plugin.package_summaries_by_prefix(prefix, flags), }) } @@ -180,12 +184,19 @@ pub mod test { .collect() } - pub fn query_prefix(&self, prefix: &str, _flags: package::Flags) -> Vec { + pub fn package_summaries_by_prefix( + &self, + prefix: &str, + _flags: package::Flags, + ) -> Vec { let prefix_lower = prefix.to_ascii_lowercase(); self.packages .iter() .filter(|pkg| pkg.meta.name.as_str().to_ascii_lowercase().starts_with(&prefix_lower)) - .map(|pkg| pkg.meta.name.clone()) + .map(|pkg| package::PackageSummary { + name: pkg.meta.name.clone(), + summary: pkg.meta.summary.clone(), + }) .collect() } diff --git a/moss/src/registry/plugin/repository.rs b/moss/src/registry/plugin/repository.rs index 287ab7e92..72a19bac7 100644 --- a/moss/src/registry/plugin/repository.rs +++ b/moss/src/registry/plugin/repository.rs @@ -82,9 +82,9 @@ impl Repository { self.query(flags, db::meta::Filter::Keyword(keyword)) } - pub fn query_prefix(&self, prefix: &str, flags: package::Flags) -> Vec { + pub fn package_summaries_by_prefix(&self, prefix: &str, flags: package::Flags) -> Vec { if flags.available || flags == package::Flags::default() { - match self.active.db.package_names(db::meta::Filter::Prefix(prefix)) { + match self.active.db.package_summaries(db::meta::Filter::Prefix(prefix)) { Ok(names) => names, Err(error) => { warn!("failed to query repository packages: {error}");