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
10#[cfg(feature = "unicode-width")]
11use console::AnsiCodeIterator;
12use console::{is_dumb, Term, TermTarget};
13#[cfg(feature = "unicode-width")]
14use unicode_width::UnicodeWidthChar;
15#[cfg(all(target_arch = "wasm32", feature = "wasmbind"))]
16use web_time::Instant;
17
18use crate::multi::{MultiProgressAlignment, MultiState};
19use crate::TermLike;
20
21#[derive(Debug)]
29pub struct ProgressDrawTarget {
30 kind: TargetKind,
31}
32
33impl ProgressDrawTarget {
34 pub fn stdout() -> Self {
38 Self::term(Term::buffered_stdout(), 20)
39 }
40
41 pub fn stderr() -> Self {
46 Self::term(Term::buffered_stderr(), 20)
47 }
48
49 pub fn stdout_with_hz(refresh_rate: u8) -> Self {
53 Self::term(Term::buffered_stdout(), refresh_rate)
54 }
55
56 pub fn stderr_with_hz(refresh_rate: u8) -> Self {
60 Self::term(Term::buffered_stderr(), refresh_rate)
61 }
62
63 pub(crate) fn new_remote(state: Arc<RwLock<MultiState>>, idx: usize) -> Self {
64 Self {
65 kind: TargetKind::Multi { state, idx },
66 }
67 }
68
69 pub fn term(term: Term, refresh_rate: u8) -> Self {
80 if !term.is_term() || is_dumb() {
81 return Self::hidden();
82 }
83 Self {
84 kind: TargetKind::Term {
85 term,
86 last_line_count: VisualLines::default(),
87 rate_limiter: RateLimiter::new(refresh_rate),
88 draw_state: DrawState::default(),
89 },
90 }
91 }
92
93 pub fn term_like(term_like: Box<dyn TermLike>) -> Self {
100 Self {
101 kind: TargetKind::TermLike {
102 inner: term_like,
103 last_line_count: VisualLines::default(),
104 rate_limiter: None,
105 draw_state: DrawState::default(),
106 },
107 }
108 }
109
110 pub fn term_like_with_hz(term_like: Box<dyn TermLike>, refresh_rate: u8) -> Self {
113 Self {
114 kind: TargetKind::TermLike {
115 inner: term_like,
116 last_line_count: VisualLines::default(),
117 rate_limiter: Option::from(RateLimiter::new(refresh_rate)),
118 draw_state: DrawState::default(),
119 },
120 }
121 }
122
123 pub fn hidden() -> Self {
127 Self {
128 kind: TargetKind::Hidden,
129 }
130 }
131
132 pub fn is_hidden(&self) -> bool {
137 match self.kind {
138 TargetKind::Hidden => true,
139 TargetKind::Term { ref term, .. } => !term.is_term(),
140 TargetKind::Multi { ref state, .. } => state.read().unwrap().draw_target.is_hidden(),
141 _ => false,
142 }
143 }
144
145 pub(crate) fn is_stderr(&self) -> bool {
148 match &self.kind {
149 TargetKind::Term { term, .. } => matches!(term.target(), TermTarget::Stderr),
150 TargetKind::Multi { state, .. } => state.read().unwrap().draw_target.is_stderr(),
151 _ => false,
152 }
153 }
154
155 pub(crate) fn width(&self) -> Option<u16> {
157 match self.kind {
158 TargetKind::Term { ref term, .. } => Some(term.size().1),
159 TargetKind::Multi { ref state, .. } => state.read().unwrap().draw_target.width(),
160 TargetKind::TermLike { ref inner, .. } => Some(inner.width()),
161 TargetKind::Hidden => None,
162 }
163 }
164
165 pub(crate) fn mark_zombie(&self) {
168 if let TargetKind::Multi { idx, state } = &self.kind {
169 state.write().unwrap().mark_zombie(*idx);
170 }
171 }
172
173 pub(crate) fn set_move_cursor(&mut self, move_cursor: bool) {
175 match &mut self.kind {
176 TargetKind::Term { draw_state, .. } => draw_state.move_cursor = move_cursor,
177 TargetKind::TermLike { draw_state, .. } => draw_state.move_cursor = move_cursor,
178 _ => {}
179 }
180 }
181
182 pub(crate) fn drawable(&mut self, force_draw: bool, now: Instant) -> Option<Drawable<'_>> {
184 match &mut self.kind {
185 TargetKind::Term {
186 term,
187 last_line_count,
188 rate_limiter,
189 draw_state,
190 } => {
191 match force_draw || rate_limiter.allow(now) {
192 true => Some(Drawable::Term {
193 term,
194 last_line_count,
195 draw_state,
196 }),
197 false => None, }
199 }
200 TargetKind::Multi { idx, state, .. } => {
201 let state = state.write().unwrap();
202 Some(Drawable::Multi {
203 idx: *idx,
204 state,
205 force_draw,
206 now,
207 })
208 }
209 TargetKind::TermLike {
210 inner,
211 last_line_count,
212 rate_limiter,
213 draw_state,
214 } => match force_draw || rate_limiter.as_mut().is_none_or(|r| r.allow(now)) {
215 true => Some(Drawable::TermLike {
216 term_like: &**inner,
217 last_line_count,
218 draw_state,
219 }),
220 false => None, },
222 _ => None,
224 }
225 }
226
227 pub(crate) fn disconnect(&self, now: Instant) {
229 match self.kind {
230 TargetKind::Term { .. } => {}
231 TargetKind::Multi { idx, ref state, .. } => {
232 let state = state.write().unwrap();
233 let _ = Drawable::Multi {
234 state,
235 idx,
236 force_draw: true,
237 now,
238 }
239 .clear();
240 }
241 TargetKind::Hidden => {}
242 TargetKind::TermLike { .. } => {}
243 };
244 }
245
246 pub(crate) fn remote(&self) -> Option<(&Arc<RwLock<MultiState>>, usize)> {
247 match &self.kind {
248 TargetKind::Multi { state, idx } => Some((state, *idx)),
249 _ => None,
250 }
251 }
252
253 pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
254 self.kind.adjust_last_line_count(adjust);
255 }
256}
257
258#[derive(Debug)]
259enum TargetKind {
260 Term {
261 term: Term,
262 last_line_count: VisualLines,
263 rate_limiter: RateLimiter,
264 draw_state: DrawState,
265 },
266 Multi {
267 state: Arc<RwLock<MultiState>>,
268 idx: usize,
269 },
270 Hidden,
271 TermLike {
272 inner: Box<dyn TermLike>,
273 last_line_count: VisualLines,
274 rate_limiter: Option<RateLimiter>,
275 draw_state: DrawState,
276 },
277}
278
279impl TargetKind {
280 fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
282 let last_line_count = match self {
283 Self::Term {
284 last_line_count, ..
285 } => last_line_count,
286 Self::TermLike {
287 last_line_count, ..
288 } => last_line_count,
289 _ => return,
290 };
291
292 match adjust {
293 LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
294 LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
295 }
296 }
297}
298
299pub(crate) enum Drawable<'a> {
300 Term {
301 term: &'a Term,
302 last_line_count: &'a mut VisualLines,
303 draw_state: &'a mut DrawState,
304 },
305 Multi {
306 state: RwLockWriteGuard<'a, MultiState>,
307 idx: usize,
308 force_draw: bool,
309 now: Instant,
310 },
311 TermLike {
312 term_like: &'a dyn TermLike,
313 last_line_count: &'a mut VisualLines,
314 draw_state: &'a mut DrawState,
315 },
316}
317
318impl Drawable<'_> {
319 pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
321 let last_line_count: &mut VisualLines = match self {
322 Drawable::Term {
323 last_line_count, ..
324 } => last_line_count,
325 Drawable::TermLike {
326 last_line_count, ..
327 } => last_line_count,
328 _ => return,
329 };
330
331 match adjust {
332 LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
333 LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
334 }
335 }
336
337 pub(crate) fn state(&mut self) -> DrawStateWrapper<'_> {
338 let mut state = match self {
339 Drawable::Term { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
340 Drawable::Multi { state, idx, .. } => state.draw_state(*idx),
341 Drawable::TermLike { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
342 };
343
344 state.reset();
345 state
346 }
347
348 pub(crate) fn clear(mut self) -> io::Result<()> {
349 let state = self.state();
350 drop(state);
351 self.draw()
352 }
353
354 pub(crate) fn draw(self) -> io::Result<()> {
355 match self {
356 Drawable::Term {
357 term,
358 last_line_count,
359 draw_state,
360 } => draw_state.draw_to_term(term, last_line_count),
361 Drawable::Multi {
362 mut state,
363 force_draw,
364 now,
365 ..
366 } => state.draw(force_draw, None, now),
367 Drawable::TermLike {
368 term_like,
369 last_line_count,
370 draw_state,
371 } => draw_state.draw_to_term(term_like, last_line_count),
372 }
373 }
374
375 pub(crate) fn width(&self) -> Option<u16> {
376 match self {
377 Self::Term { term, .. } => Some(term.size().1),
378 Self::Multi { state, .. } => state.draw_target.width(),
379 Self::TermLike { term_like, .. } => Some(term_like.width()),
380 }
381 }
382}
383
384pub(crate) enum LineAdjust {
385 Clear(VisualLines),
387 Keep(VisualLines),
389}
390
391pub(crate) struct DrawStateWrapper<'a> {
392 state: &'a mut DrawState,
393 orphan_lines: Option<&'a mut Vec<LineType>>,
394}
395
396impl<'a> DrawStateWrapper<'a> {
397 pub(crate) fn for_term(state: &'a mut DrawState) -> Self {
398 Self {
399 state,
400 orphan_lines: None,
401 }
402 }
403
404 pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<LineType>) -> Self {
405 Self {
406 state,
407 orphan_lines: Some(orphan_lines),
408 }
409 }
410}
411
412impl std::ops::Deref for DrawStateWrapper<'_> {
413 type Target = DrawState;
414
415 fn deref(&self) -> &Self::Target {
416 self.state
417 }
418}
419
420impl std::ops::DerefMut for DrawStateWrapper<'_> {
421 fn deref_mut(&mut self) -> &mut Self::Target {
422 self.state
423 }
424}
425
426impl Drop for DrawStateWrapper<'_> {
427 fn drop(&mut self) {
428 if let Some(text_lines) = &mut self.orphan_lines {
429 let mut lines = Vec::new();
432
433 for line in self.state.lines.drain(..) {
434 match &line {
435 LineType::Text(_) | LineType::Empty => text_lines.push(line),
436 _ => lines.push(line),
437 }
438 }
439
440 self.state.lines = lines;
441 }
442 }
443}
444
445#[derive(Debug)]
446struct RateLimiter {
447 interval: u16, capacity: u8,
449 prev: Instant,
450}
451
452impl RateLimiter {
454 fn new(rate: u8) -> Self {
455 Self {
456 interval: 1000 / (rate as u16), capacity: MAX_BURST,
458 prev: Instant::now(),
459 }
460 }
461
462 fn allow(&mut self, now: Instant) -> bool {
463 if now < self.prev {
464 return false;
465 }
466
467 let elapsed = now - self.prev;
468 if self.capacity == 0 && elapsed < Duration::from_millis(self.interval as u64) {
472 return false;
473 }
474
475 let (new, remainder) = (
479 elapsed.as_millis() / self.interval as u128,
480 elapsed.as_nanos() % (self.interval as u128 * 1_000_000),
481 );
482
483 self.capacity = Ord::min(MAX_BURST as u128, (self.capacity as u128) + new - 1) as u8;
486 self.prev = now
489 .checked_sub(Duration::from_nanos(remainder as u64))
490 .unwrap();
491 true
492 }
493}
494
495const MAX_BURST: u8 = 20;
496
497#[derive(Clone, Debug, Default)]
499pub(crate) struct DrawState {
500 pub(crate) lines: Vec<LineType>,
502 pub(crate) move_cursor: bool,
504 pub(crate) alignment: MultiProgressAlignment,
506}
507
508impl DrawState {
509 fn draw_to_term(
514 &mut self,
515 term: &(impl TermLike + ?Sized),
516 bar_count: &mut VisualLines, ) -> io::Result<()> {
518 if panicking() {
519 return Ok(());
520 }
521
522 if !self.lines.is_empty() && self.move_cursor {
523 term.move_cursor_up(bar_count.as_usize().saturating_sub(1))?;
525 term.write_str("\r")?;
526 } else {
527 let n = bar_count.as_usize();
529 term.move_cursor_up(n.saturating_sub(1))?;
530 for i in 0..n {
531 term.clear_line()?;
532 if i + 1 != n {
533 term.move_cursor_down(1)?;
534 }
535 }
536 term.move_cursor_up(n.saturating_sub(1))?;
537 }
538
539 let term_width = term.width() as usize;
540
541 let full_height = self.visual_line_count(.., term_width);
543
544 let shift = match self.alignment {
545 MultiProgressAlignment::Bottom if full_height < *bar_count => {
548 let shift = *bar_count - full_height;
549 for _ in 0..shift.as_usize() {
550 term.write_line("")?;
551 }
552 shift
553 }
554 _ => VisualLines::default(),
555 };
556
557 let mut real_height = VisualLines::default();
561
562 for line in self.lines.iter() {
563 let metrics = line.wrapped_metrics(term_width);
564
565 if matches!(line, LineType::Bar(_)) {
567 if real_height + metrics.height > term.height().into() {
569 break;
570 }
571
572 real_height += metrics.height;
573 }
574
575 term.write_str(line.as_ref())?;
576
577 let line_filler = term_width - metrics.last_line_width;
580 term.write_str(&" ".repeat(line_filler))?;
581 }
582
583 term.flush()?;
584 *bar_count = real_height + shift;
585
586 Ok(())
587 }
588
589 fn reset(&mut self) {
590 self.lines.clear();
591 }
592
593 pub(crate) fn visual_line_count(
594 &self,
595 range: impl SliceIndex<[LineType], Output = [LineType]>,
596 width: usize,
597 ) -> VisualLines {
598 visual_line_count(&self.lines[range], width)
599 }
600}
601
602#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
603pub(crate) struct VisualLines(usize);
604
605impl VisualLines {
606 pub(crate) fn saturating_add(&self, other: Self) -> Self {
607 Self(self.0.saturating_add(other.0))
608 }
609
610 pub(crate) fn saturating_sub(&self, other: Self) -> Self {
611 Self(self.0.saturating_sub(other.0))
612 }
613
614 pub(crate) fn as_usize(&self) -> usize {
615 self.0
616 }
617}
618
619impl Add for VisualLines {
620 type Output = Self;
621
622 fn add(self, rhs: Self) -> Self::Output {
623 Self(self.0 + rhs.0)
624 }
625}
626
627impl AddAssign for VisualLines {
628 fn add_assign(&mut self, rhs: Self) {
629 self.0 += rhs.0;
630 }
631}
632
633impl<T: Into<usize>> From<T> for VisualLines {
634 fn from(value: T) -> Self {
635 Self(value.into())
636 }
637}
638
639impl Sub for VisualLines {
640 type Output = Self;
641
642 fn sub(self, rhs: Self) -> Self::Output {
643 Self(self.0 - rhs.0)
644 }
645}
646
647pub(crate) fn visual_line_count(lines: &[LineType], width: usize) -> VisualLines {
650 lines.iter().fold(VisualLines::default(), |acc, line| {
651 acc.saturating_add(line.wrapped_height(width))
652 })
653}
654
655#[derive(Clone, Debug)]
656pub(crate) enum LineType {
657 Text(String),
658 Bar(String),
659 Empty,
660}
661
662impl LineType {
663 fn wrapped_height(&self, width: usize) -> VisualLines {
664 self.wrapped_metrics(width).height
665 }
666
667 #[cfg(feature = "unicode-width")]
668 fn wrapped_metrics(&self, width: usize) -> Metrics {
669 let str = self.as_ref();
674 let mut num_lines: usize = 1;
675 let mut column: usize = 0;
676 for (substr, is_ansi) in AnsiCodeIterator::new(str) {
677 if is_ansi {
678 continue;
679 }
680 for ch in substr.chars() {
681 let Some(ch_width) = UnicodeWidthChar::width(ch) else {
682 continue; };
684 column += ch_width;
685 if column > width {
686 num_lines += 1;
687 column = ch_width;
688 }
689 }
690 }
691 Metrics {
692 height: num_lines.into(),
693 last_line_width: column,
694 }
695 }
696
697 #[cfg(not(feature = "unicode-width"))]
698 fn wrapped_metrics(&self, width: usize) -> Metrics {
699 let unwrapped_width = self.console_width();
702 let terminal_len = (unwrapped_width as f64 / width as f64).ceil() as usize;
703
704 let height = usize::max(terminal_len, 1);
709 Metrics {
710 height: height.into(),
711 last_line_width: unwrapped_width - width * (height - 1),
712 }
713 }
714
715 #[cfg(not(feature = "unicode-width"))]
716 fn console_width(&self) -> usize {
717 console::measure_text_width(self.as_ref())
718 }
719}
720
721impl AsRef<str> for LineType {
722 fn as_ref(&self) -> &str {
723 match self {
724 LineType::Text(s) | LineType::Bar(s) => s,
725 LineType::Empty => "",
726 }
727 }
728}
729
730impl PartialEq<str> for LineType {
731 fn eq(&self, other: &str) -> bool {
732 self.as_ref() == other
733 }
734}
735
736#[derive(Debug)]
738struct Metrics {
739 height: VisualLines,
741 last_line_width: usize,
743}
744
745#[cfg(test)]
746mod tests {
747 use crate::draw_target::{LineType, TargetKind};
748 use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
749 use console::Term;
750
751 #[test]
752 fn multi_is_hidden() {
753 let mp = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
754
755 let pb = mp.add(ProgressBar::new(100));
756 assert!(mp.is_hidden());
757 assert!(pb.is_hidden());
758 }
759
760 #[test]
761 fn real_line_count_test() {
762 #[derive(Debug)]
763 struct Case {
764 lines: &'static [&'static str],
765 expectation: usize,
766 width: usize,
767 }
768
769 let lines_and_expectations = [
770 Case {
771 lines: &["1234567890"],
772 expectation: 1,
773 width: 10,
774 },
775 Case {
776 lines: &["1234567890"],
777 expectation: 2,
778 width: 5,
779 },
780 Case {
781 lines: &["1234567890"],
782 expectation: 3,
783 width: 4,
784 },
785 Case {
786 lines: &["1234567890"],
787 expectation: 4,
788 width: 3,
789 },
790 Case {
791 lines: &["1234567890", "", "1234567890"],
792 expectation: 3,
793 width: 10,
794 },
795 Case {
796 lines: &["1234567890", "", "1234567890"],
797 expectation: 5,
798 width: 5,
799 },
800 Case {
801 lines: &["1234567890", "", "1234567890"],
802 expectation: 7,
803 width: 4,
804 },
805 Case {
806 lines: &["aaaaaaaaaaaaa", "", "bbbbbbbbbbbbbbbbb", "", "ccccccc"],
807 expectation: 8,
808 width: 7,
809 },
810 Case {
811 lines: &["", "", "", "", ""],
812 expectation: 5,
813 width: 6,
814 },
815 Case {
816 lines: &["\u{1b}[1m\u{1b}[1m\u{1b}[1m", "\u{1b}[1m\u{1b}[1m\u{1b}[1m"],
818 expectation: 2,
819 width: 5,
820 },
821 Case {
822 lines: &[
824 "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
825 "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
826 ],
827 expectation: 2,
828 width: 5,
829 },
830 Case {
831 lines: &[
833 "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
834 "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
835 ],
836 expectation: 4,
837 width: 5,
838 },
839 ];
840
841 for case in lines_and_expectations.iter() {
842 let result = super::visual_line_count(
843 &case
844 .lines
845 .iter()
846 .map(|s| LineType::Text(s.to_string()))
847 .collect::<Vec<_>>(),
848 case.width,
849 );
850 assert_eq!(result, case.expectation.into(), "case: {case:?}");
851 }
852 }
853
854 #[test]
855 fn is_stderr_for_multi() {
856 let term = Term::buffered_stderr();
857 let draw_target = ProgressDrawTarget {
858 kind: TargetKind::Term {
859 term,
860 last_line_count: Default::default(),
861 rate_limiter: super::RateLimiter::new(20),
862 draw_state: Default::default(),
863 },
864 };
865 assert!(draw_target.is_stderr());
866
867 let mp = MultiProgress::with_draw_target(draw_target);
868 let multi_draw_target = ProgressDrawTarget {
869 kind: TargetKind::Multi {
870 state: mp.state.clone(),
871 idx: 0,
872 },
873 };
874 assert!(multi_draw_target.is_stderr());
875 }
876
877 #[test]
878 fn wrapped_height_cjk_at_the_end_wrap() {
879 let text = "123456789国123456789";
883 let line_type = LineType::Text(text.to_string());
884 let metrics = line_type.wrapped_metrics(10);
885 #[cfg(feature = "unicode-width")]
886 {
887 assert_eq!(metrics.height.as_usize(), 3);
888 assert_eq!(metrics.last_line_width, 1);
889 }
890 #[cfg(not(feature = "unicode-width"))]
891 {
892 assert_eq!(metrics.height.as_usize(), 2);
893 assert_eq!(metrics.last_line_width, 9);
894 }
895 }
896}