diff --git a/Cargo.lock b/Cargo.lock index 21eaecf..121d216 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,7 +253,7 @@ dependencies = [ [[package]] name = "dap-tui" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 4e1a5bc..336fa4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dap-tui" -version = "0.3.0" +version = "0.3.1" edition = "2021" description = "Device sync for portable players — TUI edition (ratatui)" license = "MIT" diff --git a/src/engine.rs b/src/engine.rs index 4347daf..5b008e7 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -944,6 +944,9 @@ mod tests { assert!(seen[1].contains("second stats line")); } + + + #[cfg(unix)] fn process_is_alive(pid: i32) -> bool { match std::fs::read_to_string(format!("/proc/{pid}/stat")) { diff --git a/src/progress.rs b/src/progress.rs index 77486d4..dd8f93b 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -193,6 +193,14 @@ pub fn parse_rclone_stats(line: &str) -> Option { return None; } + // A real byte-stats body starts with the transferred amount. This rejects + // per-file `Transferring` entries such as + // `* a.bin: 25% / 60 MiB, 0 B/s, -`, which rclone emits right after the + // byte line on every tick. They used to parse as 0% and reset the gauge. + if !body.starts_with(|c: char| c.is_ascii_digit()) { + return None; + } + let mut parts = body.split(','); let sizes = parts.next()?.trim(); let (done, total) = sizes.split_once('/')?; @@ -348,6 +356,20 @@ mod tests { assert!(parse_rclone_current(" * ").is_none()); } + #[test] + fn rejects_rclone_per_file_lines() { + // Emitted every tick right after the byte line; must never reset the + // gauge to 0%. + assert!(parse_rclone_stats( + " * a.bin: 25% / 60 MiB, 0 B/s, -" + ) + .is_none()); + assert!(parse_rclone_stats("* big.bin: 5% / 40 MiB, 2.059 MiB/s, 18s").is_none()); + // The glued variant still has to yield the trailing stats block. + let glued = "* a.bin: 25% / 60 MiB, 0 B/s, -Transferred: \t 60.086 MiB / 120 MiB, 50%, 30.041 MiB/s, ETA 1s"; + assert_eq!(parse_rclone_stats(glued).unwrap().pct, 50); + } + #[test] fn survives_real_rclone_progress_blocks() { // Captured from `rclone sync --progress --stats 1s` with a pipe: the @@ -366,14 +388,16 @@ Transferring: Checks: 0 / 0, -, Listed 7 Transferred: 2 / 6, 33% "; - let mut last: Option = None; + // Only the two byte lines may parse; every per-file or count line must + // be rejected, so the gauge can never snap back to 0 between ticks. + let mut pcts = Vec::new(); for line in sample.lines() { if let Some(p) = parse_rclone_stats(line) { - last = Some(p); + pcts.push(p.pct); } } - let last = last.expect("byte stats must parse"); - assert_eq!(last.pct, 40); + assert_eq!(pcts, vec![10, 40]); + let last = parse_rclone_stats(sample.lines().nth(7).unwrap()).unwrap(); assert_eq!(last.done, "32.082 MiB"); assert_eq!(last.total, "80 MiB"); }