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            ControlType::CursorMoveToColumn(x) => format!("\x1b[{}G", x + 1),
58            ControlType::CursorMoveTo(x, y) => format!("\x1b[{};{}H", y + 1, x + 1),
59            ControlType::EraseInLine(n) => format!("\x1b[{n}K"),
60        }
61    }
62}
63
64/// A renderable that inserts terminal control codes.
65///
66/// Mirrors `rich.control.Control`. Construct it via the factory methods
67/// ([`Control::clear`], [`Control::move`], …) or [`Control::new`] with an
68/// explicit list of codes, which are concatenated in order.
69pub struct Control {
70    segment: Segment,
71}
72
73impl Control {
74    /// Build a control from a sequence of codes, rendered end to end.
75    pub fn new(codes: &[ControlType]) -> Self {
76        let text: String = codes.iter().map(|c| c.format()).collect();
77        Control {
78            segment: Segment::control(text),
79        }
80    }
81
82    fn single(code: ControlType) -> Self {
83        Control::new(&[code])
84    }
85
86    /// Ring the terminal bell.
87    pub fn bell() -> Self {
88        Control::single(ControlType::Bell)
89    }
90
91    /// Move the cursor to the home position (top-left).
92    pub fn home() -> Self {
93        Control::single(ControlType::Home)
94    }
95
96    /// Clear the screen.
97    pub fn clear() -> Self {
98        Control::single(ControlType::Clear)
99    }
100
101    /// Move the cursor relative to its current position (`x` columns, `y` rows;
102    /// positive is right/down). Port of `Control.move`.
103    #[allow(clippy::should_implement_trait)]
104    pub fn move_(x: i32, y: i32) -> Self {
105        let mut codes = Vec::new();
106        if x != 0 {
107            codes.push(if x > 0 {
108                ControlType::CursorForward(x.unsigned_abs())
109            } else {
110                ControlType::CursorBackward(x.unsigned_abs())
111            });
112        }
113        if y != 0 {
114            codes.push(if y > 0 {
115                ControlType::CursorDown(y.unsigned_abs())
116            } else {
117                ControlType::CursorUp(y.unsigned_abs())
118            });
119        }
120        Control::new(&codes)
121    }
122
123    /// Move to a zero-based column, optionally offset the row by `y`. Port of
124    /// `Control.move_to_column`.
125    pub fn move_to_column(x: u32, y: i32) -> Self {
126        if y != 0 {
127            let vertical = if y > 0 {
128                ControlType::CursorDown(y.unsigned_abs())
129            } else {
130                ControlType::CursorUp(y.unsigned_abs())
131            };
132            Control::new(&[ControlType::CursorMoveToColumn(x), vertical])
133        } else {
134            Control::single(ControlType::CursorMoveToColumn(x))
135        }
136    }
137
138    /// Move the cursor to an absolute zero-based `(x, y)` position.
139    pub fn move_to(x: u32, y: u32) -> Self {
140        Control::single(ControlType::CursorMoveTo(x, y))
141    }
142
143    /// Show or hide the cursor.
144    pub fn show_cursor(show: bool) -> Self {
145        Control::single(if show {
146            ControlType::ShowCursor
147        } else {
148            ControlType::HideCursor
149        })
150    }
151
152    /// Enable or disable the terminal's alternate screen buffer.
153    pub fn alt_screen(enable: bool) -> Self {
154        Control::single(if enable {
155            ControlType::EnableAltScreen
156        } else {
157            ControlType::DisableAltScreen
158        })
159    }
160
161    /// The raw escape string this control emits.
162    pub fn as_str(&self) -> &str {
163        &self.segment.text
164    }
165}
166
167impl Renderable for Control {
168    fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
169        if self.segment.text.is_empty() {
170            Vec::new()
171        } else {
172            vec![self.segment.clone()]
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    // All expected strings captured from real Python `rich` 15.0.0.
182    #[test]
183    fn escape_strings_match_upstream() {
184        assert_eq!(Control::clear().as_str(), "\x1b[2J");
185        assert_eq!(Control::home().as_str(), "\x1b[H");
186        assert_eq!(Control::bell().as_str(), "\x07");
187        assert_eq!(Control::show_cursor(true).as_str(), "\x1b[?25h");
188        assert_eq!(Control::show_cursor(false).as_str(), "\x1b[?25l");
189        assert_eq!(Control::move_(2, -1).as_str(), "\x1b[2C\x1b[1A");
190        assert_eq!(Control::move_to(3, 4).as_str(), "\x1b[5;4H");
191        assert_eq!(Control::move_to_column(5, 0).as_str(), "\x1b[6G");
192        assert_eq!(Control::alt_screen(true).as_str(), "\x1b[?1049h");
193        assert_eq!(Control::alt_screen(false).as_str(), "\x1b[?1049l");
194    }
195
196    #[test]
197    fn renders_control_segment() {
198        let control = Control::clear();
199        let segments = control.rich_render(&Console::new(), &Console::new().options());
200        assert_eq!(segments.len(), 1);
201        assert!(segments[0].control);
202        assert_eq!(segments[0].cell_length(), 0);
203    }
204}