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, TermTarget};
11#[cfg(all(target_arch = "wasm32", feature = "wasmbind"))]
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 {
76 if !term.features().colors_supported() {
77 return Self::hidden();
78 }
79 Self {
80 kind: TargetKind::Term {
81 term,
82 last_line_count: VisualLines::default(),
83 rate_limiter: RateLimiter::new(refresh_rate),
84 draw_state: DrawState::default(),
85 },
86 }
87 }
88
89 pub fn term_like(term_like: Box<dyn TermLike>) -> Self {
96 Self {
97 kind: TargetKind::TermLike {
98 inner: term_like,
99 last_line_count: VisualLines::default(),
100 rate_limiter: None,
101 draw_state: DrawState::default(),
102 },
103 }
104 }
105
106 pub fn term_like_with_hz(term_like: Box<dyn TermLike>, refresh_rate: u8) -> Self {
109 Self {
110 kind: TargetKind::TermLike {
111 inner: term_like,
112 last_line_count: VisualLines::default(),
113 rate_limiter: Option::from(RateLimiter::new(refresh_rate)),
114 draw_state: DrawState::default(),
115 },
116 }
117 }
118
119 pub fn hidden() -> Self {
123 Self {
124 kind: TargetKind::Hidden,
125 }
126 }
127
128 pub fn is_hidden(&self) -> bool {
133 match self.kind {
134 TargetKind::Hidden => true,
135 TargetKind::Term { ref term, .. } => !term.is_term(),
136 TargetKind::Multi { ref state, .. } => state.read().unwrap().is_hidden(),
137 _ => false,
138 }
139 }
140
141 pub(crate) fn is_stderr(&self) -> bool {
144 match &self.kind {
145 TargetKind::Term { term, .. } => matches!(term.target(), TermTarget::Stderr),
146 _ => false,
147 }
148 }
149
150 pub(crate) fn width(&self) -> Option<u16> {
152 match self.kind {
153 TargetKind::Term { ref term, .. } => Some(term.size().1),
154 TargetKind::Multi { ref state, .. } => state.read().unwrap().width(),
155 TargetKind::TermLike { ref inner, .. } => Some(inner.width()),
156 TargetKind::Hidden => None,
157 }
158 }
159
160 pub(crate) fn mark_zombie(&self) {
163 if let TargetKind::Multi { idx, state } = &self.kind {
164 state.write().unwrap().mark_zombie(*idx);
165 }
166 }
167
168 pub(crate) fn set_move_cursor(&mut self, move_cursor: bool) {
170 match &mut self.kind {
171 TargetKind::Term { draw_state, .. } => draw_state.move_cursor = move_cursor,
172 TargetKind::TermLike { draw_state, .. } => draw_state.move_cursor = move_cursor,
173 _ => {}
174 }
175 }
176
177 pub(crate) fn drawable(&mut self, force_draw: bool, now: Instant) -> Option<Drawable<'_>> {
179 match &mut self.kind {
180 TargetKind::Term {
181 term,
182 last_line_count,
183 rate_limiter,
184 draw_state,
185 } => {
186 match force_draw || rate_limiter.allow(now) {
187 true => Some(Drawable::Term {
188 term,
189 last_line_count,
190 draw_state,
191 }),
192 false => None, }
194 }
195 TargetKind::Multi { idx, state, .. } => {
196 let state = state.write().unwrap();
197 Some(Drawable::Multi {
198 idx: *idx,
199 state,
200 force_draw,
201 now,
202 })
203 }
204 TargetKind::TermLike {
205 inner,
206 last_line_count,
207 rate_limiter,
208 draw_state,
209 } => match force_draw || rate_limiter.as_mut().map_or(true, |r| r.allow(now)) {
210 true => Some(Drawable::TermLike {
211 term_like: &**inner,
212 last_line_count,
213 draw_state,
214 }),
215 false => None, },
217 _ => None,
219 }
220 }
221
222 pub(crate) fn disconnect(&self, now: Instant) {
224 match self.kind {
225 TargetKind::Term { .. } => {}
226 TargetKind::Multi { idx, ref state, .. } => {
227 let state = state.write().unwrap();
228 let _ = Drawable::Multi {
229 state,
230 idx,
231 force_draw: true,
232 now,
233 }
234 .clear();
235 }
236 TargetKind::Hidden => {}
237 TargetKind::TermLike { .. } => {}
238 };
239 }
240
241 pub(crate) fn remote(&self) -> Option<(&Arc<RwLock<MultiState>>, usize)> {
242 match &self.kind {
243 TargetKind::Multi { state, idx } => Some((state, *idx)),
244 _ => None,
245 }
246 }
247
248 pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
249 self.kind.adjust_last_line_count(adjust);
250 }
251}
252
253#[derive(Debug)]
254enum TargetKind {
255 Term {
256 term: Term,
257 last_line_count: VisualLines,
258 rate_limiter: RateLimiter,
259 draw_state: DrawState,
260 },
261 Multi {
262 state: Arc<RwLock<MultiState>>,
263 idx: usize,
264 },
265 Hidden,
266 TermLike {
267 inner: Box<dyn TermLike>,
268 last_line_count: VisualLines,
269 rate_limiter: Option<RateLimiter>,
270 draw_state: DrawState,
271 },
272}
273
274impl TargetKind {
275 fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
277 let last_line_count = match self {
278 Self::Term {
279 last_line_count, ..
280 } => last_line_count,
281 Self::TermLike {
282 last_line_count, ..
283 } => last_line_count,
284 _ => return,
285 };
286
287 match adjust {
288 LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
289 LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
290 }
291 }
292}
293
294pub(crate) enum Drawable<'a> {
295 Term {
296 term: &'a Term,
297 last_line_count: &'a mut VisualLines,
298 draw_state: &'a mut DrawState,
299 },
300 Multi {
301 state: RwLockWriteGuard<'a, MultiState>,
302 idx: usize,
303 force_draw: bool,
304 now: Instant,
305 },
306 TermLike {
307 term_like: &'a dyn TermLike,
308 last_line_count: &'a mut VisualLines,
309 draw_state: &'a mut DrawState,
310 },
311}
312
313impl Drawable<'_> {
314 pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
316 let last_line_count: &mut VisualLines = match self {
317 Drawable::Term {
318 last_line_count, ..
319 } => last_line_count,
320 Drawable::TermLike {
321 last_line_count, ..
322 } => last_line_count,
323 _ => return,
324 };
325
326 match adjust {
327 LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
328 LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
329 }
330 }
331
332 pub(crate) fn state(&mut self) -> DrawStateWrapper<'_> {
333 let mut state = match self {
334 Drawable::Term { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
335 Drawable::Multi { state, idx, .. } => state.draw_state(*idx),
336 Drawable::TermLike { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
337 };
338
339 state.reset();
340 state
341 }
342
343 pub(crate) fn clear(mut self) -> io::Result<()> {
344 let state = self.state();
345 drop(state);
346 self.draw()
347 }
348
349 pub(crate) fn draw(self) -> io::Result<()> {
350 match self {
351 Drawable::Term {
352 term,
353 last_line_count,
354 draw_state,
355 } => draw_state.draw_to_term(term, last_line_count),
356 Drawable::Multi {
357 mut state,
358 force_draw,
359 now,
360 ..
361 } => state.draw(force_draw, None, now),
362 Drawable::TermLike {
363 term_like,
364 last_line_count,
365 draw_state,
366 } => draw_state.draw_to_term(term_like, last_line_count),
367 }
368 }
369
370 pub(crate) fn width(&self) -> Option<u16> {
371 match self {
372 Self::Term { term, .. } => Some(term.size().1),
373 Self::Multi { state, .. } => state.width(),
374 Self::TermLike { term_like, .. } => Some(term_like.width()),
375 }
376 }
377}
378
379pub(crate) enum LineAdjust {
380 Clear(VisualLines),
382 Keep(VisualLines),
384}
385
386pub(crate) struct DrawStateWrapper<'a> {
387 state: &'a mut DrawState,
388 orphan_lines: Option<&'a mut Vec<LineType>>,
389}
390
391impl<'a> DrawStateWrapper<'a> {
392 pub(crate) fn for_term(state: &'a mut DrawState) -> Self {
393 Self {
394 state,
395 orphan_lines: None,
396 }
397 }
398
399 pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<LineType>) -> Self {
400 Self {
401 state,
402 orphan_lines: Some(orphan_lines),
403 }
404 }
405}
406
407impl std::ops::Deref for DrawStateWrapper<'_> {
408 type Target = DrawState;
409
410 fn deref(&self) -> &Self::Target {
411 self.state
412 }
413}
414
415impl std::ops::DerefMut for DrawStateWrapper<'_> {
416 fn deref_mut(&mut self) -> &mut Self::Target {
417 self.state
418 }
419}
420
421impl Drop for DrawStateWrapper<'_> {
422 fn drop(&mut self) {
423 if let Some(text_lines) = &mut self.orphan_lines {
424 let mut lines = Vec::new();
427
428 for line in self.state.lines.drain(..) {
429 match &line {
430 LineType::Text(_) | LineType::Empty => text_lines.push(line),
431 _ => lines.push(line),
432 }
433 }
434
435 self.state.lines = lines;
436 }
437 }
438}
439
440#[derive(Debug)]
441struct RateLimiter {
442 interval: u16, capacity: u8,
444 prev: Instant,
445}
446
447impl RateLimiter {
449 fn new(rate: u8) -> Self {
450 Self {
451 interval: 1000 / (rate as u16), capacity: MAX_BURST,
453 prev: Instant::now(),
454 }
455 }
456
457 fn allow(&mut self, now: Instant) -> bool {
458 if now < self.prev {
459 return false;
460 }
461
462 let elapsed = now - self.prev;
463 if self.capacity == 0 && elapsed < Duration::from_millis(self.interval as u64) {
467 return false;
468 }
469
470 let (new, remainder) = (
474 elapsed.as_millis() / self.interval as u128,
475 elapsed.as_nanos() % (self.interval as u128 * 1_000_000),
476 );
477
478 self.capacity = Ord::min(MAX_BURST as u128, (self.capacity as u128) + new - 1) as u8;
481 self.prev = now
484 .checked_sub(Duration::from_nanos(remainder as u64))
485 .unwrap();
486 true
487 }
488}
489
490const MAX_BURST: u8 = 20;
491
492#[derive(Clone, Debug, Default)]
494pub(crate) struct DrawState {
495 pub(crate) lines: Vec<LineType>,
497 pub(crate) move_cursor: bool,
499 pub(crate) alignment: MultiProgressAlignment,
501}
502
503impl DrawState {
504 fn draw_to_term(
509 &mut self,
510 term: &(impl TermLike + ?Sized),
511 bar_count: &mut VisualLines, ) -> io::Result<()> {
513 if panicking() {
514 return Ok(());
515 }
516
517 if !self.lines.is_empty() && self.move_cursor {
518 term.move_cursor_up(bar_count.as_usize().saturating_sub(1))?;
520 term.write_str("\r")?;
521 } else {
522 let n = bar_count.as_usize();
524 term.move_cursor_up(n.saturating_sub(1))?;
525 for i in 0..n {
526 term.clear_line()?;
527 if i + 1 != n {
528 term.move_cursor_down(1)?;
529 }
530 }
531 term.move_cursor_up(n.saturating_sub(1))?;
532 }
533
534 let term_width = term.width() as usize;
535
536 let full_height = self.visual_line_count(.., term_width);
538
539 let shift = match self.alignment {
540 MultiProgressAlignment::Bottom if full_height < *bar_count => {
543 let shift = *bar_count - full_height;
544 for _ in 0..shift.as_usize() {
545 term.write_line("")?;
546 }
547 shift
548 }
549 _ => VisualLines::default(),
550 };
551
552 let mut real_height = VisualLines::default();
556
557 for (idx, line) in self.lines.iter().enumerate() {
558 let line_height = line.wrapped_height(term_width);
559
560 if matches!(line, LineType::Bar(_)) {
562 if real_height + line_height > term.height().into() {
564 break;
565 }
566
567 real_height += line_height;
568 }
569
570 if idx != 0 {
573 term.write_line("")?;
574 }
575
576 term.write_str(line.as_ref())?;
577
578 if idx + 1 == self.lines.len() {
579 let last_line_filler = line_height.as_usize() * term_width - line.console_width();
582 term.write_str(&" ".repeat(last_line_filler))?;
583 }
584 }
585
586 term.flush()?;
587 *bar_count = real_height + shift;
588
589 Ok(())
590 }
591
592 fn reset(&mut self) {
593 self.lines.clear();
594 }
595
596 pub(crate) fn visual_line_count(
597 &self,
598 range: impl SliceIndex<[LineType], Output = [LineType]>,
599 width: usize,
600 ) -> VisualLines {
601 visual_line_count(&self.lines[range], width)
602 }
603}
604
605#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
606pub(crate) struct VisualLines(usize);
607
608impl VisualLines {
609 pub(crate) fn saturating_add(&self, other: Self) -> Self {
610 Self(self.0.saturating_add(other.0))
611 }
612
613 pub(crate) fn saturating_sub(&self, other: Self) -> Self {
614 Self(self.0.saturating_sub(other.0))
615 }
616
617 pub(crate) fn as_usize(&self) -> usize {
618 self.0
619 }
620}
621
622impl Add for VisualLines {
623 type Output = Self;
624
625 fn add(self, rhs: Self) -> Self::Output {
626 Self(self.0 + rhs.0)
627 }
628}
629
630impl AddAssign for VisualLines {
631 fn add_assign(&mut self, rhs: Self) {
632 self.0 += rhs.0;
633 }
634}
635
636impl<T: Into<usize>> From<T> for VisualLines {
637 fn from(value: T) -> Self {
638 Self(value.into())
639 }
640}
641
642impl Sub for VisualLines {
643 type Output = Self;
644
645 fn sub(self, rhs: Self) -> Self::Output {
646 Self(self.0 - rhs.0)
647 }
648}
649
650pub(crate) fn visual_line_count(lines: &[LineType], width: usize) -> VisualLines {
653 lines.iter().fold(VisualLines::default(), |acc, line| {
654 acc.saturating_add(line.wrapped_height(width))
655 })
656}
657
658#[derive(Clone, Debug)]
659pub(crate) enum LineType {
660 Text(String),
661 Bar(String),
662 Empty,
663}
664
665impl LineType {
666 fn wrapped_height(&self, width: usize) -> VisualLines {
667 let terminal_len = (self.console_width() as f64 / width as f64).ceil() as usize;
670
671 usize::max(terminal_len, 1).into()
676 }
677
678 fn console_width(&self) -> usize {
679 console::measure_text_width(self.as_ref())
680 }
681}
682
683impl AsRef<str> for LineType {
684 fn as_ref(&self) -> &str {
685 match self {
686 LineType::Text(s) | LineType::Bar(s) => s,
687 LineType::Empty => "",
688 }
689 }
690}
691
692impl PartialEq<str> for LineType {
693 fn eq(&self, other: &str) -> bool {
694 self.as_ref() == other
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use crate::draw_target::LineType;
701 use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
702
703 #[test]
704 fn multi_is_hidden() {
705 let mp = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
706
707 let pb = mp.add(ProgressBar::new(100));
708 assert!(mp.is_hidden());
709 assert!(pb.is_hidden());
710 }
711
712 #[test]
713 fn real_line_count_test() {
714 #[derive(Debug)]
715 struct Case {
716 lines: &'static [&'static str],
717 expectation: usize,
718 width: usize,
719 }
720
721 let lines_and_expectations = [
722 Case {
723 lines: &["1234567890"],
724 expectation: 1,
725 width: 10,
726 },
727 Case {
728 lines: &["1234567890"],
729 expectation: 2,
730 width: 5,
731 },
732 Case {
733 lines: &["1234567890"],
734 expectation: 3,
735 width: 4,
736 },
737 Case {
738 lines: &["1234567890"],
739 expectation: 4,
740 width: 3,
741 },
742 Case {
743 lines: &["1234567890", "", "1234567890"],
744 expectation: 3,
745 width: 10,
746 },
747 Case {
748 lines: &["1234567890", "", "1234567890"],
749 expectation: 5,
750 width: 5,
751 },
752 Case {
753 lines: &["1234567890", "", "1234567890"],
754 expectation: 7,
755 width: 4,
756 },
757 Case {
758 lines: &["aaaaaaaaaaaaa", "", "bbbbbbbbbbbbbbbbb", "", "ccccccc"],
759 expectation: 8,
760 width: 7,
761 },
762 Case {
763 lines: &["", "", "", "", ""],
764 expectation: 5,
765 width: 6,
766 },
767 Case {
768 lines: &["\u{1b}[1m\u{1b}[1m\u{1b}[1m", "\u{1b}[1m\u{1b}[1m\u{1b}[1m"],
770 expectation: 2,
771 width: 5,
772 },
773 Case {
774 lines: &[
776 "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
777 "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
778 ],
779 expectation: 2,
780 width: 5,
781 },
782 Case {
783 lines: &[
785 "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
786 "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
787 ],
788 expectation: 4,
789 width: 5,
790 },
791 ];
792
793 for case in lines_and_expectations.iter() {
794 let result = super::visual_line_count(
795 &case
796 .lines
797 .iter()
798 .map(|s| LineType::Text(s.to_string()))
799 .collect::<Vec<_>>(),
800 case.width,
801 );
802 assert_eq!(result, case.expectation.into(), "case: {case:?}");
803 }
804 }
805}