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 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;
}

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::{
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());
}
}

View file

@ -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

View file

@ -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