boxmux_lib/
circular_buffer.rs

1use std::collections::VecDeque;
2
3/// Circular buffer for storing PTY output lines with configurable size limit
4/// Provides efficient memory-bounded storage with O(1) append operations
5#[derive(Debug, Clone)]
6pub struct CircularBuffer {
7    buffer: VecDeque<String>,
8    max_size: usize,
9    total_lines_added: u64, // Track total lines ever added for diagnostics
10}
11
12impl CircularBuffer {
13    /// Create a new circular buffer with the specified maximum size
14    pub fn new(max_size: usize) -> Self {
15        Self {
16            buffer: VecDeque::with_capacity(max_size),
17            max_size,
18            total_lines_added: 0,
19        }
20    }
21
22    /// Add a line to the buffer, removing the oldest if at capacity
23    pub fn push(&mut self, line: String) {
24        if self.buffer.len() >= self.max_size && self.max_size > 0 {
25            self.buffer.pop_front(); // Remove oldest line
26        }
27
28        self.buffer.push_back(line);
29        self.total_lines_added += 1;
30    }
31
32    /// Get the current number of lines in the buffer
33    pub fn len(&self) -> usize {
34        self.buffer.len()
35    }
36
37    /// Check if the buffer is empty
38    pub fn is_empty(&self) -> bool {
39        self.buffer.is_empty()
40    }
41
42    /// Get all lines as a vector (for display purposes)
43    pub fn get_all_lines(&self) -> Vec<String> {
44        self.buffer.iter().cloned().collect()
45    }
46
47    /// Get the last N lines (most recent)
48    pub fn get_last_lines(&self, n: usize) -> Vec<String> {
49        let start_idx = if n >= self.buffer.len() {
50            0
51        } else {
52            self.buffer.len() - n
53        };
54        self.buffer.iter().skip(start_idx).cloned().collect()
55    }
56
57    /// Get lines within a range (for scrolling)
58    pub fn get_lines_range(&self, start: usize, count: usize) -> Vec<String> {
59        if start >= self.buffer.len() {
60            return Vec::new();
61        }
62
63        let end = (start + count).min(self.buffer.len());
64        self.buffer.range(start..end).cloned().collect()
65    }
66
67    /// Get all lines as a single string (joined with newlines)
68    pub fn get_content(&self) -> String {
69        self.buffer.iter().cloned().collect::<Vec<_>>().join("\n")
70    }
71
72    /// Get the last N lines as a single string
73    pub fn get_recent_content(&self, lines: usize) -> String {
74        self.get_last_lines(lines).join("\n")
75    }
76
77    /// Clear the buffer
78    pub fn clear(&mut self) {
79        self.buffer.clear();
80        // Don't reset total_lines_added to preserve history
81    }
82
83    /// Get maximum buffer size
84    pub fn max_size(&self) -> usize {
85        self.max_size
86    }
87
88    /// Set new maximum size (may truncate existing content)
89    pub fn set_max_size(&mut self, new_max_size: usize) {
90        self.max_size = new_max_size;
91
92        // Truncate if current size exceeds new limit
93        while self.buffer.len() > new_max_size && new_max_size > 0 {
94            self.buffer.pop_front();
95        }
96
97        // Resize capacity for efficiency
98        if new_max_size > 0 {
99            self.buffer.reserve(new_max_size);
100        }
101    }
102
103    /// Get diagnostic information about the buffer
104    pub fn get_stats(&self) -> BufferStats {
105        BufferStats {
106            current_lines: self.buffer.len(),
107            max_size: self.max_size,
108            total_lines_added: self.total_lines_added,
109            memory_usage_bytes: self.estimate_memory_usage(),
110            is_at_capacity: self.buffer.len() >= self.max_size,
111        }
112    }
113
114    /// Estimate memory usage in bytes (approximate)
115    fn estimate_memory_usage(&self) -> usize {
116        let string_overhead = std::mem::size_of::<String>();
117        let content_size: usize = self.buffer.iter().map(|s| s.len()).sum();
118        let deque_overhead =
119            std::mem::size_of::<VecDeque<String>>() + (self.buffer.capacity() * string_overhead);
120
121        content_size + deque_overhead
122    }
123
124    /// Search for lines containing the given text (case-insensitive)
125    pub fn search(&self, query: &str) -> Vec<(usize, String)> {
126        let query_lower = query.to_lowercase();
127        self.buffer
128            .iter()
129            .enumerate()
130            .filter_map(|(idx, line)| {
131                if line.to_lowercase().contains(&query_lower) {
132                    Some((idx, line.clone()))
133                } else {
134                    None
135                }
136            })
137            .collect()
138    }
139
140    /// Get lines in reverse order (most recent first)
141    pub fn get_lines_reverse(&self) -> Vec<String> {
142        self.buffer.iter().rev().cloned().collect()
143    }
144}
145
146/// Statistics about the circular buffer
147#[derive(Debug, Clone)]
148pub struct BufferStats {
149    pub current_lines: usize,
150    pub max_size: usize,
151    pub total_lines_added: u64,
152    pub memory_usage_bytes: usize,
153    pub is_at_capacity: bool,
154}
155
156impl Default for CircularBuffer {
157    /// Create a buffer with default size of 1000 lines
158    fn default() -> Self {
159        Self::new(1000)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_circular_buffer_basic_operations() {
169        let mut buffer = CircularBuffer::new(3);
170
171        assert_eq!(buffer.len(), 0);
172        assert!(buffer.is_empty());
173
174        buffer.push("line1".to_string());
175        buffer.push("line2".to_string());
176        buffer.push("line3".to_string());
177
178        assert_eq!(buffer.len(), 3);
179        assert!(!buffer.is_empty());
180        assert_eq!(buffer.get_all_lines(), vec!["line1", "line2", "line3"]);
181    }
182
183    #[test]
184    fn test_circular_buffer_overflow() {
185        let mut buffer = CircularBuffer::new(2);
186
187        buffer.push("line1".to_string());
188        buffer.push("line2".to_string());
189        buffer.push("line3".to_string()); // Should remove line1
190
191        assert_eq!(buffer.len(), 2);
192        assert_eq!(buffer.get_all_lines(), vec!["line2", "line3"]);
193
194        buffer.push("line4".to_string()); // Should remove line2
195        assert_eq!(buffer.get_all_lines(), vec!["line3", "line4"]);
196    }
197
198    #[test]
199    fn test_circular_buffer_get_last_lines() {
200        let mut buffer = CircularBuffer::new(5);
201
202        for i in 1..=5 {
203            buffer.push(format!("line{}", i));
204        }
205
206        assert_eq!(buffer.get_last_lines(2), vec!["line4", "line5"]);
207        assert_eq!(
208            buffer.get_last_lines(10),
209            vec!["line1", "line2", "line3", "line4", "line5"]
210        );
211    }
212
213    #[test]
214    fn test_circular_buffer_search() {
215        let mut buffer = CircularBuffer::new(5);
216
217        buffer.push("error: file not found".to_string());
218        buffer.push("info: processing data".to_string());
219        buffer.push("warning: deprecated function".to_string());
220        buffer.push("error: connection failed".to_string());
221
222        let results = buffer.search("error");
223        assert_eq!(results.len(), 2);
224        assert_eq!(results[0].0, 0); // First error at index 0
225        assert_eq!(results[1].0, 3); // Second error at index 3
226    }
227
228    #[test]
229    fn test_circular_buffer_resize() {
230        let mut buffer = CircularBuffer::new(5);
231
232        for i in 1..=5 {
233            buffer.push(format!("line{}", i));
234        }
235
236        // Resize to smaller capacity
237        buffer.set_max_size(3);
238        assert_eq!(buffer.len(), 3);
239        assert_eq!(buffer.get_all_lines(), vec!["line3", "line4", "line5"]);
240    }
241
242    #[test]
243    fn test_circular_buffer_stats() {
244        let mut buffer = CircularBuffer::new(2);
245
246        buffer.push("test1".to_string());
247        buffer.push("test2".to_string());
248        buffer.push("test3".to_string()); // Overflow
249
250        let stats = buffer.get_stats();
251        assert_eq!(stats.current_lines, 2);
252        assert_eq!(stats.max_size, 2);
253        assert_eq!(stats.total_lines_added, 3);
254        assert!(stats.is_at_capacity);
255        assert!(stats.memory_usage_bytes > 0);
256    }
257
258    #[test]
259    fn test_circular_buffer_content_methods() {
260        let mut buffer = CircularBuffer::new(3);
261
262        buffer.push("line1".to_string());
263        buffer.push("line2".to_string());
264        buffer.push("line3".to_string());
265
266        assert_eq!(buffer.get_content(), "line1\nline2\nline3");
267        assert_eq!(buffer.get_recent_content(2), "line2\nline3");
268    }
269}