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 Drawable<'_> {
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 pub(crate) fn width(&self) -> Option<u16> {
356 match self {
357 Self::Term { term, .. } => Some(term.size().1),
358 Self::Multi { state, .. } => state.width(),
359 Self::TermLike { term_like, .. } => Some(term_like.width()),
360 }
361 }
362}
363
364pub(crate) enum LineAdjust {
365 Clear(VisualLines),
367 Keep(VisualLines),
369}
370
371pub(crate) struct DrawStateWrapper<'a> {
372 state: &'a mut DrawState,
373 orphan_lines: Option<&'a mut Vec<LineType>>,
374}
375
376impl<'a> DrawStateWrapper<'a> {
377 pub(crate) fn for_term(state: &'a mut DrawState) -> Self {
378 Self {
379 state,
380 orphan_lines: None,
381 }
382 }
383
384 pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<LineType>) -> Self {
385 Self {
386 state,
387 orphan_lines: Some(orphan_lines),
388 }
389 }
390}
391
392impl std::ops::Deref for DrawStateWrapper<'_> {
393 type Target = DrawState;
394
395 fn deref(&self) -> &Self::Target {
396 self.state
397 }
398}
399
400impl std::ops::DerefMut for DrawStateWrapper<'_> {
401 fn deref_mut(&mut self) -> &mut Self::Target {
402 self.state
403 }
404}
405
406impl Drop for DrawStateWrapper<'_> {
407 fn drop(&mut self) {
408 if let Some(text_lines) = &mut self.orphan_lines {
409 let mut lines = Vec::new();
412
413 for line in self.state.lines.drain(..) {
414 match &line {
415 LineType::Text(_) | LineType::Empty => text_lines.push(line),
416 _ => lines.push(line),
417 }
418 }
419
420 self.state.lines = lines;
421 }
422 }
423}
424
425#[derive(Debug)]
426struct RateLimiter {
427 interval: u16, capacity: u8,
429 prev: Instant,
430}
431
432impl RateLimiter {
434 fn new(rate: u8) -> Self {
435 Self {
436 interval: 1000 / (rate as u16), capacity: MAX_BURST,
438 prev: Instant::now(),
439 }
440 }
441
442 fn allow(&mut self, now: Instant) -> bool {
443 if now < self.prev {
444 return false;
445 }
446
447 let elapsed = now - self.prev;
448 if self.capacity == 0 && elapsed < Duration::from_millis(self.interval as u64) {
452 return false;
453 }
454
455 let (new, remainder) = (
459 elapsed.as_millis() / self.interval as u128,
460 elapsed.as_nanos() % (self.interval as u128 * 1_000_000),
461 );
462
463 self.capacity = Ord::min(MAX_BURST as u128, (self.capacity as u128) + new - 1) as u8;
466 self.prev = now
469 .checked_sub(Duration::from_nanos(remainder as u64))
470 .unwrap();
471 true
472 }
473}
474
475const MAX_BURST: u8 = 20;
476
477#[derive(Clone, Debug, Default)]
479pub(crate) struct DrawState {
480 pub(crate) lines: Vec<LineType>,
482 pub(crate) move_cursor: bool,
484 pub(crate) alignment: MultiProgressAlignment,
486}
487
488impl DrawState {
489 fn draw_to_term(
494 &mut self,
495 term: &(impl TermLike + ?Sized),
496 bar_count: &mut VisualLines, ) -> io::Result<()> {
498 if panicking() {
499 return Ok(());
500 }
501
502 if !self.lines.is_empty() && self.move_cursor {
503 term.move_cursor_up(bar_count.as_usize().saturating_sub(1))?;
505 term.write_str("\r")?;
506 } else {
507 let n = bar_count.as_usize();
509 term.move_cursor_up(n.saturating_sub(1))?;
510 for i in 0..n {
511 term.clear_line()?;
512 if i + 1 != n {
513 term.move_cursor_down(1)?;
514 }
515 }
516 term.move_cursor_up(n.saturating_sub(1))?;
517 }
518
519 let term_width = term.width() as usize;
520
521 let full_height = self.visual_line_count(.., term_width);
523
524 let shift = match self.alignment {
525 MultiProgressAlignment::Bottom if full_height < *bar_count => {
528 let shift = *bar_count - full_height;
529 for _ in 0..shift.as_usize() {
530 term.write_line("")?;
531 }
532 shift
533 }
534 _ => VisualLines::default(),
535 };
536
537 let mut real_height = VisualLines::default();
541
542 for (idx, line) in self.lines.iter().enumerate() {
543 let line_height = line.wrapped_height(term_width);
544
545 if matches!(line, LineType::Bar(_)) {
547 if real_height + line_height > term.height().into() {
549 break;
550 }
551
552 real_height += line_height;
553 }
554
555 if idx != 0 {
558 term.write_line("")?;
559 }
560
561 term.write_str(line.as_ref())?;
562
563 if idx + 1 == self.lines.len() {
564 let last_line_filler = line_height.as_usize() * term_width - line.console_width();
567 term.write_str(&" ".repeat(last_line_filler))?;
568 }
569 }
570
571 term.flush()?;
572 *bar_count = real_height + shift;
573
574 Ok(())
575 }
576
577 fn reset(&mut self) {
578 self.lines.clear();
579 }
580
581 pub(crate) fn visual_line_count(
582 &self,
583 range: impl SliceIndex<[LineType], Output = [LineType]>,
584 width: usize,
585 ) -> VisualLines {
586 visual_line_count(&self.lines[range], width)
587 }
588}
589
590#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
591pub(crate) struct VisualLines(usize);
592
593impl VisualLines {
594 pub(crate) fn saturating_add(&self, other: Self) -> Self {
595 Self(self.0.saturating_add(other.0))
596 }
597
598 pub(crate) fn saturating_sub(&self, other: Self) -> Self {
599 Self(self.0.saturating_sub(other.0))
600 }
601
602 pub(crate) fn as_usize(&self) -> usize {
603 self.0
604 }
605}
606
607impl Add for VisualLines {
608 type Output = Self;
609
610 fn add(self, rhs: Self) -> Self::Output {
611 Self(self.0 + rhs.0)
612 }
613}
614
615impl AddAssign for VisualLines {
616 fn add_assign(&mut self, rhs: Self) {
617 self.0 += rhs.0;
618 }
619}
620
621impl<T: Into<usize>> From<T> for VisualLines {
622 fn from(value: T) -> Self {
623 Self(value.into())
624 }
625}
626
627impl Sub for VisualLines {
628 type Output = Self;
629
630 fn sub(self, rhs: Self) -> Self::Output {
631 Self(self.0 - rhs.0)
632 }
633}
634
635pub(crate) fn visual_line_count(lines: &[LineType], width: usize) -> VisualLines {
638 lines.iter().fold(VisualLines::default(), |acc, line| {
639 acc.saturating_add(line.wrapped_height(width))
640 })
641}
642
643#[derive(Clone, Debug)]
644pub(crate) enum LineType {
645 Text(String),
646 Bar(String),
647 Empty,
648}
649
650impl LineType {
651 fn wrapped_height(&self, width: usize) -> VisualLines {
652 let terminal_len = (self.console_width() as f64 / width as f64).ceil() as usize;
655
656 usize::max(terminal_len, 1).into()
661 }
662
663 fn console_width(&self) -> usize {
664 console::measure_text_width(self.as_ref())
665 }
666}
667
668impl AsRef<str> for LineType {
669 fn as_ref(&self) -> &str {
670 match self {
671 LineType::Text(s) | LineType::Bar(s) => s,
672 LineType::Empty => "",
673 }
674 }
675}
676
677impl PartialEq<str> for LineType {
678 fn eq(&self, other: &str) -> bool {
679 self.as_ref() == other
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use crate::draw_target::LineType;
686 use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
687
688 #[test]
689 fn multi_is_hidden() {
690 let mp = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
691
692 let pb = mp.add(ProgressBar::new(100));
693 assert!(mp.is_hidden());
694 assert!(pb.is_hidden());
695 }
696
697 #[test]
698 fn real_line_count_test() {
699 #[derive(Debug)]
700 struct Case {
701 lines: &'static [&'static str],
702 expectation: usize,
703 width: usize,
704 }
705
706 let lines_and_expectations = [
707 Case {
708 lines: &["1234567890"],
709 expectation: 1,
710 width: 10,
711 },
712 Case {
713 lines: &["1234567890"],
714 expectation: 2,
715 width: 5,
716 },
717 Case {
718 lines: &["1234567890"],
719 expectation: 3,
720 width: 4,
721 },
722 Case {
723 lines: &["1234567890"],
724 expectation: 4,
725 width: 3,
726 },
727 Case {
728 lines: &["1234567890", "", "1234567890"],
729 expectation: 3,
730 width: 10,
731 },
732 Case {
733 lines: &["1234567890", "", "1234567890"],
734 expectation: 5,
735 width: 5,
736 },
737 Case {
738 lines: &["1234567890", "", "1234567890"],
739 expectation: 7,
740 width: 4,
741 },
742 Case {
743 lines: &["aaaaaaaaaaaaa", "", "bbbbbbbbbbbbbbbbb", "", "ccccccc"],
744 expectation: 8,
745 width: 7,
746 },
747 Case {
748 lines: &["", "", "", "", ""],
749 expectation: 5,
750 width: 6,
751 },
752 Case {
753 lines: &["\u{1b}[1m\u{1b}[1m\u{1b}[1m", "\u{1b}[1m\u{1b}[1m\u{1b}[1m"],
755 expectation: 2,
756 width: 5,
757 },
758 Case {
759 lines: &[
761 "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
762 "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
763 ],
764 expectation: 2,
765 width: 5,
766 },
767 Case {
768 lines: &[
770 "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
771 "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
772 ],
773 expectation: 4,
774 width: 5,
775 },
776 ];
777
778 for case in lines_and_expectations.iter() {
779 let result = super::visual_line_count(
780 &case
781 .lines
782 .iter()
783 .map(|s| LineType::Text(s.to_string()))
784 .collect::<Vec<_>>(),
785 case.width,
786 );
787 assert_eq!(result, case.expectation.into(), "case: {:?}", case);
788 }
789 }
790}