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}