Skip to main content

unicode_width_utils/
unicode_width_utils.rs

1use std::borrow::Cow;
2use std::sync::LazyLock;
3use std::sync::atomic::{AtomicBool, Ordering};
4use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
5
6static IS_CJK: LazyLock<AtomicBool> = LazyLock::new(|| {
7    let is_cjk = match std::env::var("UNICODE_WIDTH") {
8        Ok(value) => value.eq_ignore_ascii_case("cjk"),
9        _ => false,
10    };
11    AtomicBool::new(is_cjk)
12});
13
14/// A configuration helper to measure character and string widths.
15///
16/// It determines the width of Unicode characters and strings,
17/// optionally treating East Asian Ambiguous characters
18/// (such as certain Greek, Cyrillic, and CJK characters)
19/// as having a width of 2 (CJK mode).
20///
21/// The default CJK mode is initialized at startup
22/// based on the `UNICODE_WIDTH` environment variable,
23/// but can also be dynamically modified using [`UnicodeWidth::set_default_cjk`].
24#[derive(Clone, Copy, Debug)]
25pub struct UnicodeWidth {
26    is_cjk: bool,
27}
28
29impl Default for UnicodeWidth {
30    /// Create a `UnicodeWidth` instance using the default CJK mode.
31    ///
32    /// The default CJK mode is determined by the global setting,
33    /// which defaults to the value of the `UNICODE_WIDTH` environment variable
34    /// (value `"cjk"` enabling CJK mode).
35    fn default() -> Self {
36        Self {
37            is_cjk: IS_CJK.load(Ordering::Relaxed),
38        }
39    }
40}
41
42impl UnicodeWidth {
43    /// Create a `UnicodeWidth` instance using the default CJK mode.
44    ///
45    /// The default CJK mode is determined by the global setting,
46    /// which defaults to the value of the `UNICODE_WIDTH` environment variable
47    /// (value `"cjk"` enabling CJK mode).
48    ///
49    /// # Examples
50    /// ```
51    /// use unicode_width_utils::UnicodeWidth;
52    ///
53    /// let uw = UnicodeWidth::new();
54    /// ```
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Create a new `UnicodeWidth` instance with a specific CJK flag.
60    ///
61    /// If `is_cjk` is true,
62    /// East Asian Ambiguous characters will be treated as 2 columns wide.
63    /// If false, they will be treated as 1 column wide.
64    ///
65    /// # Examples
66    /// ```
67    /// use unicode_width_utils::UnicodeWidth;
68    ///
69    /// let non_cjk = UnicodeWidth::with_cjk(false);
70    /// assert_eq!(non_cjk.char('█'), Some(1));
71    ///
72    /// let cjk = UnicodeWidth::with_cjk(true);
73    /// assert_eq!(cjk.char('█'), Some(2));
74    /// ```
75    pub fn with_cjk(is_cjk: bool) -> Self {
76        Self { is_cjk }
77    }
78
79    /// Set the default CJK configuration dynamically.
80    ///
81    /// Future instances
82    /// created using [`UnicodeWidth::new`] or [`UnicodeWidth::default`]
83    /// will inherit this default value
84    /// unless explicitly overridden with [`UnicodeWidth::with_cjk`].
85    ///
86    /// # Examples
87    /// ```
88    /// use unicode_width_utils::UnicodeWidth;
89    ///
90    /// // Set default CJK mode to true
91    /// UnicodeWidth::set_default_cjk(true);
92    /// let uw = UnicodeWidth::new();
93    /// assert_eq!(uw.char('█'), Some(2));
94    ///
95    /// // Set default CJK mode to false
96    /// UnicodeWidth::set_default_cjk(false);
97    /// let uw2 = UnicodeWidth::new();
98    /// assert_eq!(uw2.char('█'), Some(1));
99    /// ```
100    pub fn set_default_cjk(is_cjk: bool) {
101        IS_CJK.store(is_cjk, Ordering::Relaxed);
102    }
103
104    /// Return the column width of a character.
105    ///
106    /// Return `None` for control characters
107    /// or other characters without a defined width.
108    ///
109    /// This is a wrapper of [`UnicodeWidthChar`],
110    /// calls `width` or `width_cjk` depending on the configuration.
111    ///
112    /// [`UnicodeWidthChar`]: https://docs.rs/unicode-width/latest/unicode_width/trait.UnicodeWidthChar.html
113    ///
114    /// # Examples
115    /// ```
116    /// use unicode_width_utils::UnicodeWidth;
117    ///
118    /// let uw = UnicodeWidth::with_cjk(false);
119    /// assert_eq!(uw.char('A'), Some(1));
120    /// assert_eq!(uw.char('\n'), None);
121    /// ```
122    pub fn char(&self, ch: char) -> Option<usize> {
123        match self.is_cjk {
124            false => UnicodeWidthChar::width(ch),
125            true => UnicodeWidthChar::width_cjk(ch),
126        }
127    }
128
129    /// Return the total column width of a string.
130    ///
131    /// This is a wrapper of [`UnicodeWidthStr`],
132    /// calls `width` or `width_cjk` depending on the configuration.
133    ///
134    /// [`UnicodeWidthStr`]: https://docs.rs/unicode-width/latest/unicode_width/trait.UnicodeWidthStr.html
135    ///
136    /// # Examples
137    /// ```
138    /// use unicode_width_utils::UnicodeWidth;
139    ///
140    /// let uw = UnicodeWidth::with_cjk(false);
141    /// assert_eq!(uw.str("Hello"), 5);
142    /// ```
143    pub fn str(&self, str: &str) -> usize {
144        match self.is_cjk {
145            false => UnicodeWidthStr::width(str),
146            true => UnicodeWidthStr::width_cjk(str),
147        }
148    }
149
150    /// Truncate a string slice to a maximum column width.
151    ///
152    /// The returned slice will be the longest prefix of `str`
153    /// whose total column width does not exceed `max_width`.
154    ///
155    /// # Examples
156    /// ```
157    /// use unicode_width_utils::UnicodeWidth;
158    ///
159    /// let uw = UnicodeWidth::with_cjk(false);
160    /// assert_eq!(uw.truncate("hello", 3), "hel");
161    ///
162    /// // Truncating CJK text (each 'あ' is 2 columns wide)
163    /// let cjk = UnicodeWidth::with_cjk(true);
164    /// assert_eq!(cjk.truncate("あああ", 3), "あ");
165    /// assert_eq!(cjk.truncate("A█B", 2), "A");
166    /// ```
167    pub fn truncate<'a>(&self, str: &'a str, max_width: usize) -> Cow<'a, str> {
168        let mut width = 0;
169        for (i, ch) in str.char_indices() {
170            let ch_width = self.char(ch).unwrap_or(0);
171            width += ch_width;
172            if width > max_width {
173                return Cow::Borrowed(&str[..i]);
174            }
175        }
176        Cow::Borrowed(str)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn char() {
186        let uw = UnicodeWidth::with_cjk(false);
187        let cjk = UnicodeWidth::with_cjk(true);
188        assert_eq!(uw.char('A'), Some(1));
189        assert_eq!(cjk.char('A'), Some(1));
190        assert_eq!(uw.char('\u{2588}'), Some(1));
191        assert_eq!(cjk.char('\u{2588}'), Some(2));
192        assert_eq!(uw.char('\u{3042}'), Some(2));
193        assert_eq!(cjk.char('\u{3042}'), Some(2));
194    }
195
196    #[test]
197    fn str() {
198        let uw = UnicodeWidth::with_cjk(false);
199        let cjk = UnicodeWidth::with_cjk(true);
200        assert_eq!(uw.str("A"), 1);
201        assert_eq!(cjk.str("A"), 1);
202        assert_eq!(uw.str("\u{2588}"), 1);
203        assert_eq!(cjk.str("\u{2588}"), 2);
204        assert_eq!(uw.str("\u{3042}"), 2);
205        assert_eq!(cjk.str("\u{3042}"), 2);
206    }
207
208    #[test]
209    fn truncate() {
210        let uw = UnicodeWidth::with_cjk(false);
211        let cjk = UnicodeWidth::with_cjk(true);
212
213        assert_eq!(uw.truncate("hello", 0), "");
214        assert_eq!(uw.truncate("hello", 4), "hell");
215        assert_eq!(uw.truncate("hello", 5), "hello");
216        assert_eq!(uw.truncate("hello", 6), "hello");
217        assert_eq!(uw.truncate("hello", 10), "hello");
218
219        // \u{2588} is 1 column wide for `uw`, and 2 columns wide for `cjk`.
220        assert_eq!(uw.truncate("A\u{2588}B", 2), "A\u{2588}");
221        assert_eq!(cjk.truncate("A\u{2588}B", 2), "A");
222
223        // \u{3042} ('あ') is 2 columns wide.
224        assert_eq!(uw.truncate("\u{3042}", 1), "");
225        assert_eq!(uw.truncate("\u{3042}", 2), "\u{3042}");
226        assert_eq!(uw.truncate("\u{3042}", 3), "\u{3042}");
227
228        // Control characters with 0 width.
229        assert_eq!(uw.truncate("A\nB", 1), "A\n");
230        assert_eq!(uw.truncate("A\nB", 2), "A\nB");
231        assert_eq!(uw.truncate("\nA", 0), "\n");
232    }
233
234    #[test]
235    fn default_cjk() {
236        let original = IS_CJK.load(Ordering::Relaxed);
237
238        UnicodeWidth::set_default_cjk(false);
239        assert_eq!(UnicodeWidth::default().char('\u{2588}'), Some(1));
240        assert_eq!(UnicodeWidth::new().char('\u{2588}'), Some(1));
241
242        UnicodeWidth::set_default_cjk(true);
243        assert_eq!(UnicodeWidth::default().char('\u{2588}'), Some(2));
244        assert_eq!(UnicodeWidth::new().char('\u{2588}'), Some(2));
245
246        UnicodeWidth::set_default_cjk(original);
247    }
248}