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