use std::io::{self, Read, Seek, SeekFrom};
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::live::Live;
use crate::progress::columns::{BarColumn, TaskProgressColumn, TextColumn, TimeRemainingColumn};
use crate::progress::task::{current_time_secs, Task, TaskId};
use crate::segment::Segment;
use crate::style::Style;
use crate::table::Table;
use crate::text::Text;
use crate::utils::filesize;
pub trait ProgressColumn: Send + Sync {
fn render(&self, task: &Task) -> Text;
fn max_refresh(&self) -> Option<f64> {
None
}
}
#[derive(Debug, Clone)]
pub struct DownloadColumn {
pub binary_units: bool,
}
impl DownloadColumn {
pub fn new() -> Self {
Self {
binary_units: false,
}
}
pub fn with_binary_units(mut self, binary: bool) -> Self {
self.binary_units = binary;
self
}
pub(crate) fn format_size(&self, size: u64) -> String {
if self.binary_units {
filesize::binary(size, 1, " ")
} else {
filesize::decimal(size, 1, " ")
}
}
}
impl Default for DownloadColumn {
fn default() -> Self {
Self::new()
}
}
impl ProgressColumn for DownloadColumn {
fn render(&self, task: &Task) -> Text {
let completed = self.format_size(task.completed as u64);
let total = match task.total {
Some(t) => self.format_size(t as u64),
None => "?".to_string(),
};
let style = Style::parse("progress.download").unwrap_or_else(|_| Style::null());
Text::new(&format!("{completed}/{total}"), style)
}
}
#[derive(Debug, Clone)]
pub struct TransferSpeedColumn {
pub binary_units: bool,
}
impl TransferSpeedColumn {
pub fn new() -> Self {
Self {
binary_units: false,
}
}
pub fn with_binary_units(mut self, binary: bool) -> Self {
self.binary_units = binary;
self
}
pub(crate) fn format_size(&self, size: u64) -> String {
if self.binary_units {
filesize::binary(size, 1, " ")
} else {
filesize::decimal(size, 1, " ")
}
}
}
impl Default for TransferSpeedColumn {
fn default() -> Self {
Self::new()
}
}
impl ProgressColumn for TransferSpeedColumn {
fn render(&self, task: &Task) -> Text {
let style = Style::parse("progress.data.speed").unwrap_or_else(|_| Style::null());
match task.speed() {
Some(speed) => {
let formatted = self.format_size(speed as u64);
Text::new(&format!("{formatted}/s"), style)
}
None => Text::new("?", style),
}
}
}
pub struct RenderableColumn {
pub callback: Box<dyn Fn(&Task) -> Text + Send + Sync>,
}
impl RenderableColumn {
pub fn new<F>(callback: F) -> Self
where
F: Fn(&Task) -> Text + Send + Sync + 'static,
{
RenderableColumn {
callback: Box::new(callback),
}
}
}
impl ProgressColumn for RenderableColumn {
fn render(&self, task: &Task) -> Text {
(self.callback)(task)
}
}
pub struct Progress {
columns: Vec<Box<dyn ProgressColumn>>,
tasks: Vec<Task>,
live: Live,
task_id_counter: usize,
speed_estimate_period: f64,
get_time: Box<dyn Fn() -> f64 + Send>,
disable: bool,
expand: bool,
}
impl Progress {
pub fn new(columns: Vec<Box<dyn ProgressColumn>>) -> Self {
Progress {
columns,
tasks: Vec::new(),
live: Live::new(Text::empty())
.with_auto_refresh(true)
.with_refresh_per_second(10.0),
task_id_counter: 0,
speed_estimate_period: 30.0,
get_time: Box::new(current_time_secs),
disable: false,
expand: false,
}
}
pub fn default_columns() -> Vec<Box<dyn ProgressColumn>> {
vec![
Box::new(TextColumn::new("{task.description}")),
Box::new(BarColumn::default()),
Box::new(TaskProgressColumn::default()),
Box::new(TimeRemainingColumn::default()),
]
}
#[must_use]
pub fn with_console(mut self, console: Console) -> Self {
self.live = self.live.with_console(console);
self
}
#[must_use]
pub fn with_auto_refresh(mut self, auto_refresh: bool) -> Self {
self.live = self.live.with_auto_refresh(auto_refresh);
self
}
#[must_use]
pub fn with_transient(mut self, transient: bool) -> Self {
self.live = self.live.with_transient(transient);
self
}
#[must_use]
pub fn with_refresh_per_second(mut self, rate: f64) -> Self {
self.live = self.live.with_refresh_per_second(rate);
self
}
#[must_use]
pub fn with_speed_estimate_period(mut self, seconds: f64) -> Self {
self.speed_estimate_period = seconds;
self
}
#[must_use]
pub fn with_disable(mut self, disable: bool) -> Self {
self.disable = disable;
self
}
#[must_use]
pub fn with_expand(mut self, expand: bool) -> Self {
self.expand = expand;
self
}
#[must_use]
pub fn with_get_time<F>(mut self, f: F) -> Self
where
F: Fn() -> f64 + Send + 'static,
{
self.get_time = Box::new(f);
self
}
pub fn add_task(&mut self, description: &str, total: Option<f64>) -> TaskId {
let id = self.task_id_counter;
self.task_id_counter += 1;
let mut task = Task::new(id, description, total);
let now = (self.get_time)();
task.start_time = Some(now);
self.tasks.push(task);
id
}
pub fn update(
&mut self,
task_id: TaskId,
completed: Option<f64>,
total: Option<f64>,
advance: Option<f64>,
description: Option<&str>,
visible: Option<bool>,
) {
let now = (self.get_time)();
let mut changed = false;
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
if let Some(desc) = description {
task.description = desc.to_string();
changed = true;
}
if let Some(t) = total {
task.total = Some(t);
changed = true;
}
if let Some(c) = completed {
task.completed = c;
changed = true;
}
if let Some(a) = advance {
task.completed += a;
changed = true;
}
if let Some(v) = visible {
task.visible = v;
changed = true;
}
if task.started() && !task.finished() {
task.record_sample(now, self.speed_estimate_period);
}
if let Some(t) = task.total {
if task.completed >= t && task.finished_time.is_none() {
task.finished_speed = task.speed();
task.finished_time = Some(now);
}
}
}
if changed {
self.mark_dirty();
}
}
pub fn advance(&mut self, task_id: TaskId, advance: f64) {
self.update(task_id, None, None, Some(advance), None, None);
}
pub fn start_task(&mut self, task_id: TaskId) {
let now = (self.get_time)();
let mut changed = false;
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
if task.start_time.is_none() {
task.start_time = Some(now);
changed = true;
}
}
if changed {
self.mark_dirty();
}
}
pub fn stop_task(&mut self, task_id: TaskId) {
let now = (self.get_time)();
let mut changed = false;
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
task.stop_time = Some(now);
changed = true;
}
if changed {
self.mark_dirty();
}
}
pub fn remove_task(&mut self, task_id: TaskId) {
self.tasks.retain(|t| t.id != task_id);
}
pub fn get_task(&self, task_id: TaskId) -> Option<&Task> {
self.tasks.iter().find(|t| t.id == task_id)
}
pub fn get_task_mut(&mut self, task_id: TaskId) -> Option<&mut Task> {
self.tasks.iter_mut().find(|t| t.id == task_id)
}
pub fn tasks(&self) -> &[Task] {
&self.tasks
}
pub fn finished_count(&self) -> usize {
self.tasks.iter().filter(|t| t.finished()).count()
}
pub fn visible_count(&self) -> usize {
self.tasks.iter().filter(|t| t.visible).count()
}
pub fn reset(&mut self, task_id: TaskId) {
let now = (self.get_time)();
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
task.completed = 0.0;
task.start_time = Some(now);
task.stop_time = None;
task.finished_time = None;
task.finished_speed = None;
task.samples.clear();
}
}
pub fn all_tasks_finished(&self) -> bool {
self.tasks
.iter()
.filter(|t| t.visible)
.all(|t| t.finished())
}
pub fn print(&self, renderable: &dyn Renderable) {
self.live.console_mut().print(renderable);
}
pub fn log(&self, message: &str) {
self.live.console_mut().log(message);
}
pub fn track<I>(
&mut self,
iter: I,
description: &str,
total: Option<f64>,
) -> ProgressTracker<'_, I::IntoIter>
where
I: IntoIterator,
{
let task_id = self.add_task(description, total);
ProgressTracker {
inner: iter.into_iter(),
progress: self,
task_id,
}
}
pub fn open_file(
&mut self,
path: impl AsRef<std::path::Path>,
description: &str,
) -> io::Result<ProgressReader<std::fs::File>> {
let file = std::fs::File::open(path)?;
let len = file.metadata()?.len();
let task_id = self.add_task(description, Some(len as f64));
let progress_ptr = self as *mut Progress;
Ok(ProgressReader::new(file, move |n| {
let progress = unsafe { &mut *progress_ptr };
progress.advance(task_id, n as f64);
}))
}
pub fn wrap_file<R: Read + Seek>(
&mut self,
mut reader: R,
description: &str,
) -> io::Result<ProgressReader<R>> {
let len = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(0))?;
let task_id = self.add_task(description, Some(len as f64));
let progress_ptr = self as *mut Progress;
Ok(ProgressReader::new(reader, move |n| {
let progress = unsafe { &mut *progress_ptr };
progress.advance(task_id, n as f64);
}))
}
pub fn start(&mut self) {
if self.disable {
return;
}
self.live.start();
}
pub fn stop(&mut self) {
if self.disable {
return;
}
self.live.stop();
}
pub fn refresh(&mut self) {
if self.disable {
return;
}
let table_text = self.render_tasks_text();
self.live.update_renderable(table_text, true);
}
fn mark_dirty(&mut self) {
if self.disable {
return;
}
let table_text = self.render_tasks_text();
self.live.update_renderable(table_text, false);
}
pub fn make_tasks_table(&self) -> Table {
let headers: Vec<&str> = self.columns.iter().map(|_| "").collect();
let mut table = Table::grid(&headers);
table.padding = (0, 1, 0, 0);
if self.expand {
table.set_expand(true);
}
for col in &mut table.columns {
col.no_wrap = true;
}
for task in &self.tasks {
if !task.visible {
continue;
}
let cells: Vec<String> = self
.columns
.iter()
.map(|col| {
let text = col.render(task);
text.plain().to_string()
})
.collect();
let cell_refs: Vec<&str> = cells.iter().map(|s| s.as_str()).collect();
table.add_row(&cell_refs);
}
table
}
fn render_tasks_text(&self) -> Text {
let visible_tasks: Vec<&Task> = self.tasks.iter().filter(|t| t.visible).collect();
if visible_tasks.is_empty() {
return Text::empty();
}
let separator = Text::new(" ", Style::null());
let mut result = Text::empty();
for (i, task) in visible_tasks.iter().enumerate() {
if i > 0 {
result.append_str("\n", None);
}
for (j, col) in self.columns.iter().enumerate() {
if j > 0 {
result.append_text(&separator);
}
let rendered = col.render(task);
result.append_text(&rendered);
}
}
result
}
}
impl Renderable for Progress {
fn gilt_console(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
let text = self.render_tasks_text();
text.render()
}
}
pub struct ProgressTracker<'a, I> {
inner: I,
progress: &'a mut Progress,
task_id: TaskId,
}
impl<'a, I> ProgressTracker<'a, I> {
pub fn task_id(&self) -> TaskId {
self.task_id
}
}
impl<I> Iterator for ProgressTracker<'_, I>
where
I: Iterator,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
let item = self.inner.next()?;
self.progress.advance(self.task_id, 1.0);
Some(item)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
pub struct TrackIterator<I> {
inner: I,
progress: Progress,
task_id: TaskId,
started: bool,
}
impl<I> TrackIterator<I>
where
I: Iterator,
{
pub fn new(iter: I, description: &str, total: Option<f64>) -> Self {
let mut progress = Progress::new(Progress::default_columns()).with_auto_refresh(false);
let task_id = progress.add_task(description, total);
TrackIterator {
inner: iter,
progress,
task_id,
started: false,
}
}
}
impl<I> Iterator for TrackIterator<I>
where
I: Iterator,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
if !self.started {
self.progress.start();
self.started = true;
}
match self.inner.next() {
Some(item) => {
self.progress.advance(self.task_id, 1.0);
self.progress.refresh();
Some(item)
}
None => {
self.progress.stop();
None
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<I> Drop for TrackIterator<I> {
fn drop(&mut self) {
if self.started {
self.progress.stop();
}
}
}
pub fn track<I>(iter: I, description: &str, total: Option<f64>) -> TrackIterator<I::IntoIter>
where
I: IntoIterator,
{
TrackIterator::new(iter.into_iter(), description, total)
}
pub trait ProgressIteratorExt: Iterator + Sized {
fn progress(self, description: &str) -> ProgressIter<Self>;
fn progress_with_total(self, description: &str, total: f64) -> ProgressIter<Self>;
}
impl<I: Iterator> ProgressIteratorExt for I {
fn progress(self, description: &str) -> ProgressIter<Self> {
let total = self.size_hint().1.map(|n| n as f64);
ProgressIter::new(self, description, total)
}
fn progress_with_total(self, description: &str, total: f64) -> ProgressIter<Self> {
ProgressIter::new(self, description, Some(total))
}
}
pub struct ProgressIter<I> {
inner: I,
progress: Progress,
task_id: TaskId,
started: bool,
}
impl<I: Iterator> ProgressIter<I> {
fn new(iter: I, description: &str, total: Option<f64>) -> Self {
let mut progress = Progress::new(Progress::default_columns()).with_auto_refresh(true);
let task_id = progress.add_task(description, total);
ProgressIter {
inner: iter,
progress,
task_id,
started: false,
}
}
pub fn task_id(&self) -> TaskId {
self.task_id
}
}
impl<I: Iterator> Iterator for ProgressIter<I> {
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
if !self.started {
self.progress.start();
self.started = true;
}
match self.inner.next() {
Some(item) => {
self.progress.advance(self.task_id, 1.0);
self.progress.refresh();
Some(item)
}
None => {
self.progress.stop();
None
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<I> Drop for ProgressIter<I> {
fn drop(&mut self) {
if self.started {
self.progress.stop();
}
}
}
pub struct ProgressReader<R> {
inner: R,
callback: Box<dyn FnMut(usize)>,
total_read: usize,
}
impl<R> ProgressReader<R> {
pub fn new(inner: R, callback: impl FnMut(usize) + 'static) -> Self {
ProgressReader {
inner,
callback: Box::new(callback),
total_read: 0,
}
}
pub fn total_read(&self) -> usize {
self.total_read
}
pub fn into_inner(self) -> R {
self.inner
}
}
impl<R: Read> Read for ProgressReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.inner.read(buf)?;
self.total_read += n;
(self.callback)(n);
Ok(n)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Cursor, Read};
fn make_progress() -> Progress {
Progress::new(Progress::default_columns()).with_disable(true)
}
#[test]
fn open_file_creates_task_with_file_length() {
let content = b"hello, progress world!";
let path = std::env::temp_dir().join("gilt_test_open_file_task.bin");
std::fs::write(&path, content).unwrap();
let mut progress = make_progress();
let _reader = progress.open_file(&path, "Reading").unwrap();
let tasks = progress.tasks();
assert_eq!(tasks.len(), 1);
assert_eq!(
tasks[0].total,
Some(content.len() as f64),
"task total should equal file byte length"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn open_file_advances_task_on_read() {
let content = b"advance me please";
let path = std::env::temp_dir().join("gilt_test_open_file_advance.bin");
std::fs::write(&path, content).unwrap();
let mut progress = make_progress();
let mut reader = progress.open_file(&path, "Reading").unwrap();
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
assert_eq!(
reader.total_read(),
content.len(),
"ProgressReader.total_read should equal bytes read"
);
let task = &progress.tasks()[0];
assert_eq!(
task.completed,
content.len() as f64,
"task.completed should equal bytes read"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn open_file_returns_error_for_missing_path() {
let mut progress = make_progress();
let result = progress.open_file("/nonexistent/path/gilt_test.bin", "Reading");
assert!(result.is_err(), "should error for nonexistent path");
}
#[test]
fn wrap_file_uses_seek_to_compute_total() {
let content = b"seekable data here";
let cursor = Cursor::new(content.to_vec());
let mut progress = make_progress();
let _reader = progress.wrap_file(cursor, "Processing").unwrap();
let tasks = progress.tasks();
assert_eq!(tasks.len(), 1);
assert_eq!(
tasks[0].total,
Some(content.len() as f64),
"task total should equal cursor length determined via seek"
);
}
#[test]
fn wrap_file_advances_task_on_read() {
let content = b"wrap and advance";
let cursor = Cursor::new(content.to_vec());
let mut progress = make_progress();
let mut reader = progress.wrap_file(cursor, "Processing").unwrap();
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
assert_eq!(buf, content);
let task = &progress.tasks()[0];
assert_eq!(
task.completed,
content.len() as f64,
"task.completed should equal bytes read"
);
}
}