use crate::console::{Console, RenderContext};
use crate::progress::columns::{
BarColumn, PercentageColumn, ProgressColumn, TextColumn, TimeRemainingColumn,
};
use crate::renderable::{Renderable, Segment};
use crate::style::{Color, Style};
use crate::text::Span;
use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct Task {
pub id: usize,
pub description: String,
pub total: Option<u64>,
pub completed: u64,
pub start_time: Instant,
pub finished: bool,
pub style: Style,
}
impl Task {
pub fn new(id: usize, description: &str, total: Option<u64>) -> Self {
Task {
id,
description: description.to_string(),
total,
completed: 0,
start_time: Instant::now(),
finished: false,
style: Style::new().foreground(Color::Cyan),
}
}
pub fn percentage(&self) -> f64 {
match self.total {
Some(total) if total > 0 => (self.completed as f64 / total as f64).min(1.0),
_ => 0.0,
}
}
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
pub fn eta(&self) -> Option<Duration> {
if self.completed == 0 {
return None;
}
let elapsed = self.elapsed().as_secs_f64();
let rate = self.completed as f64 / elapsed;
self.total.and_then(|total| {
let remaining = total.saturating_sub(self.completed);
if rate > 0.0 {
Some(Duration::from_secs_f64(remaining as f64 / rate))
} else {
None
}
})
}
pub fn speed(&self) -> f64 {
let elapsed = self.elapsed().as_secs_f64();
if elapsed > 0.0 {
self.completed as f64 / elapsed
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct ProgressBar {
pub bar_width: usize,
pub complete_char: char,
pub remaining_char: char,
pub complete_style: Style,
pub remaining_style: Style,
}
impl Default for ProgressBar {
fn default() -> Self {
Self::new()
}
}
impl ProgressBar {
pub fn new() -> Self {
ProgressBar {
bar_width: 40,
complete_char: '━',
remaining_char: '━',
complete_style: Style::new().foreground(Color::Cyan),
remaining_style: Style::new().foreground(Color::BrightBlack),
}
}
pub fn width(mut self, width: usize) -> Self {
self.bar_width = width;
self
}
}
pub struct Progress {
tasks: Arc<Mutex<Vec<Task>>>,
next_id: Arc<Mutex<usize>>,
columns: Vec<Box<dyn ProgressColumn>>,
#[allow(dead_code)]
visible: bool,
console: Option<Console>,
started: bool,
height: Arc<Mutex<usize>>,
transient: bool,
refresh_per_second: f64,
auto_refresh: bool,
refresh_thread: Option<JoinHandle<()>>,
stop_signal: Arc<AtomicBool>,
}
impl Default for Progress {
fn default() -> Self {
Self::new()
}
}
impl Progress {
pub fn new() -> Self {
Progress {
tasks: Arc::new(Mutex::new(Vec::new())),
next_id: Arc::new(Mutex::new(0)),
columns: vec![
Box::new(TextColumn::new("[progress.description]")),
Box::new(BarColumn::new(40)),
Box::new(PercentageColumn::new()),
Box::new(TimeRemainingColumn),
],
visible: true,
console: None,
started: false,
height: Arc::new(Mutex::new(0)),
transient: false,
refresh_per_second: 10.0,
auto_refresh: true,
refresh_thread: None,
stop_signal: Arc::new(AtomicBool::new(false)),
}
}
pub fn with_console(mut self, console: Console) -> Self {
self.console = Some(console);
self
}
pub fn transient(mut self, transient: bool) -> Self {
self.transient = transient;
self
}
pub fn refresh_per_second(mut self, rate: f64) -> Self {
self.refresh_per_second = rate.max(0.1); self
}
pub fn auto_refresh(mut self, enabled: bool) -> Self {
self.auto_refresh = enabled;
self
}
pub fn with_columns(mut self, columns: Vec<Box<dyn ProgressColumn>>) -> Self {
self.columns = columns;
self
}
pub fn add_task(&self, description: &str, total: Option<u64>) -> usize {
let mut next_id = self.next_id.lock().unwrap();
let id = *next_id;
*next_id += 1;
let task = Task::new(id, description, total);
self.tasks.lock().unwrap().push(task);
id
}
pub fn advance(&self, task_id: usize, amount: u64) {
if let Ok(mut tasks) = self.tasks.lock() {
if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
task.completed += amount;
if let Some(total) = task.total {
if task.completed >= total {
task.finished = true;
}
}
}
}
}
pub fn update(&self, task_id: usize, completed: u64) {
if let Ok(mut tasks) = self.tasks.lock() {
if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
task.completed = completed;
if let Some(total) = task.total {
if task.completed >= total {
task.finished = true;
}
}
}
}
}
pub fn finish(&self, task_id: usize) {
if let Ok(mut tasks) = self.tasks.lock() {
if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
task.finished = true;
}
}
}
pub fn remove(&self, task_id: usize) {
if let Ok(mut tasks) = self.tasks.lock() {
tasks.retain(|t| t.id != task_id);
}
}
pub fn is_finished(&self) -> bool {
self.tasks
.lock()
.map(|tasks| tasks.iter().all(|t| t.finished))
.unwrap_or(true)
}
pub fn render_to_string(&self) -> String {
let context = RenderContext {
width: 80,
height: None,
};
let segments = self.render(&context);
let mut result = String::new();
for segment in segments {
result.push_str(&segment.plain_text());
if segment.newline {
result.push('\n');
}
}
result
}
pub fn print(&self) {
let output = self.render_to_string();
let tasks = self.tasks.lock().unwrap();
let num_lines = tasks.len();
drop(tasks);
if num_lines > 0 {
print!("\x1B[{}A", num_lines);
}
for line in output.lines() {
println!("\x1B[2K{}", line);
}
let _ = io::stdout().flush();
}
pub fn start(&mut self) {
if self.started {
return;
}
if let Some(ref console) = self.console {
console.show_cursor(false);
}
self.started = true;
*self.height.lock().unwrap() = 0;
self.stop_signal.store(false, Ordering::SeqCst);
if self.auto_refresh && self.console.is_some() {
let _tasks = Arc::clone(&self.tasks);
let _height = Arc::clone(&self.height);
let _stop_signal = Arc::clone(&self.stop_signal);
let _interval_ms = (1000.0 / self.refresh_per_second) as u64;
}
self.refresh();
}
pub fn stop(&mut self) {
if !self.started {
return;
}
self.stop_signal.store(true, Ordering::SeqCst);
if let Some(handle) = self.refresh_thread.take() {
let _ = handle.join();
}
let current_height = *self.height.lock().unwrap();
if let Some(ref console) = self.console {
if current_height > 0 {
console.move_cursor_up(current_height as u16);
for _ in 0..current_height {
console.clear_line();
console.move_cursor_down(1);
}
console.move_cursor_up(current_height as u16);
}
if !self.transient {
console.print_renderable(self);
console.newline();
}
console.show_cursor(true);
}
self.started = false;
*self.height.lock().unwrap() = 0;
}
pub fn refresh(&mut self) {
if let Some(ref console) = self.console {
if !self.started {
console.print_renderable(self);
console.newline();
return;
}
let current_height = *self.height.lock().unwrap();
if current_height > 0 {
console.move_cursor_up(current_height as u16);
}
let width = console.get_width();
let context = RenderContext {
width,
height: None,
};
let segments = self.render(&context);
let mut lines = 0;
for segment in &segments {
if segment.newline {
lines += 1;
}
}
console.write_segments(&segments);
if !segments.is_empty() && !segments.last().unwrap().newline {
console.newline();
lines += 1;
}
if current_height > lines {
let diff = current_height - lines;
for _ in 0..diff {
console.clear_line();
console.newline();
}
console.move_cursor_up(diff as u16);
}
*self.height.lock().unwrap() = lines;
} else {
self.print();
}
}
pub fn run<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.start();
let result = f(self);
self.stop();
result
}
pub fn is_started(&self) -> bool {
self.started
}
}
impl Renderable for Progress {
fn render(&self, _context: &RenderContext) -> Vec<Segment> {
let tasks = self.tasks.lock().unwrap();
let mut segments = Vec::new();
for task in tasks.iter() {
let mut spans = Vec::new();
for (i, column) in self.columns.iter().enumerate() {
if i > 0 {
spans.push(Span::raw(" "));
}
spans.extend(column.render(task));
}
segments.push(Segment::line(spans));
}
segments
}
}
impl Drop for Progress {
fn drop(&mut self) {
if self.started {
self.stop();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_percentage() {
let mut task = Task::new(0, "Test", Some(100));
assert_eq!(task.percentage(), 0.0);
task.completed = 50;
assert!((task.percentage() - 0.5).abs() < 0.01);
task.completed = 100;
assert!((task.percentage() - 1.0).abs() < 0.01);
}
#[test]
fn test_progress_add_task() {
let progress = Progress::new();
let id1 = progress.add_task("Task 1", Some(100));
let id2 = progress.add_task("Task 2", Some(200));
assert_eq!(id1, 0);
assert_eq!(id2, 1);
}
#[test]
fn test_progress_advance() {
let progress = Progress::new();
let id = progress.add_task("Test", Some(100));
progress.advance(id, 25);
progress.advance(id, 25);
let tasks = progress.tasks.lock().unwrap();
assert_eq!(tasks[0].completed, 50);
}
#[test]
fn test_progress_bar_render() {
use crate::progress::columns::BarColumn;
let bar_col = BarColumn::new(10);
let mut task = Task::new(0, "Test", Some(100));
task.completed = 50;
let spans = bar_col.render(&task);
assert_eq!(spans.len(), 3);
}
}