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