Skip to main content

unicode_width_utils/
unicode_width_utils.rs

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