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}