src/render/width/unicode.rs

//! Thin wrapper around the `unicode-width` crate.
//!
//! We go through this indirection rather than calling the crate directly so
//! we can inject a test hook for the table renderer.

use unicode_width::UnicodeWidthStr;

/// Return the display width of `s` in terminal cells.
///
/// Unknown / zero-width characters count as zero. Control characters count as
/// zero as well (they'd mangle alignment anyway).
pub fn display_width(s: &str) -> usize {
    UnicodeWidthStr::width(s)
}

/// Truncate `s` to `budget` cells, returning the truncated substring. When
/// the input already fits, the original slice is returned unchanged.
pub fn truncate_to(s: &str, budget: usize) -> &str {
    if display_width(s) <= budget {
        return s;
    }
    let mut end = 0;
    let mut used = 0;
    for (i, ch) in s.char_indices() {
        let w = UnicodeWidthStr::width(ch.encode_utf8(&mut [0u8; 4]) as &str);
        if used + w > budget {
            return &s[..end];
        }
        used += w;
        end = i + ch.len_utf8();
    }
    &s[..end]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ascii_width() {
        assert_eq!(display_width("hello"), 5);
    }

    #[test]
    fn cjk_doubles_width() {
        assert_eq!(display_width("日本語"), 6);
    }

    #[test]
    fn truncate_ascii() {
        assert_eq!(truncate_to("hello world", 5), "hello");
    }

    #[test]
    fn truncate_cjk_respects_cell_width() {
        assert_eq!(truncate_to("日本語", 4), "日本");
    }
}