Skip to main content

rusty_rich/
live.rs

1//! Live — auto-updating display. Equivalent to Rich's `live.py`.
2//!
3//! [`Live`] manages a terminal region that updates in-place. Each refresh
4//! overwrites the previous output, creating an auto-updating display.
5//!
6//! # Quick Example
7//!
8//! ```rust,no_run
9//! use rusty_rich::{Live, panel::Panel};
10//! use std::thread;
11//! use std::time::Duration;
12//!
13//! let mut live = Live::new(Panel::new("Loading...").title("Progress"));
14//! live.start().unwrap();
15//!
16//! for i in 0..=100 {
17//!     live.update(Panel::new(format!("{}%", i)).title("Progress")).unwrap();
18//!     thread::sleep(Duration::from_millis(50));
19//! }
20//!
21//! live.stop().unwrap();
22//! ```
23//!
24//! # LiveWriter
25//!
26//! [`LiveWriter`] captures `write!` output and displays it within the live
27//! region. Use [`Live::create_writer`] to create one, then write to it while
28//! the live display is active:
29//!
30//! ```rust,no_run
31//! use rusty_rich::{Live, panel::Panel};
32//! use std::io::Write;
33//!
34//! let mut live = Live::new(Panel::new("Status").title("App"));
35//! let mut writer = Live::create_writer();
36//! live.start().unwrap();
37//!
38//! writeln!(writer, "Processing item 1...").unwrap();
39//! writeln!(writer, "Done!").unwrap();
40//!
41//! live.stop().unwrap();
42//! ```
43//!
44//! # Transient Mode
45//!
46//! Call [`Live::transient`] to erase the live region on stop — the output
47//! disappears as if it was never there. Useful for "loading…" overlays.
48
49use std::io::{self, Write};
50use std::sync::{
51    atomic::{AtomicUsize, Ordering},
52    Arc, Mutex,
53};
54use std::time::Instant;
55
56use crate::console::{ConsoleOptions, DynRenderable, Renderable};
57use crate::segment::Segment;
58
59/// A writer that captures output for live display.
60#[derive(Default)]
61pub struct LiveWriter {
62    buffer: Vec<u8>,
63}
64
65impl LiveWriter {
66    /// Create a new `LiveWriter` with an empty capture buffer.
67    pub fn new() -> Self {
68        Self { buffer: Vec::new() }
69    }
70
71    /// Return a reference to the captured output bytes.
72    pub fn capture(&self) -> &[u8] {
73        &self.buffer
74    }
75
76    /// Clear the captured output buffer.
77    pub fn clear(&mut self) {
78        self.buffer.clear();
79    }
80}
81
82impl Write for LiveWriter {
83    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
84        self.buffer.extend_from_slice(buf);
85        Ok(buf.len())
86    }
87
88    fn flush(&mut self) -> io::Result<()> {
89        Ok(())
90    }
91}
92
93/// A hook that transforms render output.
94///
95/// [`RenderHook`] provides a way to intercept and modify the rendered
96/// segment lines before they are written to the terminal. Multiple hooks
97/// can be registered on a [`Live`] display and are applied in order.
98///
99/// # Example
100///
101/// ```rust,no_run
102/// use rusty_rich::live::RenderHook;
103/// use rusty_rich::Segment;
104///
105/// let hook = RenderHook::new(|lines| {
106///     // Reverse the order of displayed lines
107///     let mut reversed = lines.to_vec();
108///     reversed.reverse();
109///     reversed
110/// });
111/// ```
112/// Type alias for the render hook function to reduce type complexity.
113type RenderHookFn = dyn Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send;
114
115pub struct RenderHook {
116    hook: Box<RenderHookFn>,
117}
118
119impl RenderHook {
120    /// Create a new [`RenderHook`] with the given transformation function.
121    ///
122    /// The function receives the current rendered lines of segments and
123    /// returns the modified lines.
124    pub fn new<F>(hook: F) -> Self
125    where
126        F: Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send + 'static,
127    {
128        Self {
129            hook: Box::new(hook),
130        }
131    }
132
133    /// Apply this hook to the given segments, returning the transformed segments.
134    pub fn apply(&self, segments: &[Vec<Segment>]) -> Vec<Vec<Segment>> {
135        (self.hook)(segments)
136    }
137}
138
139/// Manages a live-updating region of the terminal.
140///
141/// Uses [`Arc`]`<`[`Mutex`]`<T>>` for interior mutability of shared state,
142/// making [`Live`] both [`Send`] and [`Sync`] so it can be safely shared
143/// across threads.
144pub struct Live {
145    renderable: Arc<Mutex<Option<DynRenderable>>>,
146    screen: bool,
147    auto_refresh: bool,
148    refresh_per_second: f64,
149    transient: bool,
150    started: bool,
151    started_at: Option<Instant>,
152    previous_line_count: Arc<AtomicUsize>,
153    redirect_stdout: bool,
154    redirect_stderr: bool,
155    writers: Arc<Mutex<Vec<LiveWriter>>>,
156    render_hooks: Arc<Mutex<Vec<RenderHook>>>,
157}
158
159impl std::fmt::Debug for Live {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        f.debug_struct("Live")
162            .field("screen", &self.screen)
163            .field("started", &self.started)
164            .finish()
165    }
166}
167
168impl Live {
169    /// Create a new `Live` display wrapping the given [`Renderable`].
170    pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
171        Self {
172            renderable: Arc::new(Mutex::new(Some(DynRenderable::new(renderable)))),
173            screen: false,
174            auto_refresh: true,
175            refresh_per_second: 4.0,
176            transient: false,
177            started: false,
178            started_at: None,
179            previous_line_count: Arc::new(AtomicUsize::new(0)),
180            redirect_stdout: true,
181            redirect_stderr: true,
182            writers: Arc::new(Mutex::new(Vec::new())),
183            render_hooks: Arc::new(Mutex::new(Vec::new())),
184        }
185    }
186
187    /// Builder: use the alternate screen buffer for full-screen display.
188    pub fn screen(mut self) -> Self {
189        self.screen = true;
190        self
191    }
192    /// Builder: disable automatic periodic refresh.
193    pub fn no_auto_refresh(mut self) -> Self {
194        self.auto_refresh = false;
195        self
196    }
197    /// Builder: set the refresh rate in Hz (default 4.0).
198    pub fn refresh_per_second(mut self, rate: f64) -> Self {
199        self.refresh_per_second = rate;
200        self
201    }
202    /// Builder: enable transient mode (live display disappears on stop).
203    pub fn transient(mut self) -> Self {
204        self.transient = true;
205        self
206    }
207    /// Builder: redirect stdout writes into the live display.
208    pub fn redirect_stdout(mut self, redirect: bool) -> Self {
209        self.redirect_stdout = redirect;
210        self
211    }
212    /// Builder: redirect stderr writes into the live display.
213    pub fn redirect_stderr(mut self, redirect: bool) -> Self {
214        self.redirect_stderr = redirect;
215        self
216    }
217
218    /// Register a writer whose captured content will be rendered during refresh.
219    pub fn add_writer(&mut self, writer: LiveWriter) {
220        self.writers.lock().unwrap().push(writer);
221    }
222
223    /// Create a LiveWriter that captures output while Live is active.
224    pub fn create_writer() -> LiveWriter {
225        LiveWriter::new()
226    }
227
228    /// Start the live display: enter alternate screen (if configured) and hide cursor.
229    pub fn start(&mut self) -> io::Result<()> {
230        self.started = true;
231        self.started_at = Some(Instant::now());
232        if self.screen {
233            write!(io::stdout(), "{}", crate::control::ALT_SCREEN_ENTER)?;
234        }
235        write!(io::stdout(), "{}", crate::control::CURSOR_HIDE)?;
236        self.refresh()
237    }
238
239    /// Stop the live display: restore cursor, exit alternate screen, and clean up.
240    pub fn stop(&mut self) -> io::Result<()> {
241        if self.transient {
242            let prev = self.previous_line_count.load(Ordering::Relaxed);
243            for _ in 0..prev {
244                write!(
245                    io::stdout(),
246                    "{}{}",
247                    crate::control::CURSOR_UP,
248                    crate::control::ERASE_LINE
249                )?;
250            }
251        }
252        if self.screen {
253            write!(io::stdout(), "{}", crate::control::ALT_SCREEN_EXIT)?;
254        }
255        write!(io::stdout(), "{}", crate::control::CURSOR_SHOW)?;
256        io::stdout().flush()?;
257        self.started = false;
258        self.started_at = None;
259        Ok(())
260    }
261
262    /// Replace the displayed content and refresh immediately.
263    pub fn update(
264        &mut self,
265        renderable: impl Renderable + Send + Sync + 'static,
266    ) -> io::Result<()> {
267        *self.renderable.lock().unwrap() = Some(DynRenderable::new(renderable));
268        self.refresh()
269    }
270
271    /// Re-render the current content in place (cursor is moved back to overwrite previous output).
272    ///
273    /// If any [`RenderHook`]s are registered, they are applied to the rendered
274    /// segment lines before the output is written to the terminal.
275    pub fn refresh(&mut self) -> io::Result<()> {
276        let renderable_guard = self.renderable.lock().unwrap();
277        if let Some(ref renderable) = *renderable_guard {
278            let opts = ConsoleOptions::default();
279            let result = renderable.render(&opts);
280
281            let prev_lines = self.previous_line_count.load(Ordering::Relaxed);
282            if prev_lines > 0 {
283                // Move cursor up `prev_lines` rows: `\x1b[{N}F`
284                write!(io::stdout(), "\x1b[{}F", prev_lines)?;
285            }
286            drop(renderable_guard);
287
288            // Apply render hooks to transform segment lines before output
289            let hooks_guard = self.render_hooks.lock().unwrap();
290            let (ansi, line_count) = if !hooks_guard.is_empty() {
291                let mut lines = result.lines.clone();
292                for hook in hooks_guard.iter() {
293                    lines = hook.apply(&lines);
294                }
295                let mut out = String::new();
296                for line in &lines {
297                    for seg in line {
298                        out.push_str(&seg.to_ansi());
299                    }
300                }
301                (out, lines.len())
302            } else {
303                let s = result.to_ansi();
304                let c = s.lines().count();
305                (s, c)
306            };
307            drop(hooks_guard);
308
309            write!(io::stdout(), "{ansi}")?;
310            if line_count < prev_lines {
311                for _ in line_count..prev_lines {
312                    writeln!(io::stdout(), "{}", crate::control::ERASE_LINE)?;
313                }
314            }
315
316            self.previous_line_count
317                .store(line_count, Ordering::Relaxed);
318
319            // Write captured writer content
320            let writers_guard = self.writers.lock().unwrap();
321            for writer in writers_guard.iter() {
322                let captured = String::from_utf8_lossy(writer.capture());
323                if !captured.is_empty() {
324                    write!(io::stdout(), "{}", captured)?;
325                }
326            }
327
328            io::stdout().flush()?;
329        }
330        Ok(())
331    }
332
333    /// Check if the live display is currently running.
334    pub fn is_started(&self) -> bool {
335        self.started
336    }
337
338    /// Get a clone of the current renderable, if any.
339    pub fn get_renderable(&self) -> Option<DynRenderable> {
340        self.renderable.lock().unwrap().clone()
341    }
342
343    /// Get the current renderable being displayed, if any.
344    pub fn renderable(&self) -> Option<DynRenderable> {
345        self.renderable.lock().unwrap().clone()
346    }
347
348    /// Process multiple renderables through the Live display pipeline.
349    ///
350    /// Each renderable is rendered with the given options, and the resulting
351    /// segment lines are collected into a single vector.
352    pub fn process_renderables(
353        &self,
354        renderables: &[Box<dyn Renderable>],
355        options: &ConsoleOptions,
356    ) -> Vec<Vec<Segment>> {
357        let mut all_lines = Vec::new();
358        for renderable in renderables {
359            let result = renderable.render(options);
360            all_lines.extend(result.lines);
361        }
362        all_lines
363    }
364
365    /// Add a render hook to the live display.
366    ///
367    /// Hooks are applied in registration order during each refresh, allowing
368    /// transformation of the rendered segment lines before they are output.
369    pub fn add_render_hook(&mut self, hook: RenderHook) {
370        self.render_hooks.lock().unwrap().push(hook);
371    }
372}
373
374impl Drop for Live {
375    fn drop(&mut self) {
376        let _ = self.stop();
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use crate::text::Text;
384
385    #[test]
386    fn test_is_started() {
387        let mut live = Live::new(Text::new("test"));
388        assert!(!live.is_started());
389        live.start().unwrap();
390        assert!(live.is_started());
391        live.stop().unwrap();
392        assert!(!live.is_started());
393    }
394
395    #[test]
396    fn test_renderable_accessor() {
397        let live = Live::new(Text::new("hello"));
398        let r = live.get_renderable().expect("renderable should be set");
399        // Verify we get a valid renderable
400        let opts = ConsoleOptions::default();
401        let result = r.render(&opts);
402        assert!(!result.to_ansi().is_empty());
403    }
404
405    #[test]
406    fn test_render_hook_basic() {
407        let hook = RenderHook::new(|segments| segments.to_vec());
408        let input = vec![vec![Segment::new("test")]];
409        let output = hook.apply(&input);
410        assert_eq!(output.len(), 1);
411        assert_eq!(output[0][0].text, "test");
412    }
413
414    #[test]
415    fn test_render_hook_transform() {
416        let hook = RenderHook::new(|segments| {
417            let mut transformed = segments.to_vec();
418            transformed.push(vec![Segment::new("appended")]);
419            transformed
420        });
421        let input = vec![vec![Segment::new("original")]];
422        let output = hook.apply(&input);
423        assert_eq!(output.len(), 2);
424        assert_eq!(output[1][0].text, "appended");
425    }
426
427    #[test]
428    fn test_process_renderables() {
429        let live = Live::new(Text::new("dummy"));
430        let opts = ConsoleOptions::default();
431        let renderables: Vec<Box<dyn Renderable>> =
432            vec![Box::new(Text::new("first")), Box::new(Text::new("second"))];
433        let lines = live.process_renderables(&renderables, &opts);
434        assert!(!lines.is_empty());
435    }
436
437    #[test]
438    fn test_start_stop_cycle() {
439        let mut live = Live::new(Text::new("test"));
440        assert!(!live.is_started());
441        live.start().unwrap();
442        assert!(live.is_started());
443        live.stop().unwrap();
444        assert!(!live.is_started());
445    }
446
447    #[test]
448    fn test_add_render_hook() {
449        let mut live = Live::new(Text::new("test"));
450        let hook = RenderHook::new(|segments| segments.to_vec());
451        live.add_render_hook(hook);
452        assert_eq!(live.render_hooks.lock().unwrap().len(), 1);
453    }
454}