From 5b324c46b4bc2897968d41aa5f33e5ebd55a9af5 Mon Sep 17 00:00:00 2001 From: Christian Stefanescu Date: Wed, 14 Jan 2026 23:32:21 +0100 Subject: [PATCH] feat: Progress bar Also dropped custom friendly_bytes impl for indicatif's. Fixes #4 --- Cargo.lock | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/main.rs | 82 ++++++++++++++--------------- 3 files changed, 185 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15002b7..383fe7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,7 +38,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,7 +49,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -133,12 +133,31 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "fnv" version = "1.0.7" @@ -151,6 +170,19 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + [[package]] name = "infer" version = "0.19.0" @@ -185,6 +217,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.21.3" @@ -197,6 +241,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + [[package]] name = "proc-macro2" version = "1.0.105" @@ -242,6 +292,7 @@ version = "0.1.2" dependencies = [ "anyhow", "clap", + "indicatif", "infer", "itertools", "walkdir", @@ -264,6 +315,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "utf8parse" version = "0.2.2" @@ -335,13 +392,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -350,6 +417,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -358,3 +434,67 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 6920849..c228f1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ description = "summarize a directory by file type frequency" [dependencies] anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } +indicatif = "0.17" infer = "0.19" itertools = "0.14.0" walkdir = "2.5.0" diff --git a/src/main.rs b/src/main.rs index ef64aec..c45fbc8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use walkdir::WalkDir; use anyhow::{Context, Result}; use clap::Parser; +use indicatif::{HumanBytes, ProgressBar, ProgressStyle}; use itertools::Itertools; #[derive(Clone, Default, clap::ValueEnum)] @@ -29,6 +30,9 @@ struct Cli { #[arg(short, long, default_value_t = false)] mime: bool, + + #[arg(short, long, default_value_t = true)] + progress_bar: bool, } #[derive(Debug, Default, Clone)] @@ -62,7 +66,7 @@ impl Report { fn display_text(&self, data: &BTreeMap) { let num_files: i32 = data.values().sum(); - let size = friendly_bytes(self.size); + let size = HumanBytes(self.size); let error_info = if self.errors.is_empty() { String::new() } else { @@ -153,6 +157,7 @@ fn process_entry(entry: &walkdir::DirEntry, report: &mut Report) -> Result<()> { let mimetype = detect_mimetype(entry.path()) .with_context(|| format!("failed to detect mimetype for {:?}", entry.path()))?; + report .mimetypes .entry(mimetype) @@ -162,19 +167,39 @@ fn process_entry(entry: &walkdir::DirEntry, report: &mut Report) -> Result<()> { Ok(()) } -fn scan(target: PathBuf) -> Report { +fn scan(target: PathBuf, progress_bar: bool) -> Report { let mut report = Report::default(); + let pb = if progress_bar { + let progress = ProgressBar::new_spinner(); + progress.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} [{elapsed}] {wide_msg}") + .expect("failed to set progress style"), + ); + progress.set_message("Scanning target..."); + Some(progress) + } else { + None + }; + for entry in WalkDir::new(target).into_iter().skip(1) { match entry { Ok(entry) => { if entry.path().is_dir() { report.folders.push(entry.path().to_path_buf()); - } else if let Err(e) = process_entry(&entry, &mut report) { - report.errors.push(ScanError { - path: entry.path().to_path_buf(), - message: e.to_string(), - }); + } else { + if let Some(ref progress) = pb { + progress.set_message(format!("Processing: {}", entry.path().display())); + progress.tick(); + } + + if let Err(e) = process_entry(&entry, &mut report) { + report.errors.push(ScanError { + path: entry.path().to_path_buf(), + message: e.to_string(), + }); + } } } Err(e) => { @@ -187,6 +212,10 @@ fn scan(target: PathBuf) -> Report { } } + if let Some(progress) = pb { + progress.finish_with_message(format!("Completed with {} errors", report.errors.len())); + } + report } @@ -199,37 +228,17 @@ fn main() { ); std::process::exit(1); } - let report = scan(cli.target); + let report = scan(cli.target, cli.progress_bar); report.display(&cli.output, cli.mime); } -fn friendly_bytes(bytes: u64) -> String { - if bytes > 1024 { - let kb = bytes / 1024; - if kb > 1024 { - let mb = bytes / 1024 / 1024; - if mb > 1024 { - let gb = bytes / 1024 / 1024 / 1024; - if gb > 1024 { - let tb = bytes / 1024 / 1024 / 1024 / 1024; - return format!("{tb} TiB"); - } - return format!("{gb} GiB"); - } - return format!("{mb} MiB"); - } - return format!("{kb} KiB"); - } - format!("{bytes} bytes") -} - #[cfg(test)] mod tests { use super::*; #[test] fn test_with_testdata_folder() { - let report = scan("testdata".into()); + let report = scan("testdata".into(), false); let num_files: i32 = report.extensions.values().sum(); assert_eq!(num_files, 27); assert_eq!(report.folders.len(), 5); @@ -241,15 +250,6 @@ mod tests { assert_eq!(report.extensions.get("docx"), Some(&1)); } - #[test] - fn test_friendly_bytes() { - assert_eq!(friendly_bytes(123), "123 bytes".to_string()); - assert_eq!(friendly_bytes(1234), "1 KiB".to_string()); - assert_eq!(friendly_bytes(1234567), "1 MiB".to_string()); - assert_eq!(friendly_bytes(1234567890), "1 GiB".to_string()); - assert_eq!(friendly_bytes(1234567890123), "1 TiB".to_string()); - } - #[test] fn test_detect_mimetype_png() { use std::io::Write; @@ -335,7 +335,7 @@ mod tests { .write_all(b"Hello") .expect("failed to write txt"); - let report = scan(dir.clone()); + let report = scan(dir.clone(), false); assert_eq!(report.mimetypes.get("image/png"), Some(&1)); assert_eq!(report.mimetypes.get("application/pdf"), Some(&1)); @@ -348,7 +348,7 @@ mod tests { #[test] fn test_testdata_mimetypes() { - let report = scan("testdata".into()); + let report = scan("testdata".into(), false); // Verify various MIME types are detected correctly assert_eq!(report.mimetypes.get("image/png"), Some(&1)); assert_eq!(report.mimetypes.get("image/jpeg"), Some(&1)); @@ -392,7 +392,7 @@ mod tests { let readable_file = dir.join("readable.txt"); std::fs::write(&readable_file, "hello").expect("failed to write readable file"); - let report = scan(dir.clone()); + let report = scan(dir.clone(), false); // Should have scanned the readable file assert_eq!(report.extensions.get("txt"), Some(&1));