const CSI: (char, char) = ('\x1b', '[');
const ANSI_FINAL_BYTE: std::ops::RangeInclusive<char> = '\x40'..='\x7e';
#[inline]
pub(crate) fn skip_ansi_escape_sequence<I: Iterator<Item = char>>(ch: char, chars: &mut I) -> bool {
if ch == CSI.0 && chars.next() == Some(CSI.1) {
for ch in chars {
if ANSI_FINAL_BYTE.contains(&ch) {
return true;
}
}
}
false
}
#[cfg(feature = "unicode-width")]
#[inline]
fn ch_width(ch: char) -> usize {
unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0)
}
#[cfg(not(feature = "unicode-width"))]
const DOUBLE_WIDTH_CUTOFF: char = '\u{1100}';
#[cfg(not(feature = "unicode-width"))]
#[inline]
fn ch_width(ch: char) -> usize {
if ch < DOUBLE_WIDTH_CUTOFF {
1
} else {
2
}
}
pub fn display_width(text: &str) -> usize {
let mut chars = text.chars();
let mut width = 0;
while let Some(ch) = chars.next() {
if skip_ansi_escape_sequence(ch, &mut chars) {
continue;
}
width += ch_width(ch);
}
width
}
pub trait Fragment: std::fmt::Debug {
fn width(&self) -> usize;
fn whitespace_width(&self) -> usize;
fn penalty_width(&self) -> usize;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Word<'a> {
pub word: &'a str,
pub whitespace: &'a str,
pub penalty: &'a str,
pub(crate) width: usize,
}
impl std::ops::Deref for Word<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.word
}
}
impl<'a> Word<'a> {
pub fn from(word: &str) -> Word<'_> {
let trimmed = word.trim_end_matches(' ');
Word {
word: trimmed,
width: display_width(&trimmed),
whitespace: &word[trimmed.len()..],
penalty: "",
}
}
pub fn break_apart<'b>(&'b self, line_width: usize) -> impl Iterator<Item = Word<'a>> + 'b {
let mut char_indices = self.word.char_indices();
let mut offset = 0;
let mut width = 0;
std::iter::from_fn(move || {
while let Some((idx, ch)) = char_indices.next() {
if skip_ansi_escape_sequence(ch, &mut char_indices.by_ref().map(|(_, ch)| ch)) {
continue;
}
if width > 0 && width + ch_width(ch) > line_width {
let word = Word {
word: &self.word[offset..idx],
width: width,
whitespace: "",
penalty: "",
};
offset = idx;
width = ch_width(ch);
return Some(word);
}
width += ch_width(ch);
}
if offset < self.word.len() {
let word = Word {
word: &self.word[offset..],
width: width,
whitespace: self.whitespace,
penalty: self.penalty,
};
offset = self.word.len();
return Some(word);
}
None
})
}
}
impl Fragment for Word<'_> {
#[inline]
fn width(&self) -> usize {
self.width
}
#[inline]
fn whitespace_width(&self) -> usize {
self.whitespace.len()
}
#[inline]
fn penalty_width(&self) -> usize {
self.penalty.len()
}
}
pub fn break_words<'a, I>(words: I, line_width: usize) -> Vec<Word<'a>>
where
I: IntoIterator<Item = Word<'a>>,
{
let mut shortened_words = Vec::new();
for word in words {
if word.width() > line_width {
shortened_words.extend(word.break_apart(line_width));
} else {
shortened_words.push(word);
}
}
shortened_words
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "unicode-width")]
use unicode_width::UnicodeWidthChar;
#[test]
fn skip_ansi_escape_sequence_works() {
let blue_text = "\u{1b}[34mHello\u{1b}[0m";
let mut chars = blue_text.chars();
let ch = chars.next().unwrap();
assert!(skip_ansi_escape_sequence(ch, &mut chars));
assert_eq!(chars.next(), Some('H'));
}
#[test]
fn emojis_have_correct_width() {
use unic_emoji_char::is_emoji;
for ch in '\u{1}'..'\u{FF}' {
if is_emoji(ch) {
let desc = format!("{:?} U+{:04X}", ch, ch as u32);
#[cfg(feature = "unicode-width")]
assert_eq!(ch.width().unwrap(), 1, "char: {}", desc);
#[cfg(not(feature = "unicode-width"))]
assert_eq!(ch_width(ch), 1, "char: {}", desc);
}
}
for ch in '\u{FF}'..'\u{2FFFF}' {
if is_emoji(ch) {
let desc = format!("{:?} U+{:04X}", ch, ch as u32);
#[cfg(feature = "unicode-width")]
assert!(ch.width().unwrap() <= 2, "char: {}", desc);
#[cfg(not(feature = "unicode-width"))]
assert_eq!(ch_width(ch), 2, "char: {}", desc);
}
}
}
#[test]
fn display_width_works() {
assert_eq!("Café Plain".len(), 11); assert_eq!(display_width("Café Plain"), 10);
assert_eq!(display_width("\u{1b}[31mCafé Rouge\u{1b}[0m"), 10);
}
#[test]
fn display_width_narrow_emojis() {
#[cfg(feature = "unicode-width")]
assert_eq!(display_width("⁉"), 1);
#[cfg(not(feature = "unicode-width"))]
assert_eq!(display_width("⁉"), 2);
}
#[test]
fn display_width_narrow_emojis_variant_selector() {
#[cfg(feature = "unicode-width")]
assert_eq!(display_width("⁉\u{fe0f}"), 1);
#[cfg(not(feature = "unicode-width"))]
assert_eq!(display_width("⁉\u{fe0f}"), 4);
}
#[test]
fn display_width_emojis() {
assert_eq!(display_width("😂😭🥺🤣✨😍🙏🥰😊🔥"), 20);
}
}