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