use std::io;
use std::sync::{Arc, RwLock, RwLockWriteGuard};
use std::time::{Duration, Instant};
use console::Term;
use crate::multi::{MultiProgressAlignment, MultiState};
use crate::TermLike;
#[derive(Debug)]
pub struct ProgressDrawTarget {
kind: TargetKind,
}
impl ProgressDrawTarget {
pub fn stdout() -> ProgressDrawTarget {
ProgressDrawTarget::term(Term::buffered_stdout(), 20)
}
pub fn stderr() -> ProgressDrawTarget {
ProgressDrawTarget::term(Term::buffered_stderr(), 20)
}
pub fn stdout_with_hz(refresh_rate: u8) -> ProgressDrawTarget {
ProgressDrawTarget::term(Term::buffered_stdout(), refresh_rate)
}
pub fn stderr_with_hz(refresh_rate: u8) -> ProgressDrawTarget {
ProgressDrawTarget::term(Term::buffered_stderr(), refresh_rate)
}
pub(crate) fn new_remote(state: Arc<RwLock<MultiState>>, idx: usize) -> Self {
Self {
kind: TargetKind::Multi { state, idx },
}
}
pub fn term(term: Term, refresh_rate: u8) -> ProgressDrawTarget {
ProgressDrawTarget {
kind: TargetKind::Term {
term,
last_line_count: 0,
rate_limiter: RateLimiter::new(refresh_rate),
draw_state: DrawState::default(),
},
}
}
pub fn term_like(term_like: Box<dyn TermLike>) -> ProgressDrawTarget {
ProgressDrawTarget {
kind: TargetKind::TermLike {
inner: term_like,
last_line_count: 0,
draw_state: DrawState::default(),
},
}
}
pub fn hidden() -> ProgressDrawTarget {
ProgressDrawTarget {
kind: TargetKind::Hidden,
}
}
pub fn is_hidden(&self) -> bool {
match self.kind {
TargetKind::Hidden => true,
TargetKind::Term { ref term, .. } => !term.is_term(),
_ => false,
}
}
pub(crate) fn width(&self) -> u16 {
match self.kind {
TargetKind::Term { ref term, .. } => term.size().1,
TargetKind::Multi { ref state, .. } => state.read().unwrap().width(),
TargetKind::Hidden => 0,
TargetKind::TermLike { ref inner, .. } => inner.width(),
}
}
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, }
}
TargetKind::Multi { idx, state, .. } => {
let state = state.write().unwrap();
Some(Drawable::Multi {
idx: *idx,
state,
force_draw,
now,
})
}
TargetKind::TermLike {
inner,
last_line_count,
draw_state,
} => Some(Drawable::TermLike {
term_like: &**inner,
last_line_count,
draw_state,
}),
_ => None,
}
}
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,
}
}
}
#[derive(Debug)]
enum TargetKind {
Term {
term: Term,
last_line_count: usize,
rate_limiter: RateLimiter,
draw_state: DrawState,
},
Multi {
state: Arc<RwLock<MultiState>>,
idx: usize,
},
Hidden,
TermLike {
inner: Box<dyn TermLike>,
last_line_count: usize,
draw_state: DrawState,
},
}
pub(crate) enum Drawable<'a> {
Term {
term: &'a Term,
last_line_count: &'a mut usize,
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 usize,
draw_state: &'a mut DrawState,
},
}
impl<'a> Drawable<'a> {
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) 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));
self.state.orphan_lines = 0;
}
}
}
#[derive(Debug)]
struct RateLimiter {
interval: u16, capacity: u8,
prev: Instant,
}
impl RateLimiter {
fn new(rate: u8) -> Self {
Self {
interval: 1000 / (rate as u16), capacity: MAX_BURST,
prev: Instant::now(),
}
}
fn allow(&mut self, now: Instant) -> bool {
if now < self.prev {
return false;
}
let elapsed = now - self.prev;
let remaining = (MAX_BURST - self.capacity) as u128;
self.capacity += Ord::min(remaining, elapsed.as_millis() / self.interval as u128) as u8;
let interval_nanos = self.interval as u128 * 1_000_000;
self.prev = now - Duration::from_nanos((elapsed.as_nanos() % interval_nanos) as u64);
match self.capacity.checked_sub(1) {
Some(new) => {
self.capacity = new;
true
}
None => false,
}
}
}
const MAX_BURST: u8 = 20;
#[derive(Clone, Debug, Default)]
pub(crate) struct DrawState {
pub(crate) lines: Vec<String>,
pub(crate) orphan_lines: usize,
pub(crate) move_cursor: bool,
pub(crate) alignment: MultiProgressAlignment,
}
impl DrawState {
fn draw_to_term(
&mut self,
term: &(impl TermLike + ?Sized),
last_line_count: &mut usize,
) -> io::Result<()> {
if !self.lines.is_empty() && self.move_cursor {
term.move_cursor_up(*last_line_count)?;
} else {
let n = *last_line_count;
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 shift = match self.alignment {
MultiProgressAlignment::Bottom if self.lines.len() < *last_line_count => {
let shift = *last_line_count - self.lines.len();
for _ in 0..shift {
term.write_line("")?;
}
shift
}
_ => 0,
};
let len = self.lines.len();
for (idx, line) in self.lines.iter().enumerate() {
if idx + 1 != len {
term.write_line(line)?;
} else {
term.write_str(line)?;
let term_width = term.width() as usize;
let line_width = console::measure_text_width(line);
term.write_str(&" ".repeat(term_width.saturating_sub(line_width)))?;
}
}
term.flush()?;
*last_line_count = self.lines.len() - self.orphan_lines + shift;
Ok(())
}
fn reset(&mut self) {
self.lines.clear();
self.orphan_lines = 0;
}
}