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#[derive(Clone)]
14pub struct Options {
15 pub output_is_terminal: bool,
19
20 pub colored: bool,
26
27 pub timestamp: bool,
29
30 pub terminal_dimensions: (u16, u16),
32
33 pub hide_cursor: bool,
38
39 pub throughput: bool,
45
46 pub level_filter: Option<RangeInclusive<progress::key::Level>>,
50
51 pub initial_delay: Option<Duration>,
56
57 pub frames_per_second: f32,
61
62 pub keep_running_if_progress_is_empty: bool,
67}
68
69pub enum StreamKind {
71 Stdout,
73 Stderr,
75}
76
77impl Options {
79 #[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 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
125pub struct JoinHandle {
127 inner: Option<std::thread::JoinHandle<io::Result<()>>>,
128 connection: std::sync::mpsc::SyncSender<Event>,
129 disconnected: bool,
131}
132
133impl JoinHandle {
134 pub fn detach(mut self) {
136 self.disconnect();
137 self.forget();
138 }
139 pub fn disconnect(&mut self) {
143 self.disconnected = true;
144 }
145 pub fn forget(&mut self) {
147 self.inner.take();
148 }
149 pub fn wait(mut self) {
151 self.inner.take().and_then(|h| h.join().ok());
152 }
153 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 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
182pub 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 #[cfg(unix)]
313 write!(out, "\x1b[2K\r").ok(); 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#[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}