1use std::io;
10use std::io::Stdout;
11#[cfg(unix)]
13use std::sync::OnceLock;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::{Arc, Mutex};
16use std::thread;
17use std::time::Duration;
18
19use crossterm::terminal;
20
21use crate::Control;
22use crate::console::OverflowMethod;
23use crate::style::Style;
24use crate::text::Text;
25use crate::{Console, JustifyMethod, Renderable};
26
27#[cfg(unix)]
28use std::fs::File;
29#[cfg(unix)]
30use std::io::{BufRead, BufReader};
31#[cfg(unix)]
32use std::os::fd::{FromRawFd, RawFd};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum VerticalOverflowMethod {
36 Crop,
37 Ellipsis,
38 Visible,
39}
40
41impl Default for VerticalOverflowMethod {
42 fn default() -> Self {
43 Self::Ellipsis
44 }
45}
46
47#[derive(Debug, Clone)]
48pub struct LiveOptions {
49 pub screen: bool,
50 pub auto_refresh: bool,
51 pub refresh_per_second: f64,
52 pub transient: bool,
53 pub vertical_overflow: VerticalOverflowMethod,
54 pub redirect_stdout: bool,
56 pub redirect_stderr: bool,
58}
59
60impl Default for LiveOptions {
61 fn default() -> Self {
62 Self {
63 screen: false,
64 auto_refresh: true,
65 refresh_per_second: 4.0,
66 transient: false,
67 vertical_overflow: VerticalOverflowMethod::Ellipsis,
68 redirect_stdout: false,
69 redirect_stderr: false,
70 }
71 }
72}
73
74struct LiveState {
75 options: LiveOptions,
76 started: bool,
77 live_id: Option<usize>,
78 is_root: bool,
79 alt_screen: bool,
80 pending_renderable: Option<Box<dyn Renderable + Send + Sync>>,
81}
82
83pub struct Live {
89 console: Arc<Mutex<Console<Stdout>>>,
90 state: Arc<Mutex<LiveState>>,
91 stop_flag: Arc<AtomicBool>,
92 started_flag: Arc<AtomicBool>,
93 refresh_thread: Option<thread::JoinHandle<()>>,
94 #[cfg(unix)]
95 redirects: Vec<StreamRedirect>,
96 get_renderable: Option<Arc<dyn Fn() -> Box<dyn Renderable + Send + Sync> + Send + Sync>>,
99}
100
101#[cfg(unix)]
102struct StreamRedirect {
103 target_fd: RawFd,
104 original_fd: RawFd,
105 pipe_write_fd: RawFd,
106 worker: thread::JoinHandle<()>,
107}
108
109#[cfg(unix)]
110unsafe extern "C" {
111 fn dup(oldfd: i32) -> i32;
112 fn dup2(oldfd: i32, newfd: i32) -> i32;
113 fn pipe(fds: *mut i32) -> i32;
114 fn close(fd: i32) -> i32;
115}
116
117#[cfg(unix)]
118fn stream_redirect_lock() -> &'static Mutex<()> {
119 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
120 LOCK.get_or_init(|| Mutex::new(()))
121}
122
123impl Live {
124 pub fn new(renderable: Box<dyn Renderable + Send + Sync>) -> Self {
125 Self::with_console(renderable, Console::new(), LiveOptions::default())
126 }
127
128 pub fn with_options(
129 renderable: Box<dyn Renderable + Send + Sync>,
130 options: LiveOptions,
131 ) -> Self {
132 Self::with_console(renderable, Console::new(), options)
133 }
134
135 pub fn with_console(
136 renderable: Box<dyn Renderable + Send + Sync>,
137 console: Console<Stdout>,
138 options: LiveOptions,
139 ) -> Self {
140 assert!(
141 options.refresh_per_second > 0.0,
142 "refresh_per_second must be > 0"
143 );
144
145 let transient = if options.screen {
146 true
147 } else {
148 options.transient
149 };
150 let options = LiveOptions {
151 transient,
152 ..options
153 };
154 let state = LiveState {
155 options,
156 started: false,
157 live_id: None,
158 is_root: false,
159 alt_screen: false,
160 pending_renderable: Some(renderable),
161 };
162
163 Live {
164 console: Arc::new(Mutex::new(console)),
165 state: Arc::new(Mutex::new(state)),
166 stop_flag: Arc::new(AtomicBool::new(false)),
167 started_flag: Arc::new(AtomicBool::new(false)),
168 refresh_thread: None,
169 #[cfg(unix)]
170 redirects: Vec::new(),
171 get_renderable: None,
172 }
173 }
174
175 pub fn is_started(&self) -> bool {
176 self.started_flag.load(Ordering::SeqCst)
177 }
178
179 pub fn with_get_renderable(
184 mut self,
185 f: impl Fn() -> Box<dyn Renderable + Send + Sync> + Send + Sync + 'static,
186 ) -> Self {
187 self.get_renderable = Some(Arc::new(f));
188 self
189 }
190
191 pub(crate) fn started_flag(&self) -> Arc<AtomicBool> {
192 self.started_flag.clone()
193 }
194
195 pub(crate) fn refresh_per_second(&self) -> f64 {
196 self.state
197 .lock()
198 .expect("live state mutex poisoned")
199 .options
200 .refresh_per_second
201 }
202
203 pub fn start(&mut self, refresh: bool) -> io::Result<()> {
204 let mut state = self.state.lock().expect("live state mutex poisoned");
205 if state.started {
206 return Ok(());
207 }
208
209 let mut console = self.console.lock().expect("console mutex poisoned");
210 sync_terminal_size(&mut console);
211
212 let interactive = console.is_terminal() && !console.is_dumb_terminal();
213 if !interactive {
214 state.started = true;
217 state.live_id = None;
218 state.is_root = false;
219 state.alt_screen = false;
220 self.started_flag.store(false, Ordering::SeqCst);
221 return Ok(());
222 }
223
224 let renderable = state
225 .pending_renderable
226 .take()
227 .unwrap_or_else(|| Box::new(Text::plain("")));
228
229 let live_options = state.options.clone();
230 let (id, is_root) = console.live_start(renderable, live_options.vertical_overflow);
231 state.live_id = Some(id);
232 state.is_root = is_root;
233 state.started = true;
234 self.started_flag.store(true, Ordering::SeqCst);
235
236 if is_root {
237 if live_options.screen {
238 state.alt_screen = console.set_alt_screen(true)?;
239 }
240 let _ = console.show_cursor(false)?;
241 }
242
243 drop(console);
244 let auto_refresh = live_options.auto_refresh;
245 let is_root = state.is_root;
246 drop(state);
247
248 if is_root {
249 self.start_redirects(&live_options)?;
250 }
251
252 if refresh {
253 self.refresh()?;
254 }
255
256 if auto_refresh && is_root {
257 self.spawn_refresh_thread();
258 }
259
260 Ok(())
261 }
262
263 pub fn stop(&mut self) -> io::Result<()> {
264 self.stop_flag.store(true, Ordering::SeqCst);
265 if let Some(handle) = self.refresh_thread.take() {
266 let _ = handle.join();
267 }
268 self.stop_redirects();
269 self.started_flag.store(false, Ordering::SeqCst);
270
271 let mut state = self.state.lock().expect("live state mutex poisoned");
272 if !state.started {
273 return Ok(());
274 }
275 state.started = false;
276
277 let id = state.live_id.take();
278 let is_root = state.is_root;
279 state.is_root = false;
280 let alt_screen = state.alt_screen;
281 state.alt_screen = false;
282 let options = state.options.clone();
283
284 let Some(id) = id else {
285 if !options.transient {
287 let renderable = state
288 .pending_renderable
289 .take()
290 .unwrap_or_else(|| Box::new(Text::plain("")));
291 drop(state);
292 let mut console = self.console.lock().expect("console mutex poisoned");
293 let _ = console.print(renderable.as_ref(), None, None, None, false, "\n");
294 }
295 return Ok(());
296 };
297
298 let mut console = self.console.lock().expect("console mutex poisoned");
299
300 if !is_root {
302 let renderable = console.live_stop(id);
303 if !options.transient {
304 if let Some(renderable) = renderable {
305 let _ = console.print(renderable.as_ref(), None, None, None, false, "\n");
306 }
307 } else if console.is_terminal() && !console.is_dumb_terminal() {
308 let _ = console.print(&Control::new(), None, None, None, false, "");
310 }
311 return Ok(());
312 }
313
314 if is_root && console.is_terminal() && !console.is_dumb_terminal() && !alt_screen {
316 console.live_set_vertical_overflow(id, VerticalOverflowMethod::Visible);
317 let _ = console.print(&Control::new(), None, None, None, false, "");
318 }
319
320 let restore_controls = if is_root
323 && console.is_terminal()
324 && !console.is_dumb_terminal()
325 && options.transient
326 && !alt_screen
327 {
328 console.live_restore_cursor()
329 } else {
330 crate::Segments::new()
331 };
332
333 console.live_clear();
335
336 if is_root {
338 if console.is_terminal() && !alt_screen {
339 let _ = console.print(&Text::plain(""), None, None, None, false, "\n");
340 }
341
342 let _ = console.show_cursor(true);
343 if alt_screen {
344 let _ = console.set_alt_screen(false);
345 }
346 }
347
348 if !restore_controls.is_empty() {
349 let _ = console.print_segments(&restore_controls);
350 }
351
352 Ok(())
353 }
354
355 pub fn update(
356 &self,
357 renderable: Box<dyn Renderable + Send + Sync>,
358 refresh: bool,
359 ) -> io::Result<()> {
360 let (id, started) = {
361 let mut state = self.state.lock().expect("live state mutex poisoned");
362 if !state.started {
363 state.pending_renderable = Some(renderable);
364 return Ok(());
365 }
366 if state.live_id.is_none() {
367 state.pending_renderable = Some(renderable);
369 return Ok(());
370 }
371 (state.live_id, state.started)
372 };
373
374 if started {
375 if let Some(id) = id {
376 let mut console = self.console.lock().expect("console mutex poisoned");
377 console.live_update(id, renderable);
378 }
379 }
380 if refresh {
381 self.refresh()?;
382 }
383 Ok(())
384 }
385
386 pub fn refresh(&self) -> io::Result<()> {
387 let state = self.state.lock().expect("live state mutex poisoned");
388 if !state.started {
389 return Ok(());
390 }
391 if state.live_id.is_none() {
392 return Ok(());
394 }
395 drop(state);
396 let mut console = self.console.lock().expect("console mutex poisoned");
397 sync_terminal_size(&mut console);
398 console.print(&Control::new(), None, None, None, false, "")
399 }
400
401 pub fn print<R: Renderable + ?Sized>(
402 &self,
403 renderable: &R,
404 style: Option<Style>,
405 justify: Option<JustifyMethod>,
406 overflow: Option<OverflowMethod>,
407 no_wrap: bool,
408 end: &str,
409 ) -> io::Result<()> {
410 let mut console = self.console.lock().expect("console mutex poisoned");
411 console.print(renderable, style, justify, overflow, no_wrap, end)
412 }
413
414 pub fn log<R: Renderable + ?Sized>(
415 &self,
416 renderable: &R,
417 file: Option<&str>,
418 line: Option<u32>,
419 ) -> io::Result<()> {
420 let mut console = self.console.lock().expect("console mutex poisoned");
421 console.log(renderable, file, line)
422 }
423
424 fn spawn_refresh_thread(&mut self) {
425 if self.refresh_thread.is_some() {
426 return;
427 }
428
429 self.stop_flag.store(false, Ordering::SeqCst);
430 let stop_flag = self.stop_flag.clone();
431 let console = self.console.clone();
432 let state = self.state.clone();
433 let get_renderable = self.get_renderable.clone();
434 let refresh_per_second = state
435 .lock()
436 .expect("live state mutex poisoned")
437 .options
438 .refresh_per_second;
439
440 let handle = thread::spawn(move || {
441 let sleep = Duration::from_secs_f64(1.0 / refresh_per_second.max(0.001));
442 while !stop_flag.load(Ordering::SeqCst) {
443 thread::sleep(sleep);
444 if stop_flag.load(Ordering::SeqCst) {
445 break;
446 }
447 let state_guard = match state.lock() {
448 Ok(g) => g,
449 Err(_) => break,
450 };
451 if !state_guard.started {
452 continue;
453 }
454 let live_id = state_guard.live_id;
455 drop(state_guard);
456
457 if let Some(ref get_renderable) = get_renderable {
459 if let Some(id) = live_id {
460 let renderable = get_renderable();
461 let mut console_guard = match console.lock() {
462 Ok(g) => g,
463 Err(_) => break,
464 };
465 console_guard.live_update(id, renderable);
466 sync_terminal_size(&mut console_guard);
467 let _ = console_guard.print(&Control::new(), None, None, None, false, "");
468 continue;
469 }
470 }
471
472 let mut console_guard = match console.lock() {
473 Ok(g) => g,
474 Err(_) => break,
475 };
476 sync_terminal_size(&mut console_guard);
477 let _ = console_guard.print(&Control::new(), None, None, None, false, "");
478 }
479 });
480
481 self.refresh_thread = Some(handle);
482 }
483
484 #[cfg(not(unix))]
485 fn start_redirects(&mut self, _options: &LiveOptions) -> io::Result<()> {
486 Ok(())
487 }
488
489 #[cfg(unix)]
490 fn start_redirects(&mut self, options: &LiveOptions) -> io::Result<()> {
491 if options.redirect_stdout {
492 self.start_redirect_stream(1)?;
493 }
494 if options.redirect_stderr {
495 self.start_redirect_stream(2)?;
496 }
497 Ok(())
498 }
499
500 #[cfg(not(unix))]
501 fn stop_redirects(&mut self) {}
502
503 #[cfg(unix)]
504 fn stop_redirects(&mut self) {
505 for redirect in self.redirects.drain(..) {
506 let _guard = stream_redirect_lock()
507 .lock()
508 .expect("redirect lock mutex poisoned");
509 let _ = unsafe { dup2(redirect.original_fd, redirect.target_fd) };
510 let _ = unsafe { close(redirect.pipe_write_fd) };
511 let _ = unsafe { close(redirect.original_fd) };
512 drop(_guard);
513 let _ = redirect.worker.join();
514 }
515 }
516
517 #[cfg(unix)]
518 fn start_redirect_stream(&mut self, target_fd: RawFd) -> io::Result<()> {
519 let mut fds = [0_i32; 2];
520 if unsafe { pipe(fds.as_mut_ptr()) } == -1 {
521 return Err(io::Error::last_os_error());
522 }
523 let read_fd = fds[0];
524 let write_fd = fds[1];
525
526 let original_fd = unsafe { dup(target_fd) };
527 if original_fd == -1 {
528 let _ = unsafe { close(read_fd) };
529 let _ = unsafe { close(write_fd) };
530 return Err(io::Error::last_os_error());
531 }
532
533 if unsafe { dup2(write_fd, target_fd) } == -1 {
534 let _ = unsafe { close(read_fd) };
535 let _ = unsafe { close(write_fd) };
536 let _ = unsafe { close(original_fd) };
537 return Err(io::Error::last_os_error());
538 }
539
540 let console = self.console.clone();
541 let worker = thread::spawn(move || {
542 let file = unsafe { File::from_raw_fd(read_fd) };
543 let mut reader = BufReader::new(file);
544 let mut buf = Vec::<u8>::new();
545
546 loop {
547 buf.clear();
548 let bytes = match reader.read_until(b'\n', &mut buf) {
549 Ok(n) => n,
550 Err(_) => break,
551 };
552 if bytes == 0 {
553 break;
554 }
555
556 let has_newline = buf.last().copied() == Some(b'\n');
557 let text_slice = if has_newline {
558 &buf[..buf.len().saturating_sub(1)]
559 } else {
560 &buf[..]
561 };
562 if text_slice.is_empty() && has_newline {
563 continue;
564 }
565 let text = String::from_utf8_lossy(text_slice).to_string();
566 let end = if has_newline { "\n" } else { "" };
567
568 let _guard = stream_redirect_lock()
569 .lock()
570 .expect("redirect lock mutex poisoned");
571 if unsafe { dup2(original_fd, target_fd) } == -1 {
572 break;
573 }
574 {
575 let mut guard = match console.lock() {
576 Ok(g) => g,
577 Err(_) => break,
578 };
579 let _ = guard.print(&Text::plain(text), None, None, None, false, end);
580 }
581 if unsafe { dup2(write_fd, target_fd) } == -1 {
582 break;
583 }
584 }
585 });
586
587 self.redirects.push(StreamRedirect {
588 target_fd,
589 original_fd,
590 pipe_write_fd: write_fd,
591 worker,
592 });
593 Ok(())
594 }
595}
596
597impl Drop for Live {
598 fn drop(&mut self) {
599 let _ = self.stop();
601 }
602}
603
604fn sync_terminal_size(console: &mut Console<Stdout>) {
605 if !console.is_terminal() {
606 return;
607 }
608 if let Ok((w, h)) = terminal::size() {
609 let w = w as usize;
610 let h = h as usize;
611 let opts = console.options_mut();
612 opts.size = (w, h);
613 opts.max_width = w.max(1);
614 opts.max_height = h;
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn test_refresh_per_second_accessor() {
624 let live = Live::with_options(
625 Box::new(Text::plain("x")),
626 LiveOptions {
627 refresh_per_second: 7.5,
628 ..Default::default()
629 },
630 );
631 assert_eq!(live.refresh_per_second(), 7.5);
632 }
633
634 #[cfg(unix)]
635 fn redirect_test_lock() -> &'static Mutex<()> {
636 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
637 LOCK.get_or_init(|| Mutex::new(()))
638 }
639
640 #[cfg(unix)]
641 #[test]
642 fn test_redirect_stdout_lifecycle() {
643 let _guard = redirect_test_lock()
644 .lock()
645 .expect("redirect test lock poisoned");
646 let mut live = Live::with_options(
647 Box::new(Text::plain("x")),
648 LiveOptions {
649 redirect_stdout: true,
650 ..Default::default()
651 },
652 );
653 let options = LiveOptions {
654 redirect_stdout: true,
655 ..Default::default()
656 };
657 live.start_redirects(&options).unwrap();
658 assert_eq!(live.redirects.len(), 1);
659 live.stop_redirects();
660 assert!(live.redirects.is_empty());
661 }
662}