Skip to main content

rich/
live_render.rs

1//! In-place live rendering.
2//!
3//! Port of `rich/live_render.py`. A [`LiveRender`] wraps a renderable, remembers
4//! the shape (width × height) of its last render, and produces the terminal
5//! control sequences to move the cursor back over that render — the mechanism a
6//! `Live` display uses to redraw in place. The full `Live` loop (threading,
7//! timing, stdout management) is deferred; this is its byte-parity-testable core.
8//!
9//! Scope: rendering + `position_cursor`/`restore_cursor`. Vertical-overflow
10//! cropping (when content is taller than the screen) is deferred.
11
12use std::cell::Cell;
13
14use crate::console::{Console, ConsoleOptions};
15use crate::control::{Control, ControlType};
16use crate::protocol::Renderable;
17use crate::segment::Segment;
18use crate::style::Style;
19
20/// Wraps a renderable for repeated in-place redraws. Mirrors
21/// `rich.live_render.LiveRender`.
22pub struct LiveRender {
23    renderable: Box<dyn Renderable>,
24    style: Option<Style>,
25    /// `(width, height)` of the last render, or `None` before the first.
26    shape: Cell<Option<(usize, usize)>>,
27}
28
29impl LiveRender {
30    pub fn new(renderable: Box<dyn Renderable>) -> Self {
31        LiveRender {
32            renderable,
33            style: None,
34            shape: Cell::new(None),
35        }
36    }
37
38    /// Apply a style across the whole live render.
39    pub fn style(mut self, style: Style) -> Self {
40        self.style = Some(style);
41        self
42    }
43
44    /// Replace the wrapped renderable (the shape carries over until the next
45    /// render). Port of `LiveRender.set_renderable`.
46    pub fn set_renderable(&mut self, renderable: Box<dyn Renderable>) {
47        self.renderable = renderable;
48    }
49
50    /// The height of the last render, `0` before any. Port of
51    /// `LiveRender.last_render_height`.
52    pub fn last_render_height(&self) -> usize {
53        self.shape.get().map_or(0, |(_, height)| height)
54    }
55
56    /// Control codes to move the cursor to the start of the previous render,
57    /// erasing each line. Port of `LiveRender.position_cursor`.
58    pub fn position_cursor(&self) -> Control {
59        match self.shape.get() {
60            Some((_, height)) => {
61                let mut codes = vec![ControlType::CarriageReturn, ControlType::EraseInLine(2)];
62                for _ in 0..height.saturating_sub(1) {
63                    codes.push(ControlType::CursorUp(1));
64                    codes.push(ControlType::EraseInLine(2));
65                }
66                Control::new(&codes)
67            }
68            None => Control::new(&[]),
69        }
70    }
71
72    /// Control codes to clear the render and restore the cursor to where it was
73    /// before it. Port of `LiveRender.restore_cursor`.
74    pub fn restore_cursor(&self) -> Control {
75        match self.shape.get() {
76            Some((_, height)) => {
77                let mut codes = vec![ControlType::CarriageReturn];
78                for _ in 0..height {
79                    codes.push(ControlType::CursorUp(1));
80                    codes.push(ControlType::EraseInLine(2));
81                }
82                Control::new(&codes)
83            }
84            None => Control::new(&[]),
85        }
86    }
87}
88
89impl Renderable for LiveRender {
90    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
91        // Render unpadded lines (`pad=False`), applying the live style if set.
92        let mut lines = console.render_lines(self.renderable.as_ref(), options, false);
93        if let Some(style) = &self.style {
94            for line in &mut lines {
95                *line = Segment::apply_style(line, style);
96            }
97        }
98
99        // Remember the shape for the next `position_cursor`/`restore_cursor`.
100        let height = lines.len();
101        let width = lines
102            .iter()
103            .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
104            .max()
105            .unwrap_or(0);
106        self.shape.set(Some((width, height)));
107
108        let mut segments = Vec::new();
109        let last = height.saturating_sub(1);
110        for (index, line) in lines.into_iter().enumerate() {
111            segments.extend(line);
112            if index != last {
113                segments.push(Segment::line());
114            }
115        }
116        segments
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::color::ColorSystem;
124    use crate::text::Text;
125
126    fn console() -> Console {
127        Console::builder()
128            .force_terminal(true)
129            .color_system(Some(ColorSystem::Truecolor))
130            .width(20)
131            .no_color(false)
132            .build()
133    }
134
135    #[test]
136    fn renders_content_and_control_codes() {
137        let live = LiveRender::new(Box::new(Text::new("line one\nline two\nline three")));
138        let console = console();
139        // Rendering sets the shape.
140        assert_eq!(
141            console.render_to_string(&live),
142            "line one\nline two\nline three"
143        );
144        // Captured from real rich 15.0.0 (height 3).
145        assert_eq!(
146            live.position_cursor().as_str(),
147            "\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
148        );
149        assert_eq!(
150            live.restore_cursor().as_str(),
151            "\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
152        );
153    }
154
155    #[test]
156    fn single_line_shape() {
157        let live = LiveRender::new(Box::new(Text::new("solo")));
158        let console = console();
159        assert_eq!(console.render_to_string(&live), "solo");
160        assert_eq!(live.position_cursor().as_str(), "\r\x1b[2K");
161        assert_eq!(live.restore_cursor().as_str(), "\r\x1b[1A\x1b[2K");
162    }
163
164    #[test]
165    fn no_control_before_first_render() {
166        let live = LiveRender::new(Box::new(Text::new("x")));
167        assert_eq!(live.position_cursor().as_str(), "");
168        assert_eq!(live.restore_cursor().as_str(), "");
169    }
170}