use ansi_str::AnsiStr;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
const ANSI_RESET: &str = "\u{1b}[0m";
#[inline(always)]
pub fn measure_text_width(s: &str) -> usize {
s.ansi_strip().width()
}
pub fn split_line_by_delimiter(line: &str, delimiter: char) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();
let mut current_line = String::default();
let iter = console::AnsiCodeIterator::new(line);
for (str_slice, is_esc) in iter {
if is_esc {
current_line.push_str(str_slice);
} else {
let mut split = str_slice.split(delimiter);
let first = split
.next()
.expect("split always produces at least one value");
current_line.push_str(first);
for text in split {
lines.push(current_line);
current_line = text.to_string();
}
}
}
lines.push(current_line);
fix_style_in_split_str(lines.as_mut());
lines
}
pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) {
let mut head = String::with_capacity(word.len());
let mut tail = String::with_capacity(word.len());
let mut head_len = 0;
let mut head_len_last = 0;
let mut escape_count_last = 0;
let mut escapes = Vec::new();
let mut iter = console::AnsiCodeIterator::new(word);
for (str_slice, is_esc) in iter.by_ref() {
if is_esc {
escapes.push(str_slice);
if str_slice == ANSI_RESET {
escapes.clear();
}
}
let slice_len = match is_esc {
true => 0,
false => str_slice.width(),
};
if head_len + slice_len <= allowed_width {
head.push_str(str_slice);
head_len += slice_len;
if !is_esc {
head_len_last = head.len();
escape_count_last = escapes.len();
}
} else {
assert!(!is_esc);
let mut graphmes = str_slice.graphemes(true).peekable();
while let Some(c) = graphmes.peek() {
let character_width = c.width();
if allowed_width < head_len + character_width {
break;
}
head_len += character_width;
let c = graphmes.next().unwrap();
head.push_str(c);
head_len_last = head.len();
escape_count_last = escapes.len();
}
head.truncate(head_len_last);
if escape_count_last != 0 {
head.push_str(ANSI_RESET);
}
for esc in escapes {
tail.push_str(esc);
}
let remaining: String = graphmes.collect();
tail.push_str(&remaining);
break;
}
}
iter.for_each(|s| tail.push_str(s.0));
(head, tail)
}
pub fn fix_style_in_split_str(words: &mut [String]) {
let mut escapes: Vec<String> = Vec::new();
for word in words {
let prepend = if !escapes.is_empty() {
Some(escapes.join(""))
} else {
None
};
let iter = console::AnsiCodeIterator::new(word)
.filter(|(_, is_esc)| *is_esc)
.map(|v| v.0);
for esc in iter {
if esc == ANSI_RESET {
escapes.clear()
} else {
escapes.push(esc.to_string())
}
}
if let Some(prepend) = prepend {
word.insert_str(0, &prepend);
}
if !escapes.is_empty() {
word.push_str(ANSI_RESET);
}
}
}
#[cfg(test)]
mod test {
#[test]
fn ansi_aware_split_test() {
use super::split_line_by_delimiter;
let text = "\u{1b}[1m head [ middle [ tail \u{1b}[0m[ after";
let split = split_line_by_delimiter(text, '[');
assert_eq!(
split,
[
"\u{1b}[1m head \u{1b}[0m",
"\u{1b}[1m middle \u{1b}[0m",
"\u{1b}[1m tail \u{1b}[0m",
" after"
]
)
}
#[test]
#[cfg(not(feature = "custom_styling"))]
fn measure_text_width_osc8_test() {
use unicode_width::UnicodeWidthStr;
use super::measure_text_width;
let text = "\x1b]8;;https://github.com\x1b\\This is a link\x1b]8;;\x1b";
let width = measure_text_width(text);
assert_eq!(text.width(), 41);
assert_eq!(width, 14);
}
}