Skip to main content

cersei_compression/
ansi.rs

1//! ANSI escape handling.
2//!
3//! Credits: adapted from rtk (Rust Token Killer) — `rtk/src/core/utils.rs`.
4//! MIT © Patrick Szymkowiak. See LICENSE.
5
6use once_cell::sync::Lazy;
7use regex::Regex;
8
9static ANSI_RE: Lazy<Regex> = Lazy::new(|| {
10    // Covers CSI sequences (color/style) and common OSC/ESC codes.
11    Regex::new(r"\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)").unwrap()
12});
13
14pub fn strip_ansi(text: &str) -> String {
15    ANSI_RE.replace_all(text, "").into_owned()
16}
17
18/// Unicode-safe char truncation — keeps at most `max_len` chars, appends `...`
19/// when the input is longer.
20pub fn truncate(s: &str, max_len: usize) -> String {
21    let char_count = s.chars().count();
22    if char_count <= max_len {
23        s.to_string()
24    } else if max_len < 3 {
25        "...".to_string()
26    } else {
27        format!("{}...", s.chars().take(max_len - 3).collect::<String>())
28    }
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn strip_ansi_basic() {
37        assert_eq!(strip_ansi("\x1b[31mError\x1b[0m"), "Error");
38        assert_eq!(strip_ansi("plain"), "plain");
39        assert_eq!(strip_ansi("\x1b[1m\x1b[32mOK\x1b[0m\x1b[0m"), "OK");
40    }
41
42    #[test]
43    fn truncate_unicode() {
44        assert_eq!(truncate("hello world", 8), "hello...");
45        assert_eq!(truncate("hi", 10), "hi");
46        assert_eq!(truncate("abc", 3), "abc");
47        assert_eq!(truncate("hello world", 3), "...");
48        // Thai, multi-byte
49        assert_eq!(truncate("สวัสดีครับ", 5).chars().count(), 5);
50    }
51}