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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
#[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
    }
}