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