pub(crate) fn wrap_ansi_escape_each_line(ansi: &ANSI, body: impl AsRef<str>) -> String {
let ansi_escape = ansi.to_str();
body.as_ref()
.split('\n')
.map(|line| line.replace(RESET, &format!("{RESET}{ansi_escape}"))) .map(|line| format!("{ansi_escape}{line}{RESET}"))
.map(|line| line.replace(&format!("{ansi_escape}{ansi_escape}"), ansi_escape)) .map(|line| line.replace(&format!("{ansi_escape}{RESET}"), "")) .collect::<Vec<String>>()
.join("\n")
}
const RESET: &str = "\x1B[0m";
const RED: &str = "\x1B[0;31m";
const YELLOW: &str = "\x1B[0;33m";
const BOLD_CYAN: &str = "\x1B[1;36m";
const BOLD_PURPLE: &str = "\x1B[1;35m";
const BOLD_UNDERLINE_CYAN: &str = "\x1B[1;4;36m";
const DIM: &str = "\x1B[2;1m";
#[derive(Debug)]
#[allow(clippy::upper_case_acronyms)]
pub(crate) enum ANSI {
Dim,
Red,
Yellow,
BoldCyan,
BoldUnderlineCyan,
BoldPurple,
}
impl ANSI {
fn to_str(&self) -> &'static str {
match self {
ANSI::Dim => DIM,
ANSI::Red => RED,
ANSI::Yellow => YELLOW,
ANSI::BoldCyan => BOLD_CYAN,
ANSI::BoldPurple => BOLD_PURPLE,
ANSI::BoldUnderlineCyan => BOLD_UNDERLINE_CYAN,
}
}
}
pub fn strip_ansi(contents: impl AsRef<str>) -> String {
let contents = contents.as_ref();
let mut result = String::with_capacity(contents.len());
let mut in_sequence = false;
for char in contents.chars() {
if char == '\x1B' {
in_sequence = true;
} else if in_sequence {
if char == 'm' {
in_sequence = false;
}
} else {
result.push(char);
}
}
result.shrink_to_fit();
result
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_strip_ansi() {
for color in [
ANSI::Dim,
ANSI::Red,
ANSI::Yellow,
ANSI::BoldCyan,
ANSI::BoldPurple,
ANSI::BoldUnderlineCyan,
] {
match &color {
ANSI::Dim => (),
ANSI::Red => (),
ANSI::Yellow => (),
ANSI::BoldCyan => (),
ANSI::BoldUnderlineCyan => (),
ANSI::BoldPurple => (),
}
let input = "Hello world";
let output = wrap_ansi_escape_each_line(&color, input);
assert_eq!(input, &strip_ansi(&output));
}
}
#[test]
fn empty_line() {
let actual = wrap_ansi_escape_each_line(&ANSI::Red, "\n");
let expected = String::from("\n");
assert_eq!(expected, actual);
}
#[test]
fn handles_nested_color_at_start() {
let start = wrap_ansi_escape_each_line(&ANSI::BoldCyan, "hello");
let out = wrap_ansi_escape_each_line(&ANSI::Red, format!("{start} world"));
let expected = format!("{RED}{BOLD_CYAN}hello{RESET}{RED} world{RESET}");
assert_eq!(expected, out);
}
#[test]
fn handles_nested_color_in_middle() {
let middle = wrap_ansi_escape_each_line(&ANSI::BoldCyan, "middle");
let out = wrap_ansi_escape_each_line(&ANSI::Red, format!("hello {middle} color"));
let expected = format!("{RED}hello {BOLD_CYAN}middle{RESET}{RED} color{RESET}");
assert_eq!(expected, out);
}
#[test]
fn handles_nested_color_at_end() {
let end = wrap_ansi_escape_each_line(&ANSI::BoldCyan, "world");
let out = wrap_ansi_escape_each_line(&ANSI::Red, format!("hello {end}"));
let expected = format!("{RED}hello {BOLD_CYAN}world{RESET}");
assert_eq!(expected, out);
}
#[test]
fn handles_double_nested_color() {
let inner = wrap_ansi_escape_each_line(&ANSI::BoldCyan, "inner");
let outer = wrap_ansi_escape_each_line(&ANSI::Red, format!("outer {inner}"));
let out = wrap_ansi_escape_each_line(&ANSI::Yellow, format!("hello {outer}"));
let expected = format!("{YELLOW}hello {RED}outer {BOLD_CYAN}inner{RESET}");
assert_eq!(expected, out);
}
#[test]
fn splits_newlines() {
let actual = wrap_ansi_escape_each_line(&ANSI::Red, "hello\nworld");
let expected = format!("{RED}hello{RESET}\n{RED}world{RESET}");
assert_eq!(expected, actual);
}
#[test]
fn simple_case() {
let actual = wrap_ansi_escape_each_line(&ANSI::Red, "hello world");
assert_eq!(format!("{RED}hello world{RESET}"), actual);
}
}