Skip to main content

rich/
live.rs

1//! Live (in-place updating) displays.
2//!
3//! Port of the core of `rich/live.py`. A [`Live`] drives a [`LiveRender`],
4//! writing the control codes to redraw a renderable in place as it changes:
5//! `start` hides the cursor and draws, `update`/`refresh` reposition the cursor
6//! and redraw, and `stop` commits the final render and restores the cursor.
7//!
8//! Scope: the deterministic manual-refresh path (the byte stream is byte-parity
9//! with upstream's `auto_refresh=False`, `transient=False` Live), plus a
10//! background **auto-refresh thread** ([`Live::spawn`] → [`AutoLive`]) that
11//! redraws on an interval like upstream's `refresh_per_second`. The
12//! alt-screen/transient modes and IO redirection remain deferred (see the
13//! Live/progress issue).
14
15use std::io::Write;
16use std::sync::mpsc::{self, RecvTimeoutError};
17use std::thread::{self, JoinHandle};
18use std::time::Duration;
19
20use crate::console::Console;
21use crate::control::Control;
22use crate::live_render::LiveRender;
23use crate::protocol::Renderable;
24
25/// An in-place updating display over a renderable. Mirrors `rich.live.Live`
26/// (manual-refresh subset). Output is written to a generic sink `W`, so it can
27/// target stdout or, in tests, a byte buffer.
28pub struct Live<W: Write> {
29    live_render: LiveRender,
30    console: Console,
31    writer: W,
32    started: bool,
33}
34
35impl<W: Write> Live<W> {
36    /// Create a live display for `renderable`, rendering with `console` and
37    /// writing control/output bytes to `writer`.
38    pub fn new(renderable: Box<dyn Renderable>, console: Console, writer: W) -> Self {
39        Live {
40            live_render: LiveRender::new(renderable),
41            console,
42            writer,
43            started: false,
44        }
45    }
46
47    /// Begin the live display: hide the cursor and draw the first frame.
48    pub fn start(&mut self) {
49        if self.started {
50            return;
51        }
52        self.started = true;
53        let _ = write!(self.writer, "{}", Control::show_cursor(false).as_str());
54        self.refresh();
55    }
56
57    /// Swap in a new renderable and redraw in place.
58    pub fn update(&mut self, renderable: Box<dyn Renderable>) {
59        self.live_render.set_renderable(renderable);
60        self.refresh();
61    }
62
63    /// Redraw the current renderable in place (reposition over the last frame,
64    /// then render).
65    pub fn refresh(&mut self) {
66        // `position_cursor` uses the *previous* frame's shape; rendering then
67        // updates the shape for next time.
68        let position = self.live_render.position_cursor();
69        let content = self.console.render_to_string(&self.live_render);
70        let _ = write!(self.writer, "{}{}", position.as_str(), content);
71    }
72
73    /// Commit the final frame (with a trailing newline) and show the cursor.
74    pub fn stop(&mut self) {
75        if !self.started {
76            return;
77        }
78        let position = self.live_render.position_cursor();
79        let content = self.console.render_to_string(&self.live_render);
80        let _ = write!(
81            self.writer,
82            "{}{}\n{}",
83            position.as_str(),
84            content,
85            Control::show_cursor(true).as_str()
86        );
87        self.started = false;
88    }
89
90    /// The output sink (for inspecting captured bytes in tests).
91    pub fn writer(&self) -> &W {
92        &self.writer
93    }
94
95    /// Consume the display, returning its output sink.
96    pub fn into_writer(self) -> W {
97        self.writer
98    }
99}
100
101/// A message from an [`AutoLive`] handle to its background refresh thread.
102enum LiveMessage {
103    /// Swap in a new renderable and redraw.
104    Update(Box<dyn Renderable + Send>),
105    /// Redraw the current renderable now.
106    Refresh,
107    /// Commit the final frame, restore the cursor, and stop the thread.
108    Stop,
109}
110
111impl<W: Write + Send + 'static> Live<W> {
112    /// Start an auto-refreshing live display on a background thread: it draws the
113    /// first frame, then redraws every `1/refresh_per_second` seconds (and
114    /// immediately on each [`AutoLive::update`]). Mirrors upstream's
115    /// `auto_refresh`/`refresh_per_second`. The `renderable`, `console`, and
116    /// `writer` are moved into the thread, so all three must be `Send`.
117    pub fn spawn(
118        renderable: Box<dyn Renderable + Send>,
119        console: Console,
120        writer: W,
121        refresh_per_second: f64,
122    ) -> AutoLive<W> {
123        let (sender, receiver) = mpsc::channel::<LiveMessage>();
124        let interval = Duration::from_secs_f64(1.0 / refresh_per_second.max(f64::MIN_POSITIVE));
125        let handle = thread::spawn(move || {
126            // The `Live` (and its non-`Send` `LiveRender`) is built and owned
127            // entirely within this thread — only the `Send` inputs cross over.
128            let mut live = Live::new(renderable, console, writer);
129            live.start();
130            loop {
131                match receiver.recv_timeout(interval) {
132                    Ok(LiveMessage::Update(renderable)) => live.update(renderable),
133                    Ok(LiveMessage::Refresh) | Err(RecvTimeoutError::Timeout) => live.refresh(),
134                    // Stop, or the handle was dropped: finalize and exit.
135                    Ok(LiveMessage::Stop) | Err(RecvTimeoutError::Disconnected) => {
136                        live.stop();
137                        break;
138                    }
139                }
140            }
141            live.into_writer()
142        });
143        AutoLive {
144            sender,
145            handle: Some(handle),
146        }
147    }
148}
149
150/// A handle to an auto-refreshing [`Live`] running on a background thread.
151/// Dropping the handle (or calling [`stop`](Self::stop)) finalizes the display.
152pub struct AutoLive<W: Write + Send + 'static> {
153    sender: mpsc::Sender<LiveMessage>,
154    handle: Option<JoinHandle<W>>,
155}
156
157impl<W: Write + Send + 'static> AutoLive<W> {
158    /// Swap in a new renderable; the thread redraws it promptly.
159    pub fn update(&self, renderable: Box<dyn Renderable + Send>) {
160        let _ = self.sender.send(LiveMessage::Update(renderable));
161    }
162
163    /// Ask the thread to redraw the current renderable now.
164    pub fn refresh(&self) {
165        let _ = self.sender.send(LiveMessage::Refresh);
166    }
167
168    /// Commit the final frame, join the thread, and return the output sink.
169    pub fn stop(mut self) -> W {
170        let _ = self.sender.send(LiveMessage::Stop);
171        self.handle
172            .take()
173            .expect("thread handle present until stop/drop")
174            .join()
175            .expect("live refresh thread panicked")
176    }
177}
178
179impl<W: Write + Send + 'static> Drop for AutoLive<W> {
180    fn drop(&mut self) {
181        // If the caller didn't `stop()`, still finalize + join the thread.
182        if let Some(handle) = self.handle.take() {
183            let _ = self.sender.send(LiveMessage::Stop);
184            let _ = handle.join();
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::color::ColorSystem;
193    use crate::text::Text;
194
195    fn console() -> Console {
196        Console::builder()
197            .force_terminal(true)
198            .color_system(Some(ColorSystem::Truecolor))
199            .width(20)
200            .no_color(false)
201            .build()
202    }
203
204    #[test]
205    fn manual_refresh_stream_matches_upstream() {
206        let mut live = Live::new(
207            Box::new(Text::new("frame one")),
208            console(),
209            Vec::<u8>::new(),
210        );
211        live.start();
212        live.update(Box::new(Text::new("frame two")));
213        live.update(Box::new(Text::new("frame three")));
214        live.stop();
215
216        // Captured verbatim from real rich 15.0.0 (auto_refresh=False,
217        // transient=False, width 20) writing to a StringIO.
218        let expected = "\x1b[?25lframe one\r\x1b[2Kframe two\r\x1b[2Kframe three\r\x1b[2Kframe three\n\x1b[?25h";
219        assert_eq!(String::from_utf8(live.writer().clone()).unwrap(), expected);
220    }
221
222    #[test]
223    fn auto_refresh_thread_produces_the_same_stream() {
224        // A very low refresh rate (10s interval) means no timeout-driven refresh
225        // fires during the test, so the thread processes exactly start + the two
226        // updates + stop, in order — the identical byte-parity stream, now driven
227        // through the background thread (spawn / channel / join).
228        let auto = Live::spawn(
229            Box::new(Text::new("frame one")),
230            console(),
231            Vec::<u8>::new(),
232            0.1,
233        );
234        auto.update(Box::new(Text::new("frame two")));
235        auto.update(Box::new(Text::new("frame three")));
236        let output = auto.stop();
237
238        let expected = "\x1b[?25lframe one\r\x1b[2Kframe two\r\x1b[2Kframe three\r\x1b[2Kframe three\n\x1b[?25h";
239        assert_eq!(String::from_utf8(output).unwrap(), expected);
240    }
241
242    #[test]
243    fn dropping_the_handle_finalizes_the_display() {
244        // Even without an explicit stop(), Drop commits the final frame + restores
245        // the cursor (the trailing "\n" + show-cursor), so no display is left open.
246        let console = console();
247        // Route through a shared buffer so we can inspect it after the drop.
248        let auto = Live::spawn(Box::new(Text::new("only")), console, Vec::<u8>::new(), 0.1);
249        drop(auto); // no explicit stop
250                    // If Drop didn't join the thread, this test would still pass
251                    // but leak the thread; the assertion is simply that drop
252                    // returns without panicking / deadlocking.
253    }
254}