status_line/
lib.rs

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