syncable_cli/analyzer/display/
utils.rs1pub fn visual_width(s: &str) -> usize {
5 let mut width = 0;
6 let mut chars = s.chars().peekable();
7
8 while let Some(ch) = chars.next() {
9 if ch == '\x1b' {
10 if chars.peek() == Some(&'[') {
12 chars.next(); while let Some(c) = chars.next() {
14 if c.is_ascii_alphabetic() {
15 break; }
17 }
18 }
19 } else {
20 width += char_width(ch);
23 }
24 }
25
26 width
27}
28
29pub fn char_width(ch: char) -> usize {
31 match ch {
32 '\u{0000}'..='\u{001F}' | '\u{007F}' => 0,
34 '\u{0300}'..='\u{036F}' => 0,
36 '\u{2600}'..='\u{26FF}' | '\u{2700}'..='\u{27BF}' | '\u{1F000}'..='\u{1F02F}' | '\u{1F030}'..='\u{1F09F}' | '\u{1F0A0}'..='\u{1F0FF}' | '\u{1F100}'..='\u{1F1FF}' | '\u{1F200}'..='\u{1F2FF}' | '\u{1F300}'..='\u{1F5FF}' | '\u{1F600}'..='\u{1F64F}' | '\u{1F650}'..='\u{1F67F}' | '\u{1F680}'..='\u{1F6FF}' | '\u{1F700}'..='\u{1F77F}' | '\u{1F780}'..='\u{1F7FF}' | '\u{1F800}'..='\u{1F8FF}' | '\u{1F900}'..='\u{1F9FF}' | '\u{1100}'..='\u{115F}' | '\u{2E80}'..='\u{2EFF}' | '\u{2F00}'..='\u{2FDF}' | '\u{2FF0}'..='\u{2FFF}' | '\u{3000}'..='\u{303E}' | '\u{3041}'..='\u{3096}' | '\u{30A1}'..='\u{30FA}' | '\u{3105}'..='\u{312D}' | '\u{3131}'..='\u{318E}' | '\u{3190}'..='\u{31BA}' | '\u{31C0}'..='\u{31E3}' | '\u{31F0}'..='\u{31FF}' | '\u{3200}'..='\u{32FF}' | '\u{3300}'..='\u{33FF}' | '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{A000}'..='\u{A48C}' | '\u{A490}'..='\u{A4C6}' | '\u{AC00}'..='\u{D7AF}' | '\u{F900}'..='\u{FAFF}' | '\u{FE10}'..='\u{FE19}' | '\u{FE30}'..='\u{FE6F}' | '\u{FF00}'..='\u{FF60}' | '\u{FFE0}'..='\u{FFE6}' => 2,
77 _ => 1,
79 }
80}
81
82pub fn truncate_to_width(s: &str, max_width: usize) -> String {
84 let current_visual_width = visual_width(s);
85 if current_visual_width <= max_width {
86 return s.to_string();
87 }
88
89 if s.contains('\x1b') {
91 let stripped = strip_ansi_codes(s);
93 if visual_width(&stripped) <= max_width {
94 return s.to_string();
95 }
96
97 let mut result = String::new();
99 let mut width = 0;
100 for ch in stripped.chars() {
101 let ch_width = char_width(ch);
102 if width + ch_width > max_width.saturating_sub(3) {
103 result.push_str("...");
104 break;
105 }
106 result.push(ch);
107 width += ch_width;
108 }
109 return result;
110 }
111
112 let mut result = String::new();
114 let mut width = 0;
115
116 for ch in s.chars() {
117 let ch_width = char_width(ch);
118 if width + ch_width > max_width.saturating_sub(3) {
119 result.push_str("...");
120 break;
121 }
122 result.push(ch);
123 width += ch_width;
124 }
125
126 result
127}
128
129pub fn strip_ansi_codes(s: &str) -> String {
131 let mut result = String::new();
132 let mut chars = s.chars().peekable();
133
134 while let Some(ch) = chars.next() {
135 if ch == '\x1b' {
136 if chars.peek() == Some(&'[') {
138 chars.next(); while let Some(c) = chars.next() {
140 if c.is_ascii_alphabetic() {
141 break; }
143 }
144 }
145 } else {
146 result.push(ch);
147 }
148 }
149
150 result
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_visual_width_basic() {
159 assert_eq!(visual_width("hello"), 5);
160 assert_eq!(visual_width(""), 0);
161 assert_eq!(visual_width("123"), 3);
162 }
163
164 #[test]
165 fn test_visual_width_with_ansi() {
166 assert_eq!(visual_width("\x1b[31mhello\x1b[0m"), 5);
167 assert_eq!(visual_width("\x1b[1;32mtest\x1b[0m"), 4);
168 }
169
170 #[test]
171 fn test_truncate_to_width() {
172 assert_eq!(truncate_to_width("hello world", 5), "he...");
173 assert_eq!(truncate_to_width("hello", 10), "hello");
174 assert_eq!(truncate_to_width("hello world", 8), "hello...");
175 }
176
177 #[test]
178 fn test_strip_ansi_codes() {
179 assert_eq!(strip_ansi_codes("\x1b[31mhello\x1b[0m"), "hello");
180 assert_eq!(strip_ansi_codes("plain text"), "plain text");
181 assert_eq!(strip_ansi_codes("\x1b[1;32mgreen\x1b[0m text"), "green text");
182 }
183}