Skip to main content

tmux_lib/
utils.rs

1/// Trim each line of the buffer.
2fn buf_trim_trailing(buf: &[u8]) -> Vec<&[u8]> {
3    buf.split(|c| *c == b'\n')
4        .map(|line| line.trim_ascii_end())
5        .collect()
6}
7
8/// Drop all the last empty lines.
9fn drop_last_empty_lines<'a>(lines: &[&'a [u8]]) -> Vec<&'a [u8]> {
10    if let Some(last) = lines.iter().rposition(|line| !line.is_empty()) {
11        lines[0..=last].to_vec()
12    } else {
13        lines.to_vec()
14    }
15}
16
17/// Process a pane captured buffer.
18///
19/// - All lines are trimmed after capture because tmux does not allow capturing escape codes and
20///   trimming lines.
21/// - If `drop_n_last_lines` is greater than 0, the n last lines are not captured. This is used only
22///   for panes with a zsh prompt, in order to avoid polluting the history with new prompts on
23///   restore.
24/// - In addition, the last line has an additional ascii reset escape code because tmux does not
25///   capture it.
26///
27/// ```
28/// use tmux_lib::utils::cleanup_captured_buffer;
29///
30/// let buffer = b"line1  \nline2\t\n\n\n";
31/// let result = cleanup_captured_buffer(buffer, 0);
32/// let output = String::from_utf8(result).unwrap();
33///
34/// // trailing whitespace trimmed, empty trailing lines dropped, reset code appended
35/// assert_eq!(output, "line1\nline2\x1b[0m\n");
36/// ```
37pub fn cleanup_captured_buffer(buffer: &[u8], drop_n_last_lines: usize) -> Vec<u8> {
38    let trimmed_lines: Vec<&[u8]> = buf_trim_trailing(buffer);
39    let mut buffer: Vec<&[u8]> = drop_last_empty_lines(&trimmed_lines);
40    buffer.truncate(buffer.len() - drop_n_last_lines);
41
42    // Join the lines with `b'\n'`, add reset code to the last line
43    let mut final_buffer: Vec<u8> = Vec::with_capacity(buffer.len());
44    for (idx, &line) in buffer.iter().enumerate() {
45        final_buffer.extend_from_slice(line);
46
47        let is_last_line = idx == buffer.len() - 1;
48        if is_last_line {
49            let reset = "\u{001b}[0m".as_bytes();
50            final_buffer.extend_from_slice(reset);
51            final_buffer.push(b'\n');
52        } else {
53            final_buffer.push(b'\n');
54        }
55    }
56
57    final_buffer
58}
59
60#[cfg(test)]
61mod tests {
62    use super::{buf_trim_trailing, cleanup_captured_buffer, drop_last_empty_lines};
63
64    #[test]
65    fn trims_trailing_whitespaces() {
66        let input = "  text   ".as_bytes();
67        let expected = "  text".as_bytes();
68
69        let actual = input.trim_ascii_end();
70        assert_eq!(actual, expected);
71    }
72
73    #[test]
74    fn trims_whitespaces() {
75        let input = "  text   ".as_bytes();
76        let expected = "text".as_bytes();
77
78        let actual = input.trim_ascii();
79        assert_eq!(actual, expected);
80    }
81
82    #[test]
83    fn test_buf_trim_trailing() {
84        let text = "line1\n\nline3   ";
85        let actual = buf_trim_trailing(text.as_bytes());
86        let expected = vec!["line1".as_bytes(), "".as_bytes(), "line3".as_bytes()];
87        assert_eq!(actual, expected);
88    }
89
90    #[test]
91    fn test_buf_drop_last_empty_lines() {
92        let text = "line1\nline2\n\nline3   ";
93
94        let trimmed_lines = buf_trim_trailing(text.as_bytes());
95        let actual = drop_last_empty_lines(&trimmed_lines);
96        let expected = trimmed_lines;
97        assert_eq!(actual, expected);
98
99        //
100
101        let text = "line1\nline2\n\n\n     ";
102
103        let trimmed_lines = buf_trim_trailing(text.as_bytes());
104        let actual = drop_last_empty_lines(&trimmed_lines);
105        let expected = vec!["line1".as_bytes(), "line2".as_bytes()];
106        assert_eq!(actual, expected);
107    }
108
109    #[test]
110    fn test_trim_only_whitespace() {
111        let input = "   \t  ".as_bytes();
112        assert_eq!(input.trim_ascii(), &[]);
113        assert_eq!(input.trim_ascii_end(), &[]);
114    }
115
116    #[test]
117    fn test_trim_empty() {
118        let input = "".as_bytes();
119        assert_eq!(input.trim_ascii(), &[]);
120        assert_eq!(input.trim_ascii_end(), &[]);
121    }
122
123    #[test]
124    fn test_trim_tabs() {
125        let input = "\t\ttext\t\t".as_bytes();
126        assert_eq!(input.trim_ascii(), "text".as_bytes());
127        assert_eq!(input.trim_ascii_end(), "\t\ttext".as_bytes());
128    }
129
130    #[test]
131    fn test_cleanup_captured_buffer_basic() {
132        let input = "line1\nline2\n";
133        let result = cleanup_captured_buffer(input.as_bytes(), 0);
134
135        // Should have lines with reset code on last line
136        let result_str = String::from_utf8(result).unwrap();
137        assert!(result_str.contains("line1\n"));
138        assert!(result_str.contains("line2"));
139        assert!(result_str.contains("\u{001b}[0m")); // reset code
140    }
141
142    #[test]
143    fn test_cleanup_captured_buffer_trims_trailing_spaces() {
144        let input = "line1   \nline2   \n";
145        let result = cleanup_captured_buffer(input.as_bytes(), 0);
146
147        let result_str = String::from_utf8(result).unwrap();
148        // Lines should be trimmed of trailing spaces
149        assert!(result_str.starts_with("line1\n"));
150        assert!(result_str.contains("line2\u{001b}[0m\n"));
151    }
152
153    #[test]
154    fn test_cleanup_captured_buffer_drops_empty_trailing_lines() {
155        let input = "line1\nline2\n\n\n   \n";
156        let result = cleanup_captured_buffer(input.as_bytes(), 0);
157
158        let result_str = String::from_utf8(result).unwrap();
159        // Should only have line1 and line2, no trailing empty lines
160        assert_eq!(result_str, "line1\nline2\u{001b}[0m\n");
161    }
162
163    #[test]
164    fn test_cleanup_captured_buffer_drop_n_last_lines() {
165        let input = "line1\nline2\nline3\nline4\n";
166        let result = cleanup_captured_buffer(input.as_bytes(), 2);
167
168        let result_str = String::from_utf8(result).unwrap();
169        // Should drop last 2 lines (line3 and line4)
170        assert_eq!(result_str, "line1\nline2\u{001b}[0m\n");
171    }
172
173    #[test]
174    fn test_cleanup_captured_buffer_single_line() {
175        let input = "single line   \n";
176        let result = cleanup_captured_buffer(input.as_bytes(), 0);
177
178        let result_str = String::from_utf8(result).unwrap();
179        assert_eq!(result_str, "single line\u{001b}[0m\n");
180    }
181
182    #[test]
183    fn test_cleanup_captured_buffer_preserves_escape_codes() {
184        // Simulate content with existing escape codes
185        let input = "\u{001b}[32mgreen text\u{001b}[0m\n";
186        let result = cleanup_captured_buffer(input.as_bytes(), 0);
187
188        let result_str = String::from_utf8(result).unwrap();
189        // Should preserve existing escape codes and add reset at end
190        assert!(result_str.contains("\u{001b}[32m"));
191        assert!(result_str.ends_with("\u{001b}[0m\n"));
192    }
193
194    #[test]
195    fn test_drop_last_empty_lines_all_empty() {
196        let lines: Vec<&[u8]> = vec![b"", b"", b""];
197        let result = drop_last_empty_lines(&lines);
198        // When all lines are empty, should return as-is
199        assert_eq!(result, lines);
200    }
201
202    #[test]
203    fn test_drop_last_empty_lines_no_empty() {
204        let lines: Vec<&[u8]> = vec![b"a", b"b", b"c"];
205        let result = drop_last_empty_lines(&lines);
206        assert_eq!(result, lines);
207    }
208
209    #[test]
210    fn test_cleanup_captured_buffer_tabs_are_trimmed() {
211        let input = "line1\t\t\nline2\t\n";
212        let result = cleanup_captured_buffer(input.as_bytes(), 0);
213        let result_str = String::from_utf8(result).unwrap();
214        assert_eq!(result_str, "line1\nline2\u{001b}[0m\n");
215    }
216
217    #[test]
218    fn test_cleanup_captured_buffer_mixed_trailing_whitespace() {
219        let input = "line1 \t \nline2\t  \t\n";
220        let result = cleanup_captured_buffer(input.as_bytes(), 0);
221        let result_str = String::from_utf8(result).unwrap();
222        assert_eq!(result_str, "line1\nline2\u{001b}[0m\n");
223    }
224
225    #[test]
226    fn test_cleanup_captured_buffer_no_trailing_newline() {
227        let input = "line1\nline2";
228        let result = cleanup_captured_buffer(input.as_bytes(), 0);
229        let result_str = String::from_utf8(result).unwrap();
230        assert_eq!(result_str, "line1\nline2\u{001b}[0m\n");
231    }
232
233    #[test]
234    fn test_buf_trim_trailing_preserves_leading_whitespace() {
235        let text = "  indented\n\tnested\n";
236        let actual = buf_trim_trailing(text.as_bytes());
237        assert_eq!(actual[0], "  indented".as_bytes());
238        assert_eq!(actual[1], "\tnested".as_bytes());
239    }
240}