1use std::io;
2use std::ops::{Add, AddAssign, Sub};
3use std::slice::SliceIndex;
4use std::sync::{Arc, RwLock, RwLockWriteGuard};
5use std::thread::panicking;
6use std::time::Duration;
7#[cfg(not(target_arch = "wasm32"))]
8use std::time::Instant;
9
10use console::Term;
11#[cfg(target_arch = "wasm32")]
12use web_time::Instant;
13
14use crate::multi::{MultiProgressAlignment, MultiState};
15use crate::TermLike;
16
17#[derive(Debug)]
25pub struct ProgressDrawTarget {
26 kind: TargetKind,
27}
28
29impl ProgressDrawTarget {
30 pub fn stdout() -> Self {
34 Self::term(Term::buffered_stdout(), 20)
35 }
36
37 pub fn stderr() -> Self {
42 Self::term(Term::buffered_stderr(), 20)
43 }
44
45 pub fn stdout_with_hz(refresh_rate: u8) -> Self {
49 Self::term(Term::buffered_stdout(), refresh_rate)
50 }
51
52 pub fn stderr_with_hz(refresh_rate: u8) -> Self {
56 Self::term(Term::buffered_stderr(), refresh_rate)
57 }
58
59 pub(crate) fn new_remote(state: Arc<RwLock<MultiState>>, idx: usize) -> Self {
60 Self {
61 kind: TargetKind::Multi { state, idx },
62 }
63 }
64
65 pub fn term(term: Term, refresh_rate: u8) -> Self {
74 Self {
75 kind: TargetKind::Term {
76 term,
77 last_line_count: VisualLines::default(),
78 rate_limiter: RateLimiter::new(refresh_rate),
79 draw_state: DrawState::default(),
80 },
81 }
82 }
83
84 pub fn term_like(term_like: Box<dyn TermLike>) -> Self {
86 Self {
87 kind: TargetKind::TermLike {
88 inner: term_like,
89 last_line_count: VisualLines::default(),
90 rate_limiter: None,
91 draw_state: DrawState::default(),
92 },
93 }
94 }
95
96 pub fn term_like_with_hz(term_like: Box<dyn TermLike>, refresh_rate: u8) -> Self {
99 Self {
100 kind: TargetKind::TermLike {
101 inner: term_like,
102 last_line_count: VisualLines::default(),
103 rate_limiter: Option::from(RateLimiter::new(refresh_rate)),
104 draw_state: DrawState::default(),
105 },
106 }
107 }
108
109 pub fn hidden() -> Self {
113 Self {
114 kind: TargetKind::Hidden,
115 }
116 }
117
118 pub fn is_hidden(&self) -> bool {
123 match self.kind {
124 TargetKind::Hidden => true,
125 TargetKind::Term { ref term, .. } => !term.is_term(),
126 TargetKind::Multi { ref state, .. } => state.read().unwrap().is_hidden(),
127 _ => false,
128 }
129 }
130
131 pub(crate) fn width(&self) -> Option<u16> {
133 match self.kind {
134 TargetKind::Term { ref term, .. } => Some(term.size().1),
135 TargetKind::Multi { ref state, .. } => state.read().unwrap().width(),
136 TargetKind::TermLike { ref inner, .. } => Some(inner.width()),
137 TargetKind::Hidden => None,
138 }
139 }
140
141 pub(crate) fn mark_zombie(&self) {
144 if let TargetKind::Multi { idx, state } = &self.kind {
145 state.write().unwrap().mark_zombie(*idx);
146 }
147 }
148
149 pub(crate) fn set_move_cursor(&mut self, move_cursor: bool) {
151 match &mut self.kind {
152 TargetKind::Term { draw_state, .. } => draw_state.move_cursor = move_cursor,
153 TargetKind::TermLike { draw_state, .. } => draw_state.move_cursor = move_cursor,
154 _ => {}
155 }
156 }
157
158 pub(crate) fn drawable(&mut self, force_draw: bool, now: Instant) -> Option<Drawable<'_>> {
160 match &mut self.kind {
161 TargetKind::Term {
162 term,
163 last_line_count,
164 rate_limiter,
165 draw_state,
166 } => {
167 if !term.is_term() {
168 return None;
169 }
170
171 match force_draw || rate_limiter.allow(now) {
172 true => Some(Drawable::Term {
173 term,
174 last_line_count,
175 draw_state,
176 }),
177 false => None, }
179 }
180 TargetKind::Multi { idx, state, .. } => {
181 let state = state.write().unwrap();
182 Some(Drawable::Multi {
183 idx: *idx,
184 state,
185 force_draw,
186 now,
187 })
188 }
189 TargetKind::TermLike {
190 inner,
191 last_line_count,
192 rate_limiter,
193 draw_state,
194 } => match force_draw || rate_limiter.as_mut().map_or(true, |r| r.allow(now)) {
195 true => Some(Drawable::TermLike {
196 term_like: &**inner,
197 last_line_count,
198 draw_state,
199 }),
200 false => None, },
202 _ => None,
204 }
205 }
206
207 pub(crate) fn disconnect(&self, now: Instant) {
209 match self.kind {
210 TargetKind::Term { .. } => {}
211 TargetKind::Multi { idx, ref state, .. } => {
212 let state = state.write().unwrap();
213 let _ = Drawable::Multi {
214 state,
215 idx,
216 force_draw: true,
217 now,
218 }
219 .clear();
220 }
221 TargetKind::Hidden => {}
222 TargetKind::TermLike { .. } => {}
223 };
224 }
225
226 pub(crate) fn remote(&self) -> Option<(&Arc<RwLock<MultiState>>, usize)> {
227 match &self.kind {
228 TargetKind::Multi { state, idx } => Some((state, *idx)),
229 _ => None,
230 }
231 }
232
233 pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
234 self.kind.adjust_last_line_count(adjust);
235 }
236}
237
238#[derive(Debug)]
239enum TargetKind {
240 Term {
241 term: Term,
242 last_line_count: VisualLines,
243 rate_limiter: RateLimiter,
244 draw_state: DrawState,
245 },
246 Multi {
247 state: Arc<RwLock<MultiState>>,
248 idx: usize,
249 },
250 Hidden,
251 TermLike {
252 inner: Box<dyn TermLike>,
253 last_line_count: VisualLines,
254 rate_limiter: Option<RateLimiter>,
255 draw_state: DrawState,
256 },
257}
258
259impl TargetKind {
260 fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
262 let last_line_count = match self {
263 Self::Term {
264 last_line_count, ..
265 } => last_line_count,
266 Self::TermLike {
267 last_line_count, ..
268 } => last_line_count,
269 _ => return,
270 };
271
272 match adjust {
273 LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
274 LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
275 }
276 }
277}
278
279pub(crate) enum Drawable<'a> {
280 Term {
281 term: &'a Term,
282 last_line_count: &'a mut VisualLines,
283 draw_state: &'a mut DrawState,
284 },
285 Multi {
286 state: RwLockWriteGuard<'a, MultiState>,
287 idx: usize,
288 force_draw: bool,
289 now: Instant,
290 },
291 TermLike {
292 term_like: &'a dyn TermLike,
293 last_line_count: &'a mut VisualLines,
294 draw_state: &'a mut DrawState,
295 },
296}
297
298impl<'a> Drawable<'a> {
299 pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
301 let last_line_count: &mut VisualLines = match self {
302 Drawable::Term {
303 last_line_count, ..
304 } => last_line_count,
305 Drawable::TermLike {
306 last_line_count, ..
307 } => last_line_count,
308 _ => return,
309 };
310
311 match adjust {
312 LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
313 LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
314 }
315 }
316
317 pub(crate) fn state(&mut self) -> DrawStateWrapper<'_> {
318 let mut state = match self {
319 Drawable::Term { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
320 Drawable::Multi { state, idx, .. } => state.draw_state(*idx),
321 Drawable::TermLike { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
322 };
323
324 state.reset();
325 state
326 }
327
328 pub(crate) fn clear(mut self) -> io::Result<()> {
329 let state = self.state();
330 drop(state);
331 self.draw()
332 }
333
334 pub(crate) fn draw(self) -> io::Result<()> {
335 match self {
336 Drawable::Term {
337 term,
338 last_line_count,
339 draw_state,
340 } => draw_state.draw_to_term(term, last_line_count),
341 Drawable::Multi {
342 mut state,
343 force_draw,
344 now,
345 ..
346 } => state.draw(force_draw, None, now),
347 Drawable::TermLike {
348 term_like,
349 last_line_count,
350 draw_state,
351 } => draw_state.draw_to_term(term_like, last_line_count),
352 }
353 }
354}
355
356pub(crate) enum LineAdjust {
357 Clear(VisualLines),
359 Keep(VisualLines),
361}
362
363pub(crate) struct DrawStateWrapper<'a> {
364 state: &'a mut DrawState,
365 orphan_lines: Option<&'a mut Vec<String>>,
366}
367
368impl<'a> DrawStateWrapper<'a> {
369 pub(crate) fn for_term(state: &'a mut DrawState) -> Self {
370 Self {
371 state,
372 orphan_lines: None,
373 }
374 }
375
376 pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<String>) -> Self {
377 Self {
378 state,
379 orphan_lines: Some(orphan_lines),
380 }
381 }
382}
383
384impl std::ops::Deref for DrawStateWrapper<'_> {
385 type Target = DrawState;
386
387 fn deref(&self) -> &Self::Target {
388 self.state
389 }
390}
391
392impl std::ops::DerefMut for DrawStateWrapper<'_> {
393 fn deref_mut(&mut self) -> &mut Self::Target {
394 self.state
395 }
396}
397
398impl Drop for DrawStateWrapper<'_> {
399 fn drop(&mut self) {
400 if let Some(orphaned) = &mut self.orphan_lines {
401 orphaned.extend(self.state.lines.drain(..self.state.orphan_lines_count));
402 self.state.orphan_lines_count = 0;
403 }
404 }
405}
406
407#[derive(Debug)]
408struct RateLimiter {
409 interval: u16, capacity: u8,
411 prev: Instant,
412}
413
414impl RateLimiter {
416 fn new(rate: u8) -> Self {
417 Self {
418 interval: 1000 / (rate as u16), capacity: MAX_BURST,
420 prev: Instant::now(),
421 }
422 }
423
424 fn allow(&mut self, now: Instant) -> bool {
425 if now < self.prev {
426 return false;
427 }
428
429 let elapsed = now - self.prev;
430 if self.capacity == 0 && elapsed < Duration::from_millis(self.interval as u64) {
434 return false;
435 }
436
437 let (new, remainder) = (
441 elapsed.as_millis() / self.interval as u128,
442 elapsed.as_nanos() % (self.interval as u128 * 1_000_000),
443 );
444
445 self.capacity = Ord::min(MAX_BURST as u128, (self.capacity as u128) + new - 1) as u8;
448 self.prev = now
451 .checked_sub(Duration::from_nanos(remainder as u64))
452 .unwrap();
453 true
454 }
455}
456
457const MAX_BURST: u8 = 20;
458
459#[derive(Clone, Debug, Default)]
461pub(crate) struct DrawState {
462 pub(crate) lines: Vec<String>,
464 pub(crate) orphan_lines_count: usize,
468 pub(crate) move_cursor: bool,
470 pub(crate) alignment: MultiProgressAlignment,
472}
473
474impl DrawState {
475 fn draw_to_term(
476 &mut self,
477 term: &(impl TermLike + ?Sized),
478 last_line_count: &mut VisualLines,
479 ) -> io::Result<()> {
480 if panicking() {
481 return Ok(());
482 }
483
484 if !self.lines.is_empty() && self.move_cursor {
485 term.move_cursor_up(last_line_count.as_usize().saturating_sub(1))?;
487 term.write_str("\r")?;
488 } else {
489 let n = last_line_count.as_usize();
491 term.move_cursor_up(n.saturating_sub(1))?;
492 for i in 0..n {
493 term.clear_line()?;
494 if i + 1 != n {
495 term.move_cursor_down(1)?;
496 }
497 }
498 term.move_cursor_up(n.saturating_sub(1))?;
499 }
500
501 let width = term.width() as usize;
502 let visual_lines = self.visual_line_count(.., width);
503 let shift = match self.alignment {
504 MultiProgressAlignment::Bottom if visual_lines < *last_line_count => {
505 let shift = *last_line_count - visual_lines;
506 for _ in 0..shift.as_usize() {
507 term.write_line("")?;
508 }
509 shift
510 }
511 _ => VisualLines::default(),
512 };
513
514 let term_height = term.height() as usize;
515 let term_width = term.width() as usize;
516 let len = self.lines.len();
517 debug_assert!(self.orphan_lines_count <= self.lines.len());
518 let orphan_visual_line_count =
519 self.visual_line_count(..self.orphan_lines_count, term_width);
520 let mut real_len = VisualLines::default();
521 let mut last_line_filler = 0;
522 for (idx, line) in self.lines.iter().enumerate() {
523 let line_width = console::measure_text_width(line);
524 let diff = if line.is_empty() {
525 1
527 } else {
528 let terminal_len = (line_width as f64 / term_width as f64).ceil() as usize;
531
532 usize::max(terminal_len, 1)
537 }
538 .into();
539 if self.orphan_lines_count <= idx {
541 debug_assert!(orphan_visual_line_count <= real_len);
543 if real_len - orphan_visual_line_count + diff > term_height.into() {
545 break;
546 }
547 }
548 real_len += diff;
549 if idx != 0 {
550 term.write_line("")?;
551 }
552 term.write_str(line)?;
553 if idx + 1 == len {
554 last_line_filler = term_width.saturating_sub(line_width);
557 }
558 }
559 term.write_str(&" ".repeat(last_line_filler))?;
560
561 term.flush()?;
562 *last_line_count = real_len - orphan_visual_line_count + shift;
563 Ok(())
564 }
565
566 fn reset(&mut self) {
567 self.lines.clear();
568 self.orphan_lines_count = 0;
569 }
570
571 pub(crate) fn visual_line_count(
572 &self,
573 range: impl SliceIndex<[String], Output = [String]>,
574 width: usize,
575 ) -> VisualLines {
576 visual_line_count(&self.lines[range], width)
577 }
578}
579
580#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
581pub(crate) struct VisualLines(usize);
582
583impl VisualLines {
584 pub(crate) fn saturating_add(&self, other: Self) -> Self {
585 Self(self.0.saturating_add(other.0))
586 }
587
588 pub(crate) fn saturating_sub(&self, other: Self) -> Self {
589 Self(self.0.saturating_sub(other.0))
590 }
591
592 pub(crate) fn as_usize(&self) -> usize {
593 self.0
594 }
595}
596
597impl Add for VisualLines {
598 type Output = Self;
599
600 fn add(self, rhs: Self) -> Self::Output {
601 Self(self.0 + rhs.0)
602 }
603}
604
605impl AddAssign for VisualLines {
606 fn add_assign(&mut self, rhs: Self) {
607 self.0 += rhs.0;
608 }
609}
610
611impl<T: Into<usize>> From<T> for VisualLines {
612 fn from(value: T) -> Self {
613 Self(value.into())
614 }
615}
616
617impl Sub for VisualLines {
618 type Output = Self;
619
620 fn sub(self, rhs: Self) -> Self::Output {
621 Self(self.0 - rhs.0)
622 }
623}
624
625pub(crate) fn visual_line_count(lines: &[impl AsRef<str>], width: usize) -> VisualLines {
628 let mut real_lines = 0;
629 for line in lines {
630 let effective_line_length = console::measure_text_width(line.as_ref());
631 real_lines += usize::max(
632 (effective_line_length as f64 / width as f64).ceil() as usize,
633 1,
634 );
635 }
636
637 real_lines.into()
638}
639
640#[cfg(test)]
641mod tests {
642 use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
643
644 #[test]
645 fn multi_is_hidden() {
646 let mp = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
647
648 let pb = mp.add(ProgressBar::new(100));
649 assert!(mp.is_hidden());
650 assert!(pb.is_hidden());
651 }
652
653 #[test]
654 fn real_line_count_test() {
655 #[derive(Debug)]
656 struct Case {
657 lines: &'static [&'static str],
658 expectation: usize,
659 width: usize,
660 }
661
662 let lines_and_expectations = [
663 Case {
664 lines: &["1234567890"],
665 expectation: 1,
666 width: 10,
667 },
668 Case {
669 lines: &["1234567890"],
670 expectation: 2,
671 width: 5,
672 },
673 Case {
674 lines: &["1234567890"],
675 expectation: 3,
676 width: 4,
677 },
678 Case {
679 lines: &["1234567890"],
680 expectation: 4,
681 width: 3,
682 },
683 Case {
684 lines: &["1234567890", "", "1234567890"],
685 expectation: 3,
686 width: 10,
687 },
688 Case {
689 lines: &["1234567890", "", "1234567890"],
690 expectation: 5,
691 width: 5,
692 },
693 Case {
694 lines: &["1234567890", "", "1234567890"],
695 expectation: 7,
696 width: 4,
697 },
698 Case {
699 lines: &["aaaaaaaaaaaaa", "", "bbbbbbbbbbbbbbbbb", "", "ccccccc"],
700 expectation: 8,
701 width: 7,
702 },
703 Case {
704 lines: &["", "", "", "", ""],
705 expectation: 5,
706 width: 6,
707 },
708 Case {
709 lines: &["\u{1b}[1m\u{1b}[1m\u{1b}[1m", "\u{1b}[1m\u{1b}[1m\u{1b}[1m"],
711 expectation: 2,
712 width: 5,
713 },
714 Case {
715 lines: &[
717 "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
718 "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
719 ],
720 expectation: 2,
721 width: 5,
722 },
723 Case {
724 lines: &[
726 "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
727 "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
728 ],
729 expectation: 4,
730 width: 5,
731 },
732 ];
733
734 for case in lines_and_expectations.iter() {
735 let result = super::visual_line_count(case.lines, case.width);
736 assert_eq!(result, case.expectation.into(), "case: {:?}", case);
737 }
738 }
739}