diff --git a/crates/pakker-cli/src/cli/commands/add.rs b/crates/pakker-cli/src/cli/commands/add.rs index 25690f1..84567e5 100644 --- a/crates/pakker-cli/src/cli/commands/add.rs +++ b/crates/pakker-cli/src/cli/commands/add.rs @@ -1,5 +1,7 @@ use std::{collections::HashMap, time::Duration}; +use indicatif::{ProgressBar, ProgressStyle}; + use crate::{ error::{MultiError, PakkerError, Result}, http, @@ -275,7 +277,16 @@ pub async fn execute( let mut errors = MultiError::new(); // Resolve each input + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .expect("spinner template is valid"), + ); + spinner.enable_steady_tick(Duration::from_millis(80)); + for input in &args.inputs { + spinner.set_message(format!("Resolving project: {input}")); let project = match resolve_input(input, &platforms, &lockfile).await { Ok(p) => p, Err(e) => { @@ -328,10 +339,21 @@ pub async fn execute( new_projects.push(project); } + spinner.finish_and_clear(); + // Resolve dependencies unless --no-deps is specified if !args.no_deps { log::info!("Resolving dependencies..."); + let dep_spinner = ProgressBar::new_spinner(); + dep_spinner.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .expect("spinner template is valid"), + ); + dep_spinner.enable_steady_tick(Duration::from_millis(80)); + dep_spinner.set_message("Resolving dependencies..."); + let mut resolver = DependencyResolver::new(); let mut all_new_projects = new_projects.clone(); @@ -343,17 +365,26 @@ pub async fn execute( && !all_new_projects.iter().any(|p| p.pakku_id == dep.pakku_id) { // Prompt user for confirmation unless --yes flag is set - if !skip_prompts { - let prompt_msg = format!( - "Add dependency '{}' required by '{}'?", - dep.get_name(), - project.get_name() - ); - if !crate::ui_utils::prompt_yes_no(&prompt_msg, true, skip_prompts)? - { - log::info!("Skipping dependency: {}", dep.get_name()); - continue; - } + let should_add = if !skip_prompts { + dep_spinner.suspend(|| -> bool { + let prompt_msg = format!( + "Add dependency '{}' required by '{}'?", + dep.get_name(), + project.get_name() + ); + crate::ui_utils::prompt_yes_no(&prompt_msg, true, skip_prompts) + .unwrap_or_else(|e| { + log::warn!("Prompt failed, skipping dependency: {e}"); + false + }) + }) + } else { + true + }; + + if !should_add { + log::info!("Skipping dependency: {}", dep.get_name()); + continue; } log::info!("Adding dependency: {}", dep.get_name()); @@ -362,6 +393,7 @@ pub async fn execute( } } + dep_spinner.finish_and_clear(); new_projects = all_new_projects; } diff --git a/crates/pakker-cli/src/cli/commands/add_prj.rs b/crates/pakker-cli/src/cli/commands/add_prj.rs index 3db9e32..2db1c80 100644 --- a/crates/pakker-cli/src/cli/commands/add_prj.rs +++ b/crates/pakker-cli/src/cli/commands/add_prj.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, path::Path}; +use std::{collections::HashMap, path::Path, time::Duration}; + +use indicatif::{ProgressBar, ProgressStyle}; +use yansi::Paint; use crate::{ error::{PakkerError, Result}, @@ -113,6 +116,15 @@ pub async fn execute( let platform = create_platform("curseforge", cf_api_key)?; + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .expect("spinner template is valid"), + ); + spinner.enable_steady_tick(Duration::from_millis(80)); + spinner.set_message("Fetching from CurseForge..."); + let mut project = platform .request_project_with_files(&input, mc_versions, &loaders) .await @@ -122,6 +134,8 @@ pub async fn execute( )) })?; + spinner.finish_and_clear(); + // If file_id specified, filter to that file if let Some(fid) = file_id { project.files.retain(|f| f.id == fid); @@ -133,6 +147,7 @@ pub async fn execute( } projects_to_merge.push(project); + spinner.finish_and_clear(); } // Modrinth @@ -142,6 +157,15 @@ pub async fn execute( let platform = create_platform("modrinth", None)?; + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .expect("spinner template is valid"), + ); + spinner.enable_steady_tick(Duration::from_millis(80)); + spinner.set_message("Fetching from Modrinth..."); + let mut project = platform .request_project_with_files(&input, mc_versions, &loaders) .await @@ -159,6 +183,7 @@ pub async fn execute( } } + spinner.finish_and_clear(); projects_to_merge.push(project); } @@ -170,6 +195,15 @@ pub async fn execute( let gh_token = std::env::var("GITHUB_TOKEN").ok(); let platform = create_platform("github", gh_token)?; + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .expect("spinner template is valid"), + ); + spinner.enable_steady_tick(Duration::from_millis(80)); + spinner.set_message("Fetching from GitHub..."); + let repo_path = format!("{owner}/{repo}"); let mut project = platform .request_project_with_files(&repo_path, mc_versions, &loaders) @@ -190,6 +224,7 @@ pub async fn execute( } } + spinner.finish_and_clear(); projects_to_merge.push(project); } @@ -259,7 +294,10 @@ pub async fn execute( log::info!("Replacing existing project: {existing_name}"); lockfile.projects[pos] = combined_project.clone(); - println!("✓ Replaced '{existing_name}' with '{project_name}'"); + println!( + "{}", + format!("✓ Replaced '{existing_name}' with '{project_name}'").green() + ); } else { if !yes { let prompt_msg = format!("Add project '{project_name}'?"); @@ -270,13 +308,22 @@ pub async fn execute( } lockfile.add_project(combined_project.clone()); - println!("✓ Added '{project_name}'"); + println!("{}", format!("✓ Added '{project_name}'").green()); } // Resolve dependencies unless --no-deps is specified if !no_deps { log::info!("Resolving dependencies..."); + let dep_spinner = ProgressBar::new_spinner(); + dep_spinner.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .expect("spinner template is valid"), + ); + dep_spinner.enable_steady_tick(Duration::from_millis(80)); + dep_spinner.set_message("Resolving dependencies..."); + let platforms = create_all_platforms(); let mut resolver = DependencyResolver::new(); @@ -284,6 +331,8 @@ pub async fn execute( .resolve(&mut combined_project, &mut lockfile, &platforms) .await?; + dep_spinner.finish_and_clear(); + for dep in deps { // Skip if already in lockfile if lockfile.projects.iter().any(|p| { @@ -310,7 +359,7 @@ pub async fn execute( log::info!("Adding dependency: {dep_name}"); lockfile.add_project(dep); - println!(" ✓ Added dependency '{dep_name}'"); + println!("{}", format!(" ✓ Added dependency '{dep_name}'").green()); } } diff --git a/crates/pakker-cli/src/cli/commands/import.rs b/crates/pakker-cli/src/cli/commands/import.rs index 3f8eec6..becc8ba 100644 --- a/crates/pakker-cli/src/cli/commands/import.rs +++ b/crates/pakker-cli/src/cli/commands/import.rs @@ -1,5 +1,7 @@ use std::{collections::HashMap, path::Path}; +use indicatif::{ProgressBar, ProgressStyle}; + use crate::{ cli::ImportArgs, error::{PakkerError, Result}, @@ -117,7 +119,15 @@ async fn import_modrinth( // Import projects from files list if let Some(files) = index["files"].as_array() { - log::info!("Importing {} projects from modpack", files.len()); + let file_count = files.len() as u64; + log::info!("Importing {} projects from modpack", file_count); + + let pb = ProgressBar::new(file_count); + pb.set_style( + ProgressStyle::default_bar() + .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}") + .expect("progress bar template is valid"), + ); // Create platform client let creds = crate::model::credentials::ResolvedCredentials::load(); @@ -133,6 +143,7 @@ async fn import_modrinth( .and_then(|url| url.as_str()) .and_then(|url| url.split('/').rev().nth(1)) { + pb.set_message(format!("Importing: {project_id}")); log::info!("Fetching project: {project_id}"); match platform .request_project_with_files( @@ -162,8 +173,13 @@ async fn import_modrinth( log::warn!("Failed to fetch project {project_id}: {e}"); }, } + pb.inc(1); } } + pb.finish_with_message(format!( + "Imported {} projects", + lockfile.projects.len() + )); } // Create config @@ -283,7 +299,15 @@ async fn import_curseforge( // Import projects from files list if let Some(files) = manifest["files"].as_array() { - log::info!("Importing {} projects from modpack", files.len()); + let file_count = files.len() as u64; + log::info!("Importing {} projects from modpack", file_count); + + let pb = ProgressBar::new(file_count); + pb.set_style( + ProgressStyle::default_bar() + .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}") + .expect("progress bar template is valid"), + ); // Create platform client let curseforge_token = std::env::var("CURSEFORGE_TOKEN").ok(); @@ -292,6 +316,7 @@ async fn import_curseforge( for file_entry in files { if let Some(project_id) = file_entry["projectID"].as_u64() { let project_id_str = project_id.to_string(); + pb.set_message(format!("Importing: {project_id_str}")); log::info!("Fetching project: {project_id_str}"); match platform @@ -351,8 +376,13 @@ async fn import_curseforge( log::warn!("Failed to fetch project {project_id_str}: {e}"); }, } + pb.inc(1); } } + pb.finish_with_message(format!( + "Imported {} projects", + lockfile.projects.len() + )); } // Create config diff --git a/crates/pakker-cli/src/cli/commands/remote.rs b/crates/pakker-cli/src/cli/commands/remote.rs index d1a225a..76a07a9 100644 --- a/crates/pakker-cli/src/cli/commands/remote.rs +++ b/crates/pakker-cli/src/cli/commands/remote.rs @@ -1,8 +1,11 @@ use std::{ fs, path::{Path, PathBuf}, + time::Duration, }; +use indicatif::{ProgressBar, ProgressStyle}; + use crate::{ cli::RemoteArgs, error::{PakkerError, Result}, @@ -48,8 +51,18 @@ pub async fn execute(args: RemoteArgs) -> Result<()> { git::reset_to_ref(&remote_path, remote_name, ref_name)?; } else { log::info!("Cloning repository..."); + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .expect("spinner template is valid"), + ); + spinner.enable_steady_tick(Duration::from_millis(80)); + spinner.set_message(format!("Cloning repository: {url}")); let ref_name = args.branch.as_deref().unwrap_or("HEAD"); - git::clone_repository(&url, &remote_path, ref_name, None)?; + let result = git::clone_repository(&url, &remote_path, ref_name, None); + spinner.finish_and_clear(); + result?; } // Load lockfile and config from remote