summaryrefslogtreecommitdiffstats
path: root/vendor/prodash/src/render
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/prodash/src/render')
-rw-r--r--vendor/prodash/src/render/line/draw.rs350
-rw-r--r--vendor/prodash/src/render/line/engine.rs341
-rw-r--r--vendor/prodash/src/render/line/mod.rs10
-rw-r--r--vendor/prodash/src/render/mod.rs11
-rw-r--r--vendor/prodash/src/render/tui/draw/all.rs164
-rw-r--r--vendor/prodash/src/render/tui/draw/information.rs61
-rw-r--r--vendor/prodash/src/render/tui/draw/messages.rs150
-rw-r--r--vendor/prodash/src/render/tui/draw/mod.rs6
-rw-r--r--vendor/prodash/src/render/tui/draw/progress.rs506
-rw-r--r--vendor/prodash/src/render/tui/engine.rs258
-rw-r--r--vendor/prodash/src/render/tui/mod.rs59
-rw-r--r--vendor/prodash/src/render/tui/utils.rs25
12 files changed, 1941 insertions, 0 deletions
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::*};