Skip to main content

rusty_rich/
control.rs

1//! Terminal control sequence generation — equivalent to Rich's `control.py`.
2//!
3//! Provides a unified [`Control`] type for composing terminal escape sequences:
4//! cursor movement, screen manipulation, window titles, and bells. Individual
5//! control codes can be combined and rendered as ANSI escape sequences.
6//!
7//! # Quick Example
8//!
9//! ```rust
10//! use rusty_rich::control::{Control, control_bell, control_home};
11//!
12//! let bell = control_bell();
13//! assert_eq!(bell.to_ansi(), "\x07");
14//!
15//! let combined = Control::new(&["\x1b[H", "\x1b[2J"]);
16//! ```
17
18// ---------------------------------------------------------------------------
19// Control — composable control sequence
20// ---------------------------------------------------------------------------
21
22/// A composable terminal control sequence.
23///
24/// A `Control` holds one or more ANSI escape sequences and can render them
25/// as raw ANSI bytes or as a [`crate::segment::Segment`] with control codes.
26///
27/// This is the Rust equivalent of Python Rich's `Control` class.
28///
29/// # Examples
30///
31/// ```rust
32/// use rusty_rich::control::{Control, control_home, control_clear};
33///
34/// // Named constructors
35/// let home = control_home();
36///
37/// // Build combined controls
38/// let clear_and_home = Control::new(&["\x1b[2J", "\x1b[H"]);
39///
40/// // Cursor positioning
41/// let go_to = Control::cursor_to(10, 5);
42/// ```
43#[derive(Debug, Clone)]
44pub struct Control {
45    sequences: Vec<String>,
46}
47
48impl Control {
49    /// Create a `Control` from a slice of raw ANSI escape sequences.
50    pub fn new(sequences: &[&str]) -> Self {
51        Self {
52            sequences: sequences.iter().map(|s| s.to_string()).collect(),
53        }
54    }
55
56    /// Ring the terminal bell.
57    pub fn bell() -> Self {
58        Self::new(&["\x07"])
59    }
60
61    /// Move cursor to home position.
62    pub fn home() -> Self {
63        Self::new(&["\x1b[H"])
64    }
65
66    /// Clear the entire screen.
67    pub fn clear() -> Self {
68        Self::new(&["\x1b[2J"])
69    }
70
71    /// Clear screen and move cursor to home.
72    pub fn clear_home() -> Self {
73        Self::new(&["\x1b[2J", "\x1b[H"])
74    }
75
76    /// Move cursor up by `n` rows.
77    pub fn cursor_up(n: u16) -> Self {
78        Self::new(&[&format!("\x1b[{n}A")])
79    }
80
81    /// Move cursor down by `n` rows.
82    pub fn cursor_down(n: u16) -> Self {
83        Self::new(&[&format!("\x1b[{n}B")])
84    }
85
86    /// Move cursor forward by `n` columns.
87    pub fn cursor_forward(n: u16) -> Self {
88        Self::new(&[&format!("\x1b[{n}C")])
89    }
90
91    /// Move cursor back by `n` columns.
92    pub fn cursor_back(n: u16) -> Self {
93        Self::new(&[&format!("\x1b[{n}D")])
94    }
95
96    /// Move cursor to an absolute position (1-based row, column).
97    pub fn cursor_to(row: u16, col: u16) -> Self {
98        Self::new(&[&format!("\x1b[{row};{col}H")])
99    }
100
101    /// Move cursor to a specific row (1-based).
102    pub fn cursor_to_row(row: u16) -> Self {
103        Self::new(&[&format!("\x1b[{row}d")])
104    }
105
106    /// Move cursor to a specific column (1-based).
107    pub fn cursor_to_column(col: u16) -> Self {
108        Self::new(&[&format!("\x1b[{col}G")])
109    }
110
111    /// Enable or disable the alternate screen buffer.
112    pub fn alt_screen(enable: bool) -> Self {
113        if enable {
114            Self::new(&["\x1b[?1049h"])
115        } else {
116            Self::new(&["\x1b[?1049l"])
117        }
118    }
119
120    /// Show or hide the cursor.
121    pub fn show_cursor(show: bool) -> Self {
122        if show {
123            Self::new(&["\x1b[?25h"])
124        } else {
125            Self::new(&["\x1b[?25l"])
126        }
127    }
128
129    /// Set the terminal window title.
130    pub fn title(title: impl Into<String>) -> Self {
131        let t: String = title.into();
132        Self::new(&[&format!("\x1b]0;{t}\x07")])
133    }
134
135    /// Erase from cursor to end of line.
136    pub fn erase_end_line() -> Self {
137        Self::new(&["\x1b[K"])
138    }
139
140    /// Erase from cursor to beginning of line.
141    pub fn erase_start_line() -> Self {
142        Self::new(&["\x1b[1K"])
143    }
144
145    /// Erase the entire current line.
146    pub fn erase_line() -> Self {
147        Self::new(&["\x1b[2K"])
148    }
149
150    /// Erase from cursor to end of screen.
151    pub fn erase_end_screen() -> Self {
152        Self::new(&["\x1b[J"])
153    }
154
155    /// Erase from cursor to beginning of screen.
156    pub fn erase_start_screen() -> Self {
157        Self::new(&["\x1b[1J"])
158    }
159
160    /// Insert `n` blank lines at the cursor position.
161    pub fn insert_lines(n: u16) -> Self {
162        Self::new(&[&format!("\x1b[{n}L")])
163    }
164
165    /// Delete `n` lines at the cursor position.
166    pub fn delete_lines(n: u16) -> Self {
167        Self::new(&[&format!("\x1b[{n}M")])
168    }
169
170    /// Carriage return.
171    pub fn carriage_return() -> Self {
172        Self::new(&["\r"])
173    }
174
175    /// Newline.
176    pub fn newline() -> Self {
177        Self::new(&["\n"])
178    }
179
180    /// Convert this `Control` to a single ANSI escape sequence string.
181    pub fn to_ansi(&self) -> String {
182        self.sequences.concat()
183    }
184
185    /// Return the individual ANSI sequences.
186    pub fn sequences(&self) -> &[String] {
187        &self.sequences
188    }
189
190    /// Return the number of control sequences in this command.
191    pub fn len(&self) -> usize {
192        self.sequences.len()
193    }
194
195    /// Return `true` if this control has no sequences.
196    pub fn is_empty(&self) -> bool {
197        self.sequences.is_empty()
198    }
199}
200
201// ---------------------------------------------------------------------------
202// Convenience functions
203// ---------------------------------------------------------------------------
204
205/// Ring the terminal bell.
206pub fn control_bell() -> Control { Control::bell() }
207
208/// Move cursor to home position.
209pub fn control_home() -> Control { Control::home() }
210
211/// Clear the entire screen.
212pub fn control_clear() -> Control { Control::clear() }
213
214/// Move cursor to an absolute position.
215pub fn control_move_to(row: u16, col: u16) -> Control { Control::cursor_to(row, col) }
216
217/// Show or hide the cursor.
218pub fn control_show_cursor(show: bool) -> Control { Control::show_cursor(show) }
219
220/// Set terminal window title.
221pub fn control_title(title: impl Into<String>) -> Control { Control::title(title) }
222
223// ---------------------------------------------------------------------------
224// Strip/escape control characters
225// ---------------------------------------------------------------------------
226
227/// Strip control characters (bell, backspace, vertical tab, form feed) from
228/// a string.
229///
230/// Equivalent to Python Rich's `strip_control_codes()`.
231pub fn strip_control_codes(text: &str) -> String {
232    text.chars()
233        .filter(|&c| !matches!(c, '\x07' | '\x08' | '\x0b' | '\x0c'))
234        .collect()
235}
236
237/// Escape control characters in a string, replacing them with visible
238/// representations like `\\a`, `\\b`, etc.
239pub fn escape_control_codes(text: &str) -> String {
240    text.chars()
241        .map(|c| match c {
242            '\x07' => "\\a".to_string(),
243            '\x08' => "\\b".to_string(),
244            '\x0b' => "\\v".to_string(),
245            '\x0c' => "\\f".to_string(),
246            '\r' => "\\r".to_string(),
247            '\n' => "\\n".to_string(),
248            '\t' => "\\t".to_string(),
249            other => other.to_string(),
250        })
251        .collect()
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_control_bell() {
260        let c = Control::bell();
261        assert_eq!(c.to_ansi(), "\x07");
262    }
263
264    #[test]
265    fn test_control_home() {
266        let c = Control::home();
267        assert_eq!(c.to_ansi(), "\x1b[H");
268    }
269
270    #[test]
271    fn test_control_clear() {
272        let c = Control::clear();
273        assert_eq!(c.to_ansi(), "\x1b[2J");
274    }
275
276    #[test]
277    fn test_control_clear_home() {
278        let c = Control::clear_home();
279        assert_eq!(c.to_ansi(), "\x1b[2J\x1b[H");
280    }
281
282    #[test]
283    fn test_control_cursor_to() {
284        let c = Control::cursor_to(10, 5);
285        assert_eq!(c.to_ansi(), "\x1b[10;5H");
286    }
287
288    #[test]
289    fn test_control_cursor_up() {
290        let c = Control::cursor_up(5);
291        assert_eq!(c.to_ansi(), "\x1b[5A");
292    }
293
294    #[test]
295    fn test_control_show_cursor() {
296        let c = Control::show_cursor(true);
297        assert_eq!(c.to_ansi(), "\x1b[?25h");
298        let c = Control::show_cursor(false);
299        assert_eq!(c.to_ansi(), "\x1b[?25l");
300    }
301
302    #[test]
303    fn test_control_alt_screen() {
304        let c = Control::alt_screen(true);
305        assert_eq!(c.to_ansi(), "\x1b[?1049h");
306        let c = Control::alt_screen(false);
307        assert_eq!(c.to_ansi(), "\x1b[?1049l");
308    }
309
310    #[test]
311    fn test_control_title() {
312        let c = Control::title("My App");
313        assert_eq!(c.to_ansi(), "\x1b]0;My App\x07");
314    }
315
316    #[test]
317    fn test_control_erase() {
318        assert_eq!(Control::erase_line().to_ansi(), "\x1b[2K");
319        assert_eq!(Control::erase_end_line().to_ansi(), "\x1b[K");
320        assert_eq!(Control::erase_start_line().to_ansi(), "\x1b[1K");
321    }
322
323    #[test]
324    fn test_control_insert_delete_lines() {
325        assert_eq!(Control::insert_lines(3).to_ansi(), "\x1b[3L");
326        assert_eq!(Control::delete_lines(2).to_ansi(), "\x1b[2M");
327    }
328
329    #[test]
330    fn test_control_len_empty() {
331        assert_eq!(Control::bell().len(), 1);
332        assert_eq!(Control::clear_home().len(), 2);
333        assert!(!Control::bell().is_empty());
334    }
335
336    #[test]
337    fn test_strip_control_codes() {
338        assert_eq!(strip_control_codes("hello\x07world"), "helloworld");
339        assert_eq!(strip_control_codes("normal text"), "normal text");
340    }
341
342    #[test]
343    fn test_escape_control_codes() {
344        let result = escape_control_codes("\x07\x08\x0b\x0c");
345        assert_eq!(result, "\\a\\b\\v\\f");
346    }
347
348    #[test]
349    fn test_convenience_functions() {
350        assert_eq!(control_bell().to_ansi(), "\x07");
351        assert_eq!(control_home().to_ansi(), "\x1b[H");
352        assert_eq!(control_move_to(3, 8).to_ansi(), "\x1b[3;8H");
353    }
354}