tmux_lib/
utils.rs

1/// Misc utilities.
2pub 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    /// Trim leading and trailing whitespaces (`\t` and ` `) in a `&[u8]`
17    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    /// Trim trailing whitespaces (`\t` and ` `) in a `&[u8]`
30    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
39/// Trim each line of the buffer.
40fn 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) // trim each line
44        .collect();
45
46    trimmed_lines
47}
48
49/// Drop all the last empty lines.
50fn 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
58/// This function processes a pane captured bufer.
59///
60/// - All lines are trimmed after capture because tmux does not allow capturing escape codes and
61///   trimming lines.
62/// - If `drop_n_last_lines` is greater than 0, the n last line are not captured. This is used only
63///   for panes with a zsh prompt, in order to avoid polluting the history with new prompts on
64///   restore.
65/// - In addition, the last line has an additional ascii reset escape code because tmux does not
66///   capture it.
67///
68pub 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    // Join the lines with `b'\n'`, add reset code to the last line
74    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        //
131
132        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        // Should have lines with reset code on last line
167        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")); // reset code
171    }
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        // Lines should be trimmed of trailing spaces
180        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        // Should only have line1 and line2, no trailing empty lines
191        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        // Should drop last 2 lines (line3 and line4)
201        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        // Simulate content with existing escape codes
216        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        // Should preserve existing escape codes and add reset at end
221        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        // When all lines are empty, should return as-is
230        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}