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}