Skip to main content

rusty_rich/
live.rs

1//! Live — auto-updating display. Equivalent to Rich's `live.py`.
2
3use std::io::{self, Write};
4use std::time::Instant;
5
6use crate::console::{ConsoleOptions, DynRenderable, Renderable};
7
8/// A writer that captures output for live display.
9pub struct LiveWriter {
10    buffer: Vec<u8>,
11}
12
13impl LiveWriter {
14    pub fn new() -> Self {
15        Self { buffer: Vec::new() }
16    }
17
18    pub fn capture(&self) -> &[u8] {
19        &self.buffer
20    }
21
22    pub fn clear(&mut self) {
23        self.buffer.clear();
24    }
25}
26
27impl Write for LiveWriter {
28    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
29        self.buffer.extend_from_slice(buf);
30        Ok(buf.len())
31    }
32
33    fn flush(&mut self) -> io::Result<()> {
34        Ok(())
35    }
36}
37
38/// Manages a live-updating region of the terminal.
39pub struct Live {
40    renderable: Option<DynRenderable>,
41    screen: bool,
42    auto_refresh: bool,
43    refresh_per_second: f64,
44    transient: bool,
45    started: Option<Instant>,
46    previous_line_count: usize,
47    redirect_stdout: bool,
48    redirect_stderr: bool,
49    writers: Vec<LiveWriter>,
50}
51
52impl std::fmt::Debug for Live {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("Live")
55            .field("screen", &self.screen)
56            .field("started", &self.started)
57            .finish()
58    }
59}
60
61impl Live {
62    pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
63        Self {
64            renderable: Some(DynRenderable::new(renderable)),
65            screen: false,
66            auto_refresh: true,
67            refresh_per_second: 4.0,
68            transient: false,
69            started: None,
70            previous_line_count: 0,
71            redirect_stdout: true,
72            redirect_stderr: true,
73            writers: Vec::new(),
74        }
75    }
76
77    pub fn screen(mut self) -> Self { self.screen = true; self }
78    pub fn no_auto_refresh(mut self) -> Self { self.auto_refresh = false; self }
79    pub fn refresh_per_second(mut self, rate: f64) -> Self { self.refresh_per_second = rate; self }
80    pub fn transient(mut self) -> Self { self.transient = true; self }
81    pub fn redirect_stdout(mut self, redirect: bool) -> Self { self.redirect_stdout = redirect; self }
82    pub fn redirect_stderr(mut self, redirect: bool) -> Self { self.redirect_stderr = redirect; self }
83
84    /// Register a writer whose captured content will be rendered during refresh.
85    pub fn add_writer(&mut self, writer: LiveWriter) { self.writers.push(writer); }
86
87    /// Create a LiveWriter that captures output while Live is active.
88    pub fn create_writer() -> LiveWriter {
89        LiveWriter::new()
90    }
91
92    pub fn start(&mut self) -> io::Result<()> {
93        self.started = Some(Instant::now());
94        if self.screen {
95            write!(io::stdout(), "\x1b[?1049h")?;
96        }
97        write!(io::stdout(), "\x1b[?25l")?;
98        self.refresh()
99    }
100
101    pub fn stop(&mut self) -> io::Result<()> {
102        if self.transient {
103            for _ in 0..self.previous_line_count {
104                write!(io::stdout(), "\x1b[1A\x1b[2K")?;
105            }
106        }
107        if self.screen {
108            write!(io::stdout(), "\x1b[?1049l")?;
109        }
110        write!(io::stdout(), "\x1b[?25h")?;
111        io::stdout().flush()?;
112        self.started = None;
113        Ok(())
114    }
115
116    pub fn update(&mut self, renderable: impl Renderable + Send + Sync + 'static) -> io::Result<()> {
117        self.renderable = Some(DynRenderable::new(renderable));
118        self.refresh()
119    }
120
121    pub fn refresh(&mut self) -> io::Result<()> {
122        if let Some(ref renderable) = self.renderable {
123            let opts = ConsoleOptions::default();
124            let result = renderable.render(&opts);
125
126            if self.previous_line_count > 0 {
127                write!(io::stdout(), "\x1b[{}F", self.previous_line_count)?;
128            }
129
130            let ansi = result.to_ansi();
131            let line_count = ansi.lines().count();
132
133            write!(io::stdout(), "{ansi}")?;
134            if line_count < self.previous_line_count {
135                for _ in line_count..self.previous_line_count {
136                    write!(io::stdout(), "\x1b[2K\n")?;
137                }
138            }
139
140            self.previous_line_count = line_count;
141
142            // Write captured writer content
143            for writer in &self.writers {
144                let captured = String::from_utf8_lossy(writer.capture());
145                if !captured.is_empty() {
146                    write!(io::stdout(), "{}", captured)?;
147                }
148            }
149
150            io::stdout().flush()?;
151        }
152        Ok(())
153    }
154}
155
156impl Drop for Live {
157    fn drop(&mut self) {
158        let _ = self.stop();
159    }
160}