use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub fn ch_width(c: char) -> usize {
UnicodeWidthChar::width(c).unwrap_or(0)
}
pub fn str_width(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
pub fn wrap_line(s: &str, width: usize) -> Vec<String> {
let width = width.max(1);
let mut lines = Vec::new();
for logical in s.split('\n') {
let mut cur = String::new();
let mut cur_len = 0usize;
for word in logical.split_inclusive(' ') {
let wlen = str_width(word);
if cur_len + wlen > width && cur_len > 0 {
lines.push(std::mem::take(&mut cur));
cur_len = 0;
}
if wlen > width {
for ch in word.chars() {
let cw = ch_width(ch);
if cur_len + cw > width && cur_len > 0 {
lines.push(std::mem::take(&mut cur));
cur_len = 0;
}
cur.push(ch);
cur_len += cw;
}
} else {
cur.push_str(word);
cur_len += wlen;
}
}
lines.push(cur);
}
lines
}
pub fn strip_ansi(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c != '\u{1b}' {
out.push(c);
continue;
}
match chars.next() {
Some('[') => {
for c in chars.by_ref() {
if ('\u{40}'..='\u{7e}').contains(&c) {
break;
}
}
}
Some(']') => {
while let Some(c) = chars.next() {
if c == '\u{7}' {
break;
}
if c == '\u{1b}' && chars.peek() == Some(&'\\') {
chars.next();
break;
}
}
}
_ => {}
}
}
out
}
#[cfg(test)]
mod tests {
use super::{ch_width, str_width, wrap_line};
#[test]
fn wrap_line_is_byte_identical_for_ascii() {
assert_eq!(
wrap_line("cargo build --release", 11),
["cargo ", "build ", "--release"]
);
assert_eq!(
wrap_line("grep -rn foo /very/long/path/that/keeps/going", 20),
["grep -rn foo ", "/very/long/path/that", "/keeps/going"]
);
assert_eq!(
wrap_line("aaaaaaaaaaaaaaaaaaaaaaaaa", 10),
["aaaaaaaaaa", "aaaaaaaaaa", "aaaaa"]
);
assert_eq!(wrap_line("a\nb", 40), ["a", "b"]);
assert_eq!(wrap_line("", 5), [""]);
assert_eq!(
wrap_line("one two three four", 9),
["one two ", "three ", "four"]
);
}
#[test]
fn wrap_line_wraps_by_cells_not_chars() {
assert_eq!(wrap_line("日本語版", 4), ["日本", "語版"]);
}
#[test]
fn a_combining_mark_costs_no_column() {
assert_eq!(wrap_line("e\u{0301}xy", 3), ["e\u{0301}xy"]);
}
#[test]
fn a_cjk_string_measures_in_cells_not_chars_or_bytes() {
assert_eq!(str_width("日本"), 4);
assert_eq!("日本".chars().count(), 2, "the char count is NOT the width");
assert_eq!("日本".len(), 6, "and neither is the byte count");
assert_eq!(ch_width('日'), 2);
assert_eq!(ch_width('\u{0301}'), 0);
}
}