Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions moss/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 40 additions & 0 deletions moss/benches/completions.rs
Original file line number Diff line number Diff line change
@@ -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);
4 changes: 4 additions & 0 deletions moss/src/cli/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<String>,

/// Simulate the operation (dry-run)
Expand Down
31 changes: 4 additions & 27 deletions moss/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
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;
use moss::{Installation, installation};
Expand Down Expand Up @@ -94,14 +91,6 @@
.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())
Expand Down Expand Up @@ -147,16 +136,11 @@
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);

Expand All @@ -178,13 +162,6 @@
return Ok(());
}

if let Some(dir) = matches.get_one::<String>("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()
Expand Down Expand Up @@ -295,7 +272,7 @@
Boot(#[source] boot::Error),

#[error("cache")]
Cache(#[source] cache::Error),

Check failure on line 275 in moss/src/cli/mod.rs

View workflow job for this annotation

GitHub Actions / Build & Test Project

[clippy] reported by reviewdog 🐶 error: the `Err`-variant returned from this function is very large --> moss/src/cli/mod.rs:140:21 | 140 | pub fn process() -> Result<(), Error> { | ^^^^^^^^^^^^^^^^^ ... 275 | Cache(#[source] cache::Error), | ----------------------------- the largest variant contains at least 128 bytes | = help: try reducing the size of `cli::Error`, for example by boxing large elements or replacing it with `Box<cli::Error>` = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#result_large_err = note: `-D clippy::result-large-err` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::result_large_err)]` Raw Output: moss/src/cli/mod.rs:275:5:e:error: the `Err`-variant returned from this function is very large --> moss/src/cli/mod.rs:140:21 | 140 | pub fn process() -> Result<(), Error> { | ^^^^^^^^^^^^^^^^^ ... 275 | Cache(#[source] cache::Error), | ----------------------------- the largest variant contains at least 128 bytes | = help: try reducing the size of `cli::Error`, for example by boxing large elements or replacing it with `Box<cli::Error>` = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#result_large_err = note: `-D clippy::result-large-err` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::result_large_err)]` __END__

#[error("index")]
Index(#[source] index::Error),
Expand Down
5 changes: 5 additions & 0 deletions moss/src/cli/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String>,

/// Simulate the operation (dry-run)
Expand Down
10 changes: 10 additions & 0 deletions moss/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_package_summaries_by_prefix<'a>(
&'a self,
prefix: &'a str,
flags: package::Flags,
) -> impl Iterator<Item = package::PackageSummary> + 'a {
self.registry.package_summaries_by_prefix(prefix, flags)
}

/// Activates the provided state and runs system triggers once applied.
///
/// The current state gets archived.\
Expand Down
39 changes: 39 additions & 0 deletions moss/src/completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2026 AerynOS Developers
// SPDX-License-Identifier: MPL-2.0

use clap::builder::StyledStr;
use clap_complete::CompletionCandidate;
use std::path::PathBuf;

use crate::{Installation, client, package};

const MAX_RESULTS: usize = 100;

pub fn generate_results(client: &client::Client, flags: package::Flags, prefix: &str) -> Vec<CompletionCandidate> {
client
.search_package_summaries_by_prefix(prefix, flags)
.take(MAX_RESULTS)
.map(|pkg| CompletionCandidate::new(pkg.name.to_string()).help(Some(StyledStr::from(pkg.summary))))
.collect()
}

fn default_client() -> Result<client::Client, client::Error> {
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<CompletionCandidate> {
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)
}
}
61 changes: 61 additions & 0 deletions moss/src/db/meta/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub enum Filter<'a> {
Dependency(Dependency),
Name(package::Name),
Keyword(&'a str),
Prefix(&'a str),
All,
}

Expand Down Expand Up @@ -76,6 +77,62 @@ impl Database {
})
}

pub fn package_summaries(&self, filter: Filter<'_>) -> Result<Vec<package::PackageSummary>, Error> {
self.conn.exec(|conn| {
let mut stmt;
let meta_query = match filter {
Filter::Id(id) => {
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, m.summary
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, 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, summary FROM meta WHERE name = ?")?;
stmt.query([name.to_string()])
}
Filter::Keyword(kw) => {
stmt = conn.prepare(
"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, summary FROM meta")?;
stmt.query([])
}
Filter::Prefix(prefix) => {
stmt = conn.prepare("SELECT name, summary 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")?);
let summary = row.get::<_, String>("summary")?;
Ok( package::PackageSummary {name, summary})
})
.collect::<Result<Vec<_>, _>>()?;

Ok(entries)
})
}

pub fn query(&self, filter: Filter<'_>) -> Result<Vec<(package::Id, Meta)>, Error> {
self.conn.exec(|conn| {
let mut stmt;
Expand Down Expand Up @@ -114,6 +171,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<package::Id, Meta> = meta_base_query
Expand Down
1 change: 1 addition & 0 deletions moss/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions moss/src/package/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ 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)
}
}

/// 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`]
Expand Down
2 changes: 1 addition & 1 deletion moss/src/package/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions moss/src/registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ impl Registry {
self.query(move |plugin| plugin.query_keyword(keyword, 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<Item = package::PackageSummary> + 'a {
self.query(move |plugin| plugin.package_summaries_by_prefix(prefix, flags))
}

/// Return a sorted stream of [`Package`] matching the given [`Flags`]
///
/// [`Flags`]: package::Flags
Expand Down
Loading
Loading