#![allow(clippy::unreadable_literal)]
use std::cmp::Ordering;
use crate::grapheme::clusters;
include!(concat!(env!("OUT_DIR"), "/char_width_ranges.rs")); include!(concat!(env!("OUT_DIR"), "/emoji_presentation_ranges.rs"));
const VS15: char = '\u{FE0E}'; const VS16: char = '\u{FE0F}'; const KEYCAP: char = '\u{20E3}';
fn width_class(cp: u32) -> u8 {
match WIDTH_RANGES.binary_search_by(|&(start, end, _)| {
if cp < start {
Ordering::Greater
} else if cp > end {
Ordering::Less
} else {
Ordering::Equal
}
}) {
Ok(i) => WIDTH_RANGES[i].2,
Err(_) => 1,
}
}
fn in_range_set(set: &[(u32, u32)], cp: u32) -> bool {
set.binary_search_by(|&(start, end)| {
if cp < start {
Ordering::Greater
} else if cp > end {
Ordering::Less
} else {
Ordering::Equal
}
})
.is_ok()
}
fn is_regional_indicator(c: char) -> bool {
('\u{1F1E6}'..='\u{1F1FF}').contains(&c)
}
fn resolve(class: u8, ambiguous_wide: bool) -> usize {
match class {
0 => 0,
2 => 2,
3 => usize::from(ambiguous_wide) + 1, _ => 1,
}
}
pub(crate) fn grapheme_width_opts(cluster: &str, ambiguous_wide: bool) -> usize {
let mut rest = cluster.chars();
let Some(base) = rest.next() else {
return 0;
};
let base_emoji = in_range_set(EMOJI_PRESENTATION_RANGES, base as u32) || is_regional_indicator(base); let base_class = width_class(base as u32);
if base_class == 0 && !base_emoji {
return 0;
}
let mut has_vs15 = false;
let mut has_vs16 = false;
let mut has_keycap = false;
for c in rest {
match c {
VS15 => has_vs15 = true,
VS16 => has_vs16 = true,
KEYCAP => has_keycap = true,
_ => {}
}
}
let is_keycap_base = matches!(base, '0'..='9' | '#' | '*');
if has_vs15 {
return if base_emoji {
1
} else {
resolve(base_class, ambiguous_wide)
};
}
if has_vs16 || base_emoji || (has_keycap && is_keycap_base) {
return 2;
}
resolve(base_class, ambiguous_wide)
}
#[must_use]
pub(crate) fn terminal_width_opts(text: &str, ambiguous_wide: bool) -> usize {
clusters(text)
.map(|cluster| grapheme_width_opts(cluster, ambiguous_wide))
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn grapheme_width(cluster: &str) -> usize {
grapheme_width_opts(cluster, false)
}
fn terminal_width(text: &str) -> usize {
terminal_width_opts(text, false)
}
#[test]
fn golden_ascii() {
assert_eq!(terminal_width("hello"), 5);
assert_eq!(grapheme_width("a"), 1);
}
#[test]
fn golden_wide_cjk_and_hangul() {
assert_eq!(grapheme_width("世"), 2);
assert_eq!(terminal_width("世界"), 4);
assert_eq!(grapheme_width("한"), 2); }
#[test]
fn golden_fullwidth_halfwidth() {
assert_eq!(grapheme_width("A"), 2); assert_eq!(grapheme_width("ア"), 1); }
#[test]
fn golden_combining() {
assert_eq!(grapheme_width("e\u{0301}"), 1); assert_eq!(grapheme_width("\u{0301}"), 0); assert_eq!(terminal_width("café"), 4); assert_eq!(terminal_width("cafe\u{0301}"), 4); }
#[test]
fn golden_emoji_presentation() {
assert_eq!(grapheme_width("😀"), 2); assert_eq!(grapheme_width("☺\u{FE0F}"), 2); assert_eq!(grapheme_width("☺\u{FE0E}"), 1); assert_eq!(grapheme_width("⌚\u{FE0E}"), 1);
assert_eq!(grapheme_width("⌚"), 2); assert_eq!(grapheme_width("🇫🇷"), 2); assert_eq!(grapheme_width("1\u{FE0F}\u{20E3}"), 2); assert_eq!(grapheme_width("👨👩👧👦"), 2); assert_eq!(terminal_width("hi 😀"), 5); }
#[test]
fn golden_controls_and_zero_width() {
assert_eq!(terminal_width("\t"), 0); assert_eq!(terminal_width("\u{200B}"), 0); assert_eq!(terminal_width("a\u{0000}b"), 2); }
#[test]
fn ambiguous_policy() {
assert_eq!(grapheme_width_opts("¡", false), 1);
assert_eq!(grapheme_width_opts("¡", true), 2);
assert_eq!(terminal_width("¡"), 1); assert_eq!(terminal_width_opts("¡", true), 2);
}
#[test]
fn iw1_ascii_equals_len() {
for s in ["", "hello world", "a-b_c.123!"] {
assert_eq!(terminal_width(s), s.len(), "I_w1 for {s:?}");
}
}
#[test]
fn iw2_bounds() {
for s in ["世界", "café", "😀🇫🇷", "a\u{0301}b", "한국어"] {
let w = terminal_width(s);
let upper = 2 * crate::grapheme::grapheme_len(s);
assert!(w <= upper, "I_w2: {w} <= {upper} for {s:?}");
}
}
#[test]
fn iw3_additivity_no_cluster_merge() {
let a = "世界";
let b = "ok";
assert_eq!(
terminal_width(&format!("{a} {b}")),
terminal_width(a) + 1 + terminal_width(b)
);
}
#[test]
fn iw3_leading_extend_absorbs_space() {
let fitzpatrick = "\u{1F3FB}"; let joined = format!(" {fitzpatrick}");
assert_eq!(crate::grapheme::grapheme_len(&joined), 1);
assert_eq!(terminal_width(&joined), 1);
assert_eq!(terminal_width(fitzpatrick), 2);
assert_ne!(
terminal_width(&joined),
terminal_width("") + 1 + terminal_width(fitzpatrick)
);
}
#[test]
fn iw4_determinism() {
let s = "Hello 世界 😀 café";
let first = terminal_width(s);
for _ in 0..5 {
assert_eq!(terminal_width(s), first);
}
}
#[test]
fn iw5_zero_width_clusters() {
assert_eq!(grapheme_width("\u{0301}"), 0);
assert_eq!(grapheme_width("\u{200D}"), 0); assert_eq!(grapheme_width("\u{FE0F}"), 0); }
}