pub(crate) fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1B' {
match chars.next() {
Some('[') => {
while let Some(&p) = chars.peek() {
chars.next();
if matches!(p, '\x40'..='\x7E') {
break;
}
}
}
Some(']') | Some('P') | Some('X') | Some('^') | Some('_') => {
while let Some(&p) = chars.peek() {
chars.next();
if p == '\x07' {
break;
}
if p == '\x1B' {
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
}
}
Some(_) => {
}
None => break,
}
} else if c == '\t' || c == '\n' || !c.is_control() {
out.push(c);
}
}
out
}
#[cfg(test)]
mod tests {
use super::strip_ansi;
#[test]
fn strip_ansi_removes_csi_clear_screen() {
assert_eq!(strip_ansi("\x1B[2J\x1B[Hhello"), "hello");
}
#[test]
fn strip_ansi_removes_osc_bel_terminated() {
assert_eq!(strip_ansi("\x1B]0;evil-title\x07ok"), "ok");
}
#[test]
fn strip_ansi_removes_osc_st_terminated() {
assert_eq!(strip_ansi("\x1B]0;evil-title\x1B\\ok"), "ok");
}
#[test]
fn strip_ansi_removes_dcs_family() {
assert_eq!(strip_ansi("\x1BPpayload\x1B\\after"), "after");
assert_eq!(strip_ansi("\x1B_apc-data\x07after"), "after");
assert_eq!(strip_ansi("\x1B^pm-data\x07after"), "after");
assert_eq!(strip_ansi("\x1BXsos-data\x1B\\after"), "after");
}
#[test]
fn strip_ansi_preserves_tab_and_newline() {
assert_eq!(strip_ansi("a\tb\nc"), "a\tb\nc");
}
#[test]
fn strip_ansi_strips_bare_control_bytes() {
assert_eq!(strip_ansi("bell\x07ok"), "bellok");
assert_eq!(strip_ansi("nul\0ok"), "nulok");
}
#[test]
fn strip_ansi_two_byte_esc_sequence() {
assert_eq!(strip_ansi("a\x1BMb"), "ab");
}
#[test]
fn strip_ansi_does_not_panic_on_unterminated_sequence() {
assert_eq!(strip_ansi("trailing\x1B"), "trailing");
assert_eq!(strip_ansi("a\x1B[999"), "a");
}
#[test]
fn strip_ansi_passes_through_plain_text() {
assert_eq!(strip_ansi("hello world"), "hello world");
assert_eq!(strip_ansi("emoji ✅ ok"), "emoji ✅ ok");
}
}