Skip to content

Commit 971b18a

Browse files
authored
Merge pull request #206 from LuBashQ/feature/197-human-readable-eta
feature: add human readable ETA
2 parents 2ea18ae + fd7ce82 commit 971b18a

File tree

7 files changed

+164
-10
lines changed

7 files changed

+164
-10
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
.direnv
22

3+
.idea/
4+
35
.vscode/*
46
!.vscode/extensions.json
57

Cargo.lock

Lines changed: 106 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ bincode = "1.3.3"
1717
byteorder = "1.5.0"
1818
bytesize = "1.3.3"
1919
bzip2 = { version = "0.6.1", features = ["static"] }
20+
chrono = "0.4.42"
2021
clap = { version = "4.5.49", features = ["derive", "cargo", "wrap_help"] }
2122
crossterm = { version = "0.27.0", features = ["event-stream"] }
2223
derive_more = "0.99.20"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![CI](https://github.com/ifd3f/caligula/actions/workflows/ci.yml/badge.svg)](https://github.com/ifd3f/caligula/actions/workflows/ci.yml)
44

5-
![Screenshot of the Caligula TUI verifying a disk.](./images/verifying.png)
5+
![Screenshot of the Caligula TUI verifying a disk.](./images/caligula.png)
66

77
_Caligula_ is a user-friendly, lightweight TUI for imaging disks.
88

images/caligula.png

43.1 KB
Loading

images/verifying.png

-63.2 KB
Binary file not shown.

src/byteseries.rs

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
use crate::ui::utils::ByteSpeed;
2+
use std::ops::Add;
13
use std::{fmt::Display, time::Instant};
24

3-
use crate::ui::utils::ByteSpeed;
5+
#[derive(Debug, Clone, PartialEq)]
6+
pub struct EstimatedTimeInfo {
7+
secs_left: f64,
8+
now: chrono::DateTime<chrono::Local>,
9+
}
410

511
#[derive(Debug, Clone, PartialEq)]
612
pub enum EstimatedTime {
7-
Known(f64),
13+
Known(EstimatedTimeInfo),
814
Unknown,
915
}
1016

@@ -14,9 +20,9 @@ pub struct ByteSeries {
1420
start: Instant,
1521
}
1622

17-
impl From<f64> for EstimatedTime {
18-
fn from(value: f64) -> Self {
19-
if value.is_finite() {
23+
impl From<EstimatedTimeInfo> for EstimatedTime {
24+
fn from(value: EstimatedTimeInfo) -> Self {
25+
if value.secs_left.is_finite() {
2026
Self::Known(value)
2127
} else {
2228
Self::Unknown
@@ -27,7 +33,30 @@ impl From<f64> for EstimatedTime {
2733
impl Display for EstimatedTime {
2834
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2935
match self {
30-
EstimatedTime::Known(x) => write!(f, "{x:.1}s"),
36+
EstimatedTime::Known(x) => {
37+
let rounded = x.secs_left.round();
38+
let secs = rounded % 60.0;
39+
let mins = (rounded / 60.0) % 60.0;
40+
let hours = rounded / 3600.0;
41+
42+
let completion_time = x.now.add(chrono::TimeDelta::seconds(rounded as i64));
43+
44+
// Show the whole date only if completion time is at least a day ahead of now
45+
let completion_time_format = if hours >= 24.0 {
46+
"%Y-%m-%d %H:%M:%S"
47+
} else {
48+
"%H:%M:%S"
49+
};
50+
51+
write!(
52+
f,
53+
"{:0>2}:{:0>2}:{:0>2} (complete at {})",
54+
hours as u64,
55+
mins as u8,
56+
secs as u8,
57+
completion_time.format(completion_time_format)
58+
)
59+
}
3160
EstimatedTime::Unknown => write!(f, "[unknown]"),
3261
}
3362
}
@@ -66,7 +95,10 @@ impl ByteSeries {
6695
// than total bytes, due to the nature of block writing.
6796
let bytes_left = total_bytes.saturating_sub(self.bytes_encountered());
6897
let secs_left = bytes_left as f64 / speed;
69-
EstimatedTime::from(secs_left)
98+
EstimatedTime::from(EstimatedTimeInfo {
99+
secs_left,
100+
now: chrono::Local::now(),
101+
})
70102
}
71103

72104
pub fn start(&self) -> Instant {
@@ -138,9 +170,10 @@ impl ByteSeries {
138170
mod tests {
139171
use std::time::{Duration, Instant};
140172

173+
use super::EstimatedTime;
174+
use super::{ByteSeries, EstimatedTimeInfo};
141175
use approx::assert_relative_eq;
142-
143-
use super::ByteSeries;
176+
use chrono::{Local, TimeZone, Utc};
144177
use test_case::test_case;
145178

146179
fn example_2s() -> ByteSeries {
@@ -175,4 +208,16 @@ mod tests {
175208
let actual = example_2s().interp(t);
176209
assert_relative_eq!(actual, expected);
177210
}
211+
212+
#[test_case(f64::INFINITY, "[unknown]"; "non finite")]
213+
#[test_case(39_562.0, "10:59:22 (complete at 20:59:27)"; "less than a day")]
214+
#[test_case(86_400.0, "24:00:00 (complete at 2025-10-22 10:00:05)"; "exactly a day")]
215+
#[test_case(133_800.0, "37:10:00 (complete at 2025-10-22 23:10:05)"; "more than a day")]
216+
#[test_case(60.5, "00:01:01 (complete at 10:01:06)"; "round decimals up")]
217+
#[test_case(59.4, "00:00:59 (complete at 10:01:04)"; "round decimals down")]
218+
fn estimated_time_display(secs_left: f64, expected: &str) {
219+
let now = Local.with_ymd_and_hms(2025, 10, 21, 10, 0, 5).unwrap();
220+
let actual = EstimatedTime::from(EstimatedTimeInfo { secs_left, now }).to_string();
221+
assert_eq!(expected, actual);
222+
}
178223
}

0 commit comments

Comments
 (0)