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`, and
12//! `transient` displays that erase themselves on stop. The alt-screen mode and
13//! IO redirection remain deferred (see the Live/progress issue).
14
15use std::any::Any;
16use std::io::Write;
17use std::panic::{self, AssertUnwindSafe};
18use std::sync::mpsc::{self, RecvTimeoutError};
19use std::thread::{self, JoinHandle};
20use std::time::Duration;
21
22use crate::console::Console;
23use crate::control::Control;
24use crate::live_render::LiveRender;
25use crate::protocol::Renderable;
26
27/// An in-place updating display over a renderable. Mirrors `rich.live.Live`
28/// (manual-refresh subset). Output is written to a generic sink `W`, so it can
29/// target stdout or, in tests, a byte buffer.
30pub struct Live<W: Write> {
31    live_render: LiveRender,
32    console: Console,
33    writer: W,
34    started: bool,
35    transient: bool,
36}
37
38impl<W: Write> Live<W> {
39    /// Create a live display for `renderable`, rendering with `console` and
40    /// writing control/output bytes to `writer`.
41    pub fn new(renderable: Box<dyn Renderable>, console: Console, writer: W) -> Self {
42        Live {
43            live_render: LiveRender::new(renderable),
44            console,
45            writer,
46            started: false,
47            transient: false,
48        }
49    }
50
51    /// Clear the display when it stops (upstream `transient`): the final
52    /// frame is drawn, then erased and the cursor put back where it started.
53    pub fn transient(mut self, transient: bool) -> Self {
54        self.transient = transient;
55        self
56    }
57
58    /// Begin the live display: hide the cursor and draw the first frame.
59    pub fn start(&mut self) {
60        if self.started {
61            return;
62        }
63        self.started = true;
64        if self.console.is_terminal() {
65            let _ = write!(self.writer, "{}", Control::show_cursor(false).as_str());
66        }
67        self.refresh();
68    }
69
70    /// Swap in a new renderable and redraw in place.
71    pub fn update(&mut self, renderable: Box<dyn Renderable>) {
72        self.live_render.set_renderable(renderable);
73        self.refresh();
74    }
75
76    /// Redraw the current renderable in place (reposition over the last frame,
77    /// then render).
78    pub fn refresh(&mut self) {
79        if !self.console.is_terminal() {
80            return;
81        }
82        // `position_cursor` uses the *previous* frame's shape; rendering then
83        // updates the shape for next time.
84        let position = self.live_render.position_cursor();
85        let content = self.console.render_to_string(&self.live_render);
86        let _ = write!(self.writer, "{}{}", position.as_str(), content);
87    }
88
89    /// Commit the final frame (with a trailing newline) and show the cursor.
90    pub fn stop(&mut self) {
91        if !self.started {
92            return;
93        }
94        if !self.console.is_terminal() {
95            // Upstream prints the final result for files, with no newline,
96            // only when it is not transient.
97            if !self.transient {
98                let content = self.console.render_to_string(&self.live_render);
99                let _ = write!(self.writer, "{content}");
100            }
101            self.started = false;
102            return;
103        }
104        let position = self.live_render.position_cursor();
105        let content = self.console.render_to_string(&self.live_render);
106        // Upstream ends the display with `console.line()` only when the last
107        // render drew something (`_live_render.last_render_height`).
108        let newline = if self.live_render.last_render_height() > 0 {
109            "\n"
110        } else {
111            ""
112        };
113        let _ = write!(
114            self.writer,
115            "{}{}{newline}{}",
116            position.as_str(),
117            content,
118            Control::show_cursor(true).as_str()
119        );
120        if self.transient {
121            let _ = write!(
122                self.writer,
123                "{}",
124                self.live_render.restore_cursor().as_str()
125            );
126        }
127        self.started = false;
128    }
129
130    /// Restore the terminal after a render panicked mid-display: end the
131    /// partial frame's line and show the cursor, as the `finally` of
132    /// upstream's `stop()` does whatever its refresh raised.
133    fn abort(&mut self) {
134        if !self.started {
135            return;
136        }
137        self.started = false;
138        if self.console.is_terminal() {
139            let _ = write!(self.writer, "\n{}", Control::show_cursor(true).as_str());
140            let _ = self.writer.flush();
141        }
142    }
143
144    /// The output sink (for inspecting captured bytes in tests).
145    pub fn writer(&self) -> &W {
146        &self.writer
147    }
148
149    /// Consume the display, returning its output sink.
150    pub fn into_writer(self) -> W {
151        self.writer
152    }
153}
154
155/// A message from an [`AutoLive`] handle to its background refresh thread.
156enum LiveMessage {
157    /// Swap in a new renderable and redraw.
158    Update(Box<dyn Renderable + Send>),
159    /// Redraw the current renderable now.
160    Refresh,
161    /// Redraw now, then acknowledge on the channel.
162    RefreshAck(mpsc::Sender<()>),
163    /// Commit the final frame, restore the cursor, and stop the thread.
164    Stop,
165}
166
167impl<W: Write + Send + 'static> Live<W> {
168    /// Start an auto-refreshing live display on a background thread: it draws the
169    /// first frame, then redraws every `1/refresh_per_second` seconds (and
170    /// immediately on each [`AutoLive::update`]). Mirrors upstream's
171    /// `auto_refresh`/`refresh_per_second`. The `renderable`, `console`, and
172    /// `writer` are moved into the thread, so all three must be `Send`.
173    pub fn spawn(
174        renderable: Box<dyn Renderable + Send>,
175        console: Console,
176        writer: W,
177        refresh_per_second: f64,
178    ) -> AutoLive<W> {
179        Live::spawn_with(renderable, console, writer, refresh_per_second, false)
180    }
181
182    /// [`spawn`](Self::spawn), clearing the display when it stops when
183    /// `transient` is set (upstream `Live(transient=True)`).
184    pub fn spawn_with(
185        renderable: Box<dyn Renderable + Send>,
186        console: Console,
187        writer: W,
188        refresh_per_second: f64,
189        transient: bool,
190    ) -> AutoLive<W> {
191        let (sender, receiver) = mpsc::channel::<LiveMessage>();
192        let (started, wait_started) = mpsc::channel::<()>();
193        let interval = Duration::from_secs_f64(1.0 / refresh_per_second.max(f64::MIN_POSITIVE));
194        let handle = thread::spawn(move || {
195            // The `Live` (and its non-`Send` `LiveRender`) is built and owned
196            // entirely within this thread — only the `Send` inputs cross over.
197            let mut live = Live::new(renderable, console, writer).transient(transient);
198            // A panicking render must not take the writer down with it:
199            // upstream's `stop()` restores the terminal in a `finally`, so the
200            // failure is caught here, the cursor shown again, and the panic
201            // handed back to `try_stop` instead of being re-raised by `join`.
202            let outcome = panic::catch_unwind(AssertUnwindSafe(|| {
203                live.start();
204                let _ = started.send(());
205                loop {
206                    match receiver.recv_timeout(interval) {
207                        Ok(LiveMessage::Update(renderable)) => live.update(renderable),
208                        Ok(LiveMessage::Refresh) | Err(RecvTimeoutError::Timeout) => live.refresh(),
209                        Ok(LiveMessage::RefreshAck(done)) => {
210                            live.refresh();
211                            let _ = done.send(());
212                        }
213                        // Stop, or the handle was dropped: finalize and exit.
214                        Ok(LiveMessage::Stop) | Err(RecvTimeoutError::Disconnected) => {
215                            live.stop();
216                            break;
217                        }
218                    }
219                }
220            }));
221            let failure = outcome.err().map(|payload| {
222                live.abort();
223                panic_message(payload.as_ref())
224            });
225            (live.into_writer(), failure)
226        });
227        // Upstream's `Live.start()` draws the first frame before returning;
228        // without this wait, a change made right after spawning could land in
229        // the "first" frame, depending on thread scheduling.
230        let _ = wait_started.recv();
231        AutoLive {
232            sender,
233            handle: Some(handle),
234        }
235    }
236}
237
238/// A handle to an auto-refreshing [`Live`] running on a background thread.
239/// Dropping the handle (or calling [`stop`](Self::stop)) finalizes the display.
240pub struct AutoLive<W: Write + Send + 'static> {
241    sender: mpsc::Sender<LiveMessage>,
242    handle: Option<JoinHandle<(W, Option<String>)>>,
243}
244
245/// A render that panicked on an [`AutoLive`] refresh thread, returned by
246/// [`AutoLive::try_stop`]. The terminal was restored before the thread exited.
247#[derive(Debug)]
248pub struct LivePanic<W> {
249    /// The output sink, with everything written up to and including the
250    /// cursor restore.
251    pub writer: W,
252    /// The panic message (`"<non-string panic payload>"` when it had none).
253    pub message: String,
254}
255
256/// The message of a caught panic payload.
257fn panic_message(payload: &(dyn Any + Send)) -> String {
258    if let Some(message) = payload.downcast_ref::<&str>() {
259        (*message).to_string()
260    } else if let Some(message) = payload.downcast_ref::<String>() {
261        message.clone()
262    } else {
263        "<non-string panic payload>".to_string()
264    }
265}
266
267impl<W: Write + Send + 'static> AutoLive<W> {
268    /// Swap in a new renderable; the thread redraws it promptly.
269    pub fn update(&self, renderable: Box<dyn Renderable + Send>) {
270        let _ = self.sender.send(LiveMessage::Update(renderable));
271    }
272
273    /// Ask the thread to redraw the current renderable now.
274    pub fn refresh(&self) {
275        let _ = self.sender.send(LiveMessage::Refresh);
276    }
277
278    /// Redraw now and wait until the frame is written, as upstream's
279    /// `Live.refresh()` renders in the caller's thread. A renderable that reads
280    /// shared state then shows the state as of this call.
281    pub fn refresh_wait(&self) {
282        let (done, wait) = mpsc::channel();
283        if self.sender.send(LiveMessage::RefreshAck(done)).is_ok() {
284            let _ = wait.recv();
285        }
286    }
287
288    /// Commit the final frame, join the thread, and return the output sink.
289    ///
290    /// If a render panicked on the refresh thread, the cursor has already been
291    /// shown again and the failure is swallowed (the panic hook reported it
292    /// when it happened); use [`try_stop`](Self::try_stop) to observe it.
293    pub fn stop(self) -> W {
294        match self.try_stop() {
295            Ok(writer) => writer,
296            Err(failure) => failure.writer,
297        }
298    }
299
300    /// [`stop`](Self::stop), reporting a render that panicked on the refresh
301    /// thread as an error that still carries the output sink.
302    pub fn try_stop(mut self) -> Result<W, LivePanic<W>> {
303        let _ = self.sender.send(LiveMessage::Stop);
304        let handle = self
305            .handle
306            .take()
307            .expect("thread handle present until stop/drop");
308        // The thread catches every panic from rendering, so a join error
309        // cannot carry a writer; there is nothing to return but the payload.
310        let (writer, failure) = match handle.join() {
311            Ok(result) => result,
312            Err(payload) => panic::resume_unwind(payload),
313        };
314        match failure {
315            None => Ok(writer),
316            Some(message) => Err(LivePanic { writer, message }),
317        }
318    }
319}
320
321impl<W: Write + Send + 'static> Drop for AutoLive<W> {
322    fn drop(&mut self) {
323        // If the caller didn't `stop()`, still finalize + join the thread.
324        if let Some(handle) = self.handle.take() {
325            let _ = self.sender.send(LiveMessage::Stop);
326            let _ = handle.join();
327        }
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::color::ColorSystem;
335    use crate::text::Text;
336
337    fn console() -> Console {
338        Console::builder()
339            .force_terminal(true)
340            .color_system(Some(ColorSystem::Truecolor))
341            .width(20)
342            .no_color(false)
343            .build()
344    }
345
346    #[test]
347    fn redirected_live_emits_only_the_final_frame() {
348        let console = Console::builder().force_terminal(false).width(20).build();
349        let mut live = Live::new(Box::new(Text::new("first")), console, Vec::<u8>::new());
350        live.start();
351        live.update(Box::new(Text::new("last")));
352        live.refresh();
353        assert!(live.writer().is_empty());
354        live.stop();
355        assert_eq!(live.writer(), b"last");
356    }
357
358    #[test]
359    fn manual_refresh_stream_matches_upstream() {
360        let mut live = Live::new(
361            Box::new(Text::new("frame one")),
362            console(),
363            Vec::<u8>::new(),
364        );
365        live.start();
366        live.update(Box::new(Text::new("frame two")));
367        live.update(Box::new(Text::new("frame three")));
368        live.stop();
369
370        // Captured verbatim from real rich 15.0.0 (auto_refresh=False,
371        // transient=False, width 20) writing to a StringIO.
372        let expected = "\x1b[?25lframe one\r\x1b[2Kframe two\r\x1b[2Kframe three\r\x1b[2Kframe three\n\x1b[?25h";
373        assert_eq!(String::from_utf8(live.writer().clone()).unwrap(), expected);
374    }
375
376    #[test]
377    fn auto_refresh_thread_produces_the_same_stream() {
378        // A very low refresh rate (10s interval) means no timeout-driven refresh
379        // fires during the test, so the thread processes exactly start + the two
380        // updates + stop, in order — the identical byte-parity stream, now driven
381        // through the background thread (spawn / channel / join).
382        let auto = Live::spawn(
383            Box::new(Text::new("frame one")),
384            console(),
385            Vec::<u8>::new(),
386            0.1,
387        );
388        auto.update(Box::new(Text::new("frame two")));
389        auto.update(Box::new(Text::new("frame three")));
390        let output = auto.stop();
391
392        let expected = "\x1b[?25lframe one\r\x1b[2Kframe two\r\x1b[2Kframe three\r\x1b[2Kframe three\n\x1b[?25h";
393        assert_eq!(String::from_utf8(output).unwrap(), expected);
394    }
395
396    /// Renders `ok` until `fail_after` renders have happened, then panics.
397    struct PanicsLater {
398        renders: std::sync::Arc<std::sync::atomic::AtomicUsize>,
399        fail_after: usize,
400    }
401
402    impl Renderable for PanicsLater {
403        fn rich_render(
404            &self,
405            console: &Console,
406            options: &crate::console::ConsoleOptions,
407        ) -> Vec<crate::segment::Segment> {
408            use std::sync::atomic::Ordering;
409            if self.renders.fetch_add(1, Ordering::SeqCst) >= self.fail_after {
410                panic!("render failed");
411            }
412            Text::new("ok").rich_render(console, options)
413        }
414    }
415
416    #[test]
417    fn a_panicking_render_still_restores_the_cursor() {
418        // Upstream's `stop()` shows the cursor again in a `finally`, whatever
419        // the refresh raised. A render that panics on the refresh thread must
420        // neither leave the cursor hidden nor make `stop()` panic in turn.
421        let renders = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
422        let auto = Live::spawn(
423            Box::new(PanicsLater {
424                renders: renders.clone(),
425                fail_after: 1,
426            }),
427            console(),
428            Vec::<u8>::new(),
429            0.1,
430        );
431        auto.refresh_wait(); // panics on the thread; must not hang here
432        let (output, error) = match auto.try_stop() {
433            Ok(output) => (output, None),
434            Err(failure) => (failure.writer, Some(failure.message)),
435        };
436        let output = String::from_utf8(output).unwrap();
437        assert!(output.starts_with("\x1b[?25lok"), "{output:?}");
438        assert!(
439            output.ends_with("\x1b[?25h"),
440            "cursor left hidden: {output:?}"
441        );
442        assert_eq!(error.as_deref(), Some("render failed"));
443
444        // `stop()` swallows the failure after restoring the terminal.
445        let auto = Live::spawn(
446            Box::new(PanicsLater {
447                renders: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
448                fail_after: 1,
449            }),
450            console(),
451            Vec::<u8>::new(),
452            0.1,
453        );
454        auto.refresh();
455        let output = String::from_utf8(auto.stop()).unwrap();
456        assert!(
457            output.ends_with("\x1b[?25h"),
458            "cursor left hidden: {output:?}"
459        );
460    }
461
462    #[test]
463    fn dropping_the_handle_finalizes_the_display() {
464        // Even without an explicit stop(), Drop commits the final frame + restores
465        // the cursor (the trailing "\n" + show-cursor), so no display is left open.
466        let console = console();
467        // Route through a shared buffer so we can inspect it after the drop.
468        let auto = Live::spawn(Box::new(Text::new("only")), console, Vec::<u8>::new(), 0.1);
469        drop(auto); // no explicit stop
470                    // If Drop didn't join the thread, this test would still pass
471                    // but leak the thread; the assertion is simply that drop
472                    // returns without panicking / deadlocking.
473    }
474}