diff --git a/Cargo.lock b/Cargo.lock index 2af9385..6c0c463 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,12 +51,48 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "bitcode" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1bce7608560cd4bf0296a4262d0dbf13e6bcec5ff2105724c8ab88cc7fc784" +dependencies = [ + "arrayvec", + "bitcode_derive", + "bytemuck", + "glam", + "serde", +] + +[[package]] +name = "bitcode_derive" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a539389a13af092cd345a2b47ae7dec12deb306d660b2223d25cd3419b253ebe" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bytemuck" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" + [[package]] name = "cc" version = "1.0.79" @@ -115,6 +151,7 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" name = "comma" version = "1.8.0" dependencies = [ + "bitcode", "clap", "xdg", ] @@ -140,6 +177,12 @@ dependencies = [ "libc", ] +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + [[package]] name = "heck" version = "0.4.1" @@ -221,6 +264,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde" +version = "1.0.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" + [[package]] name = "strsim" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index 2fec648..9e657a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ license = "MIT" [dependencies] clap = { version = "4.3.12", features = ["derive", "cargo", "env"] } xdg = "2.5.0" +bitcode = "0.6.3" diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..cdd6699 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,59 @@ +use std::{collections::HashMap, error::Error, fs, path::PathBuf}; + +use bitcode::{Decode, Encode}; + +#[derive(Encode, Decode)] +struct CacheData(HashMap); + +pub struct Cache { + path: PathBuf, + data: CacheData, + update: bool, +} + +impl Cache { + pub fn new() -> Result> { + let path = xdg::BaseDirectories::new()?.place_state_file("comma-choices")?; + + Ok(Self { + data: if path.exists() { + let bytes = fs::read(&path)?; + bitcode::decode(&bytes)? + } else { + CacheData(HashMap::new()) + }, + path, + update: false, + }) + } + + pub fn query(&self, command: &str) -> Option { + self.data.0.get(command).cloned() + } + + pub fn update(&mut self, command: &str, derivation: &str) { + self.data.0.insert(command.into(), derivation.into()); + self.update = true; + } + + pub fn delete(&mut self, command: &str) { + self.data.0.remove(command); + self.update = true; + } + + pub fn empty(&mut self) { + self.data.0.clear(); + self.update = true; + } +} + +impl Drop for Cache { + fn drop(&mut self) { + if self.update { + let bytes = bitcode::encode(&self.data.0); + if let Err(e) = fs::write(&self.path, bytes) { + eprintln!("failed to write cache: {e}"); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index f7ee4c5..7e27277 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ +mod cache; mod index; mod shell; + use std::{ env, io::Write, @@ -7,6 +9,7 @@ use std::{ process::{self, Command, ExitCode, Stdio}, }; +use cache::Cache; use clap::crate_version; use clap::Parser; @@ -36,6 +39,43 @@ fn pick(picker: &str, derivations: &[&str]) -> Option { ) } +fn index_database(command: &str, picker: &str) -> Option { + index::check_database_updated(); + + let nix_locate_output = Command::new("nix-locate") + .args(["--top-level", "--minimal", "--at-root", "--whole-name"]) + .arg(format!("/bin/{command}")) + .output() + .expect("failed to execute nix-locate"); + + if !nix_locate_output.status.success() { + match std::str::from_utf8(&nix_locate_output.stderr) { + Ok(stderr) => eprintln!("nix-locate failed with: {stderr}"), + Err(_) => eprintln!("nix-locate failed"), + } + return None; + } + + let attrs = nix_locate_output.stdout; + + if attrs.is_empty() { + eprintln!("No executable `{command}` found in nix-index database."); + return None; + } + + let attrs = std::str::from_utf8(&attrs) + .expect("fail") + .trim() + .split('\n') + .collect::>(); + + if attrs.len() > 1 { + pick(picker, &attrs) + } else { + attrs.first().map(|s| s.trim().to_owned()) + } +} + fn run_command_or_open_shell( use_channel: bool, choice: &str, @@ -70,57 +110,57 @@ fn run_command_or_open_shell( fn main() -> ExitCode { let args = Opt::parse(); + let mut cache = Cache::new(); + if let Err(ref e) = cache { + eprintln!("failed to initialize cache, disabling related functionality: {e}"); + } + if args.update { eprintln!("\"comma --update\" has been deprecated. either obtain a prebuilt database from https://github.com/Mic92/nix-index-database or use \"nix run 'nixpkgs#nix-index' --extra-experimental-features 'nix-command flakes'\""); index::update_database(); } + if args.empty_cache { + if let Ok(ref mut cache) = cache { + cache.empty(); + } + } + // The command may not be given if `--update` was specified. if args.cmd.is_empty() { - return ExitCode::FAILURE; + return if args.update || args.empty_cache { + ExitCode::SUCCESS + } else { + ExitCode::FAILURE + }; } let command = &args.cmd[0]; let trail = &args.cmd[1..]; - index::check_database_updated(); - - let nix_locate_output = Command::new("nix-locate") - .args(["--top-level", "--minimal", "--at-root", "--whole-name"]) - .arg(format!("/bin/{command}")) - .output() - .expect("failed to execute nix-locate"); - - if !nix_locate_output.status.success() { - match std::str::from_utf8(&nix_locate_output.stderr) { - Ok(stderr) => eprintln!("nix-locate failed with: {stderr}"), - Err(_) => eprint!("nix-locate failed"), + if args.delete_entry { + if let Ok(ref mut cache) = cache { + cache.delete(command); } - return ExitCode::FAILURE; } - let attrs = nix_locate_output.stdout; - - if attrs.is_empty() { - eprintln!("No executable `{command}` found in nix-index database."); - return ExitCode::FAILURE; - } - - let attrs: Vec<_> = std::str::from_utf8(&attrs) - .expect("fail") - .trim() - .split('\n') - .collect(); + let derivation = match cache { + Ok(mut cache) => cache.query(command).or_else(|| { + index_database(command, &args.picker).map(|derivation| { + cache.update(command, &derivation); + derivation + }) + }), + Err(_) => index_database(command, &args.picker), + }; - let choice = if attrs.len() > 1 { - match pick(&args.picker, &attrs) { - Some(x) => x, - None => return ExitCode::FAILURE, - } - } else { - attrs.first().unwrap().trim().to_owned() + let derivation = match derivation { + Some(d) => d, + None => return ExitCode::FAILURE, }; + let basename = derivation.rsplit('.').last().unwrap(); + let use_channel = match env::var("NIX_PATH") { Ok(val) => val, Err(_) => String::new(), @@ -130,22 +170,27 @@ fn main() -> ExitCode { if args.print_package { println!( "Package that contains executable /bin/{}: {}", - command, - &choice.rsplit('.').last().unwrap() + command, basename ); }; if args.install { Command::new("nix-env") - .args(["-f", "", "-iA", choice.rsplit('.').last().unwrap()]) + .args(["-f", "", "-iA", basename]) .exec(); } else if args.shell { let shell_cmd = shell::select_shell_from_pid(process::id()).unwrap_or("bash".into()); - run_command_or_open_shell(use_channel, &choice, &shell_cmd, &[], &args.nixpkgs_flake); + run_command_or_open_shell( + use_channel, + &derivation, + &shell_cmd, + &[], + &args.nixpkgs_flake, + ); } else if args.print_path { run_command_or_open_shell( use_channel, - &choice, + &derivation, "sh", &[ String::from("-c"), @@ -154,7 +199,13 @@ fn main() -> ExitCode { &args.nixpkgs_flake, ); } else { - run_command_or_open_shell(use_channel, &choice, command, trail, &args.nixpkgs_flake); + run_command_or_open_shell( + use_channel, + &derivation, + command, + trail, + &args.nixpkgs_flake, + ); } ExitCode::SUCCESS @@ -195,7 +246,16 @@ struct Opt { #[clap(short = 'x', long = "print-path")] print_path: bool, + /// Empty the cache + #[clap(short, long = "empty-cache")] + empty_cache: bool, + + /// Overwrite the cache entry for the specified command. This is achieved by first deleting it + /// from the cache, then running comma as normal. + #[clap(short, long = "delete-entry")] + delete_entry: bool, + /// Command to run - #[clap(required_unless_present = "update", name = "cmd")] + #[clap(required_unless_present_any = ["update", "empty_cache"], name = "cmd")] cmd: Vec, }