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}