#![doc = include_str!("../README.md")]
const ESC: char = '\x1b';
pub fn ansi_width(s: &str) -> usize {
let mut width = 0;
let mut chars = s.chars();
#[allow(clippy::while_let_on_iterator)]
while let Some(c) = chars.next() {
if c == ESC {
let Some(c) = chars.next() else {
break;
};
match c {
'\\' => {
}
'[' => while !matches!(chars.next(), Some('\x40'..='\x7C') | None) {},
']' => {
let mut last = c;
while let Some(new) = chars.next() {
if new == '\x07' || (new == '\\' && last == ESC) {
break;
}
last = new;
}
}
_ => {
width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
}
}
} else {
width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
}
}
width
}
#[cfg(test)]
mod tests {
use super::ansi_width;
#[test]
fn ascii() {
assert_eq!(ansi_width(""), 0);
assert_eq!(ansi_width("hello"), 5);
assert_eq!(ansi_width("hello world"), 11);
assert_eq!(ansi_width("WOW!"), 4);
}
#[test]
fn c0_characters() {
assert_eq!(ansi_width("\x07"), 0);
assert_eq!(ansi_width("\x08"), 0);
assert_eq!(ansi_width("\t"), 0);
}
#[test]
fn some_escape_codes() {
assert_eq!(ansi_width("\u{1b}[34mHello\u{1b}[0m"), 5);
assert_eq!(ansi_width("\u{1b}[31mRed\u{1b}[0m"), 3);
}
#[test]
fn hyperlink() {
assert_eq!(
ansi_width("\x1b]8;;http://example.com\x1b\\This is a link\x1b]8;;\x1b\\"),
14
)
}
#[test]
fn nonstandard_hyperlink() {
assert_eq!(
ansi_width("\x1b]8;;file://coreutils.md\x07coreutils.md\x1b]8;;\x07"),
12
)
}
}