use std::collections::VecDeque;
use std::time::SystemTime;
pub type TaskId = usize;
#[derive(Debug, Clone)]
pub struct ProgressSample {
pub timestamp: f64,
pub completed: f64,
}
#[derive(Debug, Clone)]
pub struct Task {
pub id: TaskId,
pub description: String,
pub total: Option<f64>,
pub completed: f64,
pub visible: bool,
pub fields: std::collections::HashMap<String, String>,
pub start_time: Option<f64>,
pub stop_time: Option<f64>,
pub finished_time: Option<f64>,
pub finished_speed: Option<f64>,
pub samples: VecDeque<ProgressSample>,
progress: Vec<ProgressSample>,
}
impl Task {
pub fn new(id: TaskId, description: &str, total: Option<f64>) -> Self {
Task {
id,
description: description.to_string(),
total,
completed: 0.0,
visible: true,
fields: std::collections::HashMap::new(),
start_time: None,
stop_time: None,
finished_time: None,
finished_speed: None,
samples: VecDeque::new(),
progress: Vec::new(),
}
}
pub fn started(&self) -> bool {
self.start_time.is_some()
}
pub fn finished(&self) -> bool {
self.finished_time.is_some()
}
pub fn remaining(&self) -> Option<f64> {
self.total.map(|t| (t - self.completed).max(0.0))
}
pub fn elapsed(&self) -> Option<f64> {
self.start_time.map(|start| {
let end = self.stop_time.unwrap_or_else(current_time_secs);
(end - start).max(0.0)
})
}
pub fn percentage(&self) -> f64 {
match self.total {
Some(total) if total > 0.0 => ((self.completed / total) * 100.0).clamp(0.0, 100.0),
_ => 0.0,
}
}
pub fn speed(&self) -> Option<f64> {
if self.finished() {
return self.finished_speed;
}
if self.samples.len() < 2 {
return None;
}
let first = self.samples.front().expect("samples has >= 2 elements");
let last = self.samples.back().expect("samples has >= 2 elements");
let time_delta = last.timestamp - first.timestamp;
if time_delta <= 0.0 {
return None;
}
let completed_delta = last.completed - first.completed;
Some(completed_delta / time_delta)
}
pub fn time_remaining(&self) -> Option<f64> {
if self.finished() {
return Some(0.0);
}
let remaining = self.remaining()?;
let speed = self.speed()?;
if speed <= 0.0 {
return None;
}
Some(remaining / speed)
}
pub(crate) fn record_sample(&mut self, timestamp: f64, speed_estimate_period: f64) {
self.samples.push_back(ProgressSample {
timestamp,
completed: self.completed,
});
self.progress.push(ProgressSample {
timestamp,
completed: self.completed,
});
let cutoff = timestamp - speed_estimate_period;
while let Some(front) = self.samples.front() {
if front.timestamp < cutoff {
self.samples.pop_front();
} else {
break;
}
}
}
}
pub(crate) fn current_time_secs() -> f64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
}
pub fn format_time(seconds: f64) -> String {
let total = seconds.round() as u64;
let h = total / 3600;
let m = (total % 3600) / 60;
let s = total % 60;
if h > 0 {
format!("{h}:{m:02}:{s:02}")
} else {
format!("{m}:{s:02}")
}
}