Skip to main content

rich/
control.rs

1//! Terminal control codes.
2//!
3//! Port of upstream `rich/control.py`. A [`Control`] is a renderable that emits
4//! a non-printable control sequence (cursor movement, screen clear, show/hide
5//! cursor, alt-screen toggle). It renders to a single *control* [`Segment`],
6//! which the [`Console`](crate::console::Console) writes verbatim only when the
7//! output is a real terminal (control codes are meaningless when captured to a
8//! file).
9
10use crate::console::{Console, ConsoleOptions};
11use crate::protocol::Renderable;
12use crate::segment::Segment;
13
14/// Non-printable control codes which typically translate to ANSI sequences.
15///
16/// Port of `rich.segment.ControlType`. The parameterized variants carry their
17/// integer arguments so a [`Control`] can be built and its escape string
18/// derived deterministically.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ControlType {
21    Bell,
22    CarriageReturn,
23    Home,
24    Clear,
25    ShowCursor,
26    HideCursor,
27    EnableAltScreen,
28    DisableAltScreen,
29    CursorUp(u32),
30    CursorDown(u32),
31    CursorForward(u32),
32    CursorBackward(u32),
33    /// Move to a zero-based column (rendered as `column + 1`).
34    CursorMoveToColumn(u32),
35    /// Move to an absolute zero-based `(x, y)` (rendered as `y + 1;x + 1`).
36    CursorMoveTo(u32, u32),
37    /// Erase in line with the given mode parameter.
38    EraseInLine(u32),
39}
40
41impl ControlType {
42    /// The ANSI/VT escape string for this code. Port of `CONTROL_CODES_FORMAT`.
43    fn format(self) -> String {
44        match self {
45            ControlType::Bell => "\x07".to_string(),
46            ControlType::CarriageReturn => "\r".to_string(),
47            ControlType::Home => "\x1b[H".to_string(),
48            ControlType::Clear => "\x1b[2J".to_string(),
49            ControlType::EnableAltScreen => "\x1b[?1049h".to_string(),
50            ControlType::DisableAltScreen => "\x1b[?1049l".to_string(),
51            ControlType::ShowCursor => "\x1b[?25h".to_string(),
52            ControlType::HideCursor => "\x1b[?25l".to_string(),
53            ControlType::CursorUp(n) => format!("\x1b[{n}A"),
54            ControlType::CursorDown(n) => format!("\x1b[{n}B"),
55            ControlType::CursorForward(n) => format!("\x1b[{n}C"),
56            ControlType::CursorBackward(n) => format!("\x1b[{n}D"),
57            // Widened: `u32::MAX + 1` is written as upstream's unbounded
58            // Python int would be, not overflowed.
59            ControlType::CursorMoveToColumn(x) => format!("\x1b[{}G", u64::from(x) + 1),
60            ControlType::CursorMoveTo(x, y) => move_to_code(u128::from(x), u128::from(y)),
61            ControlType::EraseInLine(n) => format!("\x1b[{n}K"),
62        }
63    }
64}
65
66/// `CURSOR_MOVE_TO`'s escape for a zero-based `(x, y)` of any size (the
67/// sequence is one-based). [`Console::update_screen_lines`] positions rows
68/// with it, so `usize` coordinates are written in full, as upstream writes
69/// its unbounded ints.
70///
71/// [`Console::update_screen_lines`]: crate::console::Console::update_screen_lines
72pub(crate) fn move_to_code(x: u128, y: u128) -> String {
73    format!("\x1b[{};{}H", y + 1, x + 1)
74}
75
76/// A renderable that inserts terminal control codes.
77///
78/// Mirrors `rich.control.Control`. Construct it via the factory methods
79/// ([`Control::clear`], [`Control::move`], …) or [`Control::new`] with an
80/// explicit list of codes, which are concatenated in order.
81pub struct Control {
82    segment: Segment,
83}
84
85impl Control {
86    /// Build a control from a sequence of codes, rendered end to end.
87    pub fn new(codes: &[ControlType]) -> Self {
88        let text: String = codes.iter().map(|c| c.format()).collect();
89        Control {
90            segment: Segment::control(text),
91        }
92    }
93
94    fn single(code: ControlType) -> Self {
95        Control::new(&[code])
96    }
97
98    /// Ring the terminal bell.
99    pub fn bell() -> Self {
100        Control::single(ControlType::Bell)
101    }
102
103    /// Move the cursor to the home position (top-left).
104    pub fn home() -> Self {
105        Control::single(ControlType::Home)
106    }
107
108    /// Clear the screen.
109    pub fn clear() -> Self {
110        Control::single(ControlType::Clear)
111    }
112
113    /// Move the cursor relative to its current position (`x` columns, `y` rows;
114    /// positive is right/down). Port of `Control.move`.
115    #[allow(clippy::should_implement_trait)]
116    pub fn move_(x: i32, y: i32) -> Self {
117        let mut codes = Vec::new();
118        if x != 0 {
119            codes.push(if x > 0 {
120                ControlType::CursorForward(x.unsigned_abs())
121            } else {
122                ControlType::CursorBackward(x.unsigned_abs())
123            });
124        }
125        if y != 0 {
126            codes.push(if y > 0 {
127                ControlType::CursorDown(y.unsigned_abs())
128            } else {
129                ControlType::CursorUp(y.unsigned_abs())
130            });
131        }
132        Control::new(&codes)
133    }
134
135    /// Move to a zero-based column, optionally offset the row by `y`. Port of
136    /// `Control.move_to_column`.
137    pub fn move_to_column(x: u32, y: i32) -> Self {
138        if y != 0 {
139            let vertical = if y > 0 {
140                ControlType::CursorDown(y.unsigned_abs())
141            } else {
142                ControlType::CursorUp(y.unsigned_abs())
143            };
144            Control::new(&[ControlType::CursorMoveToColumn(x), vertical])
145        } else {
146            Control::single(ControlType::CursorMoveToColumn(x))
147        }
148    }
149
150    /// Move the cursor to an absolute zero-based `(x, y)` position.
151    pub fn move_to(x: u32, y: u32) -> Self {
152        Control::single(ControlType::CursorMoveTo(x, y))
153    }
154
155    /// Show or hide the cursor.
156    pub fn show_cursor(show: bool) -> Self {
157        Control::single(if show {
158            ControlType::ShowCursor
159        } else {
160            ControlType::HideCursor
161        })
162    }
163
164    /// Enable or disable the terminal's alternate screen buffer. Port of
165    /// `Control.alt_screen`: enabling also moves the cursor home.
166    pub fn alt_screen(enable: bool) -> Self {
167        if enable {
168            Control::new(&[ControlType::EnableAltScreen, ControlType::Home])
169        } else {
170            Control::single(ControlType::DisableAltScreen)
171        }
172    }
173
174    /// Set the terminal window title. Port of `Control.title`
175    /// (`ControlType.SET_WINDOW_TITLE`, formatted `ESC ] 0 ; title BEL`).
176    pub fn title(title: &str) -> Self {
177        Control {
178            segment: Segment::control(format!("\x1b]0;{title}\x07")),
179        }
180    }
181
182    /// The raw escape string this control emits.
183    pub fn as_str(&self) -> &str {
184        &self.segment.text
185    }
186}
187
188impl Renderable for Control {
189    fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
190        if self.segment.text.is_empty() {
191            Vec::new()
192        } else {
193            vec![self.segment.clone()]
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    // All expected strings captured from real Python `rich` 15.0.0.
203    #[test]
204    fn escape_strings_match_upstream() {
205        assert_eq!(Control::clear().as_str(), "\x1b[2J");
206        assert_eq!(Control::home().as_str(), "\x1b[H");
207        assert_eq!(Control::bell().as_str(), "\x07");
208        assert_eq!(Control::show_cursor(true).as_str(), "\x1b[?25h");
209        assert_eq!(Control::show_cursor(false).as_str(), "\x1b[?25l");
210        assert_eq!(Control::move_(2, -1).as_str(), "\x1b[2C\x1b[1A");
211        assert_eq!(Control::move_to(3, 4).as_str(), "\x1b[5;4H");
212        assert_eq!(Control::move_to_column(5, 0).as_str(), "\x1b[6G");
213        assert_eq!(Control::alt_screen(true).as_str(), "\x1b[?1049h\x1b[H");
214        assert_eq!(Control::title("my title").as_str(), "\x1b]0;my title\x07");
215        assert_eq!(Control::alt_screen(false).as_str(), "\x1b[?1049l");
216    }
217
218    #[test]
219    fn renders_control_segment() {
220        let control = Control::clear();
221        let segments = control.rich_render(&Console::new(), &Console::new().options());
222        assert_eq!(segments.len(), 1);
223        assert!(segments[0].control);
224        assert_eq!(segments[0].cell_length(), 0);
225    }
226}