prodash/render/line/
engine.rs

1#[cfg(feature = "signal-hook")]
2use std::sync::Arc;
3use std::{
4    io,
5    ops::RangeInclusive,
6    sync::atomic::{AtomicBool, Ordering},
7    time::Duration,
8};
9
10use crate::{progress, render::line::draw, Throughput, WeakRoot};
11
12/// Options used for configuring a [line renderer][render()].
13#[derive(Clone)]
14pub struct Options {
15    /// If true, _(default true)_, we assume the output stream belongs to a terminal.
16    ///
17    /// If false, we won't print any live progress, only log messages.
18    pub output_is_terminal: bool,
19
20    /// If true, _(default: true)_ we will display color. You should use `output_is_terminal && crosstermion::should_colorize()`
21    /// to determine this value.
22    ///
23    /// Please note that you can enforce color even if the output stream is not connected to a terminal by setting
24    /// this field to true.
25    pub colored: bool,
26
27    /// If true, _(default: false)_, a timestamp will be shown before each message.
28    pub timestamp: bool,
29
30    /// The amount of columns and rows to use for drawing. Defaults to (80, 20).
31    pub terminal_dimensions: (u16, u16),
32
33    /// If true, _(default: false)_, the cursor will be hidden for a more visually appealing display.
34    ///
35    /// Please note that you must make sure the line renderer is properly shut down to restore the previous cursor
36    /// settings. See the `signal-hook` documentation in the README for more information.
37    pub hide_cursor: bool,
38
39    /// If true, (default false), we will keep track of the previous progress state to derive
40    /// continuous throughput information from. Throughput will only show for units which have
41    /// explicitly enabled it, it is opt-in.
42    ///
43    /// This comes at the cost of additional memory and CPU time.
44    pub throughput: bool,
45
46    /// If set, specify all levels that should be shown. Otherwise all available levels are shown.
47    ///
48    /// This is useful to filter out high-noise lower level progress items in the tree.
49    pub level_filter: Option<RangeInclusive<progress::key::Level>>,
50
51    /// If set, progress will only actually be shown after the given duration. Log messages will always be shown without delay.
52    ///
53    /// This option can be useful to not enforce progress for short actions, causing it to flicker.
54    /// Please note that this won't affect display of messages, which are simply logged.
55    pub initial_delay: Option<Duration>,
56
57    /// The amount of frames to draw per second. If below 1.0, it determines the amount of seconds between the frame.
58    ///
59    /// *e.g.* 1.0/4.0 is one frame every 4 seconds.
60    pub frames_per_second: f32,
61
62    /// If true (default: true), we will keep waiting for progress even after we encountered an empty list of drawable progress items.
63    ///
64    /// Please note that you should add at least one item to the `prodash::Tree` before launching the application or else
65    /// risk a race causing nothing to be rendered at all.
66    pub keep_running_if_progress_is_empty: bool,
67}
68
69/// The kind of stream to use for auto-configuration.
70pub enum StreamKind {
71    /// Standard output
72    Stdout,
73    /// Standard error
74    Stderr,
75}
76
77/// Convenience
78impl Options {
79    /// Automatically configure (and overwrite) the following fields based on terminal configuration.
80    ///
81    /// * output_is_terminal
82    /// * colored
83    /// * terminal_dimensions
84    /// * hide-cursor (based on presence of 'signal-hook' feature.
85    #[cfg(feature = "render-line-autoconfigure")]
86    pub fn auto_configure(mut self, output: StreamKind) -> Self {
87        self.output_is_terminal = match output {
88            StreamKind::Stdout => is_terminal::is_terminal(std::io::stdout()),
89            StreamKind::Stderr => is_terminal::is_terminal(std::io::stderr()),
90        };
91        self.colored = self.output_is_terminal && crosstermion::color::allowed();
92        self.terminal_dimensions = crosstermion::terminal::size().unwrap_or((80, 20));
93        #[cfg(feature = "signal-hook")]
94        self.auto_hide_cursor();
95        self
96    }
97    #[cfg(all(feature = "render-line-autoconfigure", feature = "signal-hook"))]
98    fn auto_hide_cursor(&mut self) {
99        self.hide_cursor = true;
100    }
101    #[cfg(not(feature = "render-line-autoconfigure"))]
102    /// No-op - only available with the `render-line-autoconfigure` feature toggle.
103    pub fn auto_configure(self, _output: StreamKind) -> Self {
104        self
105    }
106}
107
108impl Default for Options {
109    fn default() -> Self {
110        Options {
111            output_is_terminal: true,
112            colored: true,
113            timestamp: false,
114            terminal_dimensions: (80, 20),
115            hide_cursor: false,
116            level_filter: None,
117            initial_delay: None,
118            frames_per_second: 6.0,
119            throughput: false,
120            keep_running_if_progress_is_empty: true,
121        }
122    }
123}
124
125/// A handle to the render thread, which when dropped will instruct it to stop showing progress.
126pub struct JoinHandle {
127    inner: Option<std::thread::JoinHandle<io::Result<()>>>,
128    connection: std::sync::mpsc::SyncSender<Event>,
129    // If we disconnect before sending a Quit event, the selector continuously informs about the 'Disconnect' state
130    disconnected: bool,
131}
132
133impl JoinHandle {
134    /// `detach()` and `forget()` to remove any effects associated with this handle.
135    pub fn detach(mut self) {
136        self.disconnect();
137        self.forget();
138    }
139    /// Remove the handles capability to instruct the render thread to stop, but it will still wait for it
140    /// if dropped.
141    /// Use `forget()` if it should not wait for the render thread anymore.
142    pub fn disconnect(&mut self) {
143        self.disconnected = true;
144    }
145    /// Remove the handles capability to `join()` by forgetting the threads handle
146    pub fn forget(&mut self) {
147        self.inner.take();
148    }
149    /// Wait for the thread to shutdown naturally, for example because there is no more progress to display
150    pub fn wait(mut self) {
151        self.inner.take().and_then(|h| h.join().ok());
152    }
153    /// Send the shutdown signal right after one last redraw
154    pub fn shutdown(&mut self) {
155        if !self.disconnected {
156            self.connection.send(Event::Tick).ok();
157            self.connection.send(Event::Quit).ok();
158        }
159    }
160    /// Send the signal to shutdown and wait for the thread to be shutdown.
161    pub fn shutdown_and_wait(mut self) {
162        self.shutdown();
163        self.wait();
164    }
165}
166
167impl Drop for JoinHandle {
168    fn drop(&mut self) {
169        self.shutdown();
170        self.inner.take().and_then(|h| h.join().ok());
171    }
172}
173
174#[derive(Debug)]
175enum Event {
176    Tick,
177    Quit,
178    #[cfg(feature = "signal-hook")]
179    Resize(u16, u16),
180}
181
182/// Write a line-based representation of `progress` to `out` which is assumed to be a terminal.
183///
184/// Configure it with `config`, see the [`Options`] for details.
185pub fn render(
186    mut out: impl io::Write + Send + 'static,
187    progress: impl WeakRoot + Send + 'static,
188    Options {
189        output_is_terminal,
190        colored,
191        timestamp,
192        level_filter,
193        terminal_dimensions,
194        initial_delay,
195        frames_per_second,
196        keep_running_if_progress_is_empty,
197        hide_cursor,
198        throughput,
199    }: Options,
200) -> JoinHandle {
201    #[cfg_attr(not(feature = "signal-hook"), allow(unused_mut))]
202    let mut config = draw::Options {
203        level_filter,
204        terminal_dimensions,
205        keep_running_if_progress_is_empty,
206        output_is_terminal,
207        colored,
208        timestamp,
209        hide_cursor,
210    };
211
212    let (event_send, event_recv) = std::sync::mpsc::sync_channel::<Event>(1);
213    let show_cursor = possibly_hide_cursor(&mut out, hide_cursor && output_is_terminal);
214    static SHOW_PROGRESS: AtomicBool = AtomicBool::new(false);
215    #[cfg(feature = "signal-hook")]
216    let term_signal_received: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
217    #[cfg(feature = "signal-hook")]
218    let terminal_resized: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
219    #[cfg(feature = "signal-hook")]
220    {
221        for sig in signal_hook::consts::TERM_SIGNALS {
222            signal_hook::flag::register(*sig, term_signal_received.clone()).ok();
223        }
224
225        #[cfg(unix)]
226        signal_hook::flag::register(signal_hook::consts::SIGWINCH, terminal_resized.clone()).ok();
227    }
228
229    let handle = std::thread::Builder::new()
230        .name("render-line-eventloop".into())
231        .spawn({
232            let tick_send = event_send.clone();
233            move || {
234                {
235                    let initial_delay = initial_delay.unwrap_or_default();
236                    SHOW_PROGRESS.store(initial_delay == Duration::default(), Ordering::Relaxed);
237                    if !SHOW_PROGRESS.load(Ordering::Relaxed) {
238                        std::thread::Builder::new()
239                            .name("render-line-progress-delay".into())
240                            .spawn(move || {
241                                std::thread::sleep(initial_delay);
242                                SHOW_PROGRESS.store(true, Ordering::Relaxed);
243                            })
244                            .ok();
245                    }
246                }
247
248                let mut state = draw::State::default();
249                if throughput {
250                    state.throughput = Some(Throughput::default());
251                }
252                let secs = 1.0 / frames_per_second;
253                let _ticker = std::thread::Builder::new()
254                    .name("render-line-ticker".into())
255                    .spawn(move || loop {
256                        #[cfg(feature = "signal-hook")]
257                        {
258                            if term_signal_received.load(Ordering::SeqCst) {
259                                tick_send.send(Event::Quit).ok();
260                                break;
261                            }
262                            if terminal_resized.load(Ordering::SeqCst) {
263                                terminal_resized.store(false, Ordering::SeqCst);
264                                if let Ok((x, y)) = crosstermion::terminal::size() {
265                                    tick_send.send(Event::Resize(x, y)).ok();
266                                }
267                            }
268                        }
269                        if tick_send.send(Event::Tick).is_err() {
270                            break;
271                        }
272                        std::thread::sleep(Duration::from_secs_f32(secs));
273                    })
274                    .expect("starting a thread works");
275
276                for event in event_recv {
277                    match event {
278                        #[cfg(feature = "signal-hook")]
279                        Event::Resize(x, y) => {
280                            config.terminal_dimensions = (x, y);
281                            draw::all(&mut out, SHOW_PROGRESS.load(Ordering::Relaxed), &mut state, &config)?;
282                        }
283                        Event::Tick => match progress.upgrade() {
284                            Some(progress) => {
285                                let has_changed = state.update_from_progress(&progress);
286                                draw::all(
287                                    &mut out,
288                                    SHOW_PROGRESS.load(Ordering::Relaxed) && has_changed,
289                                    &mut state,
290                                    &config,
291                                )?;
292                            }
293                            None => {
294                                state.clear();
295                                draw::all(&mut out, SHOW_PROGRESS.load(Ordering::Relaxed), &mut state, &config)?;
296                                break;
297                            }
298                        },
299                        Event::Quit => {
300                            state.clear();
301                            draw::all(&mut out, SHOW_PROGRESS.load(Ordering::Relaxed), &mut state, &config)?;
302                            break;
303                        }
304                    }
305                }
306
307                if show_cursor {
308                    crosstermion::execute!(out, crosstermion::cursor::Show).ok();
309                }
310
311                // One day we might try this out on windows, but let's not risk it now.
312                #[cfg(unix)]
313                write!(out, "\x1b[2K\r").ok(); // clear the last line.
314                Ok(())
315            }
316        })
317        .expect("starting a thread works");
318
319    JoinHandle {
320        inner: Some(handle),
321        connection: event_send,
322        disconnected: false,
323    }
324}
325
326// Not all configurations actually need it to be mut, but those with the 'signal-hook' feature do
327#[allow(unused_mut)]
328fn possibly_hide_cursor(out: &mut impl io::Write, mut hide_cursor: bool) -> bool {
329    if hide_cursor {
330        crosstermion::execute!(out, crosstermion::cursor::Hide).is_ok()
331    } else {
332        false
333    }
334}