use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;
pub fn graphemes(text: &str) -> Vec<&str> {
text.graphemes(true).collect()
}
pub fn count_text_chars(text: &str) -> usize {
text.graphemes(true).count()
}
fn grapheme_width(segment: &str) -> usize {
if segment.is_empty() {
return 0;
}
let first_cp = segment.chars().next().unwrap_or('\0');
if is_emoji(first_cp) {
return 2;
}
let mut width = 0usize;
let mut has_visible = false;
for ch in segment.chars() {
if ch == '\u{200D}' || ch == '\u{FE0F}' {
continue;
}
if is_combining_mark(ch) {
continue;
}
let w = UnicodeWidthChar::width(ch).unwrap_or(0);
width = width.max(w);
has_visible = true;
}
if has_visible { width } else { 0 }
}
pub fn count_terminal_cells(text: &str) -> usize {
text.graphemes(true).map(grapheme_width).sum()
}
fn is_emoji(cp: char) -> bool {
let c = cp as u32;
matches!(c,
0x2300..=0x27BF | 0x1F300..=0x1FAFF )
}
fn is_combining_mark(ch: char) -> bool {
let c = ch as u32;
matches!(c,
0x0300..=0x036F | 0x1AB0..=0x1AFF | 0x1DC0..=0x1DFF | 0x20D0..=0x20FF | 0xFE20..=0xFE2F )
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_char_count() {
assert_eq!(count_text_chars("hello"), 5);
}
#[test]
fn emoji_char_count_one_grapheme() {
assert_eq!(count_text_chars("π"), 1);
}
#[test]
fn cjk_terminal_width_two_cells() {
assert_eq!(count_terminal_cells("δΈ"), 2);
}
#[test]
fn ascii_terminal_width() {
assert_eq!(count_terminal_cells("abc"), 3);
}
#[test]
fn graphemes_splits_correctly() {
let gs = graphemes("abc");
assert_eq!(gs, vec!["a", "b", "c"]);
}
#[test]
fn emoji_terminal_width_two_cells() {
assert_eq!(count_terminal_cells("π"), 2);
}
#[test]
fn zwj_sequence_is_two_cells() {
let fam = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}"; let w = count_terminal_cells(fam);
assert!(w >= 1, "ZWJ sequence should have non-zero width");
}
#[test]
fn variation_selector_skipped() {
let text_emoji = "\u{2665}\u{FE0F}"; let w = count_terminal_cells(text_emoji);
assert_eq!(w, 2);
}
#[test]
fn combining_mark_does_not_add_width() {
let composed = "e\u{0301}";
let w = count_terminal_cells(composed);
assert_eq!(w, 1, "combining accent should not add extra width");
}
#[test]
fn empty_string_zero_width() {
assert_eq!(count_terminal_cells(""), 0);
assert_eq!(count_text_chars(""), 0);
}
#[test]
fn mixed_ascii_and_cjk_width() {
assert_eq!(count_terminal_cells("aδΈb"), 4);
assert_eq!(count_text_chars("aδΈb"), 3);
}
#[test]
fn misc_symbols_are_emoji_width() {
let snowman = "\u{2603}";
let w = count_terminal_cells(snowman);
assert_eq!(w, 2);
}
#[test]
fn combining_diacritical_marks_extended_covered() {
let text = "a\u{1AB0}";
let w = count_terminal_cells(text);
assert_eq!(w, 1);
}
#[test]
fn combining_half_marks_fe20_range() {
let text = "x\u{FE20}";
let w = count_terminal_cells(text);
assert_eq!(w, 1);
}
#[test]
fn only_zwj_grapheme_has_zero_width() {
let text = "\u{200D}";
let w = count_terminal_cells(text);
assert_eq!(w, 0);
}
#[test]
fn grapheme_width_empty_segment_is_zero() {
assert_eq!(count_terminal_cells(""), 0);
}
#[test]
fn combining_diacritical_supplement_1dc0() {
let text = "e\u{1DC0}";
let w = count_terminal_cells(text);
assert_eq!(w, 1);
}
#[test]
fn combining_diacritical_for_symbols_20d0() {
let text = "A\u{20D0}";
let w = count_terminal_cells(text);
assert_eq!(w, 1);
}
}