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//! Content taller than the screen is cropped, ended with an ellipsis line
10//! (the default) or left visible, per [`VerticalOverflow`].
11
12use std::cell::Cell;
13
14use crate::console::{Console, ConsoleOptions, Justify, Overflow};
15use crate::control::{Control, ControlType};
16use crate::protocol::Renderable;
17use crate::segment::Segment;
18use crate::style::Style;
19use crate::text::Text;
20
21/// What a live render does with content taller than the screen. Mirrors
22/// upstream's `VerticalOverflowMethod`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum VerticalOverflow {
25    /// Keep only the lines that fit.
26    Crop,
27    /// Keep all but the last line that fits, then a centred `...` line in
28    /// `live.ellipsis` (upstream's default).
29    #[default]
30    Ellipsis,
31    /// Render every line.
32    Visible,
33}
34
35/// Wraps a renderable for repeated in-place redraws. Mirrors
36/// `rich.live_render.LiveRender`.
37pub struct LiveRender {
38    renderable: Box<dyn Renderable>,
39    style: Option<Style>,
40    vertical_overflow: VerticalOverflow,
41    /// `(width, height)` of the last render, or `None` before the first.
42    shape: Cell<Option<(usize, usize)>>,
43}
44
45impl LiveRender {
46    pub fn new(renderable: Box<dyn Renderable>) -> Self {
47        LiveRender {
48            renderable,
49            style: None,
50            vertical_overflow: VerticalOverflow::Ellipsis,
51            shape: Cell::new(None),
52        }
53    }
54
55    /// Apply a style across the whole live render.
56    pub fn style(mut self, style: Style) -> Self {
57        self.style = Some(style);
58        self
59    }
60
61    /// What to do with content taller than the screen (upstream
62    /// `vertical_overflow`, default [`VerticalOverflow::Ellipsis`]).
63    pub fn vertical_overflow(mut self, vertical_overflow: VerticalOverflow) -> Self {
64        self.vertical_overflow = vertical_overflow;
65        self
66    }
67
68    /// Set [`vertical_overflow`](Self::vertical_overflow) in place.
69    pub fn set_vertical_overflow(&mut self, vertical_overflow: VerticalOverflow) {
70        self.vertical_overflow = vertical_overflow;
71    }
72
73    /// Replace the wrapped renderable (the shape carries over until the next
74    /// render). Port of `LiveRender.set_renderable`.
75    pub fn set_renderable(&mut self, renderable: Box<dyn Renderable>) {
76        self.renderable = renderable;
77    }
78
79    /// The wrapped renderable. Upstream's `LiveRender.renderable`.
80    pub fn renderable(&self) -> &dyn Renderable {
81        self.renderable.as_ref()
82    }
83
84    /// The height of the last render, `0` before any. Port of
85    /// `LiveRender.last_render_height`.
86    pub fn last_render_height(&self) -> usize {
87        self.shape.get().map_or(0, |(_, height)| height)
88    }
89
90    /// Control codes to move the cursor to the start of the previous render,
91    /// erasing each line. Port of `LiveRender.position_cursor`.
92    pub fn position_cursor(&self) -> Control {
93        position_cursor(self.shape.get())
94    }
95
96    /// Control codes to clear the render and restore the cursor to where it was
97    /// before it. Port of `LiveRender.restore_cursor`.
98    pub fn restore_cursor(&self) -> Control {
99        restore_cursor(self.shape.get())
100    }
101}
102
103/// [`LiveRender::position_cursor`] for a render of `shape`.
104pub(crate) fn position_cursor(shape: Option<(usize, usize)>) -> Control {
105    match shape {
106        Some((_, height)) => {
107            let mut codes = vec![ControlType::CarriageReturn, ControlType::EraseInLine(2)];
108            for _ in 0..height.saturating_sub(1) {
109                codes.push(ControlType::CursorUp(1));
110                codes.push(ControlType::EraseInLine(2));
111            }
112            Control::new(&codes)
113        }
114        None => Control::new(&[]),
115    }
116}
117
118/// [`LiveRender::restore_cursor`] for a render of `shape`.
119pub(crate) fn restore_cursor(shape: Option<(usize, usize)>) -> Control {
120    match shape {
121        Some((_, height)) => {
122            let mut codes = vec![ControlType::CarriageReturn];
123            for _ in 0..height {
124                codes.push(ControlType::CursorUp(1));
125                codes.push(ControlType::EraseInLine(2));
126            }
127            Control::new(&codes)
128        }
129        None => Control::new(&[]),
130    }
131}
132
133impl Renderable for LiveRender {
134    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
135        let (segments, shape) = render(
136            self.renderable.as_ref(),
137            self.style.as_ref(),
138            self.vertical_overflow,
139            console,
140            options,
141        );
142        // Remember the shape for the next `position_cursor`/`restore_cursor`.
143        self.shape.set(Some(shape));
144        segments
145    }
146
147    /// Upstream yields no line break after the last line, so a printed
148    /// `LiveRender` leaves the cursor at its end (where `position_cursor`
149    /// expects it).
150    fn print_end_line(&self, _options: &ConsoleOptions) -> bool {
151        false
152    }
153}
154
155/// `LiveRender.__rich_console__` over a borrowed renderable: the segments,
156/// and the `(width, height)` shape the render leaves.
157pub(crate) fn render(
158    renderable: &dyn Renderable,
159    style: Option<&Style>,
160    vertical_overflow: VerticalOverflow,
161    console: &Console,
162    options: &ConsoleOptions,
163) -> (Vec<Segment>, (usize, usize)) {
164    // Render unpadded lines (`pad=False`), applying the live style if set.
165    let mut lines = console.render_lines(renderable, options, false);
166    if let Some(style) = style {
167        for line in &mut lines {
168            *line = Segment::apply_style(line, style);
169        }
170    }
171
172    // Content taller than the screen (`options.size.height`).
173    let screen_height = options.size.height;
174    if lines.len() > screen_height {
175        match vertical_overflow {
176            VerticalOverflow::Crop => lines.truncate(screen_height),
177            VerticalOverflow::Ellipsis => {
178                // `lines[: height - 1]`: at height 0 that is Python's
179                // `lines[:-1]`, every line but the last.
180                let keep = screen_height
181                    .checked_sub(1)
182                    .unwrap_or_else(|| lines.len() - 1);
183                lines.truncate(keep);
184                let overflow_text = Text::styled("...", "live.ellipsis")
185                    .overflow(Overflow::Crop)
186                    .justify(Justify::Center);
187                lines.push(console.render(&overflow_text, None));
188            }
189            VerticalOverflow::Visible => {}
190        }
191    }
192
193    let height = lines.len();
194    let width = lines
195        .iter()
196        .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
197        .max()
198        .unwrap_or(0);
199
200    let mut segments = Vec::new();
201    let last = height.saturating_sub(1);
202    for (index, line) in lines.into_iter().enumerate() {
203        segments.extend(line);
204        if index != last {
205            segments.push(Segment::line());
206        }
207    }
208    (segments, (width, height))
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::color::ColorSystem;
215    use crate::text::Text;
216
217    fn console() -> Console {
218        Console::builder()
219            .force_terminal(true)
220            .color_system(Some(ColorSystem::Truecolor))
221            .width(20)
222            .no_color(false)
223            .build()
224    }
225
226    #[test]
227    fn renders_content_and_control_codes() {
228        let live = LiveRender::new(Box::new(Text::new("line one\nline two\nline three")));
229        let console = console();
230        // Rendering sets the shape.
231        assert_eq!(
232            console.render_to_string(&live),
233            "line one\nline two\nline three"
234        );
235        // Captured from real rich 15.0.0 (height 3).
236        assert_eq!(
237            live.position_cursor().as_str(),
238            "\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
239        );
240        assert_eq!(
241            live.restore_cursor().as_str(),
242            "\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
243        );
244    }
245
246    #[test]
247    fn single_line_shape() {
248        let live = LiveRender::new(Box::new(Text::new("solo")));
249        let console = console();
250        assert_eq!(console.render_to_string(&live), "solo");
251        assert_eq!(live.position_cursor().as_str(), "\r\x1b[2K");
252        assert_eq!(live.restore_cursor().as_str(), "\r\x1b[1A\x1b[2K");
253    }
254
255    #[test]
256    fn no_control_before_first_render() {
257        let live = LiveRender::new(Box::new(Text::new("x")));
258        assert_eq!(live.position_cursor().as_str(), "");
259        assert_eq!(live.restore_cursor().as_str(), "");
260    }
261}