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