1const TAB_REPLACEMENT: &str = " ";
2
3pub fn needs_terminal_sanitization(s: &str) -> bool {
10 s.chars()
11 .any(|c| matches!(c, '\t' | '\r' | '\x1b') || (c.is_control() && c != '\n'))
12}
13
14pub fn normalize_terminal_text(s: &str) -> String {
25 let mut result = String::with_capacity(s.len());
26 for c in s.chars() {
27 match c {
28 '\t' => result.push_str(TAB_REPLACEMENT),
29 '\n' => result.push('\n'),
30 c if c.is_control() => { }
31 c => result.push(c),
32 }
33 }
34 result
35}
36
37pub fn sanitize_terminal_text(s: &str) -> String {
42 let stripped = strip_ansi_codes(s);
43 normalize_terminal_text(&stripped)
44}
45
46pub fn sanitize_single_line_text(s: &str) -> String {
48 sanitize_terminal_text(s)
49 .chars()
50 .map(|c| if c == '\n' { ' ' } else { c })
51 .collect()
52}
53
54pub fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
57 let normalized;
58 let text = if needs_terminal_sanitization(text) {
59 normalized = sanitize_terminal_text(text);
60 normalized.as_str()
61 } else {
62 text
63 };
64 let max_width = max_width.max(2);
66 let mut result = Vec::new();
67 let mut current_line = String::new();
68 let mut current_width = 0;
69
70 for ch in text.chars() {
71 if ch == '\n' {
73 result.push(current_line.clone());
74 current_line.clear();
75 current_width = 0;
76 continue;
77 }
78 let ch_width = char_width(ch);
79 if current_width + ch_width > max_width && !current_line.is_empty() {
80 result.push(current_line.clone());
81 current_line.clear();
82 current_width = 0;
83 }
84 current_line.push(ch);
85 current_width += ch_width;
86 }
87 if !current_line.is_empty() {
88 result.push(current_line);
89 }
90 if result.is_empty() {
91 result.push(String::new());
92 }
93 result
94}
95
96pub fn display_width(s: &str) -> usize {
99 s.chars().map(char_width).sum()
100}
101
102pub fn char_width(c: char) -> usize {
105 if c == '\t' {
106 return TAB_REPLACEMENT.len();
107 }
108 if c.is_control() {
109 return 0;
110 }
111 use unicode_width::UnicodeWidthChar;
112 UnicodeWidthChar::width(c).unwrap_or(0)
113}
114
115pub fn strip_ansi_codes(s: &str) -> String {
118 use regex::Regex;
119 use std::sync::OnceLock;
120 static RE: OnceLock<Regex> = OnceLock::new();
121 let re = RE.get_or_init(|| {
122 Regex::new(r"\x1b\[[\x20-\x3f]*[\x40-\x7e]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b[^\[\]()]")
127 .expect("正则表达式编译失败,这是一个静态模式,应该总是有效的")
128 });
129 re.replace_all(s, "").into_owned()
130}
131
132pub fn sanitize_tool_output(s: &str) -> String {
134 sanitize_terminal_text(s)
135}
136
137pub fn remove_quotes(s: &str) -> String {
139 let s = s.trim();
140 if s.len() >= 2
141 && ((s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')))
142 {
143 return s[1..s.len() - 1].to_string();
144 }
145 s.to_string()
146}
147
148#[cfg(test)]
149mod tests {
150 use super::{
151 needs_terminal_sanitization, normalize_terminal_text, sanitize_single_line_text,
152 sanitize_terminal_text, sanitize_tool_output, wrap_text,
153 };
154
155 #[test]
156 fn needs_terminal_sanitization_detects_ansi_and_control_chars() {
157 assert!(needs_terminal_sanitization("a\tb"));
158 assert!(needs_terminal_sanitization("a\r\nb"));
159 assert!(needs_terminal_sanitization("\x1b[31mred\x1b[0m"));
160 assert!(!needs_terminal_sanitization("plain\ntext"));
161 }
162
163 #[test]
164 fn normalize_terminal_text_expands_tabs_and_removes_cr() {
165 assert_eq!(normalize_terminal_text("a\tb\r\nc"), "a b\nc");
166 }
167
168 #[test]
169 fn normalize_terminal_text_strips_control_chars() {
170 assert_eq!(
172 normalize_terminal_text("hello\x07world\x1b[0m"),
173 "helloworld[0m"
174 );
175 assert_eq!(normalize_terminal_text("\x00\x01\x02"), "");
176 assert_eq!(normalize_terminal_text("a\x7fb"), "ab");
177 }
178
179 #[test]
180 fn normalize_terminal_text_preserves_newline() {
181 assert_eq!(normalize_terminal_text("line1\nline2"), "line1\nline2");
183 }
184
185 #[test]
186 fn normalize_terminal_text_preserves_tab_expansion() {
187 assert_eq!(normalize_terminal_text("\titem"), " item");
189 }
190
191 #[test]
192 fn sanitize_tool_output_strips_ansi_and_controls() {
193 assert_eq!(sanitize_tool_output("\x1b[32mok\x1b[0m\x07"), "ok");
195 }
196
197 #[test]
198 fn sanitize_terminal_text_strips_full_ansi_sequences() {
199 assert_eq!(sanitize_terminal_text("a\x1b[31mred\x1b[0m\x07b"), "aredb");
200 }
201
202 #[test]
203 fn sanitize_single_line_text_flattens_newlines() {
204 assert_eq!(
205 sanitize_single_line_text("a\x1b[31mred\x1b[0m\nb"),
206 "ared b"
207 );
208 }
209
210 #[test]
211 fn wrap_text_outputs_spaces_instead_of_tabs() {
212 let wrapped = wrap_text("ab\tcd", 4);
213 assert_eq!(wrapped, vec!["ab ".to_string(), " cd".to_string()]);
214 assert!(wrapped.iter().all(|line| !line.contains('\t')));
215 }
216
217 #[test]
218 fn wrap_text_strips_ansi_sequences_instead_of_leaking_fragments() {
219 let wrapped = wrap_text("x\x1b[31mred\x1b[0my", 80);
220 assert_eq!(wrapped, vec!["xredy".to_string()]);
221 }
222}