use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
pub struct ProgressTracker {
multi: Arc<MultiProgress>,
enable_progress: bool,
}
impl ProgressTracker {
#[must_use]
pub fn new(enable_progress: bool) -> Self {
Self {
multi: Arc::new(MultiProgress::new()),
enable_progress,
}
}
#[must_use]
pub fn create_spinner(&self, message: &str) -> ProgressBar {
if !self.enable_progress {
return ProgressBar::hidden();
}
let pb = self.multi.add(ProgressBar::new_spinner());
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(Duration::from_millis(80));
pb
}
#[must_use]
pub fn create_file_progress(&self, total_files: u64, message: &str) -> ProgressBar {
if !self.enable_progress {
return ProgressBar::hidden();
}
let pb = self.multi.add(ProgressBar::new(total_files));
pb.set_style(
ProgressStyle::default_bar()
.template(
"{msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {per_sec}",
)
.unwrap()
.progress_chars("█▉▊▋▌▍▎▏ "),
);
pb.set_message(message.to_string());
pb
}
#[must_use]
pub fn create_bytes_progress(&self, total_bytes: u64, message: &str) -> ProgressBar {
if !self.enable_progress {
return ProgressBar::hidden();
}
let pb = self.multi.add(ProgressBar::new(total_bytes));
pb.set_style(
ProgressStyle::default_bar()
.template(
"{msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({percent}%)",
)
.unwrap()
.progress_chars("█▉▊▋▌▍▎▏ "),
);
pb.set_message(message.to_string());
pb
}
pub fn log_skipped_file(&self, file_path: &std::path::Path, reason: &str) {
if self.enable_progress {
eprintln!("⚠️ Skipped: {} ({})", file_path.display(), reason);
}
}
#[must_use]
pub fn create_sub_progress(&self, message: &str, total: u64) -> ProgressBar {
if !self.enable_progress {
return ProgressBar::hidden();
}
let pb = self.multi.add(ProgressBar::new(total));
pb.set_style(
ProgressStyle::default_bar()
.template(" {msg} [{bar:30.green/white}] {pos}/{len}")
.unwrap()
.progress_chars("█▉▊▋▌▍▎▏ "),
);
pb.set_message(message.to_string());
pb
}
pub fn clear(&self) {
if self.enable_progress {
self.multi.clear().ok();
}
}
}
pub struct FileClassificationReporter {
tracker: ProgressTracker,
skipped_count: std::sync::atomic::AtomicU64,
large_files_skipped: std::sync::Arc<std::sync::Mutex<Vec<std::path::PathBuf>>>,
}
impl FileClassificationReporter {
#[must_use]
pub fn new(tracker: ProgressTracker) -> Self {
Self {
tracker,
skipped_count: std::sync::atomic::AtomicU64::new(0),
large_files_skipped: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
pub fn report_skipped(
&self,
path: &std::path::Path,
reason: crate::services::file_classifier::SkipReason,
) {
use crate::services::file_classifier::SkipReason;
self.skipped_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
match reason {
SkipReason::LargeFile => {
if let Ok(mut files) = self.large_files_skipped.lock() {
files.push(path.to_path_buf());
}
self.tracker.log_skipped_file(path, "large file >500KB");
}
SkipReason::MinifiedContent => {
self.tracker.log_skipped_file(path, "minified content");
}
SkipReason::VendorDirectory => {
}
SkipReason::LineTooLong => {
self.tracker.log_skipped_file(path, "line too long");
}
_ => {}
}
}
pub fn get_summary(&self) -> (u64, Vec<std::path::PathBuf>) {
let count = self
.skipped_count
.load(std::sync::atomic::Ordering::Relaxed);
let files = self.large_files_skipped.lock().unwrap().clone();
(count, files)
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}