fn ansi_sequence_len(chars: &[char]) -> Option<usize> {
let first = chars[0];
if first != '\u{1b}' && first != '\u{9b}' {
return None;
}
let mut i = 1;
while i < chars.len()
&& matches!(chars[i], '[' | ']' | '(' | ')' | '#' | ';' | '?')
{
i += 1;
}
let body_start = i;
{
let mut j = body_start;
let is_param_char = |c: char| {
c.is_ascii_alphanumeric()
|| matches!(
c,
'-' | '/' | '#' | '&' | '.' | ':' | '=' | '?' | '%' | '@' | '~' | '_'
)
};
if j < chars.len() && chars[j] == ';' {
while j < chars.len() && chars[j] == ';' {
let mut k = j + 1;
let start = k;
while k < chars.len() && is_param_char(chars[k]) && chars[k] != ';' {
k += 1;
}
if k == start {
break;
}
j = k;
}
} else if j < chars.len() && chars[j].is_ascii_alphanumeric() {
while j < chars.len() && chars[j].is_ascii_alphanumeric() {
j += 1;
}
while j < chars.len() && chars[j] == ';' {
j += 1;
while j < chars.len() && is_param_char(chars[j]) && chars[j] != ';' {
j += 1;
}
}
}
if j < chars.len() {
if chars[j] == '\u{07}' || chars[j] == '\u{9c}' {
return Some(j + 1);
}
if chars[j] == '\u{1b}' && j + 1 < chars.len() && chars[j + 1] == '\u{5c}'
{
return Some(j + 2);
}
}
}
{
let is_term = |c: char| {
c.is_ascii_digit()
|| ('A'..='P').contains(&c)
|| ('R'..='T').contains(&c)
|| c == 'Z'
|| c == 'c'
|| ('f'..='n').contains(&c)
|| ('q'..='u').contains(&c)
|| c == 'y'
|| matches!(c, '=' | '>' | '<' | '~')
};
let mut j = body_start;
let mut digits = 0;
while j < chars.len() && chars[j].is_ascii_digit() && digits < 4 {
j += 1;
digits += 1;
}
if digits > 0 {
while j < chars.len() && chars[j] == ';' {
j += 1;
let mut d = 0;
while j < chars.len() && chars[j].is_ascii_digit() && d < 4 {
j += 1;
d += 1;
}
}
}
if j < chars.len() && is_term(chars[j]) {
return Some(j + 1);
}
let mut t = j;
while t > body_start {
t -= 1;
if is_term(chars[t]) {
return Some(t + 1);
}
}
}
None
}
pub fn strip_vt_control_characters(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < chars.len() {
if let Some(len) = ansi_sequence_len(&chars[i..]) {
i += len;
} else {
out.push(chars[i]);
i += 1;
}
}
out
}
fn is_zero_width_code_point(code: u32) -> bool {
code <= 0x1f || (0x7f..=0x9f).contains(&code) || (0x300..=0x36f).contains(&code) || (0x200b..=0x200f).contains(&code) || (0x20d0..=0x20ff).contains(&code)
|| (0xfe00..=0xfe0f).contains(&code) || (0xfe20..=0xfe2f).contains(&code) || (0xe0100..=0xe01ef).contains(&code) }
pub fn is_full_width_code_point(code: u32) -> bool {
code >= 0x1100
&& (code <= 0x115f || code == 0x2329 || code == 0x232a || ((0x2e80..=0x3247).contains(&code) && code != 0x303f)
|| (0x3250..=0x4dbf).contains(&code)
|| (0x4e00..=0xa4c6).contains(&code)
|| (0xa960..=0xa97c).contains(&code)
|| (0xac00..=0xd7a3).contains(&code)
|| (0xf900..=0xfaff).contains(&code)
|| (0xfe10..=0xfe19).contains(&code)
|| (0xfe30..=0xfe6b).contains(&code)
|| (0xff01..=0xff60).contains(&code)
|| (0xffe0..=0xffe6).contains(&code)
|| (0x1b000..=0x1b001).contains(&code)
|| (0x1f200..=0x1f251).contains(&code)
|| (0x1f300..=0x1f64f).contains(&code)
|| (0x20000..=0x3fffd).contains(&code))
}
pub fn get_string_width(s: &str, remove_control_chars: bool) -> usize {
let stripped;
let s = if remove_control_chars {
stripped = strip_vt_control_characters(s);
&stripped
} else {
s
};
let normalized = nfc_normalize(s);
let mut width = 0;
for ch in normalized.chars() {
let code = ch as u32;
if is_full_width_code_point(code) {
width += 2;
} else if !is_zero_width_code_point(code) {
width += 1;
}
}
width
}
fn nfc_normalize(s: &str) -> std::borrow::Cow<'_, str> {
use unicode_normalization::UnicodeNormalization;
if s.is_ascii() {
return std::borrow::Cow::Borrowed(s);
}
std::borrow::Cow::Owned(s.nfc().collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn widths() {
assert_eq!(get_string_width("hello", true), 5);
assert_eq!(get_string_width("\u{1b}[31mhi\u{1b}[39m", true), 2);
assert_eq!(get_string_width("デノ", true), 4);
assert_eq!(get_string_width("a\u{300}", true), 1); }
#[test]
fn strip_ansi() {
assert_eq!(
strip_vt_control_characters("\u{1b}[4mcake\u{1b}[24m"),
"cake"
);
assert_eq!(strip_vt_control_characters("no escapes"), "no escapes");
}
#[test]
fn strip_truncated_escapes() {
assert_eq!(strip_vt_control_characters("\u{1b}[5"), "");
assert_eq!(strip_vt_control_characters("\u{1b}[1;2"), "");
assert_eq!(strip_vt_control_characters("a\u{1b}[5b"), "ab");
assert_eq!(strip_vt_control_characters("\u{1b}[1;"), ";");
assert_eq!(get_string_width("\u{1b}[5", true), 0);
}
}