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 height of the last render, `0` before any. Port of
80    /// `LiveRender.last_render_height`.
81    pub fn last_render_height(&self) -> usize {
82        self.shape.get().map_or(0, |(_, height)| height)
83    }
84
85    /// Control codes to move the cursor to the start of the previous render,
86    /// erasing each line. Port of `LiveRender.position_cursor`.
87    pub fn position_cursor(&self) -> Control {
88        match self.shape.get() {
89            Some((_, height)) => {
90                let mut codes = vec![ControlType::CarriageReturn, ControlType::EraseInLine(2)];
91                for _ in 0..height.saturating_sub(1) {
92                    codes.push(ControlType::CursorUp(1));
93                    codes.push(ControlType::EraseInLine(2));
94                }
95                Control::new(&codes)
96            }
97            None => Control::new(&[]),
98        }
99    }
100
101    /// Control codes to clear the render and restore the cursor to where it was
102    /// before it. Port of `LiveRender.restore_cursor`.
103    pub fn restore_cursor(&self) -> Control {
104        match self.shape.get() {
105            Some((_, height)) => {
106                let mut codes = vec![ControlType::CarriageReturn];
107                for _ in 0..height {
108                    codes.push(ControlType::CursorUp(1));
109                    codes.push(ControlType::EraseInLine(2));
110                }
111                Control::new(&codes)
112            }
113            None => Control::new(&[]),
114        }
115    }
116}
117
118impl Renderable for LiveRender {
119    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
120        // Render unpadded lines (`pad=False`), applying the live style if set.
121        let mut lines = console.render_lines(self.renderable.as_ref(), options, false);
122        if let Some(style) = &self.style {
123            for line in &mut lines {
124                *line = Segment::apply_style(line, style);
125            }
126        }
127
128        // Content taller than the screen (`options.size.height`).
129        let screen_height = options.size.height;
130        if lines.len() > screen_height {
131            match self.vertical_overflow {
132                VerticalOverflow::Crop => lines.truncate(screen_height),
133                VerticalOverflow::Ellipsis => {
134                    // `lines[: height - 1]`: at height 0 that is Python's
135                    // `lines[:-1]`, every line but the last.
136                    let keep = screen_height
137                        .checked_sub(1)
138                        .unwrap_or_else(|| lines.len() - 1);
139                    lines.truncate(keep);
140                    let overflow_text = Text::styled("...", "live.ellipsis")
141                        .overflow(Overflow::Crop)
142                        .justify(Justify::Center);
143                    lines.push(console.render(&overflow_text, None));
144                }
145                VerticalOverflow::Visible => {}
146            }
147        }
148
149        // Remember the shape for the next `position_cursor`/`restore_cursor`.
150        let height = lines.len();
151        let width = lines
152            .iter()
153            .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
154            .max()
155            .unwrap_or(0);
156        self.shape.set(Some((width, height)));
157
158        let mut segments = Vec::new();
159        let last = height.saturating_sub(1);
160        for (index, line) in lines.into_iter().enumerate() {
161            segments.extend(line);
162            if index != last {
163                segments.push(Segment::line());
164            }
165        }
166        segments
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::color::ColorSystem;
174    use crate::text::Text;
175
176    fn console() -> Console {
177        Console::builder()
178            .force_terminal(true)
179            .color_system(Some(ColorSystem::Truecolor))
180            .width(20)
181            .no_color(false)
182            .build()
183    }
184
185    #[test]
186    fn renders_content_and_control_codes() {
187        let live = LiveRender::new(Box::new(Text::new("line one\nline two\nline three")));
188        let console = console();
189        // Rendering sets the shape.
190        assert_eq!(
191            console.render_to_string(&live),
192            "line one\nline two\nline three"
193        );
194        // Captured from real rich 15.0.0 (height 3).
195        assert_eq!(
196            live.position_cursor().as_str(),
197            "\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
198        );
199        assert_eq!(
200            live.restore_cursor().as_str(),
201            "\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
202        );
203    }
204
205    #[test]
206    fn single_line_shape() {
207        let live = LiveRender::new(Box::new(Text::new("solo")));
208        let console = console();
209        assert_eq!(console.render_to_string(&live), "solo");
210        assert_eq!(live.position_cursor().as_str(), "\r\x1b[2K");
211        assert_eq!(live.restore_cursor().as_str(), "\r\x1b[1A\x1b[2K");
212    }
213
214    #[test]
215    fn no_control_before_first_render() {
216        let live = LiveRender::new(Box::new(Text::new("x")));
217        assert_eq!(live.position_cursor().as_str(), "");
218        assert_eq!(live.restore_cursor().as_str(), "");
219    }
220}