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::time::Instant;
51
52use crate::console::{ConsoleOptions, DynRenderable, Renderable};
53
54/// A writer that captures output for live display.
55pub struct LiveWriter {
56    buffer: Vec<u8>,
57}
58
59impl LiveWriter {
60    /// Create a new `LiveWriter` with an empty capture buffer.
61    pub fn new() -> Self {
62        Self { buffer: Vec::new() }
63    }
64
65    /// Return a reference to the captured output bytes.
66    pub fn capture(&self) -> &[u8] {
67        &self.buffer
68    }
69
70    /// Clear the captured output buffer.
71    pub fn clear(&mut self) {
72        self.buffer.clear();
73    }
74}
75
76impl Write for LiveWriter {
77    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
78        self.buffer.extend_from_slice(buf);
79        Ok(buf.len())
80    }
81
82    fn flush(&mut self) -> io::Result<()> {
83        Ok(())
84    }
85}
86
87/// Manages a live-updating region of the terminal.
88pub struct Live {
89    renderable: Option<DynRenderable>,
90    screen: bool,
91    auto_refresh: bool,
92    refresh_per_second: f64,
93    transient: bool,
94    started: Option<Instant>,
95    previous_line_count: usize,
96    redirect_stdout: bool,
97    redirect_stderr: bool,
98    writers: Vec<LiveWriter>,
99}
100
101impl std::fmt::Debug for Live {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.debug_struct("Live")
104            .field("screen", &self.screen)
105            .field("started", &self.started)
106            .finish()
107    }
108}
109
110impl Live {
111    /// Create a new `Live` display wrapping the given [`Renderable`].
112    pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
113        Self {
114            renderable: Some(DynRenderable::new(renderable)),
115            screen: false,
116            auto_refresh: true,
117            refresh_per_second: 4.0,
118            transient: false,
119            started: None,
120            previous_line_count: 0,
121            redirect_stdout: true,
122            redirect_stderr: true,
123            writers: Vec::new(),
124        }
125    }
126
127    /// Builder: use the alternate screen buffer for full-screen display.
128    pub fn screen(mut self) -> Self { self.screen = true; self }
129    /// Builder: disable automatic periodic refresh.
130    pub fn no_auto_refresh(mut self) -> Self { self.auto_refresh = false; self }
131    /// Builder: set the refresh rate in Hz (default 4.0).
132    pub fn refresh_per_second(mut self, rate: f64) -> Self { self.refresh_per_second = rate; self }
133    /// Builder: enable transient mode (live display disappears on stop).
134    pub fn transient(mut self) -> Self { self.transient = true; self }
135    /// Builder: redirect stdout writes into the live display.
136    pub fn redirect_stdout(mut self, redirect: bool) -> Self { self.redirect_stdout = redirect; self }
137    /// Builder: redirect stderr writes into the live display.
138    pub fn redirect_stderr(mut self, redirect: bool) -> Self { self.redirect_stderr = redirect; self }
139
140    /// Register a writer whose captured content will be rendered during refresh.
141    pub fn add_writer(&mut self, writer: LiveWriter) { self.writers.push(writer); }
142
143    /// Create a LiveWriter that captures output while Live is active.
144    pub fn create_writer() -> LiveWriter {
145        LiveWriter::new()
146    }
147
148    /// Start the live display: enter alternate screen (if configured) and hide cursor.
149    pub fn start(&mut self) -> io::Result<()> {
150        self.started = Some(Instant::now());
151        if self.screen {
152            write!(io::stdout(), "\x1b[?1049h")?;
153        }
154        write!(io::stdout(), "\x1b[?25l")?;
155        self.refresh()
156    }
157
158    /// Stop the live display: restore cursor, exit alternate screen, and clean up.
159    pub fn stop(&mut self) -> io::Result<()> {
160        if self.transient {
161            for _ in 0..self.previous_line_count {
162                write!(io::stdout(), "\x1b[1A\x1b[2K")?;
163            }
164        }
165        if self.screen {
166            write!(io::stdout(), "\x1b[?1049l")?;
167        }
168        write!(io::stdout(), "\x1b[?25h")?;
169        io::stdout().flush()?;
170        self.started = None;
171        Ok(())
172    }
173
174    /// Replace the displayed content and refresh immediately.
175    pub fn update(&mut self, renderable: impl Renderable + Send + Sync + 'static) -> io::Result<()> {
176        self.renderable = Some(DynRenderable::new(renderable));
177        self.refresh()
178    }
179
180    /// Re-render the current content in place (cursor is moved back to overwrite previous output).
181    pub fn refresh(&mut self) -> io::Result<()> {
182        if let Some(ref renderable) = self.renderable {
183            let opts = ConsoleOptions::default();
184            let result = renderable.render(&opts);
185
186            if self.previous_line_count > 0 {
187                write!(io::stdout(), "\x1b[{}F", self.previous_line_count)?;
188            }
189
190            let ansi = result.to_ansi();
191            let line_count = ansi.lines().count();
192
193            write!(io::stdout(), "{ansi}")?;
194            if line_count < self.previous_line_count {
195                for _ in line_count..self.previous_line_count {
196                    write!(io::stdout(), "\x1b[2K\n")?;
197                }
198            }
199
200            self.previous_line_count = line_count;
201
202            // Write captured writer content
203            for writer in &self.writers {
204                let captured = String::from_utf8_lossy(writer.capture());
205                if !captured.is_empty() {
206                    write!(io::stdout(), "{}", captured)?;
207                }
208            }
209
210            io::stdout().flush()?;
211        }
212        Ok(())
213    }
214}
215
216impl Drop for Live {
217    fn drop(&mut self) {
218        let _ = self.stop();
219    }
220}