diff --git a/Cargo.lock b/Cargo.lock index 556573d..558d78d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,7 @@ dependencies = [ "dirs", "futures", "http-body-util", + "indicatif", "itertools", "octocrab", "reqwest", @@ -1074,6 +1075,19 @@ dependencies = [ "serde", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "inout" version = "0.1.3" @@ -1083,6 +1097,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1310,6 +1333,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.32.2" @@ -1502,6 +1531,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "powerfmt" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index c30b1e5..27742a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ dialoguer = "0.11" dirs = "5.0" futures = "0.3" http-body-util = "0.1" +indicatif = "0.17" itertools = "0.12" octocrab = "0.36" reqwest = { version = "0.12", default-features = false, features = [ diff --git a/src/cli/add.rs b/src/cli/add.rs index ded01b3..e0df186 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -7,10 +7,10 @@ use aftman::{ storage::Home, tool::{ToolAlias, ToolId}, }; -use tokio::time::Instant; use crate::util::{ - discover_aftman_manifest_dir, github_tool_source, prompt_for_install_trust, ToolIdOrSpec, + discover_aftman_manifest_dir, github_tool_source, new_progress_bar, prompt_for_install_trust, + ToolIdOrSpec, }; /// Adds a new tool to Aftman and installs it. @@ -33,22 +33,12 @@ pub struct AddSubcommand { impl AddSubcommand { pub async fn run(&self, home: &Home) -> Result<()> { - let start = Instant::now(); - let id: ToolId = self.tool.clone().into(); let alias: ToolAlias = match self.alias.as_ref() { Some(alias) => alias.clone(), None => self.tool.clone().into(), }; - let manifest_path = if self.global { - home.path().to_path_buf() - } else { - discover_aftman_manifest_dir().await? - }; - - let source = github_tool_source(home).await?; - // Check for trust, or prompt the user to trust the tool let trust_cache = home.trust_cache(); if !trust_cache.is_trusted(&id) { @@ -58,7 +48,15 @@ impl AddSubcommand { trust_cache.add_tool(id.clone()); } - // Load manifest and do a preflight check to make sure we don't overwrite any tool + // Load tool source, manifest, and do a preflight check + // to make sure we don't overwrite any existing tool(s) + let source = github_tool_source(home).await?; + let manifest_path = if self.global { + home.path().to_path_buf() + } else { + discover_aftman_manifest_dir().await? + }; + let mut manifest = if self.global { AftmanManifest::load_or_create(&manifest_path).await? } else { @@ -75,14 +73,18 @@ impl AddSubcommand { // If we only got an id without a specified version, we // will fetch the latest non-prerelease release and use that + let pb = new_progress_bar("Fetching", 5); let spec = match self.tool.clone() { - ToolIdOrSpec::Spec(spec) => spec, + ToolIdOrSpec::Spec(spec) => { + pb.inc(1); + spec + } ToolIdOrSpec::Id(id) => { - tracing::info!("Looking for the latest version of {id}..."); let version = source .find_latest_version(&id, false) .await? .with_context(|| format!("Failed to find latest release for {id}"))?; + pb.inc(1); id.into_spec(version) } }; @@ -90,40 +92,48 @@ impl AddSubcommand { // Add the tool spec to the desired manifest file and save it manifest.add_tool(&alias, &spec); manifest.save(manifest_path).await?; - tracing::info!("Added tool successfully: {spec}"); // Install the tool and create the link for its alias let description = Description::current(); let install_cache = home.install_cache(); let tool_storage = home.tool_storage(); - if !install_cache.is_installed(&spec) && !self.force { - tracing::info!("Downloading {spec}"); + if !install_cache.is_installed(&spec) || self.force { + pb.set_message("Downloading"); let release = source .find_release(&spec) .await? .with_context(|| format!("Failed to find release for {spec}"))?; + pb.inc(1); let artifact = source .find_compatible_artifacts(&spec, &release, &description) .first() .cloned() .with_context(|| format!("No compatible artifact found for {spec}"))?; + pb.inc(1); let contents = source .download_artifact_contents(&artifact) .await .with_context(|| format!("Failed to download contents for {spec}"))?; + pb.inc(1); - tracing::info!("Installing {spec}"); + pb.set_message("Installing"); let extracted = artifact .extract_contents(contents) .await .with_context(|| format!("Failed to extract contents for {spec}"))?; tool_storage.replace_tool_contents(&spec, extracted).await?; + pb.inc(1); install_cache.add_spec(spec.clone()); + } else { + pb.inc(4); } + pb.set_message("Linking"); tool_storage.create_tool_link(&alias).await?; - tracing::info!("Completed in {:.2?}", start.elapsed()); + pb.finish_and_clear(); + + tracing::info!("Added tool successfully: {spec}"); Ok(()) } diff --git a/src/cli/install.rs b/src/cli/install.rs index 4e794a3..7089d21 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -7,7 +7,9 @@ use aftman::{description::Description, manifests::AftmanManifest, storage::Home} use futures::{stream::FuturesUnordered, TryStreamExt}; use tokio::time::Instant; -use crate::util::{discover_aftman_manifest_dirs, github_tool_source, prompt_for_install_trust}; +use crate::util::{ + discover_aftman_manifest_dirs, github_tool_source, new_progress_bar, prompt_for_install_trust, +}; /// Adds a new tool to Aftman and installs it. #[derive(Debug, Parser)] @@ -82,17 +84,17 @@ impl InstallSubcommand { let install_cache = home.install_cache(); let tool_storage = home.tool_storage(); + let pb = new_progress_bar("Installing", tool_specs.len()); let artifacts = tool_specs .into_iter() .map(|tool_spec| async { if install_cache.is_installed(&tool_spec) && !force { - tracing::info!("Skipping already installed {tool_spec}"); + pb.inc(1); // HACK: Force the async closure to take ownership // of tool_spec by returning it from the closure return anyhow::Ok(tool_spec); } - tracing::info!("Downloading {tool_spec}"); let release = source .find_release(&tool_spec) .await? @@ -107,7 +109,6 @@ impl InstallSubcommand { .await .with_context(|| format!("Failed to download contents for {tool_spec}"))?; - tracing::info!("Installing {tool_spec}"); let extracted = artifact .extract_contents(contents) .await @@ -117,11 +118,14 @@ impl InstallSubcommand { .await?; install_cache.add_spec(tool_spec.clone()); + pb.inc(1); + Ok(tool_spec) }) .collect::>() .try_collect::>() .await?; + pb.finish_and_clear(); // 4. Link all of the (possibly new) aliases, we do this even if the // tool is already installed in case the link(s) have been corrupted diff --git a/src/util/mod.rs b/src/util/mod.rs index 9a78e14..202c2dc 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,5 +1,6 @@ mod discovery; mod id_or_spec; +mod progress; mod prompts; mod sources; mod tracing; @@ -8,6 +9,7 @@ pub use self::discovery::{ discover_aftman_manifest_dir, discover_aftman_manifest_dirs, discover_closest_tool_spec, }; pub use self::id_or_spec::ToolIdOrSpec; +pub use self::progress::new_progress_bar; pub use self::prompts::prompt_for_install_trust; pub use self::sources::github_tool_source; pub use self::tracing::init as init_tracing; diff --git a/src/util/progress.rs b/src/util/progress.rs new file mode 100644 index 0000000..06562a4 --- /dev/null +++ b/src/util/progress.rs @@ -0,0 +1,18 @@ +use std::time::Duration; + +use indicatif::{ProgressBar, ProgressStyle}; + +const PROGRESS_BAR_TEMPLATE: &str = "{msg} [{bar:32.cyan/blue}] {pos} / {len}"; +const PROGRESS_BAR_CHARACTERS: &str = "▪▸-"; + +pub fn new_progress_bar(message: impl Into, length: usize) -> ProgressBar { + let pb = ProgressBar::new(length as u64) + .with_message(message.into()) + .with_style( + ProgressStyle::with_template(PROGRESS_BAR_TEMPLATE) + .unwrap() + .progress_chars(PROGRESS_BAR_CHARACTERS), + ); + pb.enable_steady_tick(Duration::from_millis(10)); + pb +}