diff --git a/src/build/mod.rs b/src/build/mod.rs index 0573c43..30d1d54 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -2284,13 +2284,13 @@ Provides: virtual-thing (= 2.0), plain-virtual Some(dist), None, None, - None, + &crate::report::Quiet, )) .unwrap_or_else(|e| panic!("package lookup failed for {}: {}", package, e)); rt.block_on(crate::pull::pull( &package_info, Some(fetch_dir.path()), - None, + &crate::report::Quiet, true, )) .unwrap_or_else(|e| panic!("pull failed for {}: {}", package, e)); diff --git a/src/deb/mod.rs b/src/deb/mod.rs index ccde698..256f242 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -1323,11 +1323,19 @@ mod tests { log::debug!("Created temporary directory: {}", cwd.display()); log::info!("Pulling package {} from {}...", package, series); - let package_info = - crate::package_info::lookup(package, None, Some(series), "", dist, None, None, None) - .await - .expect("Cannot lookup package information"); - crate::pull::pull(&package_info, Some(cwd), None, true) + let package_info = crate::package_info::lookup( + package, + None, + Some(series), + "", + dist, + None, + None, + &crate::report::Quiet, + ) + .await + .expect("Cannot lookup package information"); + crate::pull::pull(&package_info, Some(cwd), &crate::report::Quiet, true) .await .expect("Cannot pull package"); log::info!("Successfully pulled package {}", package); diff --git a/src/lib.rs b/src/lib.rs index 1c8854b..a1d7f4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,13 +63,6 @@ pub(crate) mod test_support; /// Utility functions pub(crate) mod utils; -/// Optional callback function (taking 4 arguments) -/// - Name of the current main operation (e.g. pulling package) -/// - Name of the current nested operation (e.g. cloning git repo) -/// - Progress, position, index of current operation (e.g. amount of data downloaded) -/// - Total amount for current operation (e.g. size of the file to download) -pub type ProgressCallback<'a> = Option<&'a dyn Fn(&str, &str, usize, usize)>; - /// Returns the architecture of current CPU, debian-compatible pub fn get_current_arch() -> String { match std::env::consts::ARCH { diff --git a/src/main.rs b/src/main.rs index a7d60b3..1fb58f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -670,7 +670,7 @@ fn main() { .unwrap_or(""); let archive = sub_matches.get_one::("archive").unwrap_or(&false); - let (pb, progress_callback) = pkh::ui::create_progress_bar(&multi); + let view = pkh::ui::PullView::new(&multi); // Convert PPA to base URL if provided let base_url = match ppa.map(pkh::package_info::split_ppa) { @@ -692,17 +692,17 @@ fn main() { dist, base_url.as_deref(), repository, - Some(&progress_callback), + &view, ) .await?; - pkh::pull::pull(&package_info, None, Some(&progress_callback), *archive).await + pkh::pull::pull(&package_info, None, &view, *archive).await }) { - pb.finish_and_clear(); + // exit(1) below would skip the view's Drop + drop(view); error!("{}", e); std::process::exit(1); } - pb.finish_and_clear(); - multi.remove(&pb); + drop(view); info!("Done."); } Some(("chlog", sub_matches)) => { diff --git a/src/package_info.rs b/src/package_info.rs index cb9b07f..42d766e 100644 --- a/src/package_info.rs +++ b/src/package_info.rs @@ -4,8 +4,8 @@ use std::error::Error; use std::io::Read; use xz2::read::XzDecoder; -use crate::ProgressCallback; use crate::apt::release::{self, VerifiedRelease}; +use crate::report::BuildView; use crossterm::style::Stylize; use log::{debug, warn}; @@ -490,7 +490,7 @@ async fn find_package( version: Option<&str>, base_url: Option<&str>, launchpad_vcs: Option, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result> { let series_list = crate::distro_info::get_ordered_series_name(dist).await?; @@ -500,9 +500,7 @@ async fn find_package( let mut attempt_errors: Vec = Vec::new(); for (i, series) in series_list.iter().enumerate() { - if let Some(cb) = progress { - cb("", &format!("Checking {}...", series), i, series_list.len()); - } + view.progress(&format!("Checking {series}..."), i, series_list.len()); let pockets = if pocket.is_empty() { crate::distro_info::get_dist_pockets(dist)? @@ -770,16 +768,9 @@ pub async fn lookup_repository( version: Option<&str>, repo_url: &str, series: Option<&str>, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result> { - if let Some(cb) = progress { - cb( - &format!("Fetching repository info from {}...", repo_url), - "", - 0, - 0, - ); - } + view.message(&format!("Fetching repository info from {repo_url}...")); // Attempt to authenticate the repository. In non-strict mode only the // absence of a Release file (or of a way to verify it) is tolerated: @@ -814,14 +805,7 @@ pub async fn lookup_repository( get_flat_repo_series(repo_url).await? }; - if let Some(cb) = progress { - cb( - &format!("Resolving package info for {}...", package), - "", - 0, - 0, - ); - } + view.message(&format!("Resolving package info for {package}...")); let sources = get_flat_repo_sources(repo_url, verified.as_ref()).await?; let stanza = parse_sources(&sources, package, version)? @@ -855,7 +839,7 @@ pub async fn lookup_repository( /// * `dist` - Optional distribution name (e.g., "ubuntu", "debian") /// * `base_url` - Optional base URL for the package archive (e.g., "https://ppa.launchpadcontent.net/user/ppa/ubuntu/") /// * `repository` - Optional URL of an external flat repository suite (e.g., "https://pkg.noctalia.dev/deb/resolute/") -/// * `progress` - Optional progress callback +/// * `view` - Build view receiving the lookup progress #[allow(clippy::too_many_arguments)] pub async fn lookup( package: &str, @@ -865,12 +849,12 @@ pub async fn lookup( dist: Option<&str>, base_url: Option<&str>, repository: Option<&str>, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result> { // External flat repository: the URL is the suite root, distribution // resolution and pocket logic do not apply if let Some(repo_url) = repository { - return lookup_repository(package, version, repo_url, series, progress).await; + return lookup_repository(package, version, repo_url, series, view).await; } // Resolve the distribution early so we can check Launchpad once @@ -887,9 +871,7 @@ pub async fn lookup( // Check Launchpad repository once for Ubuntu packages, rather than // once per series/pocket combination inside get(). let launchpad_vcs = if resolved_dist == "ubuntu" && base_url.is_none() { - if let Some(cb) = progress { - cb("", &format!("Checking Launchpad for {}...", package), 0, 0); - } + view.message(&format!("Checking Launchpad for {package}...")); match check_launchpad_repo(package).await { Ok(Some(lp_url)) => { debug!("Found Launchpad URL: {}", lp_url); @@ -910,34 +892,20 @@ pub async fn lookup( // Obtain the package information, either directly in a series or with a search in all series let package_info = if let Some(s) = series { - if let Some(cb) = progress { - cb( - &format!("Resolving package info for {}...", package), - "", - 0, - 0, - ); - } + view.message(&format!("Resolving package info for {package}...")); // Get the package information from that series and pocket get(package, s, pocket, version, base_url, launchpad_vcs).await? } else { - if let Some(cb) = progress { - cb( - &format!( - "Searching for package {} in {}...", - package, - if base_url.is_none() { - resolved_dist - } else { - "ppa" - } - ), - "", - 0, - 0, - ); - } + view.message(&format!( + "Searching for package {} in {}...", + package, + if base_url.is_none() { + resolved_dist + } else { + "ppa" + } + )); // Try to find the package in all series from that dist find_package( @@ -947,7 +915,7 @@ pub async fn lookup( version, base_url, launchpad_vcs, - progress, + view, ) .await? }; @@ -1142,9 +1110,17 @@ Directory: pool/main/h/hello #[tokio::test] async fn test_find_package_fallback() { // python2.7 is in bullseye but not above - let info = find_package("python2.7", "debian", "", None, None, None, None) - .await - .unwrap(); + let info = find_package( + "python2.7", + "debian", + "", + None, + None, + None, + &crate::report::Quiet, + ) + .await + .unwrap(); assert_eq!(info.stanza.package, "python2.7"); assert_eq!(info.series, "bullseye") } @@ -1152,9 +1128,17 @@ Directory: pool/main/h/hello #[tokio::test] async fn test_find_package_devel() { // hello is in sid - let info = find_package("hello", "debian", "", None, None, None, None) - .await - .unwrap(); + let info = find_package( + "hello", + "debian", + "", + None, + None, + None, + &crate::report::Quiet, + ) + .await + .unwrap(); assert_eq!(info.stanza.package, "hello"); assert_eq!(info.series, "sid") } diff --git a/src/pull.rs b/src/pull.rs index 6044349..019a348 100644 --- a/src/pull.rs +++ b/src/pull.rs @@ -13,14 +13,14 @@ use log::debug; use regex::Regex; -use crate::ProgressCallback; +use crate::report::BuildView; fn clone_repo( url: &str, package: &str, branch: Option<&str>, cwd: Option<&Path>, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result<(), Box> { let target_path = if let Some(path) = cwd { path.join(package) @@ -29,11 +29,11 @@ fn clone_repo( }; let mut callbacks = git2::RemoteCallbacks::new(); - if let Some(ref progress_cb) = progress { + // The sideband parser is only worth running when something listens + if view.is_enabled() { // Download progress callbacks.transfer_progress(move |stats| { - (progress_cb)( - "", + view.progress( "Receiving objects...", stats.received_objects(), stats.total_objects(), @@ -42,7 +42,7 @@ fn clone_repo( }); // Remote progress: messages 'Remote: compressing objects 10% (34/340)' - // Parse progress informations to display them in callbacks + // Parse progress informations to display them in the view callbacks.sideband_progress(move |data| { let msg = String::from_utf8_lossy(data); let re = Regex::new(r"(.*):[ ]*([0-9]*)% \(([0-9]*)/([0-9]*)\)").unwrap(); @@ -61,7 +61,7 @@ fn clone_repo( .parse::() .unwrap_or(0); - (progress_cb)("", msg.as_str(), objects, total); + view.progress(msg.as_str(), objects, total); } true @@ -232,7 +232,7 @@ struct ExtractedArchive { fn extract_tar_archive( file_path: &Path, dest: &Path, - progress: ProgressCallback<'_>, + view: &dyn BuildView, decoder_factory: F, ) -> Result> where @@ -373,9 +373,7 @@ where current_entry += 1; // Report progress - if let Some(cb) = progress { - cb("", "Extracting...", current_entry, total_entries); - } + view.progress("Extracting...", current_entry, total_entries); } let wrapper = if multiple_roots { @@ -389,16 +387,16 @@ where fn extract_archive( path: &Path, dest: &Path, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result> { let filename = path.file_name().unwrap().to_string_lossy(); if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") { - extract_tar_archive(path, dest, progress, GzDecoder::new) + extract_tar_archive(path, dest, view, GzDecoder::new) } else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") { - extract_tar_archive(path, dest, progress, XzDecoder::new) + extract_tar_archive(path, dest, view, XzDecoder::new) } else if filename.ends_with(".tar.bz2") || filename.ends_with(".tbz2") { - extract_tar_archive(path, dest, progress, bzip2_rs::DecoderReader::new) + extract_tar_archive(path, dest, view, bzip2_rs::DecoderReader::new) } else { Err(format!("Unsupported archive format: {}", filename).into()) } @@ -439,7 +437,7 @@ async fn download_file_checksum( checksum: &str, algo: crate::package_info::ChecksumAlgo, target_dir: &Path, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result<(), Box> { // Archive mirrors and CDNs are busy enough that single attempts fail // spuriously (dropped connections, truncated bodies, index generations @@ -448,7 +446,7 @@ async fn download_file_checksum( const ATTEMPTS: u32 = 3; let mut last_error: Box = String::new().into(); for attempt in 1..=ATTEMPTS { - match download_file_checksum_once(url, checksum, algo, target_dir, progress).await { + match download_file_checksum_once(url, checksum, algo, target_dir, view).await { Ok(()) => return Ok(()), Err(e) => { log::warn!("download of '{url}' failed (attempt {attempt}/{ATTEMPTS}): {e}"); @@ -468,7 +466,7 @@ async fn download_file_checksum_once( checksum: &str, algo: crate::package_info::ChecksumAlgo, target_dir: &Path, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result<(), Box> { // Download with the shared client (connect timeout). Large orig tarballs // can legitimately take longer than the client's default total timeout, @@ -504,9 +502,9 @@ async fn download_file_checksum_once( file.write_all(&chunk)?; buffer.extend_from_slice(&chunk); - if let (Some(cb), Some(total)) = (progress, total_size) { + if let Some(total) = total_size { let index = min(buffer.len(), total as usize); - cb("", "Downloading...", index, total as usize); + view.progress("Downloading...", index, total as usize); } } @@ -575,7 +573,7 @@ fn setup_pristine_tar_branch(package_dir: &Path, dist: &str) -> Result<(), Box, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result<(), Box> { let package_dir = if let Some(path) = cwd { path.join(&info.stanza.package) @@ -630,7 +628,7 @@ async fn fetch_orig_tarball( &orig_file.checksum, orig_file.checksum_algo, target_dir, - progress, + view, ) .await?; } @@ -642,7 +640,7 @@ async fn fetch_orig_tarball( async fn fetch_dsc_file( info: &PackageInfo, cwd: Option<&Path>, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result<(), Box> { let target_dir = cwd.unwrap_or_else(|| Path::new(".")); @@ -662,7 +660,7 @@ async fn fetch_dsc_file( &dsc_file.checksum, dsc_file.checksum_algo, target_dir, - progress, + view, ) .await?; @@ -672,7 +670,7 @@ async fn fetch_dsc_file( async fn fetch_archive_sources( info: &PackageInfo, cwd: Option<&Path>, - progress: ProgressCallback<'_>, + view: &dyn BuildView, ) -> Result<(), Box> { let package_dir = if let Some(path) = cwd { path @@ -702,14 +700,7 @@ async fn fetch_archive_sources( for file in &files { let url = format!("{}/{}", info.archive_url, file.name); - download_file_checksum( - &url, - &file.checksum, - file.checksum_algo, - package_dir, - progress, - ) - .await?; + download_file_checksum(&url, &file.checksum, file.checksum_algo, package_dir, view).await?; // Extract all tar archives. Every tar of the source package matches // (orig, debian tar, native tar, extra components); a tarball in an @@ -725,7 +716,7 @@ async fn fetch_archive_sources( let path = package_dir.join(&file.name); let extract_dir = package_dir.join(&info.stanza.package); - let extracted = extract_archive(&path, &extract_dir, progress)?; + let extracted = extract_archive(&path, &extract_dir, view)?; // Special case: the debian tar does only contain 'debian' if file.name.contains("debian.tar.") { @@ -807,7 +798,7 @@ async fn fetch_archive_sources( pub async fn pull( package_info: &PackageInfo, cwd: Option<&Path>, - progress: ProgressCallback<'_>, + view: &dyn BuildView, force_archive: bool, ) -> Result<(), Box> { let package = &package_info.stanza.package; @@ -861,50 +852,37 @@ pub async fn pull( None }; - if let Some(cb) = progress { - cb( - &format!( - "Cloning {}{}...", - url, - if let Some(b) = &branch_name { - format!(" (branch {})", b) - } else { - String::new() - } - ), - "", - 0, - 0, - ); - } + view.message(&format!( + "Cloning {}{}...", + url, + if let Some(b) = &branch_name { + format!(" (branch {})", b) + } else { + String::new() + } + )); clone_repo( url.as_str(), package, branch_name.as_deref(), Some(&package_dir), - progress, + view, )?; if !package_info.is_native() { - if let Some(cb) = progress { - cb("Fetching orig tarball...", "", 0, 0); - } - fetch_orig_tarball(package_info, Some(&package_dir), progress).await?; + view.message("Fetching orig tarball..."); + fetch_orig_tarball(package_info, Some(&package_dir), view).await?; } else { debug!("Native package, skipping orig tarball fetch."); } - if let Some(cb) = progress { - cb("Fetching dsc file...", "", 0, 0); - } - fetch_dsc_file(package_info, Some(&package_dir), progress).await?; + view.message("Fetching dsc file..."); + fetch_dsc_file(package_info, Some(&package_dir), view).await?; } else { // Fallback to archive fetching - if let Some(cb) = progress { - cb("Downloading from archive...", "", 0, 0); - } - fetch_archive_sources(package_info, Some(&package_dir), progress).await?; + view.message("Downloading from archive..."); + fetch_archive_sources(package_info, Some(&package_dir), view).await?; } Ok(()) @@ -931,12 +909,26 @@ mod tests { let cwd = temp_dir.path(); // Main 'pull' command: the one we want to test - let info = crate::package_info::lookup(package, None, series, "", dist, None, None, None) - .await - .unwrap(); - pull(&info, Some(cwd), None, archive.unwrap_or(false)) - .await - .unwrap(); + let info = crate::package_info::lookup( + package, + None, + series, + "", + dist, + None, + None, + &crate::report::Quiet, + ) + .await + .unwrap(); + pull( + &info, + Some(cwd), + &crate::report::Quiet, + archive.unwrap_or(false), + ) + .await + .unwrap(); let package_dir = cwd.join(package); assert!(package_dir.exists()); @@ -1136,7 +1128,7 @@ mod tests { .unwrap(); let dest = temp_dir.path().join("dest"); - let result = extract_tar_archive(&tar_path, &dest, None, |f| f); + let result = extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f); let err = match result { Ok(_) => panic!("extraction of a traversal archive should fail"), @@ -1169,7 +1161,7 @@ mod tests { .unwrap(); let dest = temp_dir.path().join("dest"); - let result = extract_tar_archive(&tar_path, &dest, None, |f| f); + let result = extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f); let err = match result { Ok(_) => panic!("extraction of an absolute-path archive should fail"), @@ -1202,7 +1194,7 @@ mod tests { .unwrap(); let dest = temp_dir.path().join("dest"); - extract_tar_archive(&tar_path, &dest, None, |f| f).unwrap(); + extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f).unwrap(); assert_eq!( std::fs::read_to_string(dest.join("hello-1.0/file.txt")).unwrap(), @@ -1235,7 +1227,7 @@ mod tests { std::fs::write(&tar_path, tar_bytes).unwrap(); let dest = temp_dir.path().join("dest"); - extract_tar_archive(&tar_path, &dest, None, |f| f).unwrap(); + extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f).unwrap(); assert_eq!( std::fs::read_to_string(dest.join("pkg-1.0/docs/v1/manual.txt")).unwrap(), @@ -1276,7 +1268,8 @@ mod tests { std::fs::write(&tar_path, tar_bytes).unwrap(); let tree = temp_dir.path().join("tree"); - let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap(); + let extracted = + extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap(); unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap(); assert!(!tree.join("pax_global_header").exists()); @@ -1297,7 +1290,7 @@ mod tests { std::fs::write(&tar_path, tar_bytes).unwrap(); let dest = temp_dir.path().join("dest"); - let result = extract_tar_archive(&tar_path, &dest, None, |f| f); + let result = extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f); let err = match result { Ok(_) => panic!("extraction of an escaping hard link should fail"), @@ -1332,7 +1325,8 @@ mod tests { .unwrap(); let tree = temp_dir.path().join("tree"); - let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap(); + let extracted = + extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap(); unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap(); assert_eq!( @@ -1366,7 +1360,8 @@ mod tests { std::fs::write(&tar_path, tar_bytes).unwrap(); let tree = temp_dir.path().join("tree"); - let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap(); + let extracted = + extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap(); unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap(); let meta = std::fs::symlink_metadata(tree.join("README")).unwrap(); @@ -1392,7 +1387,8 @@ mod tests { .unwrap(); let tree = temp_dir.path().join("tree"); - let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap(); + let extracted = + extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap(); assert!(extracted.wrapper.is_none()); assert!(tree.join("Makefile").exists()); @@ -1413,7 +1409,7 @@ mod tests { let main = temp_dir.path().join("main.orig.tar"); std::fs::write(&main, build_tar(&[("dovecot-2.4.5/README", b"main")])).unwrap(); - let extracted = extract_tar_archive(&main, &tree, None, |f| f).unwrap(); + let extracted = extract_tar_archive(&main, &tree, &crate::report::Quiet, |f| f).unwrap(); unwrap_into( &tree, &extracted.wrapper.unwrap(), @@ -1430,7 +1426,8 @@ mod tests { ]), ) .unwrap(); - let extracted = extract_tar_archive(&component, &tree, None, |f| f).unwrap(); + let extracted = + extract_tar_archive(&component, &tree, &crate::report::Quiet, |f| f).unwrap(); unwrap_into( &tree, &extracted.wrapper.unwrap(), @@ -1472,7 +1469,8 @@ mod tests { .unwrap(); let tree = temp_dir.path().join("tree"); - let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap(); + let extracted = + extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap(); unwrap_into( &tree, &extracted.wrapper.unwrap(), @@ -1512,8 +1510,13 @@ mod tests { std::fs::write(&tar_path, BZ2_TAR_WRAPPED).unwrap(); let tree = temp_dir.path().join("tree"); - let extracted = - extract_tar_archive(&tar_path, &tree, None, bzip2_rs::DecoderReader::new).unwrap(); + let extracted = extract_tar_archive( + &tar_path, + &tree, + &crate::report::Quiet, + bzip2_rs::DecoderReader::new, + ) + .unwrap(); unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap(); assert_eq!( diff --git a/src/report.rs b/src/report.rs index e545129..a3b9b12 100644 --- a/src/report.rs +++ b/src/report.rs @@ -68,10 +68,12 @@ pub struct BuildTarget<'a> { /// Observer of a running build: target identification, phases, status /// messages, progress and the final outcome. /// -/// Implement this to observe the [`crate::build`] and [`crate::deb`] flows -/// from any frontend. All events arrive in order from the build thread; -/// long-lived views are expected to be `Send + Sync` because builds may run -/// inside async tasks. +/// Implement this to observe the [`crate::build`], [`crate::deb`] and +/// [`crate::pull`] flows from any frontend. All events arrive in order from +/// the build thread; long-lived views are expected to be `Send + Sync` +/// because builds may run inside async tasks. The pull flow only emits the +/// [`BuildView::message`], [`BuildView::progress`] and +/// [`BuildView::progress`] events — no target, phases or outcome. pub trait BuildView: Send + Sync { /// The build target was identified; the events that follow belong to it /// (including the earliest subprocess output, e.g. a chroot download). diff --git a/src/ui.rs b/src/ui.rs index b1689ef..d9c5d77 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -51,34 +51,56 @@ pub fn wait_spinner(msg: &str) -> ProgressBar { pb } -/// Create a spinner-style progress bar attached to `multi`, returning the bar -/// and a callback compatible with [`crate::ProgressCallback`] -pub fn create_progress_bar( - multi: &MultiProgress, -) -> (ProgressBar, impl Fn(&str, &str, usize, usize) + '_) { - let pb = multi.add(ProgressBar::new(0)); - pb.enable_steady_tick(Duration::from_millis(50)); - pb.set_style(spinner_style()); +/// Terminal build view for the pull flow: one bar line attached to the +/// shared [`MultiProgress`], spinner-style while the work is unsized, +/// transfer-style once a byte or item total is known. The bar is created +/// eagerly (an empty spinner, so the line exists from the first moment) +/// and released on drop. +pub struct PullView { + multi: MultiProgress, + pb: ProgressBar, +} - let pb_clone = pb.clone(); - let callback = move |prefix: &str, msg: &str, progress: usize, total: usize| { - let pb = &pb_clone; - if progress != 0 && total != 0 { - pb.set_style(transfer_style()); +impl PullView { + /// Attach a pull view to `multi` + pub fn new(multi: &MultiProgress) -> Self { + let pb = multi.add(ProgressBar::new(0)); + pb.enable_steady_tick(Duration::from_millis(50)); + pb.set_style(spinner_style()); + Self { + multi: multi.clone(), + pb, + } + } +} + +impl Drop for PullView { + fn drop(&mut self) { + self.pb.finish_and_clear(); + self.multi.remove(&self.pb); + } +} + +impl crate::report::BuildView for PullView { + fn message(&self, text: &str) { + self.pb.set_style(spinner_style()); + self.pb.set_message(text.to_string()); + } + + fn progress(&self, label: &str, pos: usize, total: usize) { + if pos != 0 && total != 0 { + self.pb.set_style(transfer_style()); } else { - pb.set_style(spinner_style()); + self.pb.set_style(spinner_style()); } + self.pb.set_message(label.to_string()); + self.pb.set_length(total as u64); + self.pb.set_position(pos as u64); + } - if !prefix.is_empty() { - pb.set_prefix(prefix.to_string()); - } - - pb.set_message(msg.to_string()); - pb.set_length(total as u64); - pb.set_position(progress as u64); - }; - - (pb, callback) + fn is_enabled(&self) -> bool { + true + } } /// Label of the interactive series selector, rendered by [`prompt::select`]