From 899ddf31cbd08581e421fd48288dc1095c433f85 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Wed, 31 Dec 2025 23:47:13 -0500 Subject: [PATCH] ux: use in-place progress spinner --- vid-repair/src/main.rs | 101 +++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 19 deletions(-) diff --git a/vid-repair/src/main.rs b/vid-repair/src/main.rs index 1fce538..e57f808 100644 --- a/vid-repair/src/main.rs +++ b/vid-repair/src/main.rs @@ -1,4 +1,4 @@ -use std::io::IsTerminal; +use std::io::{IsTerminal, Write}; use std::path::{Path, PathBuf}; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc, Mutex}; use std::time::{Duration, Instant}; @@ -434,6 +434,7 @@ fn run_scans(files: Vec, config: &Config, ruleset: &RuleSet) -> Result< let started = AtomicUsize::new(0); let total = files.len(); let show_progress = !config.report.json && std::io::stderr().is_terminal(); + let in_place = show_progress && jobs.is_none(); let scans = if let Some(jobs) = jobs { let pool = ThreadPoolBuilder::new().num_threads(jobs).build()?; @@ -442,17 +443,23 @@ fn run_scans(files: Vec, config: &Config, ruleset: &RuleSet) -> Result< .par_iter() .filter_map(|path| { let idx = started.fetch_add(1, Ordering::SeqCst) + 1; + if show_progress && !in_place { + eprintln!("[SCAN {}/{}] {}", idx, total, path.display()); + } else if in_place { + eprint!("[SCAN {}/{}] {}", idx, total, path.display()); + let _ = std::io::stderr().flush(); + } let progress = if show_progress { - Some(make_progress_callback("SCAN", idx, total, path)) + Some(make_progress_callback("SCAN", idx, total, path, in_place)) } else { None }; - if show_progress { - eprintln!("[SCAN {}/{}] {}", idx, total, path.display()); - } match scan_file_with_progress(path, config, ruleset, progress) { Ok(scan) => Some(scan), Err(err) => { + if in_place { + eprintln!(); + } eprintln!("[ERROR] {}: {}", path.display(), err); errors.fetch_add(1, Ordering::SeqCst); None @@ -466,17 +473,23 @@ fn run_scans(files: Vec, config: &Config, ruleset: &RuleSet) -> Result< .iter() .filter_map(|path| { let idx = started.fetch_add(1, Ordering::SeqCst) + 1; + if show_progress && !in_place { + eprintln!("[SCAN {}/{}] {}", idx, total, path.display()); + } else if in_place { + eprint!("[SCAN {}/{}] {}", idx, total, path.display()); + let _ = std::io::stderr().flush(); + } let progress = if show_progress { - Some(make_progress_callback("SCAN", idx, total, path)) + Some(make_progress_callback("SCAN", idx, total, path, in_place)) } else { None }; - if show_progress { - eprintln!("[SCAN {}/{}] {}", idx, total, path.display()); - } match scan_file_with_progress(path, config, ruleset, progress) { Ok(scan) => Some(scan), Err(err) => { + if in_place { + eprintln!(); + } eprintln!("[ERROR] {}: {}", path.display(), err); errors.fetch_add(1, Ordering::SeqCst); None @@ -523,21 +536,28 @@ fn process_fix_batch( let mut errors = 0usize; let total = files.len(); let show_progress = !config.report.json && std::io::stderr().is_terminal(); + let in_place = show_progress; let mut idx = 0usize; for path in files { idx += 1; - if show_progress { + if show_progress && !in_place { eprintln!("[FIX {}/{}] {}", idx, total, path.display()); + } else if in_place { + eprint!("[FIX {}/{}] {}", idx, total, path.display()); + let _ = std::io::stderr().flush(); } let progress = if show_progress { - Some(make_progress_callback("FIX", idx, total, &path)) + Some(make_progress_callback("FIX", idx, total, &path, in_place)) } else { None }; let scan = match scan_file_with_progress(&path, config, ruleset, progress) { Ok(scan) => scan, Err(err) => { + if in_place { + eprintln!(); + } eprintln!("[ERROR] {}: {}", path.display(), err); errors += 1; continue; @@ -584,7 +604,7 @@ fn process_fix_batch_parallel( eprintln!("[FIX {}/{}] {}", idx, total, path.display()); } let progress = if show_progress { - Some(make_progress_callback("FIX", idx, total, path)) + Some(make_progress_callback("FIX", idx, total, path, false)) } else { None }; @@ -623,11 +643,16 @@ fn process_fix_batch_parallel( fn watch_scan(paths: Vec, config: &Config, ruleset: &RuleSet) -> Result<()> { println!("Watch mode enabled. Waiting for files to settle..."); watch::watch_paths(&paths, config, |path| { - if !config.report.json { + let show_progress = !config.report.json && std::io::stderr().is_terminal(); + let in_place = show_progress; + if show_progress && !in_place { eprintln!("[SCAN] {}", path.display()); + } else if in_place { + eprint!("[SCAN] {}", path.display()); + let _ = std::io::stderr().flush(); } - let progress = if !config.report.json && std::io::stderr().is_terminal() { - Some(make_progress_callback("SCAN", 1, 1, &path)) + let progress = if show_progress { + Some(make_progress_callback("SCAN", 1, 1, &path, in_place)) } else { None }; @@ -645,11 +670,16 @@ fn watch_scan(paths: Vec, config: &Config, ruleset: &RuleSet) -> Result fn watch_fix(paths: Vec, config: &Config, ruleset: &RuleSet, dry_run: bool) -> Result<()> { println!("Watch mode enabled. Waiting for files to settle..."); watch::watch_paths(&paths, config, |path| { - if !config.report.json { + let show_progress = !config.report.json && std::io::stderr().is_terminal(); + let in_place = show_progress; + if show_progress && !in_place { eprintln!("[FIX] {}", path.display()); + } else if in_place { + eprint!("[FIX] {}", path.display()); + let _ = std::io::stderr().flush(); } - let progress = if !config.report.json && std::io::stderr().is_terminal() { - Some(make_progress_callback("FIX", 1, 1, &path)) + let progress = if show_progress { + Some(make_progress_callback("FIX", 1, 1, &path, in_place)) } else { None }; @@ -681,6 +711,7 @@ struct ProgressState { last_emit: Instant, last_percent: Option, spinner_index: usize, + last_len: usize, } fn make_progress_callback( @@ -688,6 +719,7 @@ fn make_progress_callback( idx: usize, total: usize, path: &Path, + in_place: bool, ) -> Arc { let prefix = format!("[{} {}/{}]", kind, idx, total); let path_display = path.display().to_string(); @@ -696,6 +728,7 @@ fn make_progress_callback( last_emit: now.checked_sub(Duration::from_secs(2)).unwrap_or(now), last_percent: None, spinner_index: 0, + last_len: 0, })); Arc::new(move |progress: DecodeProgress| { @@ -760,7 +793,7 @@ fn make_progress_callback( } line.push_str(&path_display); - eprintln!("{}", line); + emit_progress_line(&line, progress.done, in_place, state.clone()); }) } @@ -776,3 +809,33 @@ fn format_duration(seconds: f64) -> String { format!("{:02}:{:02}", minutes, secs) } } + +fn emit_progress_line(line: &str, done: bool, in_place: bool, state: Arc>) { + if !in_place { + eprintln!("{}", line); + return; + } + + let mut stderr = std::io::stderr(); + let mut output = String::new(); + output.push('\r'); + output.push_str(line); + + if let Ok(mut state) = state.lock() { + let line_len = line.chars().count(); + if line_len < state.last_len { + output.push_str(&" ".repeat(state.last_len - line_len)); + } + if done { + output.push('\n'); + state.last_len = 0; + } else { + state.last_len = line_len; + } + } else if done { + output.push('\n'); + } + + let _ = stderr.write_all(output.as_bytes()); + let _ = stderr.flush(); +}