ux: use in-place progress spinner

This commit is contained in:
2025-12-31 23:47:13 -05:00
parent 152701c574
commit 899ddf31cb

View File

@@ -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<PathBuf>, 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<PathBuf>, 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<PathBuf>, 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<PathBuf>, 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<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result
fn watch_fix(paths: Vec<PathBuf>, 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<u8>,
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<dyn Fn(DecodeProgress) + Send + Sync> {
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<Mutex<ProgressState>>) {
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();
}