1pub trait SliceExt {
3 fn trim(&self) -> &Self;
4 fn trim_trailing(&self) -> &Self;
5}
6
7fn is_whitespace(c: &u8) -> bool {
8 *c == b'\t' || *c == b' '
9}
10
11fn is_not_whitespace(c: &u8) -> bool {
12 !is_whitespace(c)
13}
14
15impl SliceExt for [u8] {
16 fn trim(&self) -> &[u8] {
18 if let Some(first) = self.iter().position(is_not_whitespace) {
19 if let Some(last) = self.iter().rposition(is_not_whitespace) {
20 &self[first..last + 1]
21 } else {
22 unreachable!();
23 }
24 } else {
25 &[]
26 }
27 }
28
29 fn trim_trailing(&self) -> &[u8] {
31 if let Some(last) = self.iter().rposition(is_not_whitespace) {
32 &self[0..last + 1]
33 } else {
34 &[]
35 }
36 }
37}
38
39fn buf_trim_trailing(buf: &[u8]) -> Vec<&[u8]> {
41 let trimmed_lines: Vec<&[u8]> = buf
42 .split(|c| *c == b'\n')
43 .map(SliceExt::trim_trailing) .collect();
45
46 trimmed_lines
47}
48
49fn drop_last_empty_lines<'a>(lines: &[&'a [u8]]) -> Vec<&'a [u8]> {
51 if let Some(last) = lines.iter().rposition(|line| !line.is_empty()) {
52 lines[0..=last].to_vec()
53 } else {
54 lines.to_vec()
55 }
56}
57
58pub fn cleanup_captured_buffer(buffer: &[u8], drop_n_last_lines: usize) -> Vec<u8> {
69 let trimmed_lines: Vec<&[u8]> = buf_trim_trailing(buffer);
70 let mut buffer: Vec<&[u8]> = drop_last_empty_lines(&trimmed_lines);
71 buffer.truncate(buffer.len() - drop_n_last_lines);
72
73 let mut final_buffer: Vec<u8> = Vec::with_capacity(buffer.len());
75 for (idx, &line) in buffer.iter().enumerate() {
76 final_buffer.extend_from_slice(line);
77
78 let is_last_line = idx == buffer.len() - 1;
79 if is_last_line {
80 let reset = "\u{001b}[0m".as_bytes();
81 final_buffer.extend_from_slice(reset);
82 final_buffer.push(b'\n');
83 } else {
84 final_buffer.push(b'\n');
85 }
86 }
87
88 final_buffer
89}
90
91#[cfg(test)]
92mod tests {
93 use super::{buf_trim_trailing, cleanup_captured_buffer, drop_last_empty_lines, SliceExt};
94
95 #[test]
96 fn trims_trailing_whitespaces() {
97 let input = " text ".as_bytes();
98 let expected = " text".as_bytes();
99
100 let actual = input.trim_trailing();
101 assert_eq!(actual, expected);
102 }
103
104 #[test]
105 fn trims_whitespaces() {
106 let input = " text ".as_bytes();
107 let expected = "text".as_bytes();
108
109 let actual = input.trim();
110 assert_eq!(actual, expected);
111 }
112
113 #[test]
114 fn test_buf_trim_trailing() {
115 let text = "line1\n\nline3 ";
116 let actual = buf_trim_trailing(text.as_bytes());
117 let expected = vec!["line1".as_bytes(), "".as_bytes(), "line3".as_bytes()];
118 assert_eq!(actual, expected);
119 }
120
121 #[test]
122 fn test_buf_drop_last_empty_lines() {
123 let text = "line1\nline2\n\nline3 ";
124
125 let trimmed_lines = buf_trim_trailing(text.as_bytes());
126 let actual = drop_last_empty_lines(&trimmed_lines);
127 let expected = trimmed_lines;
128 assert_eq!(actual, expected);
129
130 let text = "line1\nline2\n\n\n ";
133
134 let trimmed_lines = buf_trim_trailing(text.as_bytes());
135 let actual = drop_last_empty_lines(&trimmed_lines);
136 let expected = vec!["line1".as_bytes(), "line2".as_bytes()];
137 assert_eq!(actual, expected);
138 }
139
140 #[test]
141 fn test_trim_only_whitespace() {
142 let input = " \t ".as_bytes();
143 assert_eq!(input.trim(), &[]);
144 assert_eq!(input.trim_trailing(), &[]);
145 }
146
147 #[test]
148 fn test_trim_empty() {
149 let input = "".as_bytes();
150 assert_eq!(input.trim(), &[]);
151 assert_eq!(input.trim_trailing(), &[]);
152 }
153
154 #[test]
155 fn test_trim_tabs() {
156 let input = "\t\ttext\t\t".as_bytes();
157 assert_eq!(input.trim(), "text".as_bytes());
158 assert_eq!(input.trim_trailing(), "\t\ttext".as_bytes());
159 }
160
161 #[test]
162 fn test_cleanup_captured_buffer_basic() {
163 let input = "line1\nline2\n";
164 let result = cleanup_captured_buffer(input.as_bytes(), 0);
165
166 let result_str = String::from_utf8(result).unwrap();
168 assert!(result_str.contains("line1\n"));
169 assert!(result_str.contains("line2"));
170 assert!(result_str.contains("\u{001b}[0m")); }
172
173 #[test]
174 fn test_cleanup_captured_buffer_trims_trailing_spaces() {
175 let input = "line1 \nline2 \n";
176 let result = cleanup_captured_buffer(input.as_bytes(), 0);
177
178 let result_str = String::from_utf8(result).unwrap();
179 assert!(result_str.starts_with("line1\n"));
181 assert!(result_str.contains("line2\u{001b}[0m\n"));
182 }
183
184 #[test]
185 fn test_cleanup_captured_buffer_drops_empty_trailing_lines() {
186 let input = "line1\nline2\n\n\n \n";
187 let result = cleanup_captured_buffer(input.as_bytes(), 0);
188
189 let result_str = String::from_utf8(result).unwrap();
190 assert_eq!(result_str, "line1\nline2\u{001b}[0m\n");
192 }
193
194 #[test]
195 fn test_cleanup_captured_buffer_drop_n_last_lines() {
196 let input = "line1\nline2\nline3\nline4\n";
197 let result = cleanup_captured_buffer(input.as_bytes(), 2);
198
199 let result_str = String::from_utf8(result).unwrap();
200 assert_eq!(result_str, "line1\nline2\u{001b}[0m\n");
202 }
203
204 #[test]
205 fn test_cleanup_captured_buffer_single_line() {
206 let input = "single line \n";
207 let result = cleanup_captured_buffer(input.as_bytes(), 0);
208
209 let result_str = String::from_utf8(result).unwrap();
210 assert_eq!(result_str, "single line\u{001b}[0m\n");
211 }
212
213 #[test]
214 fn test_cleanup_captured_buffer_preserves_escape_codes() {
215 let input = "\u{001b}[32mgreen text\u{001b}[0m\n";
217 let result = cleanup_captured_buffer(input.as_bytes(), 0);
218
219 let result_str = String::from_utf8(result).unwrap();
220 assert!(result_str.contains("\u{001b}[32m"));
222 assert!(result_str.ends_with("\u{001b}[0m\n"));
223 }
224
225 #[test]
226 fn test_drop_last_empty_lines_all_empty() {
227 let lines: Vec<&[u8]> = vec![b"", b"", b""];
228 let result = drop_last_empty_lines(&lines);
229 assert_eq!(result, lines);
231 }
232
233 #[test]
234 fn test_drop_last_empty_lines_no_empty() {
235 let lines: Vec<&[u8]> = vec![b"a", b"b", b"c"];
236 let result = drop_last_empty_lines(&lines);
237 assert_eq!(result, lines);
238 }
239}