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    /// Control codes to move the cursor to the start of the previous render,
51    /// erasing each line. Port of `LiveRender.position_cursor`.
52    pub fn position_cursor(&self) -> Control {
53        match self.shape.get() {
54            Some((_, height)) => {
55                let mut codes = vec![ControlType::CarriageReturn, ControlType::EraseInLine(2)];
56                for _ in 0..height.saturating_sub(1) {
57                    codes.push(ControlType::CursorUp(1));
58                    codes.push(ControlType::EraseInLine(2));
59                }
60                Control::new(&codes)
61            }
62            None => Control::new(&[]),
63        }
64    }
65
66    /// Control codes to clear the render and restore the cursor to where it was
67    /// before it. Port of `LiveRender.restore_cursor`.
68    pub fn restore_cursor(&self) -> Control {
69        match self.shape.get() {
70            Some((_, height)) => {
71                let mut codes = vec![ControlType::CarriageReturn];
72                for _ in 0..height {
73                    codes.push(ControlType::CursorUp(1));
74                    codes.push(ControlType::EraseInLine(2));
75                }
76                Control::new(&codes)
77            }
78            None => Control::new(&[]),
79        }
80    }
81}
82
83impl Renderable for LiveRender {
84    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
85        // Render unpadded lines (`pad=False`), applying the live style if set.
86        let mut lines = console.render_lines(self.renderable.as_ref(), options, false);
87        if let Some(style) = &self.style {
88            for line in &mut lines {
89                *line = Segment::apply_style(line, style);
90            }
91        }
92
93        // Remember the shape for the next `position_cursor`/`restore_cursor`.
94        let height = lines.len();
95        let width = lines
96            .iter()
97            .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
98            .max()
99            .unwrap_or(0);
100        self.shape.set(Some((width, height)));
101
102        let mut segments = Vec::new();
103        let last = height.saturating_sub(1);
104        for (index, line) in lines.into_iter().enumerate() {
105            segments.extend(line);
106            if index != last {
107                segments.push(Segment::line());
108            }
109        }
110        segments
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::color::ColorSystem;
118    use crate::text::Text;
119
120    fn console() -> Console {
121        Console::builder()
122            .force_terminal(true)
123            .color_system(Some(ColorSystem::Truecolor))
124            .width(20)
125            .no_color(false)
126            .build()
127    }
128
129    #[test]
130    fn renders_content_and_control_codes() {
131        let live = LiveRender::new(Box::new(Text::new("line one\nline two\nline three")));
132        let console = console();
133        // Rendering sets the shape.
134        assert_eq!(
135            console.render_to_string(&live),
136            "line one\nline two\nline three"
137        );
138        // Captured from real rich 15.0.0 (height 3).
139        assert_eq!(
140            live.position_cursor().as_str(),
141            "\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
142        );
143        assert_eq!(
144            live.restore_cursor().as_str(),
145            "\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
146        );
147    }
148
149    #[test]
150    fn single_line_shape() {
151        let live = LiveRender::new(Box::new(Text::new("solo")));
152        let console = console();
153        assert_eq!(console.render_to_string(&live), "solo");
154        assert_eq!(live.position_cursor().as_str(), "\r\x1b[2K");
155        assert_eq!(live.restore_cursor().as_str(), "\r\x1b[1A\x1b[2K");
156    }
157
158    #[test]
159    fn no_control_before_first_render() {
160        let live = LiveRender::new(Box::new(Text::new("x")));
161        assert_eq!(live.position_cursor().as_str(), "");
162        assert_eq!(live.restore_cursor().as_str(), "");
163    }
164}