blob: c44d5991fb2bb4ac0c138092e68e0c6f7ed46ea1 [file] [log] [blame]
#[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,
}
/// 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 = match output {
StreamKind::Stdout => is_terminal::is_terminal(std::io::stdout()),
StreamKind::Stderr => is_terminal::is_terminal(std::io::stderr()),
};
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
}
}