cli: add progress indicators to various commands

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I49d0c21e12e5f424bad105b32845798d6a6a6964
This commit is contained in:
raf 2026-05-03 03:04:04 +03:00
commit 1873bb19ae
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 142 additions and 18 deletions

View file

@ -1,5 +1,7 @@
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, time::Duration};
use indicatif::{ProgressBar, ProgressStyle};
use crate::{ use crate::{
error::{MultiError, PakkerError, Result}, error::{MultiError, PakkerError, Result},
http, http,
@ -275,7 +277,16 @@ pub async fn execute(
let mut errors = MultiError::new(); let mut errors = MultiError::new();
// Resolve each input // 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 { for input in &args.inputs {
spinner.set_message(format!("Resolving project: {input}"));
let project = match resolve_input(input, &platforms, &lockfile).await { let project = match resolve_input(input, &platforms, &lockfile).await {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
@ -328,10 +339,21 @@ pub async fn execute(
new_projects.push(project); new_projects.push(project);
} }
spinner.finish_and_clear();
// Resolve dependencies unless --no-deps is specified // Resolve dependencies unless --no-deps is specified
if !args.no_deps { if !args.no_deps {
log::info!("Resolving dependencies..."); 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 resolver = DependencyResolver::new();
let mut all_new_projects = new_projects.clone(); 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) && !all_new_projects.iter().any(|p| p.pakku_id == dep.pakku_id)
{ {
// Prompt user for confirmation unless --yes flag is set // Prompt user for confirmation unless --yes flag is set
if !skip_prompts { let should_add = if !skip_prompts {
let prompt_msg = format!( dep_spinner.suspend(|| -> bool {
"Add dependency '{}' required by '{}'?", let prompt_msg = format!(
dep.get_name(), "Add dependency '{}' required by '{}'?",
project.get_name() dep.get_name(),
); project.get_name()
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true, skip_prompts)? );
{ crate::ui_utils::prompt_yes_no(&prompt_msg, true, skip_prompts)
log::info!("Skipping dependency: {}", dep.get_name()); .unwrap_or_else(|e| {
continue; 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()); 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; new_projects = all_new_projects;
} }

View file

@ -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::{ use crate::{
error::{PakkerError, Result}, error::{PakkerError, Result},
@ -113,6 +116,15 @@ pub async fn execute(
let platform = create_platform("curseforge", cf_api_key)?; 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 let mut project = platform
.request_project_with_files(&input, mc_versions, &loaders) .request_project_with_files(&input, mc_versions, &loaders)
.await .await
@ -122,6 +134,8 @@ pub async fn execute(
)) ))
})?; })?;
spinner.finish_and_clear();
// If file_id specified, filter to that file // If file_id specified, filter to that file
if let Some(fid) = file_id { if let Some(fid) = file_id {
project.files.retain(|f| f.id == fid); project.files.retain(|f| f.id == fid);
@ -133,6 +147,7 @@ pub async fn execute(
} }
projects_to_merge.push(project); projects_to_merge.push(project);
spinner.finish_and_clear();
} }
// Modrinth // Modrinth
@ -142,6 +157,15 @@ pub async fn execute(
let platform = create_platform("modrinth", None)?; 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 let mut project = platform
.request_project_with_files(&input, mc_versions, &loaders) .request_project_with_files(&input, mc_versions, &loaders)
.await .await
@ -159,6 +183,7 @@ pub async fn execute(
} }
} }
spinner.finish_and_clear();
projects_to_merge.push(project); projects_to_merge.push(project);
} }
@ -170,6 +195,15 @@ pub async fn execute(
let gh_token = std::env::var("GITHUB_TOKEN").ok(); let gh_token = std::env::var("GITHUB_TOKEN").ok();
let platform = create_platform("github", gh_token)?; 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 repo_path = format!("{owner}/{repo}");
let mut project = platform let mut project = platform
.request_project_with_files(&repo_path, mc_versions, &loaders) .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); projects_to_merge.push(project);
} }
@ -259,7 +294,10 @@ pub async fn execute(
log::info!("Replacing existing project: {existing_name}"); log::info!("Replacing existing project: {existing_name}");
lockfile.projects[pos] = combined_project.clone(); lockfile.projects[pos] = combined_project.clone();
println!("✓ Replaced '{existing_name}' with '{project_name}'"); println!(
"{}",
format!("✓ Replaced '{existing_name}' with '{project_name}'").green()
);
} else { } else {
if !yes { if !yes {
let prompt_msg = format!("Add project '{project_name}'?"); let prompt_msg = format!("Add project '{project_name}'?");
@ -270,13 +308,22 @@ pub async fn execute(
} }
lockfile.add_project(combined_project.clone()); lockfile.add_project(combined_project.clone());
println!("✓ Added '{project_name}'"); println!("{}", format!("✓ Added '{project_name}'").green());
} }
// Resolve dependencies unless --no-deps is specified // Resolve dependencies unless --no-deps is specified
if !no_deps { if !no_deps {
log::info!("Resolving dependencies..."); 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 platforms = create_all_platforms();
let mut resolver = DependencyResolver::new(); let mut resolver = DependencyResolver::new();
@ -284,6 +331,8 @@ pub async fn execute(
.resolve(&mut combined_project, &mut lockfile, &platforms) .resolve(&mut combined_project, &mut lockfile, &platforms)
.await?; .await?;
dep_spinner.finish_and_clear();
for dep in deps { for dep in deps {
// Skip if already in lockfile // Skip if already in lockfile
if lockfile.projects.iter().any(|p| { if lockfile.projects.iter().any(|p| {
@ -310,7 +359,7 @@ pub async fn execute(
log::info!("Adding dependency: {dep_name}"); log::info!("Adding dependency: {dep_name}");
lockfile.add_project(dep); lockfile.add_project(dep);
println!(" ✓ Added dependency '{dep_name}'"); println!("{}", format!(" ✓ Added dependency '{dep_name}'").green());
} }
} }

View file

@ -1,5 +1,7 @@
use std::{collections::HashMap, path::Path}; use std::{collections::HashMap, path::Path};
use indicatif::{ProgressBar, ProgressStyle};
use crate::{ use crate::{
cli::ImportArgs, cli::ImportArgs,
error::{PakkerError, Result}, error::{PakkerError, Result},
@ -117,7 +119,15 @@ async fn import_modrinth(
// Import projects from files list // Import projects from files list
if let Some(files) = index["files"].as_array() { 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 // Create platform client
let creds = crate::model::credentials::ResolvedCredentials::load(); 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.as_str())
.and_then(|url| url.split('/').rev().nth(1)) .and_then(|url| url.split('/').rev().nth(1))
{ {
pb.set_message(format!("Importing: {project_id}"));
log::info!("Fetching project: {project_id}"); log::info!("Fetching project: {project_id}");
match platform match platform
.request_project_with_files( .request_project_with_files(
@ -162,8 +173,13 @@ async fn import_modrinth(
log::warn!("Failed to fetch project {project_id}: {e}"); log::warn!("Failed to fetch project {project_id}: {e}");
}, },
} }
pb.inc(1);
} }
} }
pb.finish_with_message(format!(
"Imported {} projects",
lockfile.projects.len()
));
} }
// Create config // Create config
@ -283,7 +299,15 @@ async fn import_curseforge(
// Import projects from files list // Import projects from files list
if let Some(files) = manifest["files"].as_array() { 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 // Create platform client
let curseforge_token = std::env::var("CURSEFORGE_TOKEN").ok(); let curseforge_token = std::env::var("CURSEFORGE_TOKEN").ok();
@ -292,6 +316,7 @@ async fn import_curseforge(
for file_entry in files { for file_entry in files {
if let Some(project_id) = file_entry["projectID"].as_u64() { if let Some(project_id) = file_entry["projectID"].as_u64() {
let project_id_str = project_id.to_string(); let project_id_str = project_id.to_string();
pb.set_message(format!("Importing: {project_id_str}"));
log::info!("Fetching project: {project_id_str}"); log::info!("Fetching project: {project_id_str}");
match platform match platform
@ -351,8 +376,13 @@ async fn import_curseforge(
log::warn!("Failed to fetch project {project_id_str}: {e}"); 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 // Create config

View file

@ -1,8 +1,11 @@
use std::{ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Duration,
}; };
use indicatif::{ProgressBar, ProgressStyle};
use crate::{ use crate::{
cli::RemoteArgs, cli::RemoteArgs,
error::{PakkerError, Result}, error::{PakkerError, Result},
@ -48,8 +51,18 @@ pub async fn execute(args: RemoteArgs) -> Result<()> {
git::reset_to_ref(&remote_path, remote_name, ref_name)?; git::reset_to_ref(&remote_path, remote_name, ref_name)?;
} else { } else {
log::info!("Cloning repository..."); 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"); 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 // Load lockfile and config from remote