Skip to main content

rich_rs/
control.rs

1//! Control: terminal control codes as a renderable.
2//!
3//! This is a small subset of Python Rich's `Control` used by Live / Progress.
4
5use crate::segment::{ControlType, Segment, Segments};
6use crate::{Console, ConsoleOptions, Measurement, Renderable};
7
8#[derive(Debug, Clone, Default)]
9pub struct Control {
10    controls: Vec<ControlType>,
11}
12
13impl Control {
14    pub fn new() -> Self {
15        Self {
16            controls: Vec::new(),
17        }
18    }
19
20    pub fn home() -> Self {
21        Self {
22            controls: vec![ControlType::Home],
23        }
24    }
25
26    pub fn carriage_return() -> Self {
27        Self {
28            controls: vec![ControlType::CarriageReturn],
29        }
30    }
31
32    pub fn erase_in_line(mode: u8) -> Self {
33        Self {
34            controls: vec![ControlType::EraseInLine(mode)],
35        }
36    }
37
38    pub fn cursor_up(n: u16) -> Self {
39        Self {
40            controls: vec![ControlType::CursorUp(n)],
41        }
42    }
43
44    pub fn move_to(x: u16, y: u16) -> Self {
45        Self {
46            controls: vec![ControlType::MoveTo { x, y }],
47        }
48    }
49
50    /// Create a Bell control.
51    pub fn bell() -> Self {
52        Self {
53            controls: vec![ControlType::Bell],
54        }
55    }
56
57    /// Create a Clear screen control.
58    pub fn clear() -> Self {
59        Self {
60            controls: vec![ControlType::Clear],
61        }
62    }
63
64    /// Show or hide the cursor.
65    pub fn show_cursor(show: bool) -> Self {
66        Self {
67            controls: vec![if show {
68                ControlType::ShowCursor
69            } else {
70                ControlType::HideCursor
71            }],
72        }
73    }
74
75    /// Enable or disable the alternate screen buffer.
76    pub fn alt_screen(enable: bool) -> Self {
77        if enable {
78            Self {
79                controls: vec![ControlType::EnableAltScreen, ControlType::Home],
80            }
81        } else {
82            Self {
83                controls: vec![ControlType::DisableAltScreen],
84            }
85        }
86    }
87
88    /// Set the terminal window title.
89    pub fn title(_title: impl Into<String>) -> Self {
90        Self {
91            controls: vec![ControlType::SetTitle],
92        }
93    }
94
95    pub fn extend(&mut self, controls: impl IntoIterator<Item = ControlType>) {
96        self.controls.extend(controls);
97    }
98
99    pub fn push(&mut self, control: ControlType) {
100        self.controls.push(control);
101    }
102
103    pub fn is_empty(&self) -> bool {
104        self.controls.is_empty()
105    }
106
107    pub fn into_segments(self) -> Segments {
108        Segments::from_iter(self.controls.into_iter().map(Segment::control))
109    }
110}
111
112/// Strip control codes from text.
113///
114/// Removes Bell (7), Backspace (8), Vertical Tab (11), Form Feed (12),
115/// and Carriage Return (13).
116pub fn strip_control_codes(text: &str) -> String {
117    text.chars()
118        .filter(|&c| !matches!(c, '\x07' | '\x08' | '\x0B' | '\x0C' | '\r'))
119        .collect()
120}
121
122/// Escape control codes so they display as visible text.
123///
124/// Replaces Bell with \a, Backspace with \b, Vertical Tab with \v,
125/// Form Feed with \f, and Carriage Return with \r.
126pub fn escape_control_codes(text: &str) -> String {
127    let mut result = String::with_capacity(text.len());
128    for c in text.chars() {
129        match c {
130            '\x07' => result.push_str("\\a"),
131            '\x08' => result.push_str("\\b"),
132            '\x0B' => result.push_str("\\v"),
133            '\x0C' => result.push_str("\\f"),
134            '\r' => result.push_str("\\r"),
135            _ => result.push(c),
136        }
137    }
138    result
139}
140
141impl Renderable for Control {
142    fn render(&self, _console: &Console, _options: &ConsoleOptions) -> Segments {
143        Segments::from_iter(self.controls.iter().cloned().map(Segment::control))
144    }
145
146    fn measure(&self, _console: &Console, _options: &ConsoleOptions) -> Measurement {
147        Measurement::new(0, 0)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_bell() {
157        let ctrl = Control::bell();
158        assert!(!ctrl.is_empty());
159    }
160
161    #[test]
162    fn test_clear() {
163        let ctrl = Control::clear();
164        assert!(!ctrl.is_empty());
165    }
166
167    #[test]
168    fn test_show_cursor() {
169        let show = Control::show_cursor(true);
170        assert!(!show.is_empty());
171        let hide = Control::show_cursor(false);
172        assert!(!hide.is_empty());
173    }
174
175    #[test]
176    fn test_alt_screen() {
177        let enable = Control::alt_screen(true);
178        assert!(!enable.is_empty());
179        let disable = Control::alt_screen(false);
180        assert!(!disable.is_empty());
181    }
182
183    #[test]
184    fn test_title() {
185        let ctrl = Control::title("Hello");
186        assert!(!ctrl.is_empty());
187    }
188
189    #[test]
190    fn test_strip_control_codes() {
191        assert_eq!(strip_control_codes("hello"), "hello");
192        assert_eq!(strip_control_codes("he\x07llo"), "hello");
193        assert_eq!(strip_control_codes("he\x08llo"), "hello");
194        assert_eq!(strip_control_codes("he\x0Bllo"), "hello");
195        assert_eq!(strip_control_codes("he\x0Cllo"), "hello");
196        assert_eq!(strip_control_codes("he\rllo"), "hello");
197        assert_eq!(strip_control_codes("\x07\x08\x0B\x0C\rhello"), "hello");
198    }
199
200    #[test]
201    fn test_escape_control_codes() {
202        assert_eq!(escape_control_codes("hello"), "hello");
203        assert_eq!(escape_control_codes("he\x07llo"), "he\\allo");
204        assert_eq!(escape_control_codes("he\x08llo"), "he\\bllo");
205        assert_eq!(escape_control_codes("he\x0Bllo"), "he\\vllo");
206        assert_eq!(escape_control_codes("he\x0Cllo"), "he\\fllo");
207        assert_eq!(escape_control_codes("he\rllo"), "he\\rllo");
208    }
209}