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