blob: b99b2396b4556265d2391aae314c22b3c545ae67 [file] [log] [blame] [edit]
use std::io;
use std::ops::{Add, AddAssign, Sub};
use std::slice::SliceIndex;
use std::sync::{Arc, RwLock, RwLockWriteGuard};
use std::thread::panicking;
use std::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
use console::Term;
#[cfg(target_arch = "wasm32")]
use instant::Instant;
use crate::multi::{MultiProgressAlignment, MultiState};
use crate::TermLike;
/// Target for draw operations
///
/// This tells a [`ProgressBar`](crate::ProgressBar) or a
/// [`MultiProgress`](crate::MultiProgress) object where to paint to.
/// The draw target is a stateful wrapper over a drawing destination and
/// internally optimizes how often the state is painted to the output
/// device.
#[derive(Debug)]
pub struct ProgressDrawTarget {
kind: TargetKind,
}
impl ProgressDrawTarget {
/// Draw to a buffered stdout terminal at a max of 20 times a second.
///
/// For more information see [`ProgressDrawTarget::term`].
pub fn stdout() -> Self {
Self::term(Term::buffered_stdout(), 20)
}
/// Draw to a buffered stderr terminal at a max of 20 times a second.
///
/// This is the default draw target for progress bars. For more
/// information see [`ProgressDrawTarget::term`].
pub fn stderr() -> Self {
Self::term(Term::buffered_stderr(), 20)
}
/// Draw to a buffered stdout terminal at a max of `refresh_rate` times a second.
///
/// For more information see [`ProgressDrawTarget::term`].
pub fn stdout_with_hz(refresh_rate: u8) -> Self {
Self::term(Term::buffered_stdout(), refresh_rate)
}
/// Draw to a buffered stderr terminal at a max of `refresh_rate` times a second.
///
/// For more information see [`ProgressDrawTarget::term`].
pub fn stderr_with_hz(refresh_rate: u8) -> Self {
Self::term(Term::buffered_stderr(), refresh_rate)
}
pub(crate) fn new_remote(state: Arc<RwLock<MultiState>>, idx: usize) -> Self {
Self {
kind: TargetKind::Multi { state, idx },
}
}
/// Draw to a terminal, with a specific refresh rate.
///
/// Progress bars are by default drawn to terminals however if the
/// terminal is not user attended the entire progress bar will be
/// hidden. This is done so that piping to a file will not produce
/// useless escape codes in that file.
///
/// Will panic if `refresh_rate` is `0`.
pub fn term(term: Term, refresh_rate: u8) -> Self {
Self {
kind: TargetKind::Term {
term,
last_line_count: VisualLines::default(),
rate_limiter: RateLimiter::new(refresh_rate),
draw_state: DrawState::default(),
},
}
}
/// Draw to a boxed object that implements the [`TermLike`] trait.
pub fn term_like(term_like: Box<dyn TermLike>) -> Self {
Self {
kind: TargetKind::TermLike {
inner: term_like,
last_line_count: VisualLines::default(),
rate_limiter: None,
draw_state: DrawState::default(),
},
}
}
/// Draw to a boxed object that implements the [`TermLike`] trait,
/// with a specific refresh rate.
pub fn term_like_with_hz(term_like: Box<dyn TermLike>, refresh_rate: u8) -> Self {
Self {
kind: TargetKind::TermLike {
inner: term_like,
last_line_count: VisualLines::default(),
rate_limiter: Option::from(RateLimiter::new(refresh_rate)),
draw_state: DrawState::default(),
},
}
}
/// A hidden draw target.
///
/// This forces a progress bar to be not rendered at all.
pub fn hidden() -> Self {
Self {
kind: TargetKind::Hidden,
}
}
/// Returns true if the draw target is hidden.
///
/// This is internally used in progress bars to figure out if overhead
/// from drawing can be prevented.
pub fn is_hidden(&self) -> bool {
match self.kind {
TargetKind::Hidden => true,
TargetKind::Term { ref term, .. } => !term.is_term(),
TargetKind::Multi { ref state, .. } => state.read().unwrap().is_hidden(),
_ => false,
}
}
/// Returns the current width of the draw target.
pub(crate) fn width(&self) -> Option<u16> {
match self.kind {
TargetKind::Term { ref term, .. } => Some(term.size().1),
TargetKind::Multi { ref state, .. } => state.read().unwrap().width(),
TargetKind::TermLike { ref inner, .. } => Some(inner.width()),
TargetKind::Hidden => None,
}
}
/// Notifies the backing `MultiProgress` (if applicable) that the associated progress bar should
/// be marked a zombie.
pub(crate) fn mark_zombie(&self) {
if let TargetKind::Multi { idx, state } = &self.kind {
state.write().unwrap().mark_zombie(*idx);
}
}
/// Apply the given draw state (draws it).
pub(crate) fn drawable(&mut self, force_draw: bool, now: Instant) -> Option<Drawable<'_>> {
match &mut self.kind {
TargetKind::Term {
term,
last_line_count,
rate_limiter,
draw_state,
} => {
if !term.is_term() {
return None;
}
match force_draw || rate_limiter.allow(now) {
true => Some(Drawable::Term {
term,
last_line_count,
draw_state,
}),
false => None, // rate limited
}
}
TargetKind::Multi { idx, state, .. } => {
let state = state.write().unwrap();
Some(Drawable::Multi {
idx: *idx,
state,
force_draw,
now,
})
}
TargetKind::TermLike {
inner,
last_line_count,
rate_limiter,
draw_state,
} => match force_draw || rate_limiter.as_mut().map_or(true, |r| r.allow(now)) {
true => Some(Drawable::TermLike {
term_like: &**inner,
last_line_count,
draw_state,
}),
false => None, // rate limited
},
// Hidden, finished, or no need to refresh yet
_ => None,
}
}
/// Properly disconnects from the draw target
pub(crate) fn disconnect(&self, now: Instant) {
match self.kind {
TargetKind::Term { .. } => {}
TargetKind::Multi { idx, ref state, .. } => {
let state = state.write().unwrap();
let _ = Drawable::Multi {
state,
idx,
force_draw: true,
now,
}
.clear();
}
TargetKind::Hidden => {}
TargetKind::TermLike { .. } => {}
};
}
pub(crate) fn remote(&self) -> Option<(&Arc<RwLock<MultiState>>, usize)> {
match &self.kind {
TargetKind::Multi { state, idx } => Some((state, *idx)),
_ => None,
}
}
pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
self.kind.adjust_last_line_count(adjust);
}
}
#[derive(Debug)]
enum TargetKind {
Term {
term: Term,
last_line_count: VisualLines,
rate_limiter: RateLimiter,
draw_state: DrawState,
},
Multi {
state: Arc<RwLock<MultiState>>,
idx: usize,
},
Hidden,
TermLike {
inner: Box<dyn TermLike>,
last_line_count: VisualLines,
rate_limiter: Option<RateLimiter>,
draw_state: DrawState,
},
}
impl TargetKind {
/// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines
fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
let last_line_count = match self {
Self::Term {
last_line_count, ..
} => last_line_count,
Self::TermLike {
last_line_count, ..
} => last_line_count,
_ => return,
};
match adjust {
LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
}
}
}
pub(crate) enum Drawable<'a> {
Term {
term: &'a Term,
last_line_count: &'a mut VisualLines,
draw_state: &'a mut DrawState,
},
Multi {
state: RwLockWriteGuard<'a, MultiState>,
idx: usize,
force_draw: bool,
now: Instant,
},
TermLike {
term_like: &'a dyn TermLike,
last_line_count: &'a mut VisualLines,
draw_state: &'a mut DrawState,
},
}
impl<'a> Drawable<'a> {
/// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines
pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
let last_line_count: &mut VisualLines = match self {
Drawable::Term {
last_line_count, ..
} => last_line_count,
Drawable::TermLike {
last_line_count, ..
} => last_line_count,
_ => return,
};
match adjust {
LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
}
}
pub(crate) fn state(&mut self) -> DrawStateWrapper<'_> {
let mut state = match self {
Drawable::Term { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
Drawable::Multi { state, idx, .. } => state.draw_state(*idx),
Drawable::TermLike { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
};
state.reset();
state
}
pub(crate) fn clear(mut self) -> io::Result<()> {
let state = self.state();
drop(state);
self.draw()
}
pub(crate) fn draw(self) -> io::Result<()> {
match self {
Drawable::Term {
term,
last_line_count,
draw_state,
} => draw_state.draw_to_term(term, last_line_count),
Drawable::Multi {
mut state,
force_draw,
now,
..
} => state.draw(force_draw, None, now),
Drawable::TermLike {
term_like,
last_line_count,
draw_state,
} => draw_state.draw_to_term(term_like, last_line_count),
}
}
}
pub(crate) enum LineAdjust {
/// Adds to `last_line_count` so that the next draw also clears those lines
Clear(VisualLines),
/// Subtracts from `last_line_count` so that the next draw retains those lines
Keep(VisualLines),
}
pub(crate) struct DrawStateWrapper<'a> {
state: &'a mut DrawState,
orphan_lines: Option<&'a mut Vec<String>>,
}
impl<'a> DrawStateWrapper<'a> {
pub(crate) fn for_term(state: &'a mut DrawState) -> Self {
Self {
state,
orphan_lines: None,
}
}
pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<String>) -> Self {
Self {
state,
orphan_lines: Some(orphan_lines),
}
}
}
impl std::ops::Deref for DrawStateWrapper<'_> {
type Target = DrawState;
fn deref(&self) -> &Self::Target {
self.state
}
}
impl std::ops::DerefMut for DrawStateWrapper<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.state
}
}
impl Drop for DrawStateWrapper<'_> {
fn drop(&mut self) {
if let Some(orphaned) = &mut self.orphan_lines {
orphaned.extend(self.state.lines.drain(..self.state.orphan_lines_count));
self.state.orphan_lines_count = 0;
}
}
}
#[derive(Debug)]
struct RateLimiter {
interval: u16, // in milliseconds
capacity: u8,
prev: Instant,
}
/// Rate limit but allow occasional bursts above desired rate
impl RateLimiter {
fn new(rate: u8) -> Self {
Self {
interval: 1000 / (rate as u16), // between 3 and 1000 milliseconds
capacity: MAX_BURST,
prev: Instant::now(),
}
}
fn allow(&mut self, now: Instant) -> bool {
if now < self.prev {
return false;
}
let elapsed = now - self.prev;
// If `capacity` is 0 and not enough time (`self.interval` ms) has passed since
// `self.prev` to add new capacity, return `false`. The goal of this method is to
// make this decision as efficient as possible.
if self.capacity == 0 && elapsed < Duration::from_millis(self.interval as u64) {
return false;
}
// We now calculate `new`, the number of ms, since we last returned `true`,
// and `remainder`, which represents a number of ns less than 1ms which we cannot
// convert into capacity now, so we're saving it for later.
let (new, remainder) = (
elapsed.as_millis() / self.interval as u128,
elapsed.as_nanos() % (self.interval as u128 * 1_000_000),
);
// We add `new` to `capacity`, subtract one for returning `true` from here,
// then make sure it does not exceed a maximum of `MAX_BURST`, then store it.
self.capacity = Ord::min(MAX_BURST as u128, (self.capacity as u128) + new - 1) as u8;
// Store `prev` for the next iteration after subtracting the `remainder`.
// Just use `unwrap` here because it shouldn't be possible for this to underflow.
self.prev = now
.checked_sub(Duration::from_nanos(remainder as u64))
.unwrap();
true
}
}
const MAX_BURST: u8 = 20;
/// The drawn state of an element.
#[derive(Clone, Debug, Default)]
pub(crate) struct DrawState {
/// The lines to print (can contain ANSI codes)
pub(crate) lines: Vec<String>,
/// The number [`Self::lines`] entries that shouldn't be reaped by the next tick.
///
/// Note that this number may be different than the number of visual lines required to draw [`Self::lines`].
pub(crate) orphan_lines_count: usize,
/// True if we should move the cursor up when possible instead of clearing lines.
pub(crate) move_cursor: bool,
/// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top`
pub(crate) alignment: MultiProgressAlignment,
}
impl DrawState {
fn draw_to_term(
&mut self,
term: &(impl TermLike + ?Sized),
last_line_count: &mut VisualLines,
) -> io::Result<()> {
if panicking() {
return Ok(());
}
if !self.lines.is_empty() && self.move_cursor {
term.move_cursor_up(last_line_count.as_usize())?;
} else {
// Fork of console::clear_last_lines that assumes that the last line doesn't contain a '\n'
let n = last_line_count.as_usize();
term.move_cursor_up(n.saturating_sub(1))?;
for i in 0..n {
term.clear_line()?;
if i + 1 != n {
term.move_cursor_down(1)?;
}
}
term.move_cursor_up(n.saturating_sub(1))?;
}
let width = term.width() as usize;
let visual_lines = self.visual_line_count(.., width);
let shift = match self.alignment {
MultiProgressAlignment::Bottom if visual_lines < *last_line_count => {
let shift = *last_line_count - visual_lines;
for _ in 0..shift.as_usize() {
term.write_line("")?;
}
shift
}
_ => VisualLines::default(),
};
let term_height = term.height() as usize;
let term_width = term.width() as usize;
let len = self.lines.len();
debug_assert!(self.orphan_lines_count <= self.lines.len());
let orphan_visual_line_count =
self.visual_line_count(..self.orphan_lines_count, term_width);
let mut real_len = VisualLines::default();
let mut last_line_filler = 0;
for (idx, line) in self.lines.iter().enumerate() {
let line_width = console::measure_text_width(line);
let diff = if line.is_empty() {
// Empty line are new line
1
} else {
// Calculate real length based on terminal width
// This take in account linewrap from terminal
let terminal_len = (line_width as f64 / term_width as f64).ceil() as usize;
// If the line is effectively empty (for example when it consists
// solely of ANSI color code sequences, count it the same as a
// new line. If the line is measured to be len = 0, we will
// subtract with overflow later.
usize::max(terminal_len, 1)
}
.into();
// Have all orphan lines been drawn?
if self.orphan_lines_count <= idx {
// If so, then `real_len` should be at least `orphan_visual_line_count`.
debug_assert!(orphan_visual_line_count <= real_len);
// Don't consider orphan lines when comparing to terminal height.
if real_len - orphan_visual_line_count + diff > term_height.into() {
break;
}
}
real_len += diff;
if idx != 0 {
term.write_line("")?;
}
term.write_str(line)?;
if idx + 1 == len {
// Keep the cursor on the right terminal side
// So that next user writes/prints will happen on the next line
last_line_filler = term_width.saturating_sub(line_width);
}
}
term.write_str(&" ".repeat(last_line_filler))?;
term.flush()?;
*last_line_count = real_len - orphan_visual_line_count + shift;
Ok(())
}
fn reset(&mut self) {
self.lines.clear();
self.orphan_lines_count = 0;
}
pub(crate) fn visual_line_count(
&self,
range: impl SliceIndex<[String], Output = [String]>,
width: usize,
) -> VisualLines {
visual_line_count(&self.lines[range], width)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct VisualLines(usize);
impl VisualLines {
pub(crate) fn saturating_add(&self, other: Self) -> Self {
Self(self.0.saturating_add(other.0))
}
pub(crate) fn saturating_sub(&self, other: Self) -> Self {
Self(self.0.saturating_sub(other.0))
}
pub(crate) fn as_usize(&self) -> usize {
self.0
}
}
impl Add for VisualLines {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl AddAssign for VisualLines {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl<T: Into<usize>> From<T> for VisualLines {
fn from(value: T) -> Self {
Self(value.into())
}
}
impl Sub for VisualLines {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
/// Calculate the number of visual lines in the given lines, after
/// accounting for line wrapping and non-printable characters.
pub(crate) fn visual_line_count(lines: &[impl AsRef<str>], width: usize) -> VisualLines {
let mut real_lines = 0;
for line in lines {
let effective_line_length = console::measure_text_width(line.as_ref());
real_lines += usize::max(
(effective_line_length as f64 / width as f64).ceil() as usize,
1,
);
}
real_lines.into()
}
#[cfg(test)]
mod tests {
use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
#[test]
fn multi_is_hidden() {
let mp = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
let pb = mp.add(ProgressBar::new(100));
assert!(mp.is_hidden());
assert!(pb.is_hidden());
}
#[test]
fn real_line_count_test() {
#[derive(Debug)]
struct Case {
lines: &'static [&'static str],
expectation: usize,
width: usize,
}
let lines_and_expectations = [
Case {
lines: &["1234567890"],
expectation: 1,
width: 10,
},
Case {
lines: &["1234567890"],
expectation: 2,
width: 5,
},
Case {
lines: &["1234567890"],
expectation: 3,
width: 4,
},
Case {
lines: &["1234567890"],
expectation: 4,
width: 3,
},
Case {
lines: &["1234567890", "", "1234567890"],
expectation: 3,
width: 10,
},
Case {
lines: &["1234567890", "", "1234567890"],
expectation: 5,
width: 5,
},
Case {
lines: &["1234567890", "", "1234567890"],
expectation: 7,
width: 4,
},
Case {
lines: &["aaaaaaaaaaaaa", "", "bbbbbbbbbbbbbbbbb", "", "ccccccc"],
expectation: 8,
width: 7,
},
Case {
lines: &["", "", "", "", ""],
expectation: 5,
width: 6,
},
Case {
// These lines contain only ANSI escape sequences, so they should only count as 1 line
lines: &["\u{1b}[1m\u{1b}[1m\u{1b}[1m", "\u{1b}[1m\u{1b}[1m\u{1b}[1m"],
expectation: 2,
width: 5,
},
Case {
// These lines contain ANSI escape sequences and two effective chars, so they should only count as 1 line still
lines: &[
"a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
"a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
],
expectation: 2,
width: 5,
},
Case {
// These lines contain ANSI escape sequences and six effective chars, so they should count as 2 lines each
lines: &[
"aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
"aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
],
expectation: 4,
width: 5,
},
];
for case in lines_and_expectations.iter() {
let result = super::visual_line_count(case.lines, case.width);
assert_eq!(result, case.expectation.into(), "case: {:?}", case);
}
}
}