ic_host_text/width/
mod.rs1use unicode_width::UnicodeWidthChar;
3
4use crate::escape::escaped_chars;
5
6pub(crate) fn scalar_width(character: char) -> usize {
8 character.width().unwrap_or(0)
9}
10
11pub(crate) fn display_width(text: &str) -> usize {
12 text.chars().map(scalar_width).sum()
13}
14
15#[must_use]
24pub fn fit_line(line: &str, columns: usize) -> String {
25 if columns == 0 {
26 return String::new();
27 }
28 let mut width = 0;
29 let mut text = String::new();
30 let mut clip_at = None;
31 for ch in escaped_chars(line) {
32 let next = scalar_width(ch);
33 let remaining = columns - width;
34 if next > remaining {
35 text.truncate(clip_at.unwrap_or(text.len()));
36 text.push('…');
37 return text;
38 }
39 if next == remaining && clip_at.is_none() {
40 clip_at = Some(text.len());
43 }
44 text.push(ch);
45 width += next;
46 }
47 text
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 #[test]
55 fn exact_width_and_small_allowances() {
56 for (text, columns, expected) in [
57 ("", 0, ""),
58 ("", usize::MAX, ""),
59 ("abc", 0, ""),
60 ("abc", 1, "…"),
61 ("abc", 2, "a…"),
62 ("abc", 3, "abc"),
63 ("abc", usize::MAX, "abc"),
64 ("界界", 3, "界…"),
65 ("e\u{301}x", 2, "e\u{301}x"),
66 ("a\u{301}", 1, "a\u{301}"),
67 ("a\u{301}x", 1, "…"),
68 ("\u{301}界", 1, "\u{301}…"),
69 ("界\u{301}", 2, "界\u{301}"),
70 ("界\u{301}x", 2, "…"),
71 ("a\u{200d}b", 2, "a\u{200d}b"),
72 ("a\u{200d}bc", 2, "a\u{200d}…"),
73 ("\u{202e}", 8, "\\u{202e}"),
74 ("\u{202e}", 7, "\\u{202…"),
75 ] {
76 assert_eq!(fit_line(text, columns), expected);
77 }
78 }
79
80 #[test]
81 fn controls_are_visible_before_width_admission() {
82 assert_eq!(fit_line("a\nb", 4), "a\\nb");
83 assert_eq!(fit_line("a\nb", 3), "a\\…");
84 assert_eq!(fit_line("\x1b[31m", 40), "\\u{1b}[31m");
85 }
86
87 #[test]
88 fn unicode_output_obeys_each_selected_allowance() {
89 for input in ["界界界", "e\u{301}e\u{301}xx", "👩💻 ready", "\n\t\u{202e}"] {
90 for columns in 0..32 {
91 let fitted = fit_line(input, columns);
92 assert!(
93 fitted
94 .chars()
95 .map(|ch| ch.width().unwrap_or(0))
96 .sum::<usize>()
97 <= columns
98 );
99 assert!(!fitted.chars().any(char::is_control));
100 }
101 }
102 }
103}