use std::borrow::Cow;
use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::LineIterator;
use crate::WidthIterator;
static IS_CJK: LazyLock<AtomicBool> = LazyLock::new(|| {
let is_cjk = match std::env::var("UNICODE_WIDTH") {
Ok(value) => value.eq_ignore_ascii_case("cjk"),
_ => false,
};
AtomicBool::new(is_cjk)
});
#[derive(Clone, Debug)]
pub struct UnicodeWidth {
is_cjk: bool,
pub(crate) is_ansi: bool,
pub(crate) should_expand_tab: bool,
pub(crate) tab_size: u8,
pub(crate) control_size: u8,
}
impl Default for UnicodeWidth {
fn default() -> Self {
Self {
is_cjk: IS_CJK.load(Ordering::Relaxed),
is_ansi: false,
should_expand_tab: false,
tab_size: 0,
control_size: 1,
}
}
}
impl UnicodeWidth {
pub fn new() -> Self {
Self::default()
}
pub fn with_cjk(is_cjk: bool) -> Self {
Self {
is_cjk,
..Default::default()
}
}
#[inline]
pub fn set_cjk(&mut self, is_cjk: bool) {
self.is_cjk = is_cjk;
}
pub fn set_default_cjk(is_cjk: bool) {
IS_CJK.store(is_cjk, Ordering::Relaxed);
}
#[inline]
pub fn set_ansi(&mut self, is_ansi: bool) {
self.is_ansi = is_ansi;
}
#[inline]
pub fn set_control_size(&mut self, size: u8) {
self.control_size = size;
}
#[inline]
pub fn set_tab_size(&mut self, tab_size: u8) {
self.tab_size = tab_size;
}
#[inline]
pub fn set_expand_tab(&mut self, should_expand_tab: bool) {
self.should_expand_tab = should_expand_tab;
}
pub fn char(&self, ch: char) -> usize {
self.char_opt(ch).unwrap_or(self.control_size as usize)
}
pub fn char_opt(&self, ch: char) -> Option<usize> {
match self.is_cjk {
false => UnicodeWidthChar::width(ch),
true => UnicodeWidthChar::width_cjk(ch),
}
}
pub fn str(&self, str: &str) -> usize {
if self.tab_size > 0 || self.is_ansi {
let mut iter = WidthIterator::new(self, str);
iter.consume_all();
return iter.width();
}
if self.control_size != 1 {
return str.chars().map(|ch| self.char(ch)).sum();
}
match self.is_cjk {
false => UnicodeWidthStr::width(str),
true => UnicodeWidthStr::width_cjk(str),
}
}
pub fn truncate<'a>(&self, input: &'a str, max_width: usize) -> Cow<'a, str> {
let mut iter = WidthIterator::new(self, input);
iter.set_max_width(max_width);
iter.consume_all();
iter.into()
}
pub fn lines<'a>(&self, input: &'a str, max_width: usize) -> LineIterator<'_, 'a> {
LineIterator::new(self, input, max_width)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn char() {
let uw = UnicodeWidth::with_cjk(false);
let cjk = UnicodeWidth::with_cjk(true);
assert_eq!(uw.char('A'), 1);
assert_eq!(cjk.char('A'), 1);
assert_eq!(uw.char('\u{2588}'), 1);
assert_eq!(cjk.char('\u{2588}'), 2);
assert_eq!(uw.char('\u{3042}'), 2);
assert_eq!(cjk.char('\u{3042}'), 2);
}
#[test]
fn str() {
let uw = UnicodeWidth::with_cjk(false);
let cjk = UnicodeWidth::with_cjk(true);
assert_eq!(uw.str("A"), 1);
assert_eq!(cjk.str("A"), 1);
assert_eq!(uw.str("\u{2588}"), 1);
assert_eq!(cjk.str("\u{2588}"), 2);
assert_eq!(uw.str("\u{3042}"), 2);
assert_eq!(cjk.str("\u{3042}"), 2);
}
#[test]
fn str_tab() {
let mut uw = UnicodeWidth::with_cjk(false);
uw.set_tab_size(4);
assert_eq!(uw.str("A"), 1);
assert_eq!(uw.str("A\t"), 4);
assert_eq!(uw.str("A\tB"), 5);
assert_eq!(uw.str("A\t\tB"), 9);
}
#[test]
fn truncate() {
let uw = UnicodeWidth::with_cjk(false);
let cjk = UnicodeWidth::with_cjk(true);
assert_eq!(uw.truncate("hello", 0), "");
assert_eq!(uw.truncate("hello", 4), "hell");
assert_eq!(uw.truncate("hello", 5), "hello");
assert_eq!(uw.truncate("hello", 6), "hello");
assert_eq!(uw.truncate("hello", 10), "hello");
assert_eq!(uw.truncate("A\u{2588}B", 2), "A\u{2588}");
assert_eq!(cjk.truncate("A\u{2588}B", 2), "A");
assert_eq!(uw.truncate("\u{3042}", 1), "");
assert_eq!(uw.truncate("\u{3042}", 2), "\u{3042}");
assert_eq!(uw.truncate("\u{3042}", 3), "\u{3042}");
assert_eq!(uw.truncate("A\nB", 1), "A");
assert_eq!(uw.truncate("A\nB", 2), "A\n");
assert_eq!(uw.truncate("\nA", 0), "");
assert_eq!(uw.truncate("\nA", 1), "\n");
}
#[test]
fn default_cjk() {
let original = IS_CJK.load(Ordering::Relaxed);
UnicodeWidth::set_default_cjk(false);
assert_eq!(UnicodeWidth::default().char('\u{2588}'), 1);
assert_eq!(UnicodeWidth::new().char('\u{2588}'), 1);
UnicodeWidth::set_default_cjk(true);
assert_eq!(UnicodeWidth::default().char('\u{2588}'), 2);
assert_eq!(UnicodeWidth::new().char('\u{2588}'), 2);
UnicodeWidth::set_default_cjk(original);
}
#[test]
fn truncate_tabs_no_tab_size() {
let mut uw = UnicodeWidth::new();
for _ in 0..2 {
assert_eq!(uw.truncate("A\tB", 1), Cow::Borrowed("A"));
assert_eq!(uw.truncate("A\tB", 2), Cow::Borrowed("A\t"));
assert_eq!(uw.truncate("A\tB", 3), Cow::Borrowed("A\tB"));
uw.set_expand_tab(true);
}
}
#[test]
fn truncate_tabs_expand_multi() {
let mut uw = UnicodeWidth::new();
uw.set_tab_size(4);
uw.set_expand_tab(true);
assert_eq!(uw.truncate("\t\t", 7), Cow::Owned::<str>(" ".into()));
assert_eq!(uw.truncate("\t\t", 8), Cow::Owned::<str>(" ".into()));
}
#[test]
fn lines_tabs_expand() {
let mut uw = UnicodeWidth::new();
uw.set_tab_size(4);
uw.set_expand_tab(true);
let lines: Vec<_> = uw.lines("hi\tworld rust", 8).collect();
assert_eq!(lines, vec!["hi worl", "d rust"]);
}
#[test]
fn ansi_tabs() {
let mut uw = UnicodeWidth::new();
let input = "A\x1B[31mZZ";
assert_eq!(uw.str(input), 8);
uw.set_ansi(true);
assert_eq!(uw.str(input), 3);
assert_eq!(uw.truncate(input, 2), Cow::Borrowed("A\x1B[31mZ"));
uw.set_tab_size(4);
let input_tab = "A\tA\x1B[31mZZ";
assert_eq!(uw.str(input_tab), 7);
assert_eq!(uw.truncate(input_tab, 6), Cow::Borrowed("A\tA\x1B[31mZ"));
uw.set_expand_tab(true);
assert_eq!(
uw.truncate(input_tab, 6),
Cow::Owned::<str>("A A\x1B[31mZ".to_string())
);
}
}