1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
//! # status-line
//!
//! This crate handles the problem of displaying a small amount of textual information in
//! a terminal, periodically refreshing it, and finally erasing it, similar to how progress bars
//! are displayed.
//!
//! A status line can be viewed as a generalization of a progress bar.
//! Unlike progress bar drawing crates, this crate does not require
//! that you render the status text as a progress bar. It does not enforce any particular
//! data format or template, nor it doesn't help you with formatting.
//!
//! The status line text may contain any information you wish, and may even be split
//! into multiple lines. You fully control the data model, as well as how the data gets printed
//! on the screen. The standard `Display` trait is used to convert the data into printed text.
//!
//! Status updates can be made with a very high frequency, up to tens of millions of updates
//! per second. `StatusLine` decouples redrawing rate from the data update rate by using a
//! background thread to handle text printing with low frequency.
//!
//! ## Example
//! ```rust
//! use std::fmt::{Display, Formatter};
//! use std::sync::atomic::{AtomicU64, Ordering};
//! use status_line::StatusLine;
//!
//! // Define the data model representing the status of your app.
//! // Make sure it is Send + Sync, so it can be read and written from different threads:
//! struct Progress(AtomicU64);
//!
//! // Define how you want to display it:
//! impl Display for Progress {
//! fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
//! write!(f, "{}%", self.0.load(Ordering::Relaxed))
//! }
//! }
//!
//! // StatusLine takes care of displaying the progress data:
//! let status = StatusLine::new(Progress(AtomicU64::new(0))); // shows 0%
//! status.0.fetch_add(1, Ordering::Relaxed); // shows 1%
//! status.0.fetch_add(1, Ordering::Relaxed); // shows 2%
//! drop(status) // hides the status line
//! ```
//!
use std::fmt::Display;
use std::io::Write;
use std::ops::Deref;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use ansi_escapes::{CursorLeft, CursorPrevLine, EraseDown};
fn redraw(ansi: bool, state: &impl Display) {
let stderr = std::io::stderr();
let mut stderr = stderr.lock();
let contents = format!("{}", state);
if ansi {
let line_count = contents.chars().filter(|c| *c == '\n').count();
write!(&mut stderr, "{}{}{}", EraseDown, contents, CursorLeft).unwrap();
for _ in 0..line_count {
write!(&mut stderr, "{}", CursorPrevLine).unwrap();
}
} else {
writeln!(&mut stderr, "{}", contents).unwrap();
}
}
fn clear(ansi: bool) {
if ansi {
let stderr = std::io::stderr();
let mut stderr = stderr.lock();
write!(&mut stderr, "{}", EraseDown).unwrap();
}
}
struct State<D> {
data: D,
visible: AtomicBool,
}
impl<D> State<D> {
pub fn new(inner: D) -> State<D> {
State {
data: inner,
visible: AtomicBool::new(false),
}
}
}
/// Options controlling how to display the status line
pub struct Options {
/// How long to wait between subsequent refreshes of the status.
/// Defaults to 100 ms on interactive terminals (TTYs) and 1 s if the standard error
/// is not interactive, e.g. redirected to a file.
pub refresh_period: Duration,
/// Set it to false if you don't want to show the status on creation of the `StatusLine`.
/// You can change the visibility of the `StatusLine` any time by calling
/// [`StatusLine::set_visible`].
pub initially_visible: bool,
/// Set to true to enable ANSI escape codes.
/// By default set to true if the standard error is a TTY.
/// If ANSI escape codes are disabled, the status line is not erased before each refresh,
/// it is printed in a new line instead.
pub enable_ansi_escapes: bool,
}
impl Default for Options {
fn default() -> Self {
let is_tty = atty::is(atty::Stream::Stderr);
let refresh_period_ms = if is_tty { 100 } else { 1000 };
Options {
refresh_period: Duration::from_millis(refresh_period_ms),
initially_visible: true,
enable_ansi_escapes: is_tty,
}
}
}
/// Wraps arbitrary data and displays it periodically on the screen.
pub struct StatusLine<D: Display> {
state: Arc<State<D>>,
options: Options,
}
impl<D: Display + Send + Sync + 'static> StatusLine<D> {
/// Creates a new `StatusLine` with default options and shows it immediately.
pub fn new(data: D) -> StatusLine<D> {
Self::with_options(data, Default::default())
}
/// Creates a new `StatusLine` with custom options.
pub fn with_options(data: D, options: Options) -> StatusLine<D> {
let state = Arc::new(State::new(data));
state
.visible
.store(options.initially_visible, Ordering::Release);
let state_ref = state.clone();
thread::spawn(move || {
while Arc::strong_count(&state_ref) > 1 {
if state_ref.visible.load(Ordering::Acquire) {
redraw(options.enable_ansi_escapes, &state_ref.data);
}
thread::sleep(options.refresh_period);
}
});
StatusLine { state, options }
}
}
impl<D: Display> StatusLine<D> {
/// Forces redrawing the status information immediately,
/// without waiting for the next refresh cycle of the background refresh loop.
pub fn refresh(&self) {
redraw(self.options.enable_ansi_escapes, &self.state.data);
}
/// Sets the visibility of the status line.
pub fn set_visible(&self, visible: bool) {
let was_visible = self.state.visible.swap(visible, Ordering::Release);
if !visible && was_visible {
clear(self.options.enable_ansi_escapes)
} else if visible && !was_visible {
redraw(self.options.enable_ansi_escapes, &self.state.data)
}
}
/// Returns true if the status line is currently visible.
pub fn is_visible(&self) -> bool {
self.state.visible.load(Ordering::Acquire)
}
}
impl<D: Display> Deref for StatusLine<D> {
type Target = D;
fn deref(&self) -> &Self::Target {
&self.state.data
}
}
impl<D: Display> Drop for StatusLine<D> {
fn drop(&mut self) {
if self.is_visible() {
clear(self.options.enable_ansi_escapes)
}
}
}