diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
commit | 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch) | |
tree | bdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/prodash/src | |
parent | Releasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff) | |
download | rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/prodash/src')
32 files changed, 4853 insertions, 0 deletions
diff --git a/vendor/prodash/src/lib.rs b/vendor/prodash/src/lib.rs new file mode 100644 index 000000000..974cc8598 --- /dev/null +++ b/vendor/prodash/src/lib.rs @@ -0,0 +1,86 @@ +#![deny(unsafe_code, missing_docs)] + +/*! +Prodash is a dashboard for displaying the progress of concurrent application. + +It consists of two parts + +* a `Tree` to gather progress information and messages +* a terminal user interface which displays this information, along with optional free-form information provided by the application itself + +Even though the `Tree` is not async, it's meant to be transparent and non-blocking performance wise, and benchmarks seem to indicate this +is indeed the case. + +The **terminal user interface** seems to be the least transparent part, but can be configured to refresh less frequently. + +# Terminal User Interface + +By default, a TUI is provided to visualize all state. Have a look at [the example provided in the tui module](./tui/index.html). + +**Please note** that it is behind the `render-tui` feature toggle, which is enabled by default. + +# Logging + +If the feature `progress-tree-log` is enabled (default), most calls to `progress` will also be logged. +That way, even without a terminal user interface, there will be progress messages. +Please note that logging to stdout should not be performed with this feature enabled and a terminal user interface is shown, as this will +seriously interfere with the TUI. + +# A demo application + +Please have a look at the [dashboard demo](https://github.com/Byron/crates-io-cli-rs/blob/master/prodash/examples/dashboard.rs). + +[![asciicast](https://asciinema.org/a/301838.svg)](https://asciinema.org/a/301838) + +Run it with `cargo run --example dashboard` and see what else it can do by checking out `cargo run --example dashboard -- --help`. +*/ +#[cfg(feature = "atty")] +pub use atty; + +#[cfg(feature = "progress-tree")] +/// +pub mod tree; + +/// +pub mod render; + +#[cfg(feature = "progress-tree-log")] +pub use log::info; +#[cfg(feature = "progress-tree-log")] +pub use log::warn; + +#[cfg(any(feature = "humantime", feature = "time"))] +/// +pub mod time; + +/// +pub mod unit; +#[doc(inline)] +pub use unit::Unit; + +/// +pub mod messages; +/// +pub mod progress; + +mod traits; +pub use traits::{Progress, Root, WeakRoot}; + +mod throughput; +pub use crate::throughput::Throughput; + +#[cfg(not(feature = "progress-tree-log"))] +mod log { + /// Stub + #[macro_export(local_inner_macros)] + macro_rules! warn { + (target: $target:expr, $($arg:tt)+) => {}; + ($($arg:tt)+) => {}; + } + /// Stub + #[macro_export(local_inner_macros)] + macro_rules! info { + (target: $target:expr, $($arg:tt)+) => {}; + ($($arg:tt)+) => {}; + } +} diff --git a/vendor/prodash/src/messages.rs b/vendor/prodash/src/messages.rs new file mode 100644 index 000000000..2abd63830 --- /dev/null +++ b/vendor/prodash/src/messages.rs @@ -0,0 +1,126 @@ +use std::time::SystemTime; + +/// The severity of a message +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum MessageLevel { + /// Rarely sent information related to the progress, not to be confused with the progress itself + Info, + /// Used to indicate that a task has failed, along with the reason + Failure, + /// Indicates a task was completed successfully + Success, +} + +/// A message to be stored along with the progress tree. +/// +/// It is created by [`Tree::message(…)`](./struct.Item.html#method.message). +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Message { + /// The time at which the message was sent. + pub time: SystemTime, + /// The severity of the message + pub level: MessageLevel, + /// The name of the task that created the `Message` + pub origin: String, + /// The message itself + pub message: String, +} + +/// A ring buffer for messages. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MessageRingBuffer { + pub(crate) buf: Vec<Message>, + cursor: usize, + total: usize, +} + +impl MessageRingBuffer { + /// Create a new instance the ability to hold `capacity` amount of messages. + pub fn with_capacity(capacity: usize) -> MessageRingBuffer { + MessageRingBuffer { + buf: Vec::with_capacity(capacity), + cursor: 0, + total: 0, + } + } + + /// Push a `message` from `origin` at severity `level` into the buffer, possibly overwriting the last message added. + pub fn push_overwrite(&mut self, level: MessageLevel, origin: String, message: impl Into<String>) { + let msg = Message { + time: SystemTime::now(), + level, + origin, + message: message.into(), + }; + if self.has_capacity() { + self.buf.push(msg) + } else { + self.buf[self.cursor] = msg; + self.cursor = (self.cursor + 1) % self.buf.len(); + } + self.total = self.total.wrapping_add(1); + } + + /// Copy all messages currently contained in the buffer to `out`. + pub fn copy_all(&self, out: &mut Vec<Message>) { + out.clear(); + if self.buf.is_empty() { + return; + } + out.extend_from_slice(&self.buf[self.cursor % self.buf.len()..]); + if self.cursor != self.buf.len() { + out.extend_from_slice(&self.buf[..self.cursor]); + } + } + + /// Copy all new messages into `out` that where received since the last time this method was called provided + /// its `previous` return value. + pub fn copy_new(&self, out: &mut Vec<Message>, previous: Option<MessageCopyState>) -> MessageCopyState { + out.clear(); + match previous { + Some(MessageCopyState { cursor, buf_len, total }) => { + if self.total.saturating_sub(total) >= self.buf.capacity() { + self.copy_all(out); + } else { + let new_elements_below_cap = self.buf.len().saturating_sub(buf_len); + let cursor_ofs: isize = self.cursor as isize - cursor as isize; + match cursor_ofs { + // there was some capacity left without wrapping around + c if c == 0 => { + out.extend_from_slice(&self.buf[self.buf.len() - new_elements_below_cap..]); + } + // cursor advanced + c if c > 0 => { + out.extend_from_slice(&self.buf[(cursor % self.buf.len())..self.cursor]); + } + // cursor wrapped around + c if c < 0 => { + out.extend_from_slice(&self.buf[(cursor % self.buf.len())..]); + out.extend_from_slice(&self.buf[..self.cursor]); + } + _ => unreachable!("logic dictates that… yeah, you really shouldn't ever see this!"), + } + } + } + None => self.copy_all(out), + }; + MessageCopyState { + cursor: self.cursor, + buf_len: self.buf.len(), + total: self.total, + } + } + + fn has_capacity(&self) -> bool { + self.buf.len() < self.buf.capacity() + } +} + +/// State used to keep track of what's new since the last time message were copied. +/// +/// Note that due to the nature of a ring buffer, there is no guarantee that you see all messages. +pub struct MessageCopyState { + cursor: usize, + buf_len: usize, + total: usize, +} diff --git a/vendor/prodash/src/progress/key.rs b/vendor/prodash/src/progress/key.rs new file mode 100644 index 000000000..dae714d68 --- /dev/null +++ b/vendor/prodash/src/progress/key.rs @@ -0,0 +1,261 @@ +use std::ops::{Index, IndexMut}; + +use crate::progress::Task; + +/// a level in the hierarchy of key components +/// +/// _NOTE:_ This means we will show weird behaviour if there are more than 2^16 tasks at the same time on a level +/// as multiple progress handles will manipulate the same state. +pub type Level = u8; + +pub(crate) type Id = u16; + +/// A type identifying a spot in the hierarchy of `Tree` items. +#[derive(Copy, Clone, Default, Hash, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub struct Key(Option<Id>, Option<Id>, Option<Id>, Option<Id>, Option<Id>, Option<Id>); + +/// Determines if a sibling is above or below in the given level of hierarchy +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +#[allow(missing_docs)] +pub enum SiblingLocation { + Above, + Below, + AboveAndBelow, + NotFound, +} + +impl SiblingLocation { + fn merge(&mut self, other: SiblingLocation) { + use SiblingLocation::*; + *self = match (*self, other) { + (any, NotFound) => any, + (NotFound, any) => any, + (Above, Below) => AboveAndBelow, + (Below, Above) => AboveAndBelow, + (AboveAndBelow, _) => AboveAndBelow, + (_, AboveAndBelow) => AboveAndBelow, + (Above, Above) => Above, + (Below, Below) => Below, + }; + } +} + +impl Default for SiblingLocation { + fn default() -> Self { + SiblingLocation::NotFound + } +} + +/// A type providing information about what's above and below `Tree` items. +#[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub struct Adjacency( + pub SiblingLocation, + pub SiblingLocation, + pub SiblingLocation, + pub SiblingLocation, + pub SiblingLocation, + pub SiblingLocation, +); + +impl Adjacency { + /// Return the level at which this sibling is located in the hierarchy. + pub fn level(&self) -> Level { + use SiblingLocation::*; + match self { + Adjacency(NotFound, NotFound, NotFound, NotFound, NotFound, NotFound) => 0, + Adjacency(_a, NotFound, NotFound, NotFound, NotFound, NotFound) => 1, + Adjacency(_a, _b, NotFound, NotFound, NotFound, NotFound) => 2, + Adjacency(_a, _b, _c, NotFound, NotFound, NotFound) => 3, + Adjacency(_a, _b, _c, _d, NotFound, NotFound) => 4, + Adjacency(_a, _b, _c, _d, _e, NotFound) => 5, + Adjacency(_a, _b, _c, _d, _e, _f) => 6, + } + } + /// Get a reference to the sibling location at `level`. + pub fn get(&self, level: Level) -> Option<&SiblingLocation> { + Some(match level { + 1 => &self.0, + 2 => &self.1, + 3 => &self.2, + 4 => &self.3, + 5 => &self.4, + 6 => &self.5, + _ => return None, + }) + } + /// Get a mutable reference to the sibling location at `level`. + pub fn get_mut(&mut self, level: Level) -> Option<&mut SiblingLocation> { + Some(match level { + 1 => &mut self.0, + 2 => &mut self.1, + 3 => &mut self.2, + 4 => &mut self.3, + 5 => &mut self.4, + 6 => &mut self.5, + _ => return None, + }) + } +} + +impl Index<Level> for Adjacency { + type Output = SiblingLocation; + fn index(&self, index: Level) -> &Self::Output { + self.get(index).expect("adjacency index in bound") + } +} + +impl IndexMut<Level> for Adjacency { + fn index_mut(&mut self, index: Level) -> &mut Self::Output { + self.get_mut(index).expect("adjacency index in bound") + } +} + +impl Key { + /// Return the key to the child identified by `child_id` located in a new nesting level below `self`. + pub fn add_child(self, child_id: Id) -> Key { + match self { + Key(None, None, None, None, None, None) => Key(Some(child_id), None, None, None, None, None), + Key(a, None, None, None, None, None) => Key(a, Some(child_id), None, None, None, None), + Key(a, b, None, None, None, None) => Key(a, b, Some(child_id), None, None, None), + Key(a, b, c, None, None, None) => Key(a, b, c, Some(child_id), None, None), + Key(a, b, c, d, None, None) => Key(a, b, c, d, Some(child_id), None), + Key(a, b, c, d, e, _f) => { + crate::warn!("Maximum nesting level reached. Adding tasks to current parent"); + Key(a, b, c, d, e, Some(child_id)) + } + } + } + + /// The level of hierarchy a node is placed in, i.e. the amount of path components + pub fn level(&self) -> Level { + match self { + Key(None, None, None, None, None, None) => 0, + Key(Some(_), None, None, None, None, None) => 1, + Key(Some(_), Some(_), None, None, None, None) => 2, + Key(Some(_), Some(_), Some(_), None, None, None) => 3, + Key(Some(_), Some(_), Some(_), Some(_), None, None) => 4, + Key(Some(_), Some(_), Some(_), Some(_), Some(_), None) => 5, + Key(Some(_), Some(_), Some(_), Some(_), Some(_), Some(_)) => 6, + _ => unreachable!("This is a bug - Keys follow a certain pattern"), + } + } + + /// Return the identifier for the item at `level`. + fn get(&self, level: Level) -> Option<&Id> { + match level { + 1 => self.0.as_ref(), + 2 => self.1.as_ref(), + 3 => self.2.as_ref(), + 4 => self.3.as_ref(), + 5 => self.4.as_ref(), + 6 => self.5.as_ref(), + _ => None, + } + } + + /// Return true if the item identified by `other` shares the parent at `parent_level`. + pub fn shares_parent_with(&self, other: &Key, parent_level: Level) -> bool { + if parent_level < 1 { + return true; + } + for level in 1..=parent_level { + if let (Some(lhs), Some(rhs)) = (self.get(level), other.get(level)) { + if lhs != rhs { + return false; + } + } else { + return false; + } + } + true + } + + /// Compute the adjacency map for the key in `sorted` at the given `index`. + /// + /// It's vital that the invariant of `sorted` to actually be sorted by key is upheld + /// for the result to be reliable. + pub fn adjacency(sorted: &[(Key, Task)], index: usize) -> Adjacency { + use SiblingLocation::*; + let key = &sorted[index].0; + let key_level = key.level(); + let mut adjecency = Adjacency::default(); + if key_level == 0 { + return adjecency; + } + + fn search<'a>( + iter: impl Iterator<Item = &'a (Key, Task)>, + key: &Key, + key_level: Level, + current_level: Level, + _id_at_level: Id, + ) -> Option<usize> { + iter.map(|(k, _)| k) + .take_while(|other| key.shares_parent_with(other, current_level.saturating_sub(1))) + .enumerate() + .find(|(_idx, k)| { + if current_level == key_level { + k.level() == key_level || k.level() + 1 == key_level + } else { + k.level() == current_level + } + }) + .map(|(idx, _)| idx) + } + + let upward_iter = |from: usize, key: &Key, level: Level, id_at_level: Id| { + search(sorted[..from].iter().rev(), key, key_level, level, id_at_level) + }; + let downward_iter = |from: usize, key: &Key, level: Level, id_at_level: Id| { + sorted + .get(from + 1..) + .and_then(|s| search(s.iter(), key, key_level, level, id_at_level)) + }; + + { + let mut cursor = index; + for level in (1..=key_level).rev() { + if level == 1 { + adjecency[level].merge(Above); // the root or any other sibling on level one + continue; + } + if let Some(key_offset) = upward_iter(cursor, key, level, key[level]) { + cursor = index.saturating_sub(key_offset); + adjecency[level].merge(Above); + } + } + } + { + let mut cursor = index; + for level in (1..=key_level).rev() { + if let Some(key_offset) = downward_iter(cursor, key, level, key[level]) { + cursor = index + key_offset; + adjecency[level].merge(Below); + } + } + } + for level in 1..key_level { + if key_level == 1 && index + 1 == sorted.len() { + continue; + } + adjecency[level] = match adjecency[level] { + Above | Below | NotFound => NotFound, + AboveAndBelow => AboveAndBelow, + }; + } + adjecency + } + + /// The maximum amount of path components we can represent. + pub const fn max_level() -> Level { + 6 + } +} + +impl Index<Level> for Key { + type Output = Id; + + fn index(&self, index: Level) -> &Self::Output { + self.get(index).expect("key index in bound") + } +} diff --git a/vendor/prodash/src/progress/log.rs b/vendor/prodash/src/progress/log.rs new file mode 100644 index 000000000..3410a5dc2 --- /dev/null +++ b/vendor/prodash/src/progress/log.rs @@ -0,0 +1,151 @@ +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; + +use crate::{ + messages::MessageLevel, + progress::{Id, Step, StepShared}, + Progress, Unit, +}; + +/// A [`Progress`] implementation which displays progress as it happens without the use of a renderer. +/// +/// Note that this incurs considerable performance cost as each progress calls ends up getting the system time +/// to see if progress information should actually be emitted. +pub struct Log { + name: String, + id: Id, + max: Option<usize>, + unit: Option<Unit>, + step: usize, + current_level: usize, + max_level: usize, + trigger: Arc<AtomicBool>, +} + +const EMIT_LOG_EVERY_S: f32 = 0.5; +const SEP: &str = "::"; + +impl Log { + /// Create a new instance from `name` while displaying progress information only up to `max_level`. + pub fn new(name: impl Into<String>, max_level: Option<usize>) -> Self { + let trigger = Arc::new(AtomicBool::new(true)); + std::thread::spawn({ + let duration = Duration::from_secs_f32(EMIT_LOG_EVERY_S); + let trigger = Arc::downgrade(&trigger); + move || { + while let Some(t) = trigger.upgrade() { + t.store(true, Ordering::Relaxed); + std::thread::sleep(duration); + } + } + }); + Log { + name: name.into(), + id: crate::progress::UNKNOWN, + current_level: 0, + max_level: max_level.unwrap_or(usize::MAX), + max: None, + step: 0, + unit: None, + trigger, + } + } +} + +impl Progress for Log { + type SubProgress = Log; + + fn add_child(&mut self, name: impl Into<String>) -> Self::SubProgress { + self.add_child_with_id(name, crate::progress::UNKNOWN) + } + + fn add_child_with_id(&mut self, name: impl Into<String>, id: Id) -> Self::SubProgress { + Log { + name: format!("{}{}{}", self.name, SEP, Into::<String>::into(name)), + id, + current_level: self.current_level + 1, + max_level: self.max_level, + step: 0, + max: None, + unit: None, + trigger: Arc::clone(&self.trigger), + } + } + + fn init(&mut self, max: Option<usize>, unit: Option<Unit>) { + self.max = max; + self.unit = unit; + } + + fn set(&mut self, step: usize) { + self.step = step; + if self.current_level > self.max_level { + return; + } + if self.trigger.swap(false, Ordering::Relaxed) { + match (self.max, &self.unit) { + (max, Some(unit)) => log::info!("{} → {}", self.name, unit.display(step, max, None)), + (Some(max), None) => log::info!("{} → {} / {}", self.name, step, max), + (None, None) => log::info!("{} → {}", self.name, step), + } + } + } + + fn unit(&self) -> Option<Unit> { + self.unit.clone() + } + + fn max(&self) -> Option<usize> { + self.max + } + + fn set_max(&mut self, max: Option<Step>) -> Option<Step> { + let prev = self.max; + self.max = max; + prev + } + + fn step(&self) -> usize { + self.step + } + + fn inc_by(&mut self, step: usize) { + self.set(self.step + step) + } + + fn set_name(&mut self, name: impl Into<String>) { + let name = name.into(); + self.name = self + .name + .split("::") + .next() + .map(|parent| format!("{}{}{}", parent.to_owned(), SEP, name)) + .unwrap_or(name); + } + + fn name(&self) -> Option<String> { + self.name.split(SEP).nth(1).map(ToOwned::to_owned) + } + + fn id(&self) -> Id { + self.id + } + + fn message(&mut self, level: MessageLevel, message: impl Into<String>) { + let message: String = message.into(); + match level { + MessageLevel::Info => log::info!("ℹ{} → {}", self.name, message), + MessageLevel::Failure => log::error!("𐄂{} → {}", self.name, message), + MessageLevel::Success => log::info!("✓{} → {}", self.name, message), + } + } + + fn counter(&self) -> Option<StepShared> { + None + } +} diff --git a/vendor/prodash/src/progress/mod.rs b/vendor/prodash/src/progress/mod.rs new file mode 100644 index 000000000..0d9c694cf --- /dev/null +++ b/vendor/prodash/src/progress/mod.rs @@ -0,0 +1,112 @@ +use std::{ + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::SystemTime, +}; + +use crate::unit::Unit; + +/// +pub mod key; +#[doc(inline)] +pub use key::Key; + +mod utils; + +#[cfg(feature = "progress-log")] +mod log; +pub use utils::{Discard, DoOrDiscard, Either, ThroughputOnDrop}; + +#[cfg(feature = "progress-log")] +pub use self::log::Log; + +/// Four bytes of function-local unique and stable identifier for each item added as progress, +/// like b"TREE" or b"FILE". +/// +/// Note that uniqueness only relates to one particular method call where those interested in its progress +/// may assume certain stable ids to look for when selecting specific bits of progress to process. +pub type Id = [u8; 4]; + +/// The default Id to use if there is no need for an id. +/// +/// This is the default unless applications wish to make themselves more introspectable. +pub const UNKNOWN: Id = *b"\0\0\0\0"; + +/// The amount of steps a progress can make +pub type Step = usize; + +/// As step, but shareable. +pub type StepShared = Arc<AtomicUsize>; + +/// Indicate whether a progress can or cannot be made. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] +pub enum State { + /// Indicates a task is blocked and cannot indicate progress, optionally until the + /// given time. The task cannot easily be interrupted. + Blocked(&'static str, Option<SystemTime>), + /// Indicates a task cannot indicate progress, optionally until the + /// given time. The task can be interrupted. + Halted(&'static str, Option<SystemTime>), + /// The task is running + Running, +} + +impl Default for State { + fn default() -> Self { + State::Running + } +} + +/// Progress associated with some item in the progress tree. +#[derive(Clone, Default, Debug)] +pub struct Value { + /// The amount of progress currently made + pub step: StepShared, + /// The step at which no further progress has to be made. + /// + /// If unset, the progress is unbounded. + pub done_at: Option<Step>, + /// The unit associated with the progress. + pub unit: Option<Unit>, + /// Whether progress can be made or not + pub state: State, +} + +impl std::hash::Hash for Value { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + let Self { + step, + done_at, + unit, + state: our_state, + } = self; + done_at.hash(state); + unit.hash(state); + our_state.hash(state); + step.load(Ordering::Relaxed).hash(state); + } +} + +impl Value { + /// Returns a number between `Some(0.0)` and `Some(1.0)`, or `None` if the progress is unbounded. + /// + /// A task half done would return `Some(0.5)`. + pub fn fraction(&self) -> Option<f32> { + self.done_at + .map(|done_at| self.step.load(Ordering::SeqCst) as f32 / done_at as f32) + } +} + +/// The value associated with a spot in the hierarchy. +#[derive(Clone, Default, Debug, Hash)] +pub struct Task { + /// The name of the `Item` or task. + pub name: String, + /// The stable identifier of this task. + /// Useful for selecting specific tasks out of a set of them. + pub id: Id, + /// The progress itself, unless this value belongs to an `Item` serving as organizational unit. + pub progress: Option<Value>, +} diff --git a/vendor/prodash/src/progress/utils.rs b/vendor/prodash/src/progress/utils.rs new file mode 100644 index 000000000..549a0b6e5 --- /dev/null +++ b/vendor/prodash/src/progress/utils.rs @@ -0,0 +1,337 @@ +use crate::{messages::MessageLevel, progress::Id, Progress, Unit}; + +/// An implementation of [`Progress`] which discards all calls. +pub struct Discard; + +impl Progress for Discard { + type SubProgress = Discard; + + fn add_child(&mut self, _name: impl Into<String>) -> Self::SubProgress { + Discard + } + + fn add_child_with_id(&mut self, _name: impl Into<String>, _id: Id) -> Self::SubProgress { + Discard + } + + fn init(&mut self, _max: Option<usize>, _unit: Option<Unit>) {} + + fn set(&mut self, _step: usize) {} + + fn set_max(&mut self, _max: Option<Step>) -> Option<Step> { + None + } + + fn step(&self) -> usize { + 0 + } + + fn inc_by(&mut self, _step: usize) {} + + fn set_name(&mut self, _name: impl Into<String>) {} + + fn name(&self) -> Option<String> { + None + } + + fn id(&self) -> Id { + crate::progress::UNKNOWN + } + + fn message(&mut self, _level: MessageLevel, _message: impl Into<String>) {} + + fn counter(&self) -> Option<StepShared> { + None + } +} + +/// An implementation of [`Progress`] showing either one or the other implementation. +/// +/// Useful in conjunction with [`Discard`] and a working implementation, making it as a form of `Option<Progress>` which +/// can be passed to methods requiring `impl Progress`. +/// See [`DoOrDiscard`] for an incarnation of this. +#[allow(missing_docs)] +pub enum Either<L, R> { + Left(L), + Right(R), +} + +impl<L, R> Progress for Either<L, R> +where + L: Progress, + R: Progress, +{ + type SubProgress = Either<L::SubProgress, R::SubProgress>; + + fn add_child(&mut self, name: impl Into<String>) -> Self::SubProgress { + match self { + Either::Left(l) => Either::Left(l.add_child(name)), + Either::Right(r) => Either::Right(r.add_child(name)), + } + } + + fn add_child_with_id(&mut self, name: impl Into<String>, id: Id) -> Self::SubProgress { + match self { + Either::Left(l) => Either::Left(l.add_child_with_id(name, id)), + Either::Right(r) => Either::Right(r.add_child_with_id(name, id)), + } + } + + fn init(&mut self, max: Option<usize>, unit: Option<Unit>) { + match self { + Either::Left(l) => l.init(max, unit), + Either::Right(r) => r.init(max, unit), + } + } + + fn set(&mut self, step: usize) { + match self { + Either::Left(l) => l.set(step), + Either::Right(r) => r.set(step), + } + } + + fn unit(&self) -> Option<Unit> { + match self { + Either::Left(l) => l.unit(), + Either::Right(r) => r.unit(), + } + } + + fn max(&self) -> Option<usize> { + match self { + Either::Left(l) => l.max(), + Either::Right(r) => r.max(), + } + } + + fn set_max(&mut self, max: Option<Step>) -> Option<Step> { + match self { + Either::Left(l) => l.set_max(max), + Either::Right(r) => r.set_max(max), + } + } + + fn step(&self) -> usize { + match self { + Either::Left(l) => l.step(), + Either::Right(r) => r.step(), + } + } + + fn inc_by(&mut self, step: usize) { + match self { + Either::Left(l) => l.inc_by(step), + Either::Right(r) => r.inc_by(step), + } + } + + fn set_name(&mut self, name: impl Into<String>) { + match self { + Either::Left(l) => l.set_name(name), + Either::Right(r) => r.set_name(name), + } + } + + fn name(&self) -> Option<String> { + match self { + Either::Left(l) => l.name(), + Either::Right(r) => r.name(), + } + } + + fn id(&self) -> Id { + todo!() + } + + fn message(&mut self, level: MessageLevel, message: impl Into<String>) { + match self { + Either::Left(l) => l.message(level, message), + Either::Right(r) => r.message(level, message), + } + } + + fn counter(&self) -> Option<StepShared> { + match self { + Either::Left(l) => l.counter(), + Either::Right(r) => r.counter(), + } + } +} + +/// An implementation of `Progress` which can be created easily from `Option<impl Progress>`. +pub struct DoOrDiscard<T>(Either<T, Discard>); + +impl<T> From<Option<T>> for DoOrDiscard<T> +where + T: Progress, +{ + fn from(p: Option<T>) -> Self { + match p { + Some(p) => DoOrDiscard(Either::Left(p)), + None => DoOrDiscard(Either::Right(Discard)), + } + } +} + +impl<T: Progress> DoOrDiscard<T> { + /// Obtain either the original [`Progress`] implementation or `None`. + pub fn into_inner(self) -> Option<T> { + match self { + DoOrDiscard(Either::Left(p)) => Some(p), + DoOrDiscard(Either::Right(_)) => None, + } + } + + /// Take out the implementation of [`Progress`] and replace it with [`Discard`]. + pub fn take(&mut self) -> Option<T> { + let this = std::mem::replace(self, DoOrDiscard::from(None)); + match this { + DoOrDiscard(Either::Left(p)) => Some(p), + DoOrDiscard(Either::Right(_)) => None, + } + } +} + +impl<T> Progress for DoOrDiscard<T> +where + T: Progress, +{ + type SubProgress = DoOrDiscard<T::SubProgress>; + + fn add_child(&mut self, name: impl Into<String>) -> Self::SubProgress { + DoOrDiscard(self.0.add_child(name)) + } + + fn add_child_with_id(&mut self, name: impl Into<String>, id: Id) -> Self::SubProgress { + DoOrDiscard(self.0.add_child_with_id(name, id)) + } + + fn init(&mut self, max: Option<usize>, unit: Option<Unit>) { + self.0.init(max, unit) + } + + fn set(&mut self, step: usize) { + self.0.set(step) + } + + fn unit(&self) -> Option<Unit> { + self.0.unit() + } + + fn max(&self) -> Option<usize> { + self.0.max() + } + + fn set_max(&mut self, max: Option<Step>) -> Option<Step> { + self.0.set_max(max) + } + + fn step(&self) -> usize { + self.0.step() + } + + fn inc_by(&mut self, step: usize) { + self.0.inc_by(step) + } + + fn set_name(&mut self, name: impl Into<String>) { + self.0.set_name(name); + } + + fn name(&self) -> Option<String> { + self.0.name() + } + + fn id(&self) -> Id { + self.0.id() + } + + fn message(&mut self, level: MessageLevel, message: impl Into<String>) { + self.0.message(level, message) + } + + fn counter(&self) -> Option<StepShared> { + self.0.counter() + } +} + +use std::time::Instant; + +use crate::progress::{Step, StepShared}; + +/// Emit a message with throughput information when the instance is dropped. +pub struct ThroughputOnDrop<T: Progress>(T, Instant); + +impl<T: Progress> ThroughputOnDrop<T> { + /// Create a new instance by providing the `inner` [`Progress`] implementation. + pub fn new(inner: T) -> Self { + ThroughputOnDrop(inner, Instant::now()) + } +} + +impl<T: Progress> Progress for ThroughputOnDrop<T> { + type SubProgress = T::SubProgress; + + fn add_child(&mut self, name: impl Into<String>) -> Self::SubProgress { + self.0.add_child(name) + } + + fn add_child_with_id(&mut self, name: impl Into<String>, id: Id) -> Self::SubProgress { + self.0.add_child_with_id(name, id) + } + + fn init(&mut self, max: Option<usize>, unit: Option<Unit>) { + self.0.init(max, unit) + } + + fn set(&mut self, step: usize) { + self.0.set(step) + } + + fn unit(&self) -> Option<Unit> { + self.0.unit() + } + + fn max(&self) -> Option<usize> { + self.0.max() + } + + fn set_max(&mut self, max: Option<Step>) -> Option<Step> { + self.0.set_max(max) + } + + fn step(&self) -> usize { + self.0.step() + } + + fn inc_by(&mut self, step: usize) { + self.0.inc_by(step) + } + + fn set_name(&mut self, name: impl Into<String>) { + self.0.set_name(name) + } + + fn name(&self) -> Option<String> { + self.0.name() + } + + fn id(&self) -> Id { + self.0.id() + } + + fn message(&mut self, level: MessageLevel, message: impl Into<String>) { + self.0.message(level, message) + } + + fn counter(&self) -> Option<StepShared> { + self.0.counter() + } +} + +impl<T: Progress> Drop for ThroughputOnDrop<T> { + fn drop(&mut self) { + self.0.show_throughput(self.1) + } +} diff --git a/vendor/prodash/src/render/line/draw.rs b/vendor/prodash/src/render/line/draw.rs new file mode 100644 index 000000000..cfd5eaae3 --- /dev/null +++ b/vendor/prodash/src/render/line/draw.rs @@ -0,0 +1,350 @@ +use std::{ + collections::{hash_map::DefaultHasher, VecDeque}, + hash::{Hash, Hasher}, + io, + ops::RangeInclusive, + sync::atomic::Ordering, +}; + +use crosstermion::{ + ansi_term::{ANSIString, ANSIStrings, Color, Style}, + color, +}; +use unicode_width::UnicodeWidthStr; + +use crate::{ + messages::{Message, MessageCopyState, MessageLevel}, + progress::{self, Value}, + unit, Root, Throughput, +}; + +#[derive(Default)] +pub struct State { + tree: Vec<(progress::Key, progress::Task)>, + messages: Vec<Message>, + for_next_copy: Option<MessageCopyState>, + /// The size of the message origin, tracking the terminal height so things potentially off screen don't influence width anymore. + message_origin_size: VecDeque<usize>, + /// The maximum progress midpoint (point till progress bar starts) seen at the last tick + last_progress_midpoint: Option<u16>, + /// The amount of blocks per line we have written last time. + blocks_per_line: VecDeque<u16>, + pub throughput: Option<Throughput>, +} + +impl State { + pub(crate) fn update_from_progress(&mut self, progress: &impl Root) -> bool { + let mut hasher = DefaultHasher::new(); + self.tree.hash(&mut hasher); + let prev_hash = hasher.finish(); + + progress.sorted_snapshot(&mut self.tree); + let mut hasher = DefaultHasher::new(); + self.tree.hash(&mut hasher); + let cur_hash = hasher.finish(); + + self.for_next_copy = progress + .copy_new_messages(&mut self.messages, self.for_next_copy.take()) + .into(); + prev_hash != cur_hash + } + pub(crate) fn clear(&mut self) { + self.tree.clear(); + self.messages.clear(); + self.for_next_copy.take(); + } +} + +pub struct Options { + pub level_filter: Option<RangeInclusive<progress::key::Level>>, + pub terminal_dimensions: (u16, u16), + pub keep_running_if_progress_is_empty: bool, + pub output_is_terminal: bool, + pub colored: bool, + pub timestamp: bool, + pub hide_cursor: bool, +} + +fn messages( + out: &mut impl io::Write, + state: &mut State, + colored: bool, + max_height: usize, + timestamp: bool, +) -> io::Result<()> { + let mut brush = color::Brush::new(colored); + fn to_color(level: MessageLevel) -> Color { + use crate::messages::MessageLevel::*; + match level { + Info => Color::White, + Success => Color::Green, + Failure => Color::Red, + } + } + let mut tokens: Vec<ANSIString<'_>> = Vec::with_capacity(6); + let mut current_maximum = state.message_origin_size.iter().max().cloned().unwrap_or(0); + for Message { + time, + level, + origin, + message, + } in &state.messages + { + tokens.clear(); + let blocks_drawn_during_previous_tick = state.blocks_per_line.pop_front().unwrap_or(0); + let message_block_len = origin.width(); + current_maximum = current_maximum.max(message_block_len); + if state.message_origin_size.len() == max_height { + state.message_origin_size.pop_front(); + } + state.message_origin_size.push_back(message_block_len); + + let color = to_color(*level); + tokens.push(" ".into()); + if timestamp { + tokens.push( + brush + .style(color.dimmed().on(Color::Yellow)) + .paint(crate::time::format_time_for_messages(*time)), + ); + tokens.push(Style::default().paint(" ")); + } else { + tokens.push("".into()); + }; + tokens.push(brush.style(Style::default().dimmed()).paint(format!( + "{:>fill_size$}{}", + "", + origin, + fill_size = current_maximum - message_block_len, + ))); + tokens.push(" ".into()); + tokens.push(brush.style(color.bold()).paint(message)); + let message_block_count = block_count_sans_ansi_codes(&tokens); + write!(out, "{}", ANSIStrings(tokens.as_slice()))?; + + if blocks_drawn_during_previous_tick > message_block_count { + newline_with_overdraw(out, &tokens, blocks_drawn_during_previous_tick)?; + } else { + writeln!(out)?; + } + } + Ok(()) +} + +pub fn all(out: &mut impl io::Write, show_progress: bool, state: &mut State, config: &Options) -> io::Result<()> { + if !config.keep_running_if_progress_is_empty && state.tree.is_empty() { + return Err(io::Error::new(io::ErrorKind::Other, "stop as progress is empty")); + } + messages( + out, + state, + config.colored, + config.terminal_dimensions.1 as usize, + config.timestamp, + )?; + + if show_progress && config.output_is_terminal { + if let Some(tp) = state.throughput.as_mut() { + tp.update_elapsed(); + } + let level_range = config + .level_filter + .clone() + .unwrap_or(RangeInclusive::new(0, progress::key::Level::max_value())); + let lines_to_be_drawn = state + .tree + .iter() + .filter(|(k, _)| level_range.contains(&k.level())) + .count(); + if state.blocks_per_line.len() < lines_to_be_drawn { + state.blocks_per_line.resize(lines_to_be_drawn, 0); + } + let mut tokens: Vec<ANSIString<'_>> = Vec::with_capacity(4); + let mut max_midpoint = 0; + for ((key, value), ref mut blocks_in_last_iteration) in state + .tree + .iter() + .filter(|(k, _)| level_range.contains(&k.level())) + .zip(state.blocks_per_line.iter_mut()) + { + max_midpoint = max_midpoint.max( + format_progress( + key, + value, + config.terminal_dimensions.0, + config.colored, + state.last_progress_midpoint, + state + .throughput + .as_mut() + .and_then(|tp| tp.update_and_get(key, value.progress.as_ref())), + &mut tokens, + ) + .unwrap_or(0), + ); + write!(out, "{}", ANSIStrings(tokens.as_slice()))?; + + **blocks_in_last_iteration = newline_with_overdraw(out, &tokens, **blocks_in_last_iteration)?; + } + if let Some(tp) = state.throughput.as_mut() { + tp.reconcile(&state.tree); + } + state.last_progress_midpoint = Some(max_midpoint); + // overwrite remaining lines that we didn't touch naturally + let lines_drawn = lines_to_be_drawn; + if state.blocks_per_line.len() > lines_drawn { + for blocks_in_last_iteration in state.blocks_per_line.iter().skip(lines_drawn) { + writeln!(out, "{:>width$}", "", width = *blocks_in_last_iteration as usize)?; + } + // Move cursor back to end of the portion we have actually drawn + crosstermion::execute!(out, crosstermion::cursor::MoveUp(state.blocks_per_line.len() as u16))?; + state.blocks_per_line.resize(lines_drawn, 0); + } else if lines_drawn > 0 { + crosstermion::execute!(out, crosstermion::cursor::MoveUp(lines_drawn as u16))?; + } + } + Ok(()) +} + +/// Must be called directly after `tokens` were drawn, without newline. Takes care of adding the newline. +fn newline_with_overdraw( + out: &mut impl io::Write, + tokens: &[ANSIString<'_>], + blocks_in_last_iteration: u16, +) -> io::Result<u16> { + let current_block_count = block_count_sans_ansi_codes(tokens); + if blocks_in_last_iteration > current_block_count { + // fill to the end of line to overwrite what was previously there + writeln!( + out, + "{:>width$}", + "", + width = (blocks_in_last_iteration - current_block_count) as usize + )?; + } else { + writeln!(out)?; + }; + Ok(current_block_count) +} + +fn block_count_sans_ansi_codes(strings: &[ANSIString<'_>]) -> u16 { + strings.iter().map(|s| s.width() as u16).sum() +} + +fn draw_progress_bar<'a>( + p: &Value, + style: Style, + mut blocks_available: u16, + colored: bool, + buf: &mut Vec<ANSIString<'a>>, +) { + let mut brush = color::Brush::new(colored); + let styled_brush = brush.style(style); + + blocks_available = blocks_available.saturating_sub(3); // account for…I don't really know it's magic + buf.push(" [".into()); + match p.fraction() { + Some(mut fraction) => { + fraction = fraction.min(1.0); + blocks_available = blocks_available.saturating_sub(1); // account for '>' apparently + let progress_blocks = (blocks_available as f32 * fraction).floor() as usize; + buf.push(styled_brush.paint(format!("{:=<width$}", "", width = progress_blocks))); + buf.push(styled_brush.paint(">")); + buf.push(styled_brush.style(style.dimmed()).paint(format!( + "{:-<width$}", + "", + width = (blocks_available - progress_blocks as u16) as usize + ))); + } + None => { + const CHARS: [char; 6] = ['=', '=', '=', ' ', ' ', ' ']; + buf.push( + styled_brush.paint( + (p.step.load(Ordering::SeqCst)..std::usize::MAX) + .take(blocks_available as usize) + .map(|idx| CHARS[idx % CHARS.len()]) + .rev() + .collect::<String>(), + ), + ); + } + } + buf.push("]".into()); +} + +fn progress_style(p: &Value) -> Style { + use crate::progress::State::*; + match p.state { + Running => if let Some(fraction) = p.fraction() { + if fraction > 0.8 { + Color::Green + } else { + Color::Yellow + } + } else { + Color::White + } + .normal(), + Halted(_, _) => Color::Red.dimmed(), + Blocked(_, _) => Color::Red.normal(), + } +} + +fn format_progress<'a>( + key: &progress::Key, + value: &'a progress::Task, + column_count: u16, + colored: bool, + midpoint: Option<u16>, + throughput: Option<unit::display::Throughput>, + buf: &mut Vec<ANSIString<'a>>, +) -> Option<u16> { + let mut brush = color::Brush::new(colored); + buf.clear(); + + buf.push(Style::new().paint(format!("{:>level$}", "", level = key.level() as usize))); + match value.progress.as_ref() { + Some(progress) => { + let style = progress_style(progress); + buf.push(brush.style(Color::Cyan.bold()).paint(&value.name)); + buf.push(" ".into()); + + let pre_unit = buf.len(); + let values_brush = brush.style(Style::new().bold().dimmed()); + match progress.unit.as_ref() { + Some(unit) => { + let mut display = unit.display(progress.step.load(Ordering::SeqCst), progress.done_at, throughput); + buf.push(values_brush.paint(display.values().to_string())); + buf.push(" ".into()); + buf.push(display.unit().to_string().into()); + } + None => { + buf.push(values_brush.paint(match progress.done_at { + Some(done_at) => format!("{}/{}", progress.step.load(Ordering::SeqCst), done_at), + None => format!("{}", progress.step.load(Ordering::SeqCst)), + })); + } + } + let desired_midpoint = block_count_sans_ansi_codes(buf.as_slice()); + let actual_midpoint = if let Some(midpoint) = midpoint { + let padding = midpoint.saturating_sub(desired_midpoint); + if padding > 0 { + buf.insert(pre_unit, " ".repeat(padding as usize).into()); + } + block_count_sans_ansi_codes(buf.as_slice()) + } else { + desired_midpoint + }; + let blocks_left = column_count.saturating_sub(actual_midpoint); + if blocks_left > 0 { + draw_progress_bar(progress, style, blocks_left, colored, buf); + } + Some(desired_midpoint) + } + None => { + // headline only - FIXME: would have to truncate it if it is too long for the line… + buf.push(brush.style(Color::White.bold()).paint(&value.name)); + None + } + } +} diff --git a/vendor/prodash/src/render/line/engine.rs b/vendor/prodash/src/render/line/engine.rs new file mode 100644 index 000000000..d440fb159 --- /dev/null +++ b/vendor/prodash/src/render/line/engine.rs @@ -0,0 +1,341 @@ +#[cfg(feature = "signal-hook")] +use std::sync::Arc; +use std::{ + io, + ops::RangeInclusive, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; + +use crate::{progress, render::line::draw, Throughput, WeakRoot}; + +/// Options used for configuring a [line renderer][render()]. +#[derive(Clone)] +pub struct Options { + /// If true, _(default true)_, we assume the output stream belongs to a terminal. + /// + /// If false, we won't print any live progress, only log messages. + pub output_is_terminal: bool, + + /// If true, _(default: true)_ we will display color. You should use `output_is_terminal && crosstermion::should_colorize()` + /// to determine this value. + /// + /// Please note that you can enforce color even if the output stream is not connected to a terminal by setting + /// this field to true. + pub colored: bool, + + /// If true, _(default: false)_, a timestamp will be shown before each message. + pub timestamp: bool, + + /// The amount of columns and rows to use for drawing. Defaults to (80, 20). + pub terminal_dimensions: (u16, u16), + + /// If true, _(default: false)_, the cursor will be hidden for a more visually appealing display. + /// + /// Please note that you must make sure the line renderer is properly shut down to restore the previous cursor + /// settings. See the `signal-hook` documentation in the README for more information. + pub hide_cursor: bool, + + /// If true, (default false), we will keep track of the previous progress state to derive + /// continuous throughput information from. Throughput will only show for units which have + /// explicitly enabled it, it is opt-in. + /// + /// This comes at the cost of additional memory and CPU time. + pub throughput: bool, + + /// If set, specify all levels that should be shown. Otherwise all available levels are shown. + /// + /// This is useful to filter out high-noise lower level progress items in the tree. + pub level_filter: Option<RangeInclusive<progress::key::Level>>, + + /// If set, progress will only actually be shown after the given duration. Log messages will always be shown without delay. + /// + /// This option can be useful to not enforce progress for short actions, causing it to flicker. + /// Please note that this won't affect display of messages, which are simply logged. + pub initial_delay: Option<Duration>, + + /// The amount of frames to draw per second. If below 1.0, it determines the amount of seconds between the frame. + /// + /// *e.g.* 1.0/4.0 is one frame every 4 seconds. + pub frames_per_second: f32, + + /// If true (default: true), we will keep waiting for progress even after we encountered an empty list of drawable progress items. + /// + /// Please note that you should add at least one item to the `prodash::Tree` before launching the application or else + /// risk a race causing nothing to be rendered at all. + pub keep_running_if_progress_is_empty: bool, +} + +/// The kind of stream to use for auto-configuration. +pub enum StreamKind { + /// Standard output + Stdout, + /// Standard error + Stderr, +} + +#[cfg(feature = "render-line-autoconfigure")] +impl From<StreamKind> for atty::Stream { + fn from(s: StreamKind) -> Self { + match s { + StreamKind::Stdout => atty::Stream::Stdout, + StreamKind::Stderr => atty::Stream::Stderr, + } + } +} + +/// Convenience +impl Options { + /// Automatically configure (and overwrite) the following fields based on terminal configuration. + /// + /// * output_is_terminal + /// * colored + /// * terminal_dimensions + /// * hide-cursor (based on presence of 'signal-hook' feature. + #[cfg(feature = "render-line-autoconfigure")] + pub fn auto_configure(mut self, output: StreamKind) -> Self { + self.output_is_terminal = atty::is(output.into()); + self.colored = self.output_is_terminal && crosstermion::color::allowed(); + self.terminal_dimensions = crosstermion::terminal::size().unwrap_or((80, 20)); + #[cfg(feature = "signal-hook")] + self.auto_hide_cursor(); + self + } + #[cfg(all(feature = "render-line-autoconfigure", feature = "signal-hook"))] + fn auto_hide_cursor(&mut self) { + self.hide_cursor = true; + } + #[cfg(not(feature = "render-line-autoconfigure"))] + /// No-op - only available with the `render-line-autoconfigure` feature toggle. + pub fn auto_configure(self, _output: StreamKind) -> Self { + self + } +} + +impl Default for Options { + fn default() -> Self { + Options { + output_is_terminal: true, + colored: true, + timestamp: false, + terminal_dimensions: (80, 20), + hide_cursor: false, + level_filter: None, + initial_delay: None, + frames_per_second: 6.0, + throughput: false, + keep_running_if_progress_is_empty: true, + } + } +} + +/// A handle to the render thread, which when dropped will instruct it to stop showing progress. +pub struct JoinHandle { + inner: Option<std::thread::JoinHandle<io::Result<()>>>, + connection: std::sync::mpsc::SyncSender<Event>, + // If we disconnect before sending a Quit event, the selector continuously informs about the 'Disconnect' state + disconnected: bool, +} + +impl JoinHandle { + /// `detach()` and `forget()` to remove any effects associated with this handle. + pub fn detach(mut self) { + self.disconnect(); + self.forget(); + } + /// Remove the handles capability to instruct the render thread to stop, but it will still wait for it + /// if dropped. + /// Use `forget()` if it should not wait for the render thread anymore. + pub fn disconnect(&mut self) { + self.disconnected = true; + } + /// Remove the handles capability to `join()` by forgetting the threads handle + pub fn forget(&mut self) { + self.inner.take(); + } + /// Wait for the thread to shutdown naturally, for example because there is no more progress to display + pub fn wait(mut self) { + self.inner.take().and_then(|h| h.join().ok()); + } + /// Send the shutdown signal right after one last redraw + pub fn shutdown(&mut self) { + if !self.disconnected { + self.connection.send(Event::Tick).ok(); + self.connection.send(Event::Quit).ok(); + } + } + /// Send the signal to shutdown and wait for the thread to be shutdown. + pub fn shutdown_and_wait(mut self) { + self.shutdown(); + self.wait(); + } +} + +impl Drop for JoinHandle { + fn drop(&mut self) { + self.shutdown(); + self.inner.take().and_then(|h| h.join().ok()); + } +} + +#[derive(Debug)] +enum Event { + Tick, + Quit, + #[cfg(feature = "signal-hook")] + Resize(u16, u16), +} + +/// Write a line-based representation of `progress` to `out` which is assumed to be a terminal. +/// +/// Configure it with `config`, see the [`Options`] for details. +pub fn render( + mut out: impl io::Write + Send + 'static, + progress: impl WeakRoot + Send + 'static, + Options { + output_is_terminal, + colored, + timestamp, + level_filter, + terminal_dimensions, + initial_delay, + frames_per_second, + keep_running_if_progress_is_empty, + hide_cursor, + throughput, + }: Options, +) -> JoinHandle { + #[cfg_attr(not(feature = "signal-hook"), allow(unused_mut))] + let mut config = draw::Options { + level_filter, + terminal_dimensions, + keep_running_if_progress_is_empty, + output_is_terminal, + colored, + timestamp, + hide_cursor, + }; + + let (event_send, event_recv) = std::sync::mpsc::sync_channel::<Event>(1); + let show_cursor = possibly_hide_cursor(&mut out, hide_cursor && output_is_terminal); + static SHOW_PROGRESS: AtomicBool = AtomicBool::new(false); + #[cfg(feature = "signal-hook")] + let term_signal_received: Arc<AtomicBool> = Arc::new(AtomicBool::new(false)); + #[cfg(feature = "signal-hook")] + let terminal_resized: Arc<AtomicBool> = Arc::new(AtomicBool::new(false)); + #[cfg(feature = "signal-hook")] + { + for sig in signal_hook::consts::TERM_SIGNALS { + signal_hook::flag::register(*sig, term_signal_received.clone()).ok(); + } + + #[cfg(unix)] + signal_hook::flag::register(signal_hook::consts::SIGWINCH, terminal_resized.clone()).ok(); + } + + let handle = std::thread::Builder::new() + .name("render-line-eventloop".into()) + .spawn({ + let tick_send = event_send.clone(); + move || { + { + let initial_delay = initial_delay.unwrap_or_default(); + SHOW_PROGRESS.store(initial_delay == Duration::default(), Ordering::Relaxed); + if !SHOW_PROGRESS.load(Ordering::Relaxed) { + std::thread::Builder::new() + .name("render-line-progress-delay".into()) + .spawn(move || { + std::thread::sleep(initial_delay); + SHOW_PROGRESS.store(true, Ordering::Relaxed); + }) + .ok(); + } + } + + let mut state = draw::State::default(); + if throughput { + state.throughput = Some(Throughput::default()); + } + let secs = 1.0 / frames_per_second; + let _ticker = std::thread::Builder::new() + .name("render-line-ticker".into()) + .spawn(move || loop { + #[cfg(feature = "signal-hook")] + { + if term_signal_received.load(Ordering::SeqCst) { + tick_send.send(Event::Quit).ok(); + break; + } + if terminal_resized.load(Ordering::SeqCst) { + terminal_resized.store(false, Ordering::SeqCst); + if let Ok((x, y)) = crosstermion::terminal::size() { + tick_send.send(Event::Resize(x, y)).ok(); + } + } + } + if tick_send.send(Event::Tick).is_err() { + break; + } + std::thread::sleep(Duration::from_secs_f32(secs)); + }) + .expect("starting a thread works"); + + for event in event_recv { + match event { + #[cfg(feature = "signal-hook")] + Event::Resize(x, y) => { + config.terminal_dimensions = (x, y); + draw::all(&mut out, SHOW_PROGRESS.load(Ordering::Relaxed), &mut state, &config)?; + } + Event::Tick => match progress.upgrade() { + Some(progress) => { + let has_changed = state.update_from_progress(&progress); + draw::all( + &mut out, + SHOW_PROGRESS.load(Ordering::Relaxed) && has_changed, + &mut state, + &config, + )?; + } + None => { + state.clear(); + draw::all(&mut out, SHOW_PROGRESS.load(Ordering::Relaxed), &mut state, &config)?; + break; + } + }, + Event::Quit => { + state.clear(); + draw::all(&mut out, SHOW_PROGRESS.load(Ordering::Relaxed), &mut state, &config)?; + break; + } + } + } + + if show_cursor { + crosstermion::execute!(out, crosstermion::cursor::Show).ok(); + } + + // One day we might try this out on windows, but let's not risk it now. + #[cfg(unix)] + write!(out, "\x1b[2K\r").ok(); // clear the last line. + Ok(()) + } + }) + .expect("starting a thread works"); + + JoinHandle { + inner: Some(handle), + connection: event_send, + disconnected: false, + } +} + +// Not all configurations actually need it to be mut, but those with the 'signal-hook' feature do +#[allow(unused_mut)] +fn possibly_hide_cursor(out: &mut impl io::Write, mut hide_cursor: bool) -> bool { + if hide_cursor { + crosstermion::execute!(out, crosstermion::cursor::Hide).is_ok() + } else { + false + } +} diff --git a/vendor/prodash/src/render/line/mod.rs b/vendor/prodash/src/render/line/mod.rs new file mode 100644 index 000000000..37a49e242 --- /dev/null +++ b/vendor/prodash/src/render/line/mod.rs @@ -0,0 +1,10 @@ +#[cfg(all( + feature = "render-line", + not(any(feature = "render-line-crossterm", feature = "render-line-termion")) +))] +compile_error!("Please choose either one of these features: 'render-line-crossterm' or 'render-line-termion'"); + +mod draw; +mod engine; + +pub use engine::{render, JoinHandle, Options, StreamKind}; diff --git a/vendor/prodash/src/render/mod.rs b/vendor/prodash/src/render/mod.rs new file mode 100644 index 000000000..f1780df7d --- /dev/null +++ b/vendor/prodash/src/render/mod.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "render-tui")] +/// +pub mod tui; +#[cfg(feature = "render-tui")] +pub use self::tui::render as tui; + +#[cfg(feature = "render-line")] +/// +pub mod line; +#[cfg(feature = "render-line")] +pub use self::line::render as line; diff --git a/vendor/prodash/src/render/tui/draw/all.rs b/vendor/prodash/src/render/tui/draw/all.rs new file mode 100644 index 000000000..74a3bc464 --- /dev/null +++ b/vendor/prodash/src/render/tui/draw/all.rs @@ -0,0 +1,164 @@ +use std::time::Duration; + +use tui::{ + buffer::Buffer, + layout::Rect, + style::{Modifier, Style}, + text::Span, + widgets::{Block, Borders, Widget}, +}; + +use crate::{ + messages::Message, + progress::{Key, Task}, + render::tui::{ + draw, + utils::{block_width, rect}, + InterruptDrawInfo, Line, + }, + Throughput, +}; + +#[derive(Default)] +pub struct State { + pub title: String, + pub task_offset: u16, + pub message_offset: u16, + pub hide_messages: bool, + pub messages_fullscreen: bool, + pub user_provided_window_size: Option<Rect>, + pub duration_per_frame: Duration, + pub information: Vec<Line>, + pub hide_info: bool, + pub maximize_info: bool, + pub last_tree_column_width: Option<u16>, + pub next_tree_column_width: Option<u16>, + pub throughput: Option<Throughput>, +} + +pub(crate) fn all( + state: &mut State, + interrupt_mode: InterruptDrawInfo, + entries: &[(Key, Task)], + messages: &[Message], + bound: Rect, + buf: &mut Buffer, +) { + let (bound, info_pane) = compute_info_bound( + bound, + if state.hide_info { &[] } else { &state.information }, + state.maximize_info, + ); + let bold = Style::default().add_modifier(Modifier::BOLD); + let window = Block::default() + .title(Span::styled(state.title.as_str(), bold)) + .borders(Borders::ALL); + let inner_area = window.inner(bound); + window.render(bound, buf); + if bound.width < 4 || bound.height < 4 { + return; + } + + let border_width = 1; + draw::progress::headline( + entries, + interrupt_mode, + state.duration_per_frame, + buf, + rect::offset_x( + Rect { + height: 1, + width: bound.width.saturating_sub(border_width), + ..bound + }, + block_width(&state.title) + (border_width * 2), + ), + ); + + let (progress_pane, messages_pane) = compute_pane_bounds( + if state.hide_messages { &[] } else { messages }, + inner_area, + state.messages_fullscreen, + ); + + draw::progress::pane(entries, progress_pane, buf, state); + if let Some(messages_pane) = messages_pane { + draw::messages::pane( + messages, + messages_pane, + Rect { + width: messages_pane.width + 2, + ..rect::line_bound(bound, bound.height.saturating_sub(1) as usize) + }, + &mut state.message_offset, + buf, + ); + } + + if let Some(info_pane) = info_pane { + draw::information::pane(&state.information, info_pane, buf); + } +} + +fn compute_pane_bounds(messages: &[Message], inner: Rect, messages_fullscreen: bool) -> (Rect, Option<Rect>) { + if messages.is_empty() { + (inner, None) + } else { + let (task_percent, messages_percent) = if messages_fullscreen { (0.1, 0.9) } else { (0.75, 0.25) }; + let tasks_height: u16 = (inner.height as f32 * task_percent).ceil() as u16; + let messages_height: u16 = (inner.height as f32 * messages_percent).floor() as u16; + if messages_height < 2 { + (inner, None) + } else { + let messages_title = 1u16; + let new_messages_height = messages_height.min((messages.len() + messages_title as usize) as u16); + let tasks_height = tasks_height.saturating_add(messages_height - new_messages_height); + let messages_height = new_messages_height; + ( + Rect { + height: tasks_height, + ..inner + }, + Some(rect::intersect( + Rect { + y: tasks_height + messages_title, + height: messages_height, + ..inner + }, + inner, + )), + ) + } + } +} + +fn compute_info_bound(bound: Rect, info: &[Line], maximize: bool) -> (Rect, Option<Rect>) { + if info.is_empty() { + return (bound, None); + } + let margin = 1; + let max_line_width = info.iter().fold(0, |state, l| { + state.max( + block_width(match l { + Line::Text(s) | Line::Title(s) => s, + }) + margin * 2, + ) + }); + let pane_width = if maximize { + bound.width.saturating_sub(8).min(max_line_width) + } else { + (bound.width / 3).min(max_line_width) + }; + + if pane_width < max_line_width / 3 { + return (bound, None); + } + + ( + Rect { + width: bound.width.saturating_sub(pane_width), + ..bound + }, + Some(rect::snap_to_right(bound, pane_width)), + ) +} diff --git a/vendor/prodash/src/render/tui/draw/information.rs b/vendor/prodash/src/render/tui/draw/information.rs new file mode 100644 index 000000000..0d5f7a76e --- /dev/null +++ b/vendor/prodash/src/render/tui/draw/information.rs @@ -0,0 +1,61 @@ +use tui::{ + buffer::Buffer, + layout::Rect, + style::{Modifier, Style}, + text::Span, + widgets::{Block, Borders, Widget}, +}; + +use crate::render::tui::{ + utils::{block_width, draw_text_with_ellipsis_nowrap, rect}, + Line, +}; + +pub fn pane(lines: &[Line], bound: Rect, buf: &mut Buffer) { + let bold = Style::default().add_modifier(Modifier::BOLD); + let block = Block::default() + .title(Span::styled("Information", bold)) + .borders(Borders::TOP | Borders::BOTTOM); + let inner_bound = block.inner(bound); + block.render(bound, buf); + + let help_text = " ⨯ = [ | ▢ = { "; + draw_text_with_ellipsis_nowrap(rect::snap_to_right(bound, block_width(help_text)), buf, help_text, bold); + + let bound = Rect { + width: inner_bound.width.saturating_sub(1), + ..inner_bound + }; + let mut offset = 0; + for (line, info) in lines.windows(2).enumerate() { + let (info, next_info) = (&info[0], &info[1]); + let line = line + offset; + if line >= bound.height as usize { + break; + } + let line_bound = rect::line_bound(bound, line); + match info { + Line::Title(text) => { + let blocks_drawn = draw_text_with_ellipsis_nowrap(line_bound, buf, text, bold); + let lines_rect = rect::offset_x(line_bound, blocks_drawn + 1); + for x in lines_rect.left()..lines_rect.right() { + buf.get_mut(x, lines_rect.y).symbol = "─".into(); + } + offset += 1; + } + Line::Text(text) => { + draw_text_with_ellipsis_nowrap(rect::offset_x(line_bound, 1), buf, text, None); + } + }; + if let Line::Title(_) = next_info { + offset += 1; + } + } + + if let Some(Line::Text(text)) = lines.last() { + let line = lines.len().saturating_sub(1) + offset; + if line < bound.height as usize { + draw_text_with_ellipsis_nowrap(rect::offset_x(rect::line_bound(bound, line), 1), buf, text, bold); + } + } +} diff --git a/vendor/prodash/src/render/tui/draw/messages.rs b/vendor/prodash/src/render/tui/draw/messages.rs new file mode 100644 index 000000000..c493a7387 --- /dev/null +++ b/vendor/prodash/src/render/tui/draw/messages.rs @@ -0,0 +1,150 @@ +use std::time::SystemTime; + +use tui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Modifier, Style}, + text::Span, + widgets::{Block, Borders, Widget}, +}; +use unicode_width::UnicodeWidthStr; + +use crate::{ + messages::{Message, MessageLevel}, + render::tui::utils::{block_width, draw_text_with_ellipsis_nowrap, rect, sanitize_offset, VERTICAL_LINE}, + time::{format_time_for_messages, DATE_TIME_HMS}, +}; + +pub fn pane(messages: &[Message], bound: Rect, overflow_bound: Rect, offset: &mut u16, buf: &mut Buffer) { + let bold = Style::default().add_modifier(Modifier::BOLD); + let block = Block::default() + .title(Span::styled("Messages", bold)) + .borders(Borders::TOP); + let inner_bound = block.inner(bound); + block.render(bound, buf); + let help_text = " ⨯ = `| ▢ = ~ "; + draw_text_with_ellipsis_nowrap(rect::snap_to_right(bound, block_width(help_text)), buf, help_text, bold); + + let bound = inner_bound; + *offset = sanitize_offset(*offset, messages.len(), bound.height); + let max_origin_width = messages + .iter() + .rev() + .skip(*offset as usize) + .take(bound.height as usize) + .fold(0, |state, message| state.max(block_width(&message.origin))); + for ( + line, + Message { + time, + message, + level, + origin, + }, + ) in messages + .iter() + .rev() + .skip(*offset as usize) + .take(bound.height as usize) + .enumerate() + { + let line_bound = rect::line_bound(bound, line); + let (time_bound, level_bound, origin_bound, message_bound) = compute_bounds(line_bound, max_origin_width); + if let Some(time_bound) = time_bound { + draw_text_with_ellipsis_nowrap(time_bound, buf, format_time_column(time), None); + } + if let Some(level_bound) = level_bound { + draw_text_with_ellipsis_nowrap( + level_bound, + buf, + format_level_column(*level), + Some(level_to_style(*level)), + ); + draw_text_with_ellipsis_nowrap(rect::offset_x(level_bound, LEVEL_TEXT_WIDTH), buf, VERTICAL_LINE, None); + } + if let Some(origin_bound) = origin_bound { + draw_text_with_ellipsis_nowrap(origin_bound, buf, origin, None); + draw_text_with_ellipsis_nowrap(rect::offset_x(origin_bound, max_origin_width), buf, "→", None); + } + draw_text_with_ellipsis_nowrap(message_bound, buf, message, None); + } + + if (bound.height as usize) < messages.len().saturating_sub(*offset as usize) + || (*offset).min(messages.len() as u16) > 0 + { + let messages_below = messages + .len() + .saturating_sub(bound.height.saturating_add(*offset) as usize); + let messages_skipped = (*offset).min(messages.len() as u16); + draw_text_with_ellipsis_nowrap( + rect::offset_x(overflow_bound, 1), + buf, + format!("… {} skipped and {} more", messages_skipped, messages_below), + bold, + ); + let help_text = " ⇊ = D|↓ = J|⇈ = U|↑ = K ┘"; + draw_text_with_ellipsis_nowrap( + rect::snap_to_right(overflow_bound, block_width(help_text)), + buf, + help_text, + bold, + ); + } +} + +const LEVEL_TEXT_WIDTH: u16 = 4; +fn format_level_column(level: MessageLevel) -> &'static str { + use MessageLevel::*; + match level { + Info => "info", + Failure => "fail", + Success => "done", + } +} + +fn level_to_style(level: MessageLevel) -> Style { + use MessageLevel::*; + Style::default() + .fg(Color::Black) + .add_modifier(Modifier::BOLD) + .bg(match level { + Info => Color::White, + Failure => Color::Red, + Success => Color::Green, + }) +} + +fn format_time_column(time: &SystemTime) -> String { + format!("{}{}", format_time_for_messages(*time), VERTICAL_LINE) +} + +fn compute_bounds(line: Rect, max_origin_width: u16) -> (Option<Rect>, Option<Rect>, Option<Rect>, Rect) { + let vertical_line_width = VERTICAL_LINE.width() as u16; + let mythical_offset_we_should_not_need = 1; + + let time_bound = Rect { + width: DATE_TIME_HMS as u16 + vertical_line_width, + ..line + }; + + let mut cursor = time_bound.width + mythical_offset_we_should_not_need; + let level_bound = Rect { + x: cursor, + width: LEVEL_TEXT_WIDTH + vertical_line_width, + ..line + }; + cursor += level_bound.width; + + let origin_bound = Rect { + x: cursor, + width: max_origin_width + vertical_line_width, + ..line + }; + cursor += origin_bound.width; + + let message_bound = rect::intersect(rect::offset_x(line, cursor), line); + if message_bound.width < 30 { + return (None, None, None, line); + } + (Some(time_bound), Some(level_bound), Some(origin_bound), message_bound) +} diff --git a/vendor/prodash/src/render/tui/draw/mod.rs b/vendor/prodash/src/render/tui/draw/mod.rs new file mode 100644 index 000000000..903f0d081 --- /dev/null +++ b/vendor/prodash/src/render/tui/draw/mod.rs @@ -0,0 +1,6 @@ +mod all; +mod information; +mod messages; +mod progress; + +pub(crate) use all::{all, State}; diff --git a/vendor/prodash/src/render/tui/draw/progress.rs b/vendor/prodash/src/render/tui/draw/progress.rs new file mode 100644 index 000000000..acabac5d5 --- /dev/null +++ b/vendor/prodash/src/render/tui/draw/progress.rs @@ -0,0 +1,506 @@ +use std::{ + fmt, + sync::atomic::Ordering, + time::{Duration, SystemTime}, +}; + +use humantime::format_duration; +use tui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Modifier, Style}, +}; +use tui_react::fill_background; + +use crate::{ + progress::{self, Key, Step, Task, Value}, + render::tui::{ + draw::State, + utils::{ + block_width, draw_text_nowrap_fn, draw_text_with_ellipsis_nowrap, rect, sanitize_offset, + GraphemeCountWriter, VERTICAL_LINE, + }, + InterruptDrawInfo, + }, + time::format_now_datetime_seconds, + unit, Throughput, +}; + +const MIN_TREE_WIDTH: u16 = 20; + +pub fn pane(entries: &[(Key, progress::Task)], mut bound: Rect, buf: &mut Buffer, state: &mut State) { + state.task_offset = sanitize_offset(state.task_offset, entries.len(), bound.height); + let needs_overflow_line = + if entries.len() > bound.height as usize || (state.task_offset).min(entries.len() as u16) > 0 { + bound.height = bound.height.saturating_sub(1); + true + } else { + false + }; + state.task_offset = sanitize_offset(state.task_offset, entries.len(), bound.height); + + if entries.is_empty() { + return; + } + + let initial_column_width = bound.width / 3; + let desired_max_tree_draw_width = *state.next_tree_column_width.as_ref().unwrap_or(&initial_column_width); + { + if initial_column_width >= MIN_TREE_WIDTH { + let tree_bound = Rect { + width: desired_max_tree_draw_width, + ..bound + }; + let computed = draw_tree(entries, buf, tree_bound, state.task_offset); + state.last_tree_column_width = Some(computed); + } else { + state.last_tree_column_width = Some(0); + }; + } + + { + if let Some(tp) = state.throughput.as_mut() { + tp.update_elapsed(); + } + + let progress_area = rect::offset_x(bound, desired_max_tree_draw_width); + draw_progress( + entries, + buf, + progress_area, + state.task_offset, + state.throughput.as_mut(), + ); + + if let Some(tp) = state.throughput.as_mut() { + tp.reconcile(entries); + } + } + + if needs_overflow_line { + let overflow_rect = Rect { + y: bound.height + 1, + height: 1, + ..bound + }; + draw_overflow( + entries, + buf, + overflow_rect, + desired_max_tree_draw_width, + bound.height, + state.task_offset, + ); + } +} + +pub(crate) fn headline( + entries: &[(Key, Task)], + interrupt_mode: InterruptDrawInfo, + duration_per_frame: Duration, + buf: &mut Buffer, + bound: Rect, +) { + let (num_running_tasks, num_blocked_tasks, num_groups) = entries.iter().fold( + (0, 0, 0), + |(mut running, mut blocked, mut groups), (_key, Task { progress, .. })| { + match progress.as_ref().map(|p| p.state) { + Some(progress::State::Running) => running += 1, + Some(progress::State::Blocked(_, _)) | Some(progress::State::Halted(_, _)) => blocked += 1, + None => groups += 1, + } + (running, blocked, groups) + }, + ); + let text = format!( + " {} {} {:3} running + {:3} blocked + {:3} groups = {} ", + match interrupt_mode { + InterruptDrawInfo::Instantly => "'q' or CTRL+c to quit", + InterruptDrawInfo::Deferred(interrupt_requested) => { + if interrupt_requested { + "interrupt requested - please wait" + } else { + "cannot interrupt current operation" + } + } + }, + if duration_per_frame > Duration::from_secs(1) { + format!( + " Every {}s → {}", + duration_per_frame.as_secs(), + format_now_datetime_seconds() + ) + } else { + "".into() + }, + num_running_tasks, + num_blocked_tasks, + num_groups, + entries.len() + ); + + let bold = Style::default().add_modifier(Modifier::BOLD); + draw_text_with_ellipsis_nowrap(rect::snap_to_right(bound, block_width(&text) + 1), buf, text, bold); +} + +struct ProgressFormat<'a>(&'a Option<Value>, u16, Option<unit::display::Throughput>); + +impl<'a> fmt::Display for ProgressFormat<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Some(p) => match p.unit.as_ref() { + Some(unit) => write!( + f, + "{}", + unit.display(p.step.load(Ordering::SeqCst), p.done_at, self.2.clone()) + ), + None => match p.done_at { + Some(done_at) => write!(f, "{}/{}", p.step.load(Ordering::SeqCst), done_at), + None => write!(f, "{}", p.step.load(Ordering::SeqCst)), + }, + }, + None => write!(f, "{:─<width$}", '─', width = self.1 as usize), + } + } +} + +fn has_child(entries: &[(Key, Task)], index: usize) -> bool { + entries + .get(index + 1) + .and_then(|(other_key, other_val)| { + entries.get(index).map(|(cur_key, _)| { + cur_key.shares_parent_with(other_key, cur_key.level()) && other_val.progress.is_some() + }) + }) + .unwrap_or(false) +} + +pub fn draw_progress( + entries: &[(Key, Task)], + buf: &mut Buffer, + bound: Rect, + offset: u16, + mut throughput: Option<&mut Throughput>, +) { + let title_spacing = 2u16 + 1; // 2 on the left, 1 on the right + let max_progress_label_width = entries + .iter() + .skip(offset as usize) + .take(bound.height as usize) + .map(|(_, Task { progress, .. })| progress) + .fold(0, |state, progress| match progress { + progress @ Some(_) => { + use std::io::Write; + let mut w = GraphemeCountWriter::default(); + write!(w, "{}", ProgressFormat(progress, 0, None)).expect("never fails"); + state.max(w.0) + } + None => state, + }); + + for ( + line, + ( + entry_index, + ( + key, + Task { + progress, + name: title, + id: _, + }, + ), + ), + ) in entries + .iter() + .enumerate() + .skip(offset as usize) + .take(bound.height as usize) + .enumerate() + { + let throughput = throughput + .as_mut() + .and_then(|tp| tp.update_and_get(key, progress.as_ref())); + let line_bound = rect::line_bound(bound, line); + let progress_text = format!( + " {progress}", + progress = ProgressFormat( + progress, + if has_child(entries, entry_index) { + bound.width.saturating_sub(title_spacing) + } else { + 0 + }, + throughput + ) + ); + + draw_text_with_ellipsis_nowrap(line_bound, buf, VERTICAL_LINE, None); + + let tree_prefix = level_prefix(entries, entry_index); + let progress_rect = rect::offset_x(line_bound, block_width(&tree_prefix)); + draw_text_with_ellipsis_nowrap(line_bound, buf, tree_prefix, None); + match progress + .as_ref() + .map(|p| (p.fraction(), p.state, p.step.load(Ordering::SeqCst))) + { + Some((Some(fraction), state, _step)) => { + let mut progress_text = progress_text; + add_block_eta(state, &mut progress_text); + let (bound, style) = draw_progress_bar_fn(buf, progress_rect, fraction, |fraction| match state { + progress::State::Blocked(_, _) => Color::Red, + progress::State::Halted(_, _) => Color::LightRed, + progress::State::Running => { + if fraction >= 0.8 { + Color::Green + } else { + Color::Yellow + } + } + }); + let style_fn = move |_t: &str, x: u16, _y: u16| { + if x < bound.right() { + style + } else { + Style::default() + } + }; + draw_text_nowrap_fn(progress_rect, buf, progress_text, style_fn); + } + Some((None, state, step)) => { + let mut progress_text = progress_text; + add_block_eta(state, &mut progress_text); + draw_text_with_ellipsis_nowrap(progress_rect, buf, progress_text, None); + let bar_rect = rect::offset_x(line_bound, max_progress_label_width as u16); + draw_spinner( + buf, + bar_rect, + step, + line, + match state { + progress::State::Blocked(_, _) => Color::Red, + progress::State::Halted(_, _) => Color::LightRed, + progress::State::Running => Color::White, + }, + ); + } + None => { + let bold = Style::default().add_modifier(Modifier::BOLD); + draw_text_nowrap_fn(progress_rect, buf, progress_text, |_, _, _| Style::default()); + draw_text_with_ellipsis_nowrap(progress_rect, buf, format!(" {} ", title), bold); + } + } + } +} + +fn add_block_eta(state: progress::State, progress_text: &mut String) { + match state { + progress::State::Blocked(reason, maybe_eta) | progress::State::Halted(reason, maybe_eta) => { + progress_text.push_str(" ["); + progress_text.push_str(reason); + progress_text.push(']'); + if let Some(eta) = maybe_eta { + let now = SystemTime::now(); + if eta > now { + use std::fmt::Write; + write!( + progress_text, + " → {} to {}", + format_duration(eta.duration_since(now).expect("computation to work")), + if let progress::State::Blocked(_, _) = state { + "unblock" + } else { + "continue" + } + ) + .expect("in-memory writes never fail"); + } + } + } + progress::State::Running => {} + } +} + +fn draw_spinner(buf: &mut Buffer, bound: Rect, step: Step, seed: usize, color: Color) { + if bound.width == 0 { + return; + } + let x = bound.x + ((step + seed) % bound.width as usize) as u16; + let width = 5; + let bound = rect::intersect(Rect { x, width, ..bound }, bound); + tui_react::fill_background(bound, buf, color); +} + +fn draw_progress_bar_fn( + buf: &mut Buffer, + bound: Rect, + fraction: f32, + style: impl FnOnce(f32) -> Color, +) -> (Rect, Style) { + if bound.width == 0 { + return (Rect::default(), Style::default()); + } + let mut fractional_progress_rect = Rect { + width: ((bound.width as f32 * fraction).floor() as u16).min(bound.width), + ..bound + }; + let color = style(fraction); + for y in fractional_progress_rect.top()..fractional_progress_rect.bottom() { + for x in fractional_progress_rect.left()..fractional_progress_rect.right() { + let cell = buf.get_mut(x, y); + cell.set_fg(color); + cell.set_symbol(tui::symbols::block::FULL); + } + } + if fractional_progress_rect.width < bound.width { + static BLOCK_SECTIONS: [&str; 9] = [ + " ", + tui::symbols::block::ONE_EIGHTH, + tui::symbols::block::ONE_QUARTER, + tui::symbols::block::THREE_EIGHTHS, + tui::symbols::block::HALF, + tui::symbols::block::FIVE_EIGHTHS, + tui::symbols::block::THREE_QUARTERS, + tui::symbols::block::SEVEN_EIGHTHS, + tui::symbols::block::FULL, + ]; + // Get the index based on how filled the remaining part is + let index = ((((bound.width as f32 * fraction) - fractional_progress_rect.width as f32) * 8f32).round() + as usize) + % BLOCK_SECTIONS.len(); + let cell = buf.get_mut(fractional_progress_rect.right(), bound.y); + cell.set_symbol(BLOCK_SECTIONS[index]); + cell.set_fg(color); + fractional_progress_rect.width += 1; + } + (fractional_progress_rect, Style::default().bg(color).fg(Color::Black)) +} + +pub fn draw_tree(entries: &[(Key, Task)], buf: &mut Buffer, bound: Rect, offset: u16) -> u16 { + let mut max_prefix_len = 0; + for (line, (entry_index, entry)) in entries + .iter() + .enumerate() + .skip(offset as usize) + .take(bound.height as usize) + .enumerate() + { + let mut line_bound = rect::line_bound(bound, line); + line_bound.x = line_bound.x.saturating_sub(1); + line_bound.width = line_bound.width.saturating_sub(1); + let tree_prefix = format!("{} {} ", level_prefix(entries, entry_index), entry.1.name); + max_prefix_len = max_prefix_len.max(block_width(&tree_prefix)); + + let style = if entry.1.progress.is_none() { + Style::default().add_modifier(Modifier::BOLD).into() + } else { + None + }; + draw_text_with_ellipsis_nowrap(line_bound, buf, tree_prefix, style); + } + max_prefix_len +} + +fn level_prefix(entries: &[(Key, Task)], entry_index: usize) -> String { + let adj = Key::adjacency(entries, entry_index); + let key = entries[entry_index].0; + let key_level = key.level(); + let is_orphan = adj.level() != key_level; + let mut buf = String::with_capacity(key_level as usize); + for level in 1..=key_level { + use crate::progress::key::SiblingLocation::*; + let is_child_level = level == key_level; + if level != 1 { + buf.push(' '); + } + if level == 1 && is_child_level { + buf.push(match adj[level] { + AboveAndBelow | Above => '├', + NotFound | Below => '│', + }); + } else { + let c = if is_child_level { + match adj[level] { + NotFound => { + if is_orphan { + ' ' + } else { + '·' + } + } + Above => '└', + Below => '┌', + AboveAndBelow => '├', + } + } else { + match adj[level] { + NotFound => { + if level == 1 { + '│' + } else if is_orphan { + '·' + } else { + ' ' + } + } + Above => '└', + Below => '┌', + AboveAndBelow => '│', + } + }; + buf.push(c) + } + } + buf +} + +pub fn draw_overflow( + entries: &[(Key, Task)], + buf: &mut Buffer, + bound: Rect, + label_offset: u16, + num_entries_on_display: u16, + offset: u16, +) { + let (count, mut progress_fraction) = entries + .iter() + .take(offset as usize) + .chain(entries.iter().skip((offset + num_entries_on_display) as usize)) + .fold((0usize, 0f32), |(count, progress_fraction), (_key, value)| { + let progress = value.progress.as_ref().and_then(|p| p.fraction()).unwrap_or_default(); + (count + 1, progress_fraction + progress) + }); + progress_fraction /= count as f32; + let label = format!( + "{} …{} skipped and {} more", + if label_offset == 0 { "" } else { VERTICAL_LINE }, + offset, + entries + .len() + .saturating_sub((offset + num_entries_on_display + 1) as usize) + ); + let (progress_rect, style) = draw_progress_bar_fn(buf, bound, progress_fraction, |_| Color::Green); + + let bg_color = Color::Red; + fill_background(rect::offset_x(bound, progress_rect.right() - 1), buf, bg_color); + let color_text_according_to_progress = move |_g: &str, x: u16, _y: u16| { + if x < progress_rect.right() { + style + } else { + style.bg(bg_color) + } + }; + draw_text_nowrap_fn( + rect::offset_x(bound, label_offset), + buf, + label, + color_text_according_to_progress, + ); + let help_text = "⇊ = d|↓ = j|⇈ = u|↑ = k "; + draw_text_nowrap_fn( + rect::snap_to_right(bound, block_width(help_text)), + buf, + help_text, + color_text_according_to_progress, + ); +} diff --git a/vendor/prodash/src/render/tui/engine.rs b/vendor/prodash/src/render/tui/engine.rs new file mode 100644 index 000000000..9658d02a7 --- /dev/null +++ b/vendor/prodash/src/render/tui/engine.rs @@ -0,0 +1,258 @@ +use std::{ + io::{self, Write}, + time::Duration, +}; + +use futures_lite::StreamExt; +use tui::layout::Rect; + +use crate::{ + render::tui::{draw, ticker}, + Root, Throughput, WeakRoot, +}; + +/// Configure the terminal user interface +#[derive(Clone)] +pub struct Options { + /// The initial title to show for the whole window. + /// + /// Can be adjusted later by sending `Event::SetTitle(…)` + /// into the event stream, see see [`tui::render_with_input(…events)`](./fn.render_with_input.html) function. + pub title: String, + /// The amount of frames to draw per second. If below 1.0, it determines the amount of seconds between the frame. + /// + /// *e.g.* 1.0/4.0 is one frame every 4 seconds. + pub frames_per_second: f32, + + /// If true, (default false), we will keep track of the previous progress state to derive + /// continuous throughput information from. Throughput will only show for units which have + /// explicitly enabled it, it is opt-in. + /// + /// This comes at the cost of additional memory and CPU time. + pub throughput: bool, + + /// If set, recompute the column width of the task tree only every given frame. Otherwise the width will be recomputed every frame. + /// + /// Use this if there are many short-running tasks with varying names paired with high refresh rates of multiple frames per second to + /// stabilize the appearance of the TUI. + /// + /// For example, setting the value to 40 will with a frame rate of 20 per second will recompute the column width to fit all task names + /// every 2 seconds. + pub recompute_column_width_every_nth_frame: Option<usize>, + /// The initial window size. + /// + /// If unset, it will be retrieved from the current terminal. + pub window_size: Option<Rect>, + + /// If true (default: true), we will stop running the TUI once the progress isn't available anymore (went out of scope). + pub stop_if_progress_missing: bool, +} + +impl Default for Options { + fn default() -> Self { + Options { + title: "Progress Dashboard".into(), + frames_per_second: 10.0, + throughput: false, + recompute_column_width_every_nth_frame: None, + window_size: None, + stop_if_progress_missing: true, + } + } +} + +/// A line as used in [`Event::SetInformation`](./enum.Event.html#variant.SetInformation) +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Line { + /// Set a title with the given text + Title(String), + /// Set a line of text with the given content + Text(String), +} + +/// The variants represented here allow the user to control when the GUI can be shutdown. +#[derive(Debug, Clone, Copy)] +pub enum Interrupt { + /// Immediately exit the GUI event loop when there is an interrupt request. + /// + /// This is the default when the event loop is entered. + Instantly, + /// Instead of exiting the event loop instantly, wait until the next Interrupt::Instantly + /// event is coming in. + Deferred, +} + +#[derive(Clone, Copy)] +pub(crate) enum InterruptDrawInfo { + Instantly, + /// Boolean signals if interrupt is requested + Deferred(bool), +} + +#[cfg(not(any(feature = "render-tui-crossterm", feature = "render-tui-termion")))] +compile_error!( + "Please set either the 'render-tui-crossterm' or 'render-tui-termion' feature whne using the 'render-tui'" +); + +use crosstermion::{ + input::{key_input_stream, Key}, + terminal::{tui::new_terminal, AlternateRawScreen}, +}; + +/// An event to be sent in the [`tui::render_with_input(…events)`](./fn.render_with_input.html) stream. +/// +/// This way, the TUI can be instructed to draw frames or change the information to be displayed. +#[derive(Debug, Clone)] +pub enum Event { + /// Draw a frame + Tick, + /// Send any key - can be used to simulate user input, and is typically generated by the TUI's own input loop. + Input(Key), + /// Change the size of the window to the given rectangle. + /// + /// Useful to embed the TUI into other terminal user interfaces that can resize dynamically. + SetWindowSize(Rect), + /// Set the title of the progress dashboard + SetTitle(String), + /// Provide a list of titles and lines to populate the side bar on the right. + SetInformation(Vec<Line>), + /// The way the GUI will respond to interrupt requests. See `Interrupt` for more information. + SetInterruptMode(Interrupt), +} + +/// Returns a future that draws the terminal user interface indefinitely. +/// +/// * `progress` is the progress tree whose information to visualize. +/// It will usually be changing constantly while the TUI holds it. +/// * `options` are configuring the TUI. +/// * `events` is a stream of `Event`s which manipulate the TUI while it is running +/// +/// Failure may occour if there is no terminal to draw into. +pub fn render_with_input( + out: impl std::io::Write, + progress: impl WeakRoot, + options: Options, + events: impl futures_core::Stream<Item = Event> + Send + Unpin, +) -> Result<impl std::future::Future<Output = ()>, std::io::Error> { + let Options { + title, + frames_per_second, + window_size, + recompute_column_width_every_nth_frame, + throughput, + stop_if_progress_missing, + } = options; + let mut terminal = new_terminal(AlternateRawScreen::try_from(out)?)?; + terminal.hide_cursor()?; + + let duration_per_frame = Duration::from_secs_f32(1.0 / frames_per_second); + let key_receive = key_input_stream(); + + let render_fut = async move { + let mut state = draw::State { + title, + duration_per_frame, + ..draw::State::default() + }; + if throughput { + state.throughput = Some(Throughput::default()); + } + let mut interrupt_mode = InterruptDrawInfo::Instantly; + let (entries_cap, messages_cap) = progress + .upgrade() + .map(|p| (p.num_tasks(), p.messages_capacity())) + .unwrap_or_default(); + let mut entries = Vec::with_capacity(entries_cap); + let mut messages = Vec::with_capacity(messages_cap); + let mut events = ticker(duration_per_frame) + .map(|_| Event::Tick) + .or(key_receive.map(Event::Input)) + .or(events); + + let mut tick = 0usize; + let store_task_size_every = recompute_column_width_every_nth_frame.unwrap_or(1).max(1); + while let Some(event) = events.next().await { + let mut skip_redraw = false; + match event { + Event::Tick => {} + Event::Input(key) => match key { + Key::Esc | Key::Char('q') | Key::Ctrl('c') | Key::Ctrl('[') => match interrupt_mode { + InterruptDrawInfo::Instantly => break, + InterruptDrawInfo::Deferred(_) => interrupt_mode = InterruptDrawInfo::Deferred(true), + }, + Key::Char('`') => state.hide_messages = !state.hide_messages, + Key::Char('~') => state.messages_fullscreen = !state.messages_fullscreen, + Key::Char('J') => state.message_offset = state.message_offset.saturating_add(1), + Key::Char('D') => state.message_offset = state.message_offset.saturating_add(10), + Key::Char('j') => state.task_offset = state.task_offset.saturating_add(1), + Key::Char('d') => state.task_offset = state.task_offset.saturating_add(10), + Key::Char('K') => state.message_offset = state.message_offset.saturating_sub(1), + Key::Char('U') => state.message_offset = state.message_offset.saturating_sub(10), + Key::Char('k') => state.task_offset = state.task_offset.saturating_sub(1), + Key::Char('u') => state.task_offset = state.task_offset.saturating_sub(10), + Key::Char('[') => state.hide_info = !state.hide_info, + Key::Char('{') => state.maximize_info = !state.maximize_info, + _ => skip_redraw = true, + }, + Event::SetWindowSize(bound) => state.user_provided_window_size = Some(bound), + Event::SetTitle(title) => state.title = title, + Event::SetInformation(info) => state.information = info, + Event::SetInterruptMode(mode) => { + interrupt_mode = match mode { + Interrupt::Instantly => { + if let InterruptDrawInfo::Deferred(true) = interrupt_mode { + break; + } + InterruptDrawInfo::Instantly + } + Interrupt::Deferred => InterruptDrawInfo::Deferred(match interrupt_mode { + InterruptDrawInfo::Deferred(interrupt_requested) => interrupt_requested, + _ => false, + }), + }; + } + } + if !skip_redraw { + tick += 1; + + let progress = match progress.upgrade() { + Some(progress) => progress, + None if stop_if_progress_missing => break, + None => continue, + }; + progress.sorted_snapshot(&mut entries); + if stop_if_progress_missing && entries.is_empty() { + break; + } + let terminal_window_size = terminal.pre_render().expect("pre-render to work"); + let window_size = state + .user_provided_window_size + .or(window_size) + .unwrap_or(terminal_window_size); + let buf = terminal.current_buffer_mut(); + if !state.hide_messages { + progress.copy_messages(&mut messages); + } + + draw::all(&mut state, interrupt_mode, &entries, &messages, window_size, buf); + if tick == 1 || tick % store_task_size_every == 0 || state.last_tree_column_width.unwrap_or(0) == 0 { + state.next_tree_column_width = state.last_tree_column_width; + } + terminal.post_render().expect("post render to work"); + } + } + // Make sure the terminal responds right away when this future stops, to reset back to the 'non-alternate' buffer + drop(terminal); + io::stdout().flush().ok(); + }; + Ok(render_fut) +} + +/// An easy-to-use version of `render_with_input(…)` that does not allow state manipulation via an event stream. +pub fn render( + out: impl std::io::Write, + progress: impl WeakRoot, + config: Options, +) -> Result<impl std::future::Future<Output = ()>, std::io::Error> { + render_with_input(out, progress, config, futures_lite::stream::pending()) +} diff --git a/vendor/prodash/src/render/tui/mod.rs b/vendor/prodash/src/render/tui/mod.rs new file mode 100644 index 000000000..07c0732ad --- /dev/null +++ b/vendor/prodash/src/render/tui/mod.rs @@ -0,0 +1,59 @@ +/*! +* A module implementing a *terminal user interface* capable of visualizing all information stored in +* [progress trees](../tree/struct.Root.html). +* +* **Please note** that it is behind the `render-tui` feature toggle, which is enabled by default. +* +* # Example +* +* ```should_panic +* # fn main() -> Result<(), Box<dyn std::error::Error>> { +* use futures::task::{LocalSpawnExt, SpawnExt}; +* use prodash::render::tui::ticker; +* use prodash::Root; +* // obtain a progress tree +* let root = prodash::tree::Root::new(); +* // Configure the gui, provide it with a handle to the ever-changing tree +* let render_fut = prodash::render::tui::render( +* std::io::stdout(), +* root.downgrade(), +* prodash::render::tui::Options { +* title: "minimal example".into(), +* ..Default::default() +* } +* )?; +* // As it runs forever, we want a way to stop it. +* let (render_fut, abort_handle) = futures::future::abortable(render_fut); +* let pool = futures::executor::LocalPool::new(); +* // Spawn the gui into the background… +* let gui = pool.spawner().spawn_with_handle(async { render_fut.await.ok(); () })?; +* // …and run tasks which provide progress +* pool.spawner().spawn_local({ +* use futures::StreamExt; +* let mut progress = root.add_child("task"); +* async move { +* progress.init(None, None); +* let mut count = 0; +* let mut ticks = ticker(std::time::Duration::from_millis(100)); +* while let Some(_) = ticks.next().await { +* progress.set(count); +* count += 1; +* } +* } +* })?; +* // …when we are done, tell the GUI to stop +* abort_handle.abort(); +* //…and wait until it is done +* futures::executor::block_on(gui); +* # Ok(()) +* # } +* ``` +*/ +mod draw; +mod engine; +mod utils; + +pub use engine::*; +/// Useful for bringing up the TUI without bringing in the `tui` crate yourself +pub use tui as tui_export; +pub use utils::ticker; diff --git a/vendor/prodash/src/render/tui/utils.rs b/vendor/prodash/src/render/tui/utils.rs new file mode 100644 index 000000000..7a261edce --- /dev/null +++ b/vendor/prodash/src/render/tui/utils.rs @@ -0,0 +1,25 @@ +use std::{future::Future, pin::Pin, task::Poll, time::Duration}; + +use async_io::Timer; + +/// Returns a stream of 'ticks', each being duration `dur` apart. +/// +/// Can be useful to provide the TUI with additional events in regular intervals, +/// when using the [`tui::render_with_input(…events)`](./fn.render_with_input.html) function. +pub fn ticker(dur: Duration) -> impl futures_core::Stream<Item = ()> { + let mut delay = Timer::after(dur); + futures_lite::stream::poll_fn(move |ctx| { + let res = Pin::new(&mut delay).poll(ctx); + match res { + Poll::Pending => Poll::Pending, + Poll::Ready(_) => { + delay = Timer::after(dur); + Poll::Ready(Some(())) + } + } + }) +} + +pub const VERTICAL_LINE: &str = "│"; + +pub use tui_react::{draw_text_nowrap_fn, draw_text_with_ellipsis_nowrap, util::*}; diff --git a/vendor/prodash/src/throughput.rs b/vendor/prodash/src/throughput.rs new file mode 100644 index 000000000..6e8eebbd1 --- /dev/null +++ b/vendor/prodash/src/throughput.rs @@ -0,0 +1,124 @@ +use std::{ + collections::VecDeque, + sync::atomic::Ordering, + time::{Duration, SystemTime}, +}; + +use crate::{progress, unit}; + +const THROTTLE_INTERVAL: Duration = Duration::from_secs(1); +const ONCE_A_SECOND: Duration = Duration::from_secs(1); + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +struct State { + observed: Duration, + last_value: progress::Step, + elapsed_values: VecDeque<(Duration, progress::Step)>, + + last_update_duration: Duration, + precomputed_throughput: Option<progress::Step>, +} + +impl State { + fn new(value: progress::Step, elapsed: Duration) -> Self { + State { + observed: elapsed, + last_value: value, + elapsed_values: { + let mut v = VecDeque::with_capacity(6); // default frames per second + v.push_back((elapsed, value)); + v + }, + + last_update_duration: elapsed, + precomputed_throughput: None, + } + } + + fn compute_throughput(&mut self) -> progress::Step { + let mut observed: Duration = self.elapsed_values.iter().map(|e| e.0).sum(); + while !self.elapsed_values.is_empty() && observed > ONCE_A_SECOND { + let candidate = self + .elapsed_values + .front() + .map(|e| e.0) + .expect("at least one item as we are in the checked loop"); + if observed.checked_sub(candidate).unwrap_or_default() <= ONCE_A_SECOND { + break; + } + observed -= candidate; + self.elapsed_values.pop_front(); + } + let observed_value: progress::Step = self.elapsed_values.iter().map(|e| e.1).sum(); + ((observed_value as f64 / observed.as_secs_f64()) * ONCE_A_SECOND.as_secs_f64()) as progress::Step + } + + fn update(&mut self, value: progress::Step, elapsed: Duration) -> Option<unit::display::Throughput> { + self.observed += elapsed; + self.elapsed_values + .push_back((elapsed, value.saturating_sub(self.last_value))); + self.last_value = value; + if self.observed - self.last_update_duration > THROTTLE_INTERVAL { + self.precomputed_throughput = Some(self.compute_throughput()); + self.last_update_duration = self.observed; + } + self.throughput() + } + + fn throughput(&self) -> Option<unit::display::Throughput> { + self.precomputed_throughput.map(|tp| unit::display::Throughput { + value_change_in_timespan: tp, + timespan: ONCE_A_SECOND, + }) + } +} + +/// A utility to compute throughput of a set of progress values usually available to a renderer. +#[derive(Default)] +pub struct Throughput { + sorted_by_key: Vec<(progress::Key, State)>, + updated_at: Option<SystemTime>, + elapsed: Option<Duration>, +} + +impl Throughput { + /// Called at the beginning of the drawing of a renderer to remember at which time progress values are + /// going to be updated with [`update_and_get(…)`][Throughput::update_and_get()]. + pub fn update_elapsed(&mut self) { + let now = SystemTime::now(); + self.elapsed = self.updated_at.and_then(|then| now.duration_since(then).ok()); + self.updated_at = Some(now); + } + + /// Lookup or create the progress value at `key` and set its current `progress`, returning its computed + /// throughput. + pub fn update_and_get( + &mut self, + key: &progress::Key, + progress: Option<&progress::Value>, + ) -> Option<unit::display::Throughput> { + progress.and_then(|progress| { + self.elapsed + .and_then(|elapsed| match self.sorted_by_key.binary_search_by_key(key, |t| t.0) { + Ok(index) => self.sorted_by_key[index] + .1 + .update(progress.step.load(Ordering::SeqCst), elapsed), + Err(index) => { + let state = State::new(progress.step.load(Ordering::SeqCst), elapsed); + let tp = state.throughput(); + self.sorted_by_key.insert(index, (*key, state)); + tp + } + }) + }) + } + + /// Compare the keys in `sorted_values` with our internal state and remove all missing tasks from it. + /// + /// This should be called after [`update_and_get(…)`][Throughput::update_and_get()] to pick up removed/finished + /// progress. + pub fn reconcile(&mut self, sorted_values: &[(progress::Key, progress::Task)]) { + self.sorted_by_key + .retain(|(key, _)| sorted_values.binary_search_by_key(key, |e| e.0).is_ok()); + } +} diff --git a/vendor/prodash/src/time.rs b/vendor/prodash/src/time.rs new file mode 100644 index 000000000..43d9af10f --- /dev/null +++ b/vendor/prodash/src/time.rs @@ -0,0 +1,63 @@ +#[cfg(feature = "local-time")] +mod localtime { + use std::time::SystemTime; + + /// Return a string representing the current date and time as localtime. + /// + /// Available with the `localtime` feature toggle. + pub fn format_now_datetime_seconds() -> String { + let t = time::OffsetDateTime::now_utc(); + t.to_offset(time::UtcOffset::local_offset_at(t).unwrap_or(time::UtcOffset::UTC)) + .format(&time::format_description::parse("%F %T").expect("format known to work")) + .expect("formatting always works") + } + + /// Return a string representing the current time as localtime. + /// + /// Available with the `localtime` feature toggle. + pub fn format_time_for_messages(time: SystemTime) -> String { + time::OffsetDateTime::from(time) + .to_offset(time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC)) + .format(&time::format_description::parse("[hour]:[minute]:[second]").expect("format known to work")) + .expect("formatting always works") + } +} + +/// An `hours:minute:seconds` format. +pub const DATE_TIME_HMS: usize = "00:51:45".len(); + +#[cfg(not(feature = "local-time"))] +mod utc { + use std::time::SystemTime; + + use super::DATE_TIME_HMS; + const DATE_TIME_YMD: usize = "2020-02-13T".len(); + + /// Return a string representing the current date and time as UTC. + /// + /// Available without the `localtime` feature toggle. + pub fn format_time_for_messages(time: SystemTime) -> String { + String::from_utf8_lossy( + &humantime::format_rfc3339_seconds(time).to_string().as_bytes() + [DATE_TIME_YMD..DATE_TIME_YMD + DATE_TIME_HMS], + ) + .into_owned() + } + + /// Return a string representing the current time as UTC. + /// + /// Available without the `localtime` feature toggle. + pub fn format_now_datetime_seconds() -> String { + String::from_utf8_lossy( + &humantime::format_rfc3339_seconds(std::time::SystemTime::now()) + .to_string() + .as_bytes()[.."2020-02-13T00:51:45".len()], + ) + .into_owned() + } +} + +#[cfg(feature = "local-time")] +pub use localtime::*; +#[cfg(not(feature = "local-time"))] +pub use utc::*; diff --git a/vendor/prodash/src/traits.rs b/vendor/prodash/src/traits.rs new file mode 100644 index 000000000..a2a17c760 --- /dev/null +++ b/vendor/prodash/src/traits.rs @@ -0,0 +1,300 @@ +use std::time::Instant; + +use crate::{messages::MessageLevel, progress, progress::Id, Unit}; + +/// A trait for describing hierarchical process. +pub trait Progress: Send { + /// The type of progress returned by [`add_child()`][Progress::add_child()]. + type SubProgress: Progress; + + /// Adds a new child, whose parent is this instance, with the given `name`. + /// + /// This will make the child progress to appear contained in the parent progress. + /// Note that such progress does not have a stable identifier, which can be added + /// with [`add_child_with_id()`][Progress::add_child_with_id()] if desired. + fn add_child(&mut self, name: impl Into<String>) -> Self::SubProgress; + + /// Adds a new child, whose parent is this instance, with the given `name` and `id`. + /// + /// This will make the child progress to appear contained in the parent progress, and it can be identified + /// using `id`. + fn add_child_with_id(&mut self, name: impl Into<String>, id: Id) -> Self::SubProgress; + + /// Initialize the Item for receiving progress information. + /// + /// If `max` is `Some(…)`, it will be treated as upper bound. When progress is [set(…)](./struct.Item.html#method.set) + /// it should not exceed the given maximum. + /// If `max` is `None`, the progress is unbounded. Use this if the amount of work cannot accurately + /// be determined in advance. + /// + /// If `unit` is `Some(…)`, it is used for display purposes only. See `prodash::Unit` for more information. + /// + /// If both `unit` and `max` are `None`, the item will be reset to be equivalent to 'uninitialized'. + /// + /// If this method is never called, this `Progress` instance will serve as organizational unit, useful to add more structure + /// to the progress tree (e.g. a headline). + /// + /// **Note** that this method can be called multiple times, changing the bounded-ness and unit at will. + fn init(&mut self, max: Option<progress::Step>, unit: Option<Unit>); + + /// Set the current progress to the given `step`. The cost of this call is negligible, + /// making manual throttling *not* necessary. + /// + /// **Note**: that this call has no effect unless `init(…)` was called before. + fn set(&mut self, step: progress::Step); + + /// Returns the (cloned) unit associated with this Progress + fn unit(&self) -> Option<Unit> { + None + } + + /// Returns the maximum about of items we expect, as provided with the `init(…)` call + fn max(&self) -> Option<progress::Step> { + None + } + + /// Set the maximum value to `max` and return the old maximum value. + fn set_max(&mut self, _max: Option<progress::Step>) -> Option<progress::Step> { + None + } + + /// Returns the current step, as controlled by `inc*(…)` calls + fn step(&self) -> progress::Step; + + /// Increment the current progress to the given `step`. + /// The cost of this call is negligible, making manual throttling *not* necessary. + fn inc_by(&mut self, step: progress::Step); + + /// Increment the current progress to the given 1. The cost of this call is negligible, + /// making manual throttling *not* necessary. + fn inc(&mut self) { + self.inc_by(1) + } + + /// Set the name of the instance, altering the value given when crating it with `add_child(…)` + /// The progress is allowed to discard it. + fn set_name(&mut self, name: impl Into<String>); + + /// Get the name of the instance as given when creating it with `add_child(…)` + /// The progress is allowed to not be named, thus there is no guarantee that a previously set names 'sticks'. + fn name(&self) -> Option<String>; + + /// Get a stable identifier for the progress instance. + /// Note that it could be [unknown][crate::progress::UNKNOWN]. + fn id(&self) -> Id; + + /// Create a `message` of the given `level` and store it with the progress tree. + /// + /// Use this to provide additional,human-readable information about the progress + /// made, including indicating success or failure. + fn message(&mut self, level: MessageLevel, message: impl Into<String>); + + /// If available, return an atomic counter for direct access to the underlying state. + /// + /// This is useful if multiple threads want to access the same progress, without the need + /// for provide each their own progress and aggregating the result. + fn counter(&self) -> Option<StepShared> { + None + } + + /// Create a message providing additional information about the progress thus far. + fn info(&mut self, message: impl Into<String>) { + self.message(MessageLevel::Info, message) + } + /// Create a message indicating the task is done successfully + fn done(&mut self, message: impl Into<String>) { + self.message(MessageLevel::Success, message) + } + /// Create a message indicating the task failed + fn fail(&mut self, message: impl Into<String>) { + self.message(MessageLevel::Failure, message) + } + /// A shorthand to print throughput information + fn show_throughput(&mut self, start: Instant) { + let step = self.step(); + match self.unit() { + Some(unit) => self.show_throughput_with(start, step, unit, MessageLevel::Info), + None => { + let elapsed = start.elapsed().as_secs_f32(); + let steps_per_second = (step as f32 / elapsed) as progress::Step; + self.info(format!( + "done {} items in {:.02}s ({} items/s)", + step, elapsed, steps_per_second + )) + } + }; + } + + /// A shorthand to print throughput information, with the given step and unit, and message level. + fn show_throughput_with(&mut self, start: Instant, step: progress::Step, unit: Unit, level: MessageLevel) { + use std::fmt::Write; + let elapsed = start.elapsed().as_secs_f32(); + let steps_per_second = (step as f32 / elapsed) as progress::Step; + let mut buf = String::with_capacity(128); + let unit = unit.as_display_value(); + let push_unit = |buf: &mut String| { + buf.push(' '); + let len_before_unit = buf.len(); + unit.display_unit(buf, step).ok(); + if buf.len() == len_before_unit { + buf.pop(); + } + }; + + buf.push_str("done "); + unit.display_current_value(&mut buf, step, None).ok(); + push_unit(&mut buf); + + buf.write_fmt(format_args!(" in {:.02}s (", elapsed)).ok(); + unit.display_current_value(&mut buf, steps_per_second, None).ok(); + push_unit(&mut buf); + buf.push_str("/s)"); + + self.message(level, buf); + } +} + +use crate::{ + messages::{Message, MessageCopyState}, + progress::StepShared, +}; + +/// The top-level root as weak handle, which needs an upgrade to become a usable root. +/// +/// If the underlying reference isn't present anymore, such upgrade will fail permanently. +pub trait WeakRoot { + /// The type implementing the `Root` trait + type Root: Root; + + /// Equivalent to `std::sync::Weak::upgrade()`. + fn upgrade(&self) -> Option<Self::Root>; +} + +/// The top level of a progress task hierarchy, with `progress::Task`s identified with `progress::Key`s +pub trait Root { + /// The type implementing the `WeakRoot` trait + type WeakRoot: WeakRoot; + + /// Returns the maximum amount of messages we can keep before overwriting older ones. + fn messages_capacity(&self) -> usize; + + /// Returns the current amount of tasks underneath the root, transitively. + /// **Note** that this is at most a guess as tasks can be added and removed in parallel. + fn num_tasks(&self) -> usize; + + /// Copy the entire progress tree into the given `out` vector, so that + /// it can be traversed from beginning to end in order of hierarchy. + /// The `out` vec will be cleared automatically. + fn sorted_snapshot(&self, out: &mut Vec<(progress::Key, progress::Task)>); + + /// Copy all messages from the internal ring buffer into the given `out` + /// vector. Messages are ordered from oldest to newest. + fn copy_messages(&self, out: &mut Vec<Message>); + + /// Copy only new messages from the internal ring buffer into the given `out` + /// vector. Messages are ordered from oldest to newest. + fn copy_new_messages(&self, out: &mut Vec<Message>, prev: Option<MessageCopyState>) -> MessageCopyState; + + /// Similar to `Arc::downgrade()` + fn downgrade(&self) -> Self::WeakRoot; +} + +mod impls { + use std::{ + ops::{Deref, DerefMut}, + time::Instant, + }; + + use crate::{ + messages::MessageLevel, + progress::{Id, Step, StepShared}, + Progress, Unit, + }; + + impl<'a, T> Progress for &'a mut T + where + T: Progress, + { + type SubProgress = <T as Progress>::SubProgress; + + fn add_child(&mut self, name: impl Into<String>) -> Self::SubProgress { + self.deref_mut().add_child(name) + } + + fn add_child_with_id(&mut self, name: impl Into<String>, id: Id) -> Self::SubProgress { + self.deref_mut().add_child_with_id(name, id) + } + + fn init(&mut self, max: Option<Step>, unit: Option<Unit>) { + self.deref_mut().init(max, unit) + } + + fn set(&mut self, step: Step) { + self.deref_mut().set(step) + } + + fn unit(&self) -> Option<Unit> { + self.deref().unit() + } + + fn max(&self) -> Option<Step> { + self.deref().max() + } + + fn set_max(&mut self, max: Option<Step>) -> Option<Step> { + self.deref_mut().set_max(max) + } + + fn step(&self) -> Step { + self.deref().step() + } + + fn inc_by(&mut self, step: Step) { + self.deref_mut().inc_by(step) + } + + fn inc(&mut self) { + self.deref_mut().inc() + } + + fn set_name(&mut self, name: impl Into<String>) { + self.deref_mut().set_name(name) + } + + fn name(&self) -> Option<String> { + self.deref().name() + } + + fn id(&self) -> Id { + todo!() + } + + fn message(&mut self, level: MessageLevel, message: impl Into<String>) { + self.deref_mut().message(level, message) + } + + fn counter(&self) -> Option<StepShared> { + self.deref().counter() + } + + fn info(&mut self, message: impl Into<String>) { + self.deref_mut().info(message) + } + + fn done(&mut self, message: impl Into<String>) { + self.deref_mut().done(message) + } + + fn fail(&mut self, message: impl Into<String>) { + self.deref_mut().fail(message) + } + + fn show_throughput(&mut self, start: Instant) { + self.deref_mut().show_throughput(start) + } + + fn show_throughput_with(&mut self, start: Instant, step: Step, unit: Unit, level: MessageLevel) { + self.deref_mut().show_throughput_with(start, step, unit, level) + } + } +} diff --git a/vendor/prodash/src/tree/item.rs b/vendor/prodash/src/tree/item.rs new file mode 100644 index 000000000..d8c4a7548 --- /dev/null +++ b/vendor/prodash/src/tree/item.rs @@ -0,0 +1,406 @@ +use std::{ + fmt::Debug, + ops::Deref, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::SystemTime, +}; + +use parking_lot::Mutex; + +use crate::{ + messages::MessageLevel, + progress::{Id, State, Step, StepShared, Task, Value}, + tree::Item, + unit::Unit, +}; + +impl Drop for Item { + fn drop(&mut self) { + self.tree.remove(&self.key); + } +} + +impl Debug for Item { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Item") + .field("key", &self.key) + .field("value", &self.value) + .finish_non_exhaustive() + } +} + +impl Item { + /// Initialize the Item for receiving progress information. + /// + /// If `max` is `Some(…)`, it will be treated as upper bound. When progress is [set(…)](./struct.Item.html#method.set) + /// it should not exceed the given maximum. + /// If `max` is `None`, the progress is unbounded. Use this if the amount of work cannot accurately + /// be determined. + /// + /// If `unit` is `Some(…)`, it is used for display purposes only. It should be using the plural. + /// + /// If this method is never called, this `Item` will serve as organizational unit, useful to add more structure + /// to the progress tree. + /// + /// **Note** that this method can be called multiple times, changing the bounded-ness and unit at will. + pub fn init(&mut self, max: Option<usize>, unit: Option<Unit>) { + #[cfg(feature = "progress-tree-hp-hashmap")] + { + if let Some(mut r) = self.tree.get_mut(&self.key) { + self.value.store(0, Ordering::SeqCst); + r.value_mut().progress = (max.is_some() || unit.is_some()).then(|| Value { + done_at: max, + unit, + step: Arc::clone(&self.value), + ..Default::default() + }) + }; + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + self.tree.get_mut(&self.key, |v| { + self.value.store(0, Ordering::SeqCst); + v.progress = (max.is_some() || unit.is_some()).then(|| Value { + done_at: max, + unit, + step: Arc::clone(&self.value), + ..Default::default() + }); + }); + } + } + + fn alter_progress(&mut self, f: impl FnMut(&mut Value)) { + #[cfg(feature = "progress-tree-hp-hashmap")] + { + if let Some(mut r) = self.tree.get_mut(&self.key) { + // NOTE: since we wrap around, if there are more tasks than we can have IDs for, + // and if all these tasks are still alive, two progress trees may see the same ID + // when these go out of scope, they delete the key and the other tree will not find + // its value anymore. Besides, it's probably weird to see tasks changing their progress + // all the time… + r.value_mut().progress.as_mut().map(f); + }; + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + self.tree.get_mut(&self.key, |v| { + v.progress.as_mut().map(f); + }); + } + } + + /// Set the name of this task's progress to the given `name`. + pub fn set_name(&mut self, name: impl Into<String>) { + #[cfg(feature = "progress-tree-hp-hashmap")] + { + if let Some(mut r) = self.tree.get_mut(&self.key) { + r.value_mut().name = name.into(); + }; + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + self.tree.get_mut(&self.key, |v| { + v.name = name.into(); + }); + } + } + + /// Get the name of this task's progress + pub fn name(&self) -> Option<String> { + #[cfg(feature = "progress-tree-hp-hashmap")] + { + self.tree.get(&self.key).map(|r| r.value().name.to_owned()) + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + self.tree.get(&self.key, |v| v.name.to_owned()) + } + } + + /// Get the stable identifier of this instance. + pub fn id(&self) -> Id { + #[cfg(feature = "progress-tree-hp-hashmap")] + { + self.tree + .get(&self.key) + .map(|r| r.value().id) + .unwrap_or(crate::progress::UNKNOWN) + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + self.tree.get(&self.key, |v| v.id).unwrap_or(crate::progress::UNKNOWN) + } + } + + /// Returns the current step, as controlled by `inc*(…)` calls + pub fn step(&self) -> Option<Step> { + self.value.load(Ordering::Relaxed).into() + } + + /// Returns the maximum about of items we expect, as provided with the `init(…)` call + pub fn max(&self) -> Option<Step> { + #[cfg(feature = "progress-tree-hp-hashmap")] + { + self.tree + .get(&self.key) + .and_then(|r| r.value().progress.as_ref().and_then(|p| p.done_at)) + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + self.tree + .get(&self.key, |v| v.progress.as_ref().and_then(|p| p.done_at)) + .flatten() + } + } + + /// Set the maximum value to `max` and return the old maximum value. + pub fn set_max(&mut self, max: Option<Step>) -> Option<Step> { + #[cfg(feature = "progress-tree-hp-hashmap")] + { + self.tree + .get_mut(&self.key)? + .value_mut() + .progress + .as_mut() + .and_then(|mut p| { + let prev = p.done_at; + p.done_at = max; + prev + }) + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + self.tree + .get_mut(&self.key, |v| { + v.progress.as_mut().and_then(|mut p| { + let prev = p.done_at; + p.done_at = max; + prev + }) + }) + .flatten() + } + } + + /// Returns the (cloned) unit associated with this Progress + pub fn unit(&self) -> Option<Unit> { + #[cfg(feature = "progress-tree-hp-hashmap")] + { + self.tree + .get(&self.key) + .and_then(|r| r.value().progress.as_ref().and_then(|p| p.unit.clone())) + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + self.tree + .get(&self.key, |v| v.progress.as_ref().and_then(|p| p.unit.clone())) + .flatten() + } + } + + /// Set the current progress to the given `step`. + /// + /// **Note**: that this call has no effect unless `init(…)` was called before. + pub fn set(&mut self, step: Step) { + self.value.store(step, Ordering::SeqCst); + } + + /// Increment the current progress by the given `step`. + /// + /// **Note**: that this call has no effect unless `init(…)` was called before. + pub fn inc_by(&mut self, step: Step) { + self.value.fetch_add(step, Ordering::SeqCst); + } + + /// Increment the current progress by one. + /// + /// **Note**: that this call has no effect unless `init(…)` was called before. + pub fn inc(&mut self) { + self.value.fetch_add(1, Ordering::SeqCst); + } + + /// Call to indicate that progress cannot be indicated, and that the task cannot be interrupted. + /// Use this, as opposed to `halted(…)`, if a non-interruptable call is about to be made without support + /// for any progress indication. + /// + /// If `eta` is `Some(…)`, it specifies the time at which this task is expected to + /// make progress again. + /// + /// The halted-state is undone next time [`tree::Item::running(…)`][Item::running()] is called. + pub fn blocked(&mut self, reason: &'static str, eta: Option<SystemTime>) { + self.alter_progress(|p| p.state = State::Blocked(reason, eta)); + } + + /// Call to indicate that progress cannot be indicated, even though the task can be interrupted. + /// Use this, as opposed to `blocked(…)`, if an interruptable call is about to be made without support + /// for any progress indication. + /// + /// If `eta` is `Some(…)`, it specifies the time at which this task is expected to + /// make progress again. + /// + /// The halted-state is undone next time [`tree::Item::running(…)`][Item::running()] is called. + pub fn halted(&mut self, reason: &'static str, eta: Option<SystemTime>) { + self.alter_progress(|p| p.state = State::Halted(reason, eta)); + } + + /// Call to indicate that progress is back in running state, which should be called after the reason for + /// calling `blocked()` or `halted()` has passed. + pub fn running(&mut self) { + self.alter_progress(|p| p.state = State::Running); + } + + /// Adds a new child `Tree`, whose parent is this instance, with the given `name`. + /// + /// **Important**: The depth of the hierarchy is limited to [`tree::Key::max_level`](./struct.Key.html#method.max_level). + /// Exceeding the level will be ignored, and new tasks will be added to this instance's + /// level instead. + pub fn add_child(&mut self, name: impl Into<String>) -> Item { + self.add_child_with_id(name, crate::progress::UNKNOWN) + } + + /// Adds a new child `Tree`, whose parent is this instance, with the given `name` and `id`. + /// + /// **Important**: The depth of the hierarchy is limited to [`tree::Key::max_level`](./struct.Key.html#method.max_level). + /// Exceeding the level will be ignored, and new tasks will be added to this instance's + /// level instead. + pub fn add_child_with_id(&mut self, name: impl Into<String>, id: Id) -> Item { + let child_key = self.key.add_child(self.highest_child_id); + let task = Task { + name: name.into(), + id, + progress: None, + }; + #[cfg(feature = "progress-tree-hp-hashmap")] + self.tree.insert(child_key, task); + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + self.tree.insert(child_key, task); + self.highest_child_id = self.highest_child_id.wrapping_add(1); + Item { + highest_child_id: 0, + value: Default::default(), + key: child_key, + tree: Arc::clone(&self.tree), + messages: Arc::clone(&self.messages), + } + } + + /// Create a `message` of the given `level` and store it with the progress tree. + /// + /// Use this to provide additional,human-readable information about the progress + /// made, including indicating success or failure. + pub fn message(&mut self, level: MessageLevel, message: impl Into<String>) { + let message: String = message.into(); + self.messages.lock().push_overwrite( + level, + { + let name; + #[cfg(feature = "progress-tree-hp-hashmap")] + { + name = self.tree.get(&self.key).map(|v| v.name.to_owned()).unwrap_or_default(); + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + name = self.tree.get(&self.key, |v| v.name.to_owned()).unwrap_or_default() + } + + #[cfg(feature = "progress-tree-log")] + match level { + MessageLevel::Failure => crate::warn!("{} → {}", name, message), + MessageLevel::Info | MessageLevel::Success => crate::info!("{} → {}", name, message), + }; + + name + }, + message, + ) + } + + /// Create a message indicating the task is done + pub fn done(&mut self, message: impl Into<String>) { + self.message(MessageLevel::Success, message) + } + + /// Create a message indicating the task failed + pub fn fail(&mut self, message: impl Into<String>) { + self.message(MessageLevel::Failure, message) + } + + /// Create a message providing additional information about the progress thus far. + pub fn info(&mut self, message: impl Into<String>) { + self.message(MessageLevel::Info, message) + } + + pub(crate) fn deep_clone(&self) -> Item { + Item { + key: self.key, + value: Arc::new(AtomicUsize::new(self.value.load(Ordering::SeqCst))), + highest_child_id: self.highest_child_id, + tree: Arc::new(self.tree.deref().clone()), + messages: Arc::new(Mutex::new(self.messages.lock().clone())), + } + } +} + +impl crate::Progress for Item { + type SubProgress = Item; + + fn add_child(&mut self, name: impl Into<String>) -> Self::SubProgress { + Item::add_child(self, name) + } + + fn add_child_with_id(&mut self, name: impl Into<String>, id: Id) -> Self::SubProgress { + Item::add_child_with_id(self, name, id) + } + + fn init(&mut self, max: Option<Step>, unit: Option<Unit>) { + Item::init(self, max, unit) + } + + fn set(&mut self, step: usize) { + Item::set(self, step) + } + + fn unit(&self) -> Option<Unit> { + Item::unit(self) + } + + fn max(&self) -> Option<usize> { + Item::max(self) + } + + fn set_max(&mut self, max: Option<Step>) -> Option<Step> { + Item::set_max(self, max) + } + + fn step(&self) -> usize { + Item::step(self).unwrap_or(0) + } + + fn inc_by(&mut self, step: usize) { + self.inc_by(step) + } + + fn set_name(&mut self, name: impl Into<String>) { + Item::set_name(self, name) + } + + fn name(&self) -> Option<String> { + Item::name(self) + } + + fn id(&self) -> Id { + Item::id(self) + } + + fn message(&mut self, level: MessageLevel, message: impl Into<String>) { + Item::message(self, level, message) + } + + fn counter(&self) -> Option<StepShared> { + Some(Arc::clone(&self.value)) + } +} diff --git a/vendor/prodash/src/tree/mod.rs b/vendor/prodash/src/tree/mod.rs new file mode 100644 index 000000000..bbc846d3e --- /dev/null +++ b/vendor/prodash/src/tree/mod.rs @@ -0,0 +1,89 @@ +use crate::messages::MessageRingBuffer; + +/// The top-level of the progress tree. +#[derive(Debug)] +pub struct Root { + pub(crate) inner: parking_lot::Mutex<Item>, +} + +/// A `Tree` represents an element of the progress tree. +/// +/// It can be used to set progress and send messages. +/// ```rust +/// let tree = prodash::tree::Root::new(); +/// let mut progress = tree.add_child("task 1"); +/// +/// progress.init(Some(10), Some("elements".into())); +/// for p in 0..10 { +/// progress.set(p); +/// } +/// progress.done("great success"); +/// let mut sub_progress = progress.add_child_with_id("sub-task 1", *b"TSK2"); +/// sub_progress.init(None, None); +/// sub_progress.set(5); +/// sub_progress.fail("couldn't finish"); +/// ``` +pub struct Item { + pub(crate) key: crate::progress::Key, + pub(crate) value: crate::progress::StepShared, + pub(crate) highest_child_id: crate::progress::key::Id, + pub(crate) tree: std::sync::Arc<HashMap<crate::progress::Key, crate::progress::Task>>, + pub(crate) messages: std::sync::Arc<parking_lot::Mutex<MessageRingBuffer>>, +} + +#[cfg(feature = "dashmap")] +type HashMap<K, V> = dashmap::DashMap<K, V>; + +#[cfg(not(feature = "dashmap"))] +type HashMap<K, V> = sync::HashMap<K, V>; + +#[cfg(not(feature = "dashmap"))] +pub(crate) mod sync { + pub struct HashMap<K, V>(parking_lot::Mutex<std::collections::HashMap<K, V>>); + + impl<K, V> HashMap<K, V> + where + K: Eq + std::hash::Hash, + { + pub fn with_capacity(cap: usize) -> Self { + HashMap(parking_lot::Mutex::new(std::collections::HashMap::with_capacity(cap))) + } + pub fn extend_to(&self, out: &mut Vec<(K, V)>) + where + K: Clone, + V: Clone, + { + let lock = self.0.lock(); + out.extend(lock.iter().map(|(k, v)| (k.clone(), v.clone()))) + } + pub fn remove(&self, key: &K) -> Option<V> { + self.0.lock().remove(key) + } + pub fn get<T>(&self, key: &K, cb: impl FnOnce(&V) -> T) -> Option<T> { + self.0.lock().get(key).map(cb) + } + pub fn get_mut<T>(&self, key: &K, cb: impl FnOnce(&mut V) -> T) -> Option<T> { + self.0.lock().get_mut(key).map(cb) + } + pub fn insert(&self, key: K, value: V) { + self.0.lock().insert(key, value); + } + pub fn len(&self) -> usize { + self.0.lock().len() + } + pub fn clone(&self) -> Self + where + K: Clone, + V: Clone, + { + HashMap(parking_lot::Mutex::new(self.0.lock().clone())) + } + } +} + +mod item; +/// +pub mod root; + +#[cfg(test)] +mod tests; diff --git a/vendor/prodash/src/tree/root.rs b/vendor/prodash/src/tree/root.rs new file mode 100644 index 000000000..4ff2a9978 --- /dev/null +++ b/vendor/prodash/src/tree/root.rs @@ -0,0 +1,179 @@ +use std::{ + ops::Deref, + sync::{atomic::AtomicUsize, Arc, Weak}, +}; + +use parking_lot::Mutex; + +use crate::{ + messages::{Message, MessageCopyState, MessageRingBuffer}, + progress::{Id, Key, Task}, + tree::{Item, Root}, +}; + +impl Root { + /// Create a new tree with default configuration. + /// + /// As opposed to [Item](./struct.Item.html) instances, this type can be closed and sent + /// safely across threads. + pub fn new() -> Arc<Root> { + Options::default().into() + } + + /// Returns the maximum amount of messages we can keep before overwriting older ones. + pub fn messages_capacity(&self) -> usize { + self.inner.lock().messages.lock().buf.capacity() + } + + /// Returns the current amount of `Item`s stored in the tree. + /// **Note** that this is at most a guess as tasks can be added and removed in parallel. + pub fn num_tasks(&self) -> usize { + #[cfg(feature = "progress-tree-hp-hashmap")] + { + self.inner.lock().tree.len() + } + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + { + self.inner.lock().tree.len() + } + } + + /// Adds a new child `tree::Item`, whose parent is this instance, with the given `name`. + /// + /// This builds a hierarchy of `tree::Item`s, each having their own progress. + /// Use this method to [track progress](./struct.Item.html) of your first tasks. + pub fn add_child(&self, name: impl Into<String>) -> Item { + self.inner.lock().add_child(name) + } + + /// Adds a new child `tree::Item`, whose parent is this instance, with the given `name` and `id`. + /// + /// This builds a hierarchy of `tree::Item`s, each having their own progress. + /// Use this method to [track progress](./struct.Item.html) of your first tasks. + pub fn add_child_with_id(&self, name: impl Into<String>, id: Id) -> Item { + self.inner.lock().add_child_with_id(name, id) + } + + /// Copy the entire progress tree into the given `out` vector, so that + /// it can be traversed from beginning to end in order of hierarchy. + pub fn sorted_snapshot(&self, out: &mut Vec<(Key, Task)>) { + out.clear(); + #[cfg(feature = "progress-tree-hp-hashmap")] + out.extend(self.inner.lock().tree.iter().map(|r| (*r.key(), r.value().clone()))); + #[cfg(not(feature = "progress-tree-hp-hashmap"))] + self.inner.lock().tree.extend_to(out); + out.sort_by_key(|t| t.0); + } + + /// Copy all messages from the internal ring buffer into the given `out` + /// vector. Messages are ordered from oldest to newest. + pub fn copy_messages(&self, out: &mut Vec<Message>) { + self.inner.lock().messages.lock().copy_all(out); + } + + /// Copy only new messages from the internal ring buffer into the given `out` + /// vector. Messages are ordered from oldest to newest. + pub fn copy_new_messages(&self, out: &mut Vec<Message>, prev: Option<MessageCopyState>) -> MessageCopyState { + self.inner.lock().messages.lock().copy_new(out, prev) + } + + /// Duplicate all content and return it. + /// + /// This is an expensive operation, whereas `clone()` is not as it is shallow. + pub fn deep_clone(&self) -> Arc<Root> { + Arc::new(Root { + inner: Mutex::new(self.inner.lock().deep_clone()), + }) + } +} + +/// A way to configure new [`tree::Root`](./tree/struct.Root.html) instances +/// ```rust +/// let tree = prodash::tree::root::Options::default().create(); +/// let tree2 = prodash::tree::root::Options { message_buffer_capacity: 100, ..Default::default() }.create(); +/// ``` +#[derive(Clone, Debug)] +pub struct Options { + /// The amount of [items][Item] the tree can hold without being forced to allocate. + pub initial_capacity: usize, + /// The amount of messages we can hold before we start overwriting old ones. + pub message_buffer_capacity: usize, +} + +impl Options { + /// Create a new [`Root`](./tree/struct.Root.html) instance from the + /// configuration within. + pub fn create(self) -> Root { + self.into() + } +} + +impl Default for Options { + fn default() -> Self { + Options { + initial_capacity: 100, + message_buffer_capacity: 20, + } + } +} + +impl From<Options> for Arc<Root> { + fn from(opts: Options) -> Self { + Arc::new(opts.into()) + } +} + +impl From<Options> for Root { + fn from( + Options { + initial_capacity, + message_buffer_capacity, + }: Options, + ) -> Self { + Root { + inner: Mutex::new(Item { + highest_child_id: 0, + value: Arc::new(AtomicUsize::default()), + key: Key::default(), + tree: Arc::new(crate::tree::HashMap::with_capacity(initial_capacity)), + messages: Arc::new(Mutex::new(MessageRingBuffer::with_capacity(message_buffer_capacity))), + }), + } + } +} + +impl crate::WeakRoot for Weak<Root> { + type Root = Arc<Root>; + + fn upgrade(&self) -> Option<Self::Root> { + Weak::upgrade(self) + } +} + +impl crate::Root for Arc<Root> { + type WeakRoot = Weak<Root>; + + fn messages_capacity(&self) -> usize { + self.deref().messages_capacity() + } + + fn num_tasks(&self) -> usize { + self.deref().num_tasks() + } + + fn sorted_snapshot(&self, out: &mut Vec<(Key, Task)>) { + self.deref().sorted_snapshot(out) + } + + fn copy_messages(&self, out: &mut Vec<Message>) { + self.deref().copy_messages(out) + } + + fn copy_new_messages(&self, out: &mut Vec<Message>, prev: Option<MessageCopyState>) -> MessageCopyState { + self.deref().copy_new_messages(out, prev) + } + + fn downgrade(&self) -> Self::WeakRoot { + Arc::downgrade(self) + } +} diff --git a/vendor/prodash/src/tree/tests.rs b/vendor/prodash/src/tree/tests.rs new file mode 100644 index 000000000..03a103df9 --- /dev/null +++ b/vendor/prodash/src/tree/tests.rs @@ -0,0 +1,115 @@ +mod message_buffer { + use crate::messages::{Message, MessageLevel, MessageRingBuffer}; + + fn push(buf: &mut MessageRingBuffer, msg: impl Into<String>) { + buf.push_overwrite(MessageLevel::Info, "test".into(), msg); + } + fn push_and_copy_all(buf: &mut MessageRingBuffer, msg: impl Into<String>, out: &mut Vec<Message>) { + push(buf, msg); + buf.copy_all(out); + } + + fn assert_messages(actual: &[Message], expected: &[&'static str]) { + let actual: Vec<_> = actual.iter().map(|m| m.message.as_str()).collect(); + assert_eq!(expected, actual.as_slice(), "messages are ordered old to new"); + } + + #[test] + fn copy_all() { + let mut buf = MessageRingBuffer::with_capacity(2); + let mut out = Vec::new(); + buf.copy_all(&mut out); + assert_eq!(out, buf.buf); + + push_and_copy_all(&mut buf, "one", &mut out); + assert_eq!(out, buf.buf); + + push_and_copy_all(&mut buf, "two", &mut out); + assert_eq!(out, buf.buf); + + push_and_copy_all(&mut buf, "three", &mut out); + assert_messages(&out, &["two", "three"]); + + push_and_copy_all(&mut buf, "four", &mut out); + assert_messages(&out, &["three", "four"]); + + push_and_copy_all(&mut buf, "five", &mut out); + buf.copy_all(&mut out); + assert_messages(&out, &["four", "five"]); + } + + mod copy_new { + use crate::{ + messages::{Message, MessageCopyState, MessageRingBuffer}, + tree::tests::message_buffer::{assert_messages, push}, + }; + + #[test] + fn without_state() { + fn push_and_copy_new(buf: &mut MessageRingBuffer, msg: impl Into<String>, out: &mut Vec<Message>) { + push(buf, msg); + buf.copy_new(out, None); + } + + let mut buf = MessageRingBuffer::with_capacity(2); + let mut out = Vec::new(); + buf.copy_new(&mut out, None); + assert_eq!(out, buf.buf); + + push_and_copy_new(&mut buf, "one", &mut out); + assert_eq!(out, buf.buf); + + push_and_copy_new(&mut buf, "two", &mut out); + assert_eq!(out, buf.buf); + + push_and_copy_new(&mut buf, "three", &mut out); + assert_messages(&out, &["two", "three"]); + } + + #[test] + fn with_continous_state() { + fn push_and_copy_new( + buf: &mut MessageRingBuffer, + msg: impl Into<String>, + out: &mut Vec<Message>, + state: Option<MessageCopyState>, + ) -> Option<MessageCopyState> { + push(buf, msg); + Some(buf.copy_new(out, state)) + } + let mut buf = MessageRingBuffer::with_capacity(2); + let mut out = Vec::new(); + let mut state = push_and_copy_new(&mut buf, "one", &mut out, None); + assert_eq!(out, buf.buf); + + state = push_and_copy_new(&mut buf, "two", &mut out, state); + assert_messages(&out, &["two"]); + + state = push_and_copy_new(&mut buf, "three", &mut out, state); + assert_messages(&out, &["three"]); + + state = push_and_copy_new(&mut buf, "four", &mut out, state); + assert_messages(&out, &["four"]); + + push_and_copy_new(&mut buf, "five", &mut out, state); + assert_messages(&out, &["five"]); + + state = push_and_copy_new(&mut buf, "six", &mut out, None); + assert_messages(&out, &["five", "six"]); + + state = Some(buf.copy_new(&mut out, state)); + assert_messages(&out, &[]); + + push(&mut buf, "seven"); + push(&mut buf, "eight"); + state = Some(buf.copy_new(&mut out, state)); + assert_messages(&out, &["seven", "eight"]); + + push(&mut buf, "1"); + push(&mut buf, "2"); + push(&mut buf, "3"); + buf.copy_new(&mut out, state); + assert_messages(&out, &["2", "3"]); + } + } +} diff --git a/vendor/prodash/src/unit/bytes.rs b/vendor/prodash/src/unit/bytes.rs new file mode 100644 index 000000000..a79f97195 --- /dev/null +++ b/vendor/prodash/src/unit/bytes.rs @@ -0,0 +1,34 @@ +use std::fmt; + +use crate::{progress::Step, unit::DisplayValue}; + +/// A marker for formatting numbers as bytes in renderers. +#[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub struct Bytes; + +impl Bytes { + fn format_bytes(w: &mut dyn fmt::Write, value: Step) -> fmt::Result { + let string = bytesize::to_string(value as u64, false); + for token in string.split(' ') { + w.write_str(token)?; + } + Ok(()) + } +} + +impl DisplayValue for Bytes { + fn display_current_value(&self, w: &mut dyn fmt::Write, value: Step, _upper: Option<Step>) -> fmt::Result { + Self::format_bytes(w, value) + } + fn display_upper_bound(&self, w: &mut dyn fmt::Write, upper_bound: Step, _value: Step) -> fmt::Result { + Self::format_bytes(w, upper_bound) + } + + fn dyn_hash(&self, state: &mut dyn std::hash::Hasher) { + state.write(&[]) + } + + fn display_unit(&self, _w: &mut dyn fmt::Write, _value: Step) -> fmt::Result { + Ok(()) + } +} diff --git a/vendor/prodash/src/unit/display.rs b/vendor/prodash/src/unit/display.rs new file mode 100644 index 000000000..592692ff5 --- /dev/null +++ b/vendor/prodash/src/unit/display.rs @@ -0,0 +1,188 @@ +use std::fmt::{self, Write}; + +use crate::{ + progress::Step, + unit::{DisplayValue, Unit}, +}; + +/// The location at which [`Throughput`] or [`UnitDisplays`][UnitDisplay] should be placed. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] +#[allow(missing_docs)] +pub enum Location { + BeforeValue, + AfterUnit, +} + +/// A structure able to display throughput, a value change within a given duration. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub struct Throughput { + /// The change of value between the current value and the previous one. + pub value_change_in_timespan: Step, + /// The amount of time passed between the previous and the current value. + pub timespan: std::time::Duration, +} + +impl Throughput { + /// A convenience method to create a new ThroughPut from `value_change_in_timespan` and `timespan`. + pub fn new(value_change_in_timespan: Step, timespan: std::time::Duration) -> Self { + Throughput { + value_change_in_timespan, + timespan, + } + } +} + +/// A way to display a [Unit]. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] +pub struct Mode { + location: Location, + percent: bool, + throughput: bool, +} + +impl Mode { + fn percent_location(&self) -> Option<Location> { + if self.percent { + Some(self.location) + } else { + None + } + } + + fn throughput_location(&self) -> Option<Location> { + if self.throughput { + Some(self.location) + } else { + None + } + } +} + +/// initialization and modification +impl Mode { + /// Create a mode instance with percentage only. + pub fn with_percentage() -> Self { + Mode { + percent: true, + throughput: false, + location: Location::AfterUnit, + } + } + /// Create a mode instance with throughput only. + pub fn with_throughput() -> Self { + Mode { + percent: false, + throughput: true, + location: Location::AfterUnit, + } + } + /// Turn on percentage display on the current instance. + pub fn and_percentage(mut self) -> Self { + self.percent = true; + self + } + /// Turn on throughput display on the current instance. + pub fn and_throughput(mut self) -> Self { + self.throughput = true; + self + } + /// Change the display location to show up in front of the value. + pub fn show_before_value(mut self) -> Self { + self.location = Location::BeforeValue; + self + } +} + +/// A utility to implement [Display][std::fmt::Display]. +pub struct UnitDisplay<'a> { + pub(crate) current_value: Step, + pub(crate) upper_bound: Option<Step>, + pub(crate) throughput: Option<Throughput>, + pub(crate) parent: &'a Unit, + pub(crate) display: What, +} + +pub(crate) enum What { + ValuesAndUnit, + Unit, + Values, +} + +impl What { + fn values(&self) -> bool { + matches!(self, What::Values | What::ValuesAndUnit) + } + fn unit(&self) -> bool { + matches!(self, What::Unit | What::ValuesAndUnit) + } +} + +impl<'a> UnitDisplay<'a> { + /// Display everything, values and the unit. + pub fn all(&mut self) -> &Self { + self.display = What::ValuesAndUnit; + self + } + /// Display only values. + pub fn values(&mut self) -> &Self { + self.display = What::Values; + self + } + /// Display only units. + pub fn unit(&mut self) -> &Self { + self.display = What::Unit; + self + } +} + +impl<'a> fmt::Display for UnitDisplay<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let unit: &dyn DisplayValue = self.parent.as_display_value(); + let mode = self.parent.mode; + + let percent_location_and_fraction = self.upper_bound.and_then(|upper| { + mode.and_then(|m| m.percent_location()) + .map(|location| (location, ((self.current_value as f64 / upper as f64) * 100.0).floor())) + }); + let throughput_and_location = self.throughput.as_ref().and_then(|throughput| { + mode.and_then(|m| m.throughput_location()) + .map(|location| (location, throughput)) + }); + if self.display.values() { + if let Some((Location::BeforeValue, fraction)) = percent_location_and_fraction { + unit.display_percentage(f, fraction)?; + f.write_char(' ')?; + } + if let Some((Location::BeforeValue, throughput)) = throughput_and_location { + unit.display_throughput(f, throughput)?; + f.write_char(' ')?; + } + unit.display_current_value(f, self.current_value, self.upper_bound)?; + if let Some(upper) = self.upper_bound { + unit.separator(f, self.current_value, self.upper_bound)?; + unit.display_upper_bound(f, upper, self.current_value)?; + } + } + if self.display.unit() { + let mut buf = String::with_capacity(10); + if self.display.values() { + buf.write_char(' ')?; + } + unit.display_unit(&mut buf, self.current_value)?; + if buf.len() > 1 { + // did they actually write a unit? + f.write_str(&buf)?; + } + + if let Some((Location::AfterUnit, fraction)) = percent_location_and_fraction { + f.write_char(' ')?; + unit.display_percentage(f, fraction)?; + } + if let Some((Location::AfterUnit, throughput)) = throughput_and_location { + f.write_char(' ')?; + unit.display_throughput(f, throughput)?; + } + } + Ok(()) + } +} diff --git a/vendor/prodash/src/unit/duration.rs b/vendor/prodash/src/unit/duration.rs new file mode 100644 index 000000000..0da23e89b --- /dev/null +++ b/vendor/prodash/src/unit/duration.rs @@ -0,0 +1,27 @@ +use std::fmt; + +use crate::{progress::Step, unit::DisplayValue}; + +/// A marker for formatting numbers as duration in renderers, as in `7d4h20m10s`. +#[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub struct Duration; + +impl DisplayValue for Duration { + fn display_current_value(&self, w: &mut dyn fmt::Write, value: Step, _upper: Option<Step>) -> fmt::Result { + w.write_str(&compound_duration::format_dhms(value)) + } + fn separator(&self, w: &mut dyn fmt::Write, _value: Step, _upper: Option<Step>) -> fmt::Result { + w.write_str(" of ") + } + fn display_upper_bound(&self, w: &mut dyn fmt::Write, upper_bound: Step, _value: Step) -> fmt::Result { + w.write_str(&compound_duration::format_dhms(upper_bound)) + } + + fn dyn_hash(&self, state: &mut dyn std::hash::Hasher) { + state.write(&[]) + } + + fn display_unit(&self, _w: &mut dyn fmt::Write, _value: Step) -> fmt::Result { + Ok(()) + } +} diff --git a/vendor/prodash/src/unit/human.rs b/vendor/prodash/src/unit/human.rs new file mode 100644 index 000000000..4b27f1e36 --- /dev/null +++ b/vendor/prodash/src/unit/human.rs @@ -0,0 +1,47 @@ +use std::{fmt, fmt::Debug, hash::Hasher}; + +pub use human_format::{Formatter, Scales}; + +use crate::{progress::Step, unit::DisplayValue}; + +/// A helper for formatting numbers in a format easily read by humans in renderers, as in `2.54 million objects` +#[derive(Debug)] +pub struct Human { + /// The name of the represented unit, like 'items' or 'objects'. + pub name: &'static str, + /// The formatter to format the actual numbers. + pub formatter: Formatter, +} + +impl Human { + /// A convenience method to create a new new instance and its `formatter` and `name` fields. + pub fn new(formatter: Formatter, name: &'static str) -> Self { + Human { name, formatter } + } + fn format_bytes(&self, w: &mut dyn fmt::Write, value: Step) -> fmt::Result { + let string = self.formatter.format(value as f64); + for token in string.split(' ') { + w.write_str(token)?; + } + Ok(()) + } +} + +impl DisplayValue for Human { + fn display_current_value(&self, w: &mut dyn fmt::Write, value: Step, _upper: Option<Step>) -> fmt::Result { + self.format_bytes(w, value) + } + + fn display_upper_bound(&self, w: &mut dyn fmt::Write, upper_bound: Step, _value: Step) -> fmt::Result { + self.format_bytes(w, upper_bound) + } + + fn dyn_hash(&self, state: &mut dyn Hasher) { + state.write(self.name.as_bytes()); + state.write_u8(0); + } + + fn display_unit(&self, w: &mut dyn fmt::Write, _value: Step) -> fmt::Result { + w.write_str(self.name) + } +} diff --git a/vendor/prodash/src/unit/mod.rs b/vendor/prodash/src/unit/mod.rs new file mode 100644 index 000000000..3ebfa1c74 --- /dev/null +++ b/vendor/prodash/src/unit/mod.rs @@ -0,0 +1,140 @@ +use std::{fmt, ops::Deref, sync::Arc}; + +use crate::progress::Step; + +#[cfg(feature = "unit-bytes")] +mod bytes; +#[cfg(feature = "unit-bytes")] +pub use bytes::Bytes; + +#[cfg(feature = "unit-duration")] +mod duration; +#[cfg(feature = "unit-duration")] +pub use duration::Duration; + +#[cfg(feature = "unit-human")] +/// +pub mod human; +#[cfg(feature = "unit-human")] +#[doc(inline)] +pub use human::Human; + +mod range; +pub use range::Range; + +mod traits; +pub use traits::DisplayValue; + +/// Various utilities to display values and units. +pub mod display; + +/// A configurable and flexible unit for use in [Progress::init()][crate::Progress::init()]. +#[derive(Debug, Clone, Hash)] +pub struct Unit { + kind: Kind, + mode: Option<display::Mode>, +} + +/// Either a static label or a dynamic one implementing [`DisplayValue`]. +#[derive(Clone)] +pub enum Kind { + /// Display only the given statically known label. + Label(&'static str), + /// Display a label created dynamically. + Dynamic(Arc<dyn DisplayValue + Send + Sync>), +} + +impl std::hash::Hash for Kind { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + match self { + Kind::Label(s) => { + 0.hash(state); + s.dyn_hash(state) + } + Kind::Dynamic(label) => { + 1.hash(state); + label.dyn_hash(state); + } + } + } +} + +impl fmt::Debug for Kind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Kind::Label(name) => f.write_fmt(format_args!("Unit::Label({:?})", name)), + Kind::Dynamic(_) => f.write_fmt(format_args!("Unit::Dynamic(..)")), + } + } +} + +impl From<&'static str> for Unit { + fn from(v: &'static str) -> Self { + label(v) + } +} + +/// Returns a unit that is a static `label`. +pub fn label(label: &'static str) -> Unit { + Unit { + kind: Kind::Label(label), + mode: None, + } +} + +/// Returns a unit that is a static `label` along with information on where to display a fraction and throughput. +pub fn label_and_mode(label: &'static str, mode: display::Mode) -> Unit { + Unit { + kind: Kind::Label(label), + mode: Some(mode), + } +} + +/// Returns a unit that is a dynamic `label`. +pub fn dynamic(label: impl DisplayValue + Send + Sync + 'static) -> Unit { + Unit { + kind: Kind::Dynamic(Arc::new(label)), + mode: None, + } +} + +/// Returns a unit that is a dynamic `label` along with information on where to display a fraction and throughput. +pub fn dynamic_and_mode(label: impl DisplayValue + Send + Sync + 'static, mode: display::Mode) -> Unit { + Unit { + kind: Kind::Dynamic(Arc::new(label)), + mode: Some(mode), + } +} + +/// Display and utilities +impl Unit { + /// Create a representation of `self` implementing [`Display`][std::fmt::Display] in configurable fashion. + /// + /// * `current_value` is the progress value to display. + /// * `upper_bound` is the possibly available upper bound of `current_value`. + /// * `throughput` configures how throughput should be displayed if already available. + /// + /// Note that `throughput` is usually not available the first time a value is displayed. + pub fn display( + &self, + current_value: Step, + upper_bound: Option<Step>, + throughput: impl Into<Option<display::Throughput>>, + ) -> display::UnitDisplay { + display::UnitDisplay { + current_value, + upper_bound, + throughput: throughput.into(), + parent: self, + display: display::What::ValuesAndUnit, + } + } + + /// Return `self` as trait object implementing `DisplayValue`. + pub fn as_display_value(&self) -> &dyn DisplayValue { + match self.kind { + Kind::Label(ref unit) => unit, + Kind::Dynamic(ref unit) => unit.deref(), + } + } +} diff --git a/vendor/prodash/src/unit/range.rs b/vendor/prodash/src/unit/range.rs new file mode 100644 index 000000000..4f455bf96 --- /dev/null +++ b/vendor/prodash/src/unit/range.rs @@ -0,0 +1,34 @@ +use std::{fmt, hash::Hasher}; + +use crate::{progress::Step, unit::DisplayValue}; + +/// A helper for formatting numbers representing ranges in renderers as in `2 of 5 steps`. +#[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub struct Range { + /// The name of the unit to be appended to the range. + pub name: &'static str, +} + +impl Range { + /// A convenience method to create a new instance of `name`. + pub fn new(name: &'static str) -> Self { + Range { name } + } +} + +impl DisplayValue for Range { + fn display_current_value(&self, w: &mut dyn fmt::Write, value: Step, _upper: Option<Step>) -> fmt::Result { + w.write_fmt(format_args!("{}", value + 1)) + } + fn separator(&self, w: &mut dyn fmt::Write, _value: Step, _upper: Option<Step>) -> fmt::Result { + w.write_str(" of ") + } + + fn dyn_hash(&self, state: &mut dyn Hasher) { + self.name.dyn_hash(state) + } + + fn display_unit(&self, w: &mut dyn fmt::Write, _value: Step) -> fmt::Result { + w.write_str(self.name) + } +} diff --git a/vendor/prodash/src/unit/traits.rs b/vendor/prodash/src/unit/traits.rs new file mode 100644 index 000000000..33b529e4a --- /dev/null +++ b/vendor/prodash/src/unit/traits.rs @@ -0,0 +1,93 @@ +use std::{fmt, hash::Hasher}; + +use crate::{progress::Step, unit::display}; + +/// A trait to encapsulate all capabilities needed to display a value with unit within a renderer. +pub trait DisplayValue { + /// Display the absolute `value` representing the current progress of an operation and write it to `w`. + /// + /// The `upper` bound is possibly provided when known to add context, even though it is not to be output + /// as part of this method call. + fn display_current_value(&self, w: &mut dyn fmt::Write, value: Step, _upper: Option<Step>) -> fmt::Result { + fmt::write(w, format_args!("{}", value)) + } + /// Emit a token to separate two values. + /// + /// The `value` and its `upper` bound are provided to add context, even though it is not to be output + /// as part of this method call. + fn separator(&self, w: &mut dyn fmt::Write, _value: Step, _upper: Option<Step>) -> fmt::Result { + w.write_str("/") + } + + /// Emit the `upper_bound` to `w`. + /// + /// The `value` is provided to add context, even though it is not to be output as part of this method call. + fn display_upper_bound(&self, w: &mut dyn fmt::Write, upper_bound: Step, _value: Step) -> fmt::Result { + fmt::write(w, format_args!("{}", upper_bound)) + } + + /// A way to hash our state without using generics. + /// + /// This helps to determine quickly if something changed. + fn dyn_hash(&self, state: &mut dyn std::hash::Hasher); + + /// Emit the unit of `value` to `w`. + /// + /// The `value` is provided to add context, even though it is not to be output as part of this method call. + fn display_unit(&self, w: &mut dyn fmt::Write, value: Step) -> fmt::Result; + + /// Emit `percentage` to `w`. + fn display_percentage(&self, w: &mut dyn fmt::Write, percentage: f64) -> fmt::Result { + w.write_fmt(format_args!("[{}%]", percentage as usize)) + } + + /// Emit the `throughput` of an operation to `w`. + fn display_throughput(&self, w: &mut dyn fmt::Write, throughput: &display::Throughput) -> fmt::Result { + let (fraction, unit) = self.fraction_and_time_unit(throughput.timespan); + w.write_char('|')?; + self.display_current_value(w, throughput.value_change_in_timespan, None)?; + w.write_char('/')?; + match fraction { + Some(fraction) => w.write_fmt(format_args!("{}", fraction)), + None => Ok(()), + }?; + w.write_fmt(format_args!("{}|", unit)) + } + + /// Given a `timespan`, return a fraction of the timespan based on the given unit, i.e. `(possible fraction, unit`). + fn fraction_and_time_unit(&self, timespan: std::time::Duration) -> (Option<f64>, &'static str) { + fn skip_one(v: f64) -> Option<f64> { + if (v - 1.0).abs() < f64::EPSILON { + None + } else { + Some(v) + } + } + const HOUR_IN_SECS: u64 = 60 * 60; + let secs = timespan.as_secs(); + let h = secs / HOUR_IN_SECS; + if h > 0 { + return (skip_one(secs as f64 / HOUR_IN_SECS as f64), "h"); + } + const MINUTES_IN_SECS: u64 = 60; + let m = secs / MINUTES_IN_SECS; + if m > 0 { + return (skip_one(secs as f64 / MINUTES_IN_SECS as f64), "m"); + } + if secs > 0 { + return (skip_one(secs as f64), "s"); + } + + (skip_one(timespan.as_millis() as f64), "ms") + } +} + +impl DisplayValue for &'static str { + fn dyn_hash(&self, state: &mut dyn Hasher) { + state.write(self.as_bytes()) + } + + fn display_unit(&self, w: &mut dyn fmt::Write, _value: usize) -> fmt::Result { + w.write_fmt(format_args!("{}", self)) + } +} |