From 0e305b982981028504abf50926abc2bc7c8f68ae Mon Sep 17 00:00:00 2001 From: LuBashQ Date: Tue, 21 Oct 2025 19:38:40 +0300 Subject: [PATCH 1/3] feat(ui): human readable ETA Signed-off-by: LuBashQ --- src/byteseries.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/byteseries.rs b/src/byteseries.rs index 54ec1e4..eacab4c 100644 --- a/src/byteseries.rs +++ b/src/byteseries.rs @@ -27,7 +27,18 @@ impl From for EstimatedTime { impl Display for EstimatedTime { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - EstimatedTime::Known(x) => write!(f, "{x:.1}s"), + EstimatedTime::Known(x) => { + let rounded = x.round(); + let secs = rounded % 60.0; + let mins = (rounded / 60.0) % 60.0; + let hours = rounded / 3600.0; + + write!( + f, + "{:0>2}:{:0>2}:{:0>2}", + hours as u64, mins as u8, secs as u8, + ) + } EstimatedTime::Unknown => write!(f, "[unknown]"), } } @@ -141,6 +152,7 @@ mod tests { use approx::assert_relative_eq; use super::ByteSeries; + use super::EstimatedTime; use test_case::test_case; fn example_2s() -> ByteSeries { @@ -175,4 +187,16 @@ mod tests { let actual = example_2s().interp(t); assert_relative_eq!(actual, expected); } + + #[test_case(f64::INFINITY, "[unknown]".into(); "non finite")] + #[test_case(10.0, "00:00:10".into(); "seconds")] + #[test_case(123.0, "00:02:03".into(); "minutes")] + #[test_case(39562.0, "10:59:22".into(); "less than a day")] + #[test_case(133800.0, "37:10:00".into(); "more than a day")] + #[test_case(60.5, "00:01:01".into(); "round decimals up")] + #[test_case(59.4, "00:00:59".into(); "round decimals down")] + fn estimated_time_display(t: f64, expected: String) { + let actual = EstimatedTime::from(t).to_string(); + assert_eq!(expected, actual); + } } From 8fa51f17696a0e9001c37e15c3b244e4ac390f03 Mon Sep 17 00:00:00 2001 From: LuBashQ Date: Tue, 21 Oct 2025 21:06:33 +0300 Subject: [PATCH 2/3] feat(ui): add estimated completion date-time Signed-off-by: LuBashQ --- Cargo.lock | 106 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/byteseries.rs | 64 +++++++++++++++++++--------- 3 files changed, 150 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42b8893..4d27cb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -228,6 +237,7 @@ dependencies = [ "bytesize", "bzip2", "cc", + "chrono", "clap", "crossterm 0.27.0", "derive_more", @@ -305,6 +315,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -395,6 +418,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -748,6 +777,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indicatif" version = "0.17.11" @@ -1933,12 +1986,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index d47214a..c369159 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ bincode = "1.3.3" byteorder = "1.5.0" bytesize = "1.3.3" bzip2 = { version = "0.6.1", features = ["static"] } +chrono = "0.4.42" clap = { version = "4.5.49", features = ["derive", "cargo", "wrap_help"] } crossterm = { version = "0.27.0", features = ["event-stream"] } derive_more = "0.99.20" diff --git a/src/byteseries.rs b/src/byteseries.rs index eacab4c..af3d102 100644 --- a/src/byteseries.rs +++ b/src/byteseries.rs @@ -1,10 +1,17 @@ +use crate::ui::utils::ByteSpeed; +use chrono::Datelike; +use std::ops::Add; use std::{fmt::Display, time::Instant}; -use crate::ui::utils::ByteSpeed; +#[derive(Debug, Clone, PartialEq)] +pub struct EstimatedTimeInfo { + secs_left: f64, + now: chrono::DateTime, +} #[derive(Debug, Clone, PartialEq)] pub enum EstimatedTime { - Known(f64), + Known(EstimatedTimeInfo), Unknown, } @@ -14,9 +21,9 @@ pub struct ByteSeries { start: Instant, } -impl From for EstimatedTime { - fn from(value: f64) -> Self { - if value.is_finite() { +impl From for EstimatedTime { + fn from(value: EstimatedTimeInfo) -> Self { + if value.secs_left.is_finite() { Self::Known(value) } else { Self::Unknown @@ -28,15 +35,27 @@ impl Display for EstimatedTime { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EstimatedTime::Known(x) => { - let rounded = x.round(); + let rounded = x.secs_left.round(); let secs = rounded % 60.0; let mins = (rounded / 60.0) % 60.0; let hours = rounded / 3600.0; + let completion_time = x.now.add(chrono::TimeDelta::seconds(rounded as i64)); + + // Show the whole date only if completion time is at least a day ahead of now + let completion_time_format = if hours >= 24.0 { + "%Y-%m-%d %H:%M:%S" + } else { + "%H:%M:%S" + }; + write!( f, - "{:0>2}:{:0>2}:{:0>2}", - hours as u64, mins as u8, secs as u8, + "{:0>2}:{:0>2}:{:0>2} (complete at {})", + hours as u64, + mins as u8, + secs as u8, + completion_time.format(completion_time_format) ) } EstimatedTime::Unknown => write!(f, "[unknown]"), @@ -77,7 +96,10 @@ impl ByteSeries { // than total bytes, due to the nature of block writing. let bytes_left = total_bytes.saturating_sub(self.bytes_encountered()); let secs_left = bytes_left as f64 / speed; - EstimatedTime::from(secs_left) + EstimatedTime::from(EstimatedTimeInfo { + secs_left, + now: chrono::Local::now(), + }) } pub fn start(&self) -> Instant { @@ -149,10 +171,10 @@ impl ByteSeries { mod tests { use std::time::{Duration, Instant}; - use approx::assert_relative_eq; - - use super::ByteSeries; use super::EstimatedTime; + use super::{ByteSeries, EstimatedTimeInfo}; + use approx::assert_relative_eq; + use chrono::{Local, TimeZone, Utc}; use test_case::test_case; fn example_2s() -> ByteSeries { @@ -188,15 +210,15 @@ mod tests { assert_relative_eq!(actual, expected); } - #[test_case(f64::INFINITY, "[unknown]".into(); "non finite")] - #[test_case(10.0, "00:00:10".into(); "seconds")] - #[test_case(123.0, "00:02:03".into(); "minutes")] - #[test_case(39562.0, "10:59:22".into(); "less than a day")] - #[test_case(133800.0, "37:10:00".into(); "more than a day")] - #[test_case(60.5, "00:01:01".into(); "round decimals up")] - #[test_case(59.4, "00:00:59".into(); "round decimals down")] - fn estimated_time_display(t: f64, expected: String) { - let actual = EstimatedTime::from(t).to_string(); + #[test_case(f64::INFINITY, "[unknown]"; "non finite")] + #[test_case(39_562.0, "10:59:22 (complete at 20:59:27)"; "less than a day")] + #[test_case(86_400.0, "24:00:00 (complete at 2025-10-22 10:00:05)"; "exactly a day")] + #[test_case(133_800.0, "37:10:00 (complete at 2025-10-22 23:10:05)"; "more than a day")] + #[test_case(60.5, "00:01:01 (complete at 10:01:06)"; "round decimals up")] + #[test_case(59.4, "00:00:59 (complete at 10:01:04)"; "round decimals down")] + fn estimated_time_display(secs_left: f64, expected: &str) { + let now = Local.with_ymd_and_hms(2025, 10, 21, 10, 0, 5).unwrap(); + let actual = EstimatedTime::from(EstimatedTimeInfo { secs_left, now }).to_string(); assert_eq!(expected, actual); } } From 450dcf90d777bef2b01f7a0ca9d8f745b2e9421e Mon Sep 17 00:00:00 2001 From: LuBashQ Date: Tue, 21 Oct 2025 21:23:00 +0300 Subject: [PATCH 3/3] chore: ignore IntelliJ folder Signed-off-by: LuBashQ --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index cf168c2..38ae9fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .direnv +.idea/ + .vscode/* !.vscode/extensions.json