1use crate::console::RenderContext;
2use crate::progress::columns::{
3 BarColumn, PercentageColumn, ProgressColumn, TextColumn, TimeRemainingColumn,
4};
5use crate::renderable::{Renderable, Segment};
6use crate::style::{Color, Style};
7use crate::text::Span;
8use std::io::{self, Write};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11
12#[derive(Debug, Clone)]
14pub struct Task {
15 pub id: usize,
17 pub description: String,
19 pub total: Option<u64>,
21 pub completed: u64,
23 pub start_time: Instant,
25 pub finished: bool,
27 pub style: Style,
29}
30
31impl Task {
32 pub fn new(id: usize, description: &str, total: Option<u64>) -> Self {
34 Task {
35 id,
36 description: description.to_string(),
37 total,
38 completed: 0,
39 start_time: Instant::now(),
40 finished: false,
41 style: Style::new().foreground(Color::Cyan),
42 }
43 }
44
45 pub fn percentage(&self) -> f64 {
47 match self.total {
48 Some(total) if total > 0 => (self.completed as f64 / total as f64).min(1.0),
49 _ => 0.0,
50 }
51 }
52
53 pub fn elapsed(&self) -> Duration {
55 self.start_time.elapsed()
56 }
57
58 pub fn eta(&self) -> Option<Duration> {
60 if self.completed == 0 {
61 return None;
62 }
63
64 let elapsed = self.elapsed().as_secs_f64();
65 let rate = self.completed as f64 / elapsed;
66
67 self.total.and_then(|total| {
68 let remaining = total.saturating_sub(self.completed);
69 if rate > 0.0 {
70 Some(Duration::from_secs_f64(remaining as f64 / rate))
71 } else {
72 None
73 }
74 })
75 }
76
77 pub fn speed(&self) -> f64 {
79 let elapsed = self.elapsed().as_secs_f64();
80 if elapsed > 0.0 {
81 self.completed as f64 / elapsed
82 } else {
83 0.0
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
91pub struct ProgressBar {
92 pub bar_width: usize,
94 pub complete_char: char,
96 pub remaining_char: char,
98 pub complete_style: Style,
100 pub remaining_style: Style,
102}
103
104impl Default for ProgressBar {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl ProgressBar {
111 pub fn new() -> Self {
113 ProgressBar {
114 bar_width: 40,
115 complete_char: '━',
116 remaining_char: '━',
117 complete_style: Style::new().foreground(Color::Cyan),
118 remaining_style: Style::new().foreground(Color::BrightBlack),
119 }
120 }
121 pub fn width(mut self, width: usize) -> Self {
124 self.bar_width = width;
125 self
126 }
127}
128
129#[derive(Debug)]
131pub struct Progress {
132 tasks: Arc<Mutex<Vec<Task>>>,
134 next_id: Arc<Mutex<usize>>,
136 columns: Vec<Box<dyn ProgressColumn>>,
138 #[allow(dead_code)]
140 visible: bool,
141 #[allow(dead_code)]
143 refresh_rate_ms: u64,
144}
145
146impl Default for Progress {
147 fn default() -> Self {
148 Self::new()
149 }
150}
151
152impl Progress {
153 pub fn new() -> Self {
155 Progress {
156 tasks: Arc::new(Mutex::new(Vec::new())),
157 next_id: Arc::new(Mutex::new(0)),
158 columns: vec![
159 Box::new(TextColumn::new("[progress.description]")),
160 Box::new(BarColumn::new(40)),
161 Box::new(PercentageColumn::new()),
162 Box::new(TimeRemainingColumn),
163 ],
164 visible: true,
165 refresh_rate_ms: 100,
166 }
167 }
168
169 pub fn with_columns(mut self, columns: Vec<Box<dyn ProgressColumn>>) -> Self {
171 self.columns = columns;
172 self
173 }
174
175 pub fn add_task(&self, description: &str, total: Option<u64>) -> usize {
177 let mut next_id = self.next_id.lock().unwrap();
178 let id = *next_id;
179 *next_id += 1;
180
181 let task = Task::new(id, description, total);
182 self.tasks.lock().unwrap().push(task);
183
184 id
185 }
186
187 pub fn advance(&self, task_id: usize, amount: u64) {
189 if let Ok(mut tasks) = self.tasks.lock() {
190 if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
191 task.completed += amount;
192 if let Some(total) = task.total {
193 if task.completed >= total {
194 task.finished = true;
195 }
196 }
197 }
198 }
199 }
200
201 pub fn update(&self, task_id: usize, completed: u64) {
203 if let Ok(mut tasks) = self.tasks.lock() {
204 if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
205 task.completed = completed;
206 if let Some(total) = task.total {
207 if task.completed >= total {
208 task.finished = true;
209 }
210 }
211 }
212 }
213 }
214
215 pub fn finish(&self, task_id: usize) {
217 if let Ok(mut tasks) = self.tasks.lock() {
218 if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
219 task.finished = true;
220 }
221 }
222 }
223
224 pub fn remove(&self, task_id: usize) {
226 if let Ok(mut tasks) = self.tasks.lock() {
227 tasks.retain(|t| t.id != task_id);
228 }
229 }
230
231 pub fn is_finished(&self) -> bool {
233 self.tasks
234 .lock()
235 .map(|tasks| tasks.iter().all(|t| t.finished))
236 .unwrap_or(true)
237 }
238
239 pub fn render_to_string(&self) -> String {
241 let context = RenderContext {
242 width: 80,
243 height: None,
244 };
245 let segments = self.render(&context);
246
247 let mut result = String::new();
248 for segment in segments {
249 result.push_str(&segment.plain_text());
250 if segment.newline {
251 result.push('\n');
252 }
253 }
254 result
255 }
256
257 pub fn print(&self) {
259 let output = self.render_to_string();
260
261 let tasks = self.tasks.lock().unwrap();
263 let num_lines = tasks.len();
264 drop(tasks);
265
266 if num_lines > 0 {
267 print!("\x1B[{}A", num_lines);
269 }
270
271 for line in output.lines() {
273 println!("\x1B[2K{}", line);
274 }
275
276 let _ = io::stdout().flush();
277 }
278}
279
280impl Renderable for Progress {
281 fn render(&self, _context: &RenderContext) -> Vec<Segment> {
282 let tasks = self.tasks.lock().unwrap();
283 let mut segments = Vec::new();
284
285 for task in tasks.iter() {
286 let mut spans = Vec::new();
287
288 for (i, column) in self.columns.iter().enumerate() {
289 if i > 0 {
290 spans.push(Span::raw(" "));
291 }
292 spans.extend(column.render(task));
293 }
294
295 segments.push(Segment::line(spans));
296 }
297
298 segments
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_task_percentage() {
308 let mut task = Task::new(0, "Test", Some(100));
309 assert_eq!(task.percentage(), 0.0);
310
311 task.completed = 50;
312 assert!((task.percentage() - 0.5).abs() < 0.01);
313
314 task.completed = 100;
315 assert!((task.percentage() - 1.0).abs() < 0.01);
316 }
317
318 #[test]
319 fn test_progress_add_task() {
320 let progress = Progress::new();
321 let id1 = progress.add_task("Task 1", Some(100));
322 let id2 = progress.add_task("Task 2", Some(200));
323
324 assert_eq!(id1, 0);
325 assert_eq!(id2, 1);
326 }
327
328 #[test]
329 fn test_progress_advance() {
330 let progress = Progress::new();
331 let id = progress.add_task("Test", Some(100));
332
333 progress.advance(id, 25);
334 progress.advance(id, 25);
335
336 let tasks = progress.tasks.lock().unwrap();
337 assert_eq!(tasks[0].completed, 50);
338 }
339
340 #[test]
341 fn test_progress_bar_render() {
342 use crate::progress::columns::BarColumn;
343 let bar_col = BarColumn::new(10);
344 let mut task = Task::new(0, "Test", Some(100));
345 task.completed = 50;
346
347 let spans = bar_col.render(&task);
348 assert_eq!(spans.len(), 2);
349 }
350}