Skip to content

Commit 9329d4d

Browse files
committed
--timings and prettier progress
1 parent 1b83e56 commit 9329d4d

File tree

3 files changed

+92
-35
lines changed

3 files changed

+92
-35
lines changed

cli/src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ pub struct ExtractArgs {
142142
#[arg(short = 'j', long = "jobs")]
143143
pub jobs: Option<usize>,
144144

145+
/// Show timing breakdown
146+
#[arg(long)]
147+
pub timings: bool,
148+
145149
/// Specific files to extract (extracts all if none specified)
146150
pub files: Vec<PathBuf>,
147151
}

cli/src/commands/extract.rs

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
use std::collections::HashMap;
2+
use std::io::IsTerminal;
3+
use std::sync::{Arc, Mutex};
4+
15
use box_format::{BoxFileReader, BoxPath, ExtractOptions, ExtractProgress, ExtractStats};
2-
use indicatif::ProgressBar;
6+
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
37

48
use crate::cli::ExtractArgs;
59
use crate::error::{Error, Result};
6-
use crate::util::create_progress_bar;
710

811
pub async fn run(args: ExtractArgs) -> Result<()> {
912
let bf = BoxFileReader::open(&args.archive)
@@ -26,11 +29,7 @@ pub async fn run(args: ExtractArgs) -> Result<()> {
2629
})?;
2730

2831
let total_files = bf.metadata().iter().count() as u64;
29-
let progress: Option<ProgressBar> = if args.quiet {
30-
None
31-
} else {
32-
Some(create_progress_bar(total_files, "Extracting"))
33-
};
32+
let show_progress = !args.quiet && std::io::stdout().is_terminal();
3433

3534
let options = ExtractOptions {
3635
verify_checksums: !args.no_checksum,
@@ -39,50 +38,91 @@ pub async fn run(args: ExtractArgs) -> Result<()> {
3938
let stats: ExtractStats = if args.files.is_empty() {
4039
// Extract all files
4140
if args.serial {
42-
// Sequential extraction
41+
// Sequential extraction (simple progress bar)
42+
let progress = if !show_progress {
43+
None
44+
} else {
45+
let style = ProgressStyle::default_bar()
46+
.template(" {bar:40.cyan/blue} {pos:>7}/{len:7} files")
47+
.unwrap()
48+
.progress_chars("━━─");
49+
let pb = ProgressBar::new(total_files);
50+
pb.set_style(style);
51+
Some(pb)
52+
};
53+
4354
let stats = bf
4455
.extract_all_with_options(&output_path, options)
4556
.await
4657
.map_err(|source| Error::Extract { source })?;
4758

4859
if let Some(pb) = progress {
49-
pb.finish_with_message("Done");
60+
pb.finish_and_clear();
5061
}
5162
stats
5263
} else {
53-
// Parallel extraction (default)
64+
// Parallel extraction (default) with multi-progress display
5465
let concurrency = args.jobs.unwrap_or_else(num_cpus::get);
5566

5667
// Set up progress channel
5768
let (progress_tx, mut progress_rx) =
5869
tokio::sync::mpsc::unbounded_channel::<ExtractProgress>();
5970

71+
// Set up multi-progress display
72+
let multi = MultiProgress::new();
73+
let main_style = ProgressStyle::default_bar()
74+
.template(" {bar:40.cyan/blue} {pos:>7}/{len:7} files")
75+
.unwrap()
76+
.progress_chars("━━─");
77+
let main_bar = multi.add(ProgressBar::new(total_files));
78+
main_bar.set_style(main_style);
79+
80+
let file_style = ProgressStyle::default_spinner()
81+
.template(" {spinner:.green} {msg}")
82+
.unwrap();
83+
84+
// Track active file spinners
85+
let active_bars: Arc<Mutex<HashMap<String, ProgressBar>>> =
86+
Arc::new(Mutex::new(HashMap::new()));
87+
6088
// Spawn progress handler task
61-
let progress_clone = progress.clone();
62-
let progress_task = tokio::spawn(async move {
63-
while let Some(update) = progress_rx.recv().await {
64-
if let Some(ref pb) = progress_clone {
89+
let progress_task = if !show_progress {
90+
tokio::spawn(async move { while progress_rx.recv().await.is_some() {} })
91+
} else {
92+
let active_bars = active_bars.clone();
93+
let multi = multi.clone();
94+
let file_style = file_style.clone();
95+
let main_bar = main_bar.clone();
96+
97+
tokio::spawn(async move {
98+
while let Some(update) = progress_rx.recv().await {
6599
match update {
66100
ExtractProgress::Started { total_files, .. } => {
67-
pb.set_length(total_files);
68-
pb.set_message("Starting...");
101+
main_bar.set_length(total_files);
69102
}
70103
ExtractProgress::Extracting { ref path } => {
71-
pb.set_message(format!("Extracting: {}", path));
104+
let pb = multi.add(ProgressBar::new_spinner());
105+
pb.set_style(file_style.clone());
106+
pb.set_message(path.to_string());
107+
pb.enable_steady_tick(std::time::Duration::from_millis(80));
108+
active_bars.lock().unwrap().insert(path.to_string(), pb);
72109
}
73-
ExtractProgress::Extracted {
74-
files_extracted, ..
75-
} => {
76-
pb.set_position(files_extracted);
110+
ExtractProgress::Extracted { ref path, .. } => {
111+
let path_str = path.to_string();
112+
if let Some(pb) = active_bars.lock().unwrap().remove(&path_str) {
113+
pb.finish_and_clear();
114+
multi.remove(&pb);
115+
}
116+
main_bar.inc(1);
77117
}
78118
ExtractProgress::Finished => {
79-
pb.finish_with_message("Done");
119+
main_bar.finish_and_clear();
80120
}
81121
_ => {}
82122
}
83123
}
84-
}
85-
});
124+
})
125+
};
86126

87127
let stats = bf
88128
.extract_all_parallel_with_progress(
@@ -101,6 +141,18 @@ pub async fn run(args: ExtractArgs) -> Result<()> {
101141
}
102142
} else {
103143
// Extract specific files (always sequential for now)
144+
let progress = if !show_progress {
145+
None
146+
} else {
147+
let style = ProgressStyle::default_bar()
148+
.template(" {bar:40.cyan/blue} {pos:>7}/{len:7} files")
149+
.unwrap()
150+
.progress_chars("━━─");
151+
let pb = ProgressBar::new(args.files.len() as u64);
152+
pb.set_style(style);
153+
Some(pb)
154+
};
155+
104156
let mut total_stats = ExtractStats::default();
105157
for file_path in &args.files {
106158
let box_path = BoxPath::new(file_path).map_err(|source| Error::InvalidPath {
@@ -121,7 +173,7 @@ pub async fn run(args: ExtractArgs) -> Result<()> {
121173
}
122174

123175
if let Some(pb) = progress {
124-
pb.finish_with_message("Done");
176+
pb.finish_and_clear();
125177
}
126178
total_stats
127179
};
@@ -140,15 +192,16 @@ pub async fn run(args: ExtractArgs) -> Result<()> {
140192
);
141193
}
142194

143-
// Print timing breakdown
144-
let timing = &stats.timing;
145-
let total = timing.collect + timing.directories + timing.decompress + timing.symlinks;
146-
println!("\nTiming:");
147-
println!(" Collect: {:>8.2?}", timing.collect);
148-
println!(" Directories: {:>8.2?}", timing.directories);
149-
println!(" Decompress: {:>8.2?}", timing.decompress);
150-
println!(" Symlinks: {:>8.2?}", timing.symlinks);
151-
println!(" Total: {:>8.2?}", total);
195+
if args.timings {
196+
let timing = &stats.timing;
197+
let total = timing.collect + timing.directories + timing.decompress + timing.symlinks;
198+
println!("\nTiming:");
199+
println!(" Collect: {:>8.2?}", timing.collect);
200+
println!(" Directories: {:>8.2?}", timing.directories);
201+
println!(" Decompress: {:>8.2?}", timing.decompress);
202+
println!(" Symlinks: {:>8.2?}", timing.symlinks);
203+
println!(" Total: {:>8.2?}", total);
204+
}
152205
}
153206

154207
if stats.checksum_failures > 0 {

src/file/reader.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use std::io::SeekFrom;
22
use std::num::NonZeroU64;
33
use std::ops::AddAssign;
44
use std::path::{Path, PathBuf};
5-
use std::time::{Duration, Instant};
65
use std::sync::Arc;
6+
use std::time::{Duration, Instant};
77

88
use mmap_io::MemoryMappedFile;
99
use mmap_io::segment::Segment;

0 commit comments

Comments
 (0)