Skip to main content

ftui_widgets/
log_ring.rs

1#![forbid(unsafe_code)]
2
3//! Bounded circular buffer for log storage.
4//!
5//! [`LogRing`] provides memory-efficient storage for log lines that evicts
6//! oldest entries when full. It supports absolute indexing across the entire
7//! history (even for evicted items) and optional overflow file persistence.
8//!
9//! # Example
10//!
11//! ```
12//! use ftui_widgets::LogRing;
13//!
14//! let mut ring = LogRing::new(3);
15//! ring.push("line 1");
16//! ring.push("line 2");
17//! ring.push("line 3");
18//! ring.push("line 4"); // evicts "line 1"
19//!
20//! assert_eq!(ring.len(), 3);
21//! assert_eq!(ring.total_count(), 4);
22//! assert_eq!(ring.get(3), Some(&"line 4"));
23//! assert_eq!(ring.get(0), None); // evicted
24//! ```
25
26use std::collections::VecDeque;
27use std::ops::Range;
28
29/// Circular buffer for log storage with FIFO eviction.
30///
31/// Memory-efficient storage that maintains a sliding window of the most recent
32/// items. Older items are evicted when capacity is reached.
33#[derive(Debug, Clone)]
34pub struct LogRing<T> {
35    /// Circular buffer storage
36    ring: VecDeque<T>,
37
38    /// Maximum capacity
39    capacity: usize,
40
41    /// Total items ever added (for accurate absolute indexing)
42    total_count: usize,
43}
44
45impl<T> LogRing<T> {
46    /// Create a new LogRing with the specified capacity.
47    ///
48    /// # Panics
49    ///
50    /// Panics if capacity is 0.
51    #[must_use]
52    pub fn new(capacity: usize) -> Self {
53        assert!(capacity > 0, "LogRing capacity must be greater than 0");
54        Self {
55            ring: VecDeque::with_capacity(capacity),
56            capacity,
57            total_count: 0,
58        }
59    }
60
61    /// Add an item to the ring.
62    ///
63    /// If the ring is at capacity, the oldest item is evicted first.
64    pub fn push(&mut self, item: T) {
65        self.total_count = self.total_count.saturating_add(1);
66
67        if self.ring.len() >= self.capacity {
68            self.ring.pop_front();
69        }
70
71        self.ring.push_back(item);
72    }
73
74    /// Add multiple items efficiently.
75    pub fn extend(&mut self, items: impl IntoIterator<Item = T>) {
76        for item in items {
77            self.push(item);
78        }
79    }
80
81    /// Get item by absolute index (across entire history).
82    ///
83    /// Returns `None` if the index is out of range or the item has been evicted.
84    #[must_use]
85    pub fn get(&self, absolute_idx: usize) -> Option<&T> {
86        let ring_start = self.first_index();
87
88        if absolute_idx >= ring_start && absolute_idx < self.total_count {
89            self.ring.get(absolute_idx - ring_start)
90        } else {
91            None
92        }
93    }
94
95    /// Get mutable reference by absolute index.
96    #[must_use]
97    pub fn get_mut(&mut self, absolute_idx: usize) -> Option<&mut T> {
98        let ring_start = self.first_index();
99
100        if absolute_idx >= ring_start && absolute_idx < self.total_count {
101            self.ring.get_mut(absolute_idx - ring_start)
102        } else {
103            None
104        }
105    }
106
107    /// Get a range of items by absolute indices.
108    ///
109    /// Returns references to items that are still in memory within the range.
110    /// Items that have been evicted are skipped.
111    pub fn get_range(&self, range: Range<usize>) -> impl Iterator<Item = &T> {
112        let ring_start = self.first_index();
113        let ring_end = self.total_count;
114
115        // Clamp range to what's in memory
116        let start = range.start.max(ring_start);
117        let end = range.end.min(ring_end);
118
119        (start..end).filter_map(move |i| self.get(i))
120    }
121
122    /// Total items ever added (including evicted).
123    #[must_use]
124    pub const fn total_count(&self) -> usize {
125        self.total_count
126    }
127
128    /// Number of items currently in memory.
129    #[must_use]
130    pub fn len(&self) -> usize {
131        self.ring.len()
132    }
133
134    /// Check if the ring is empty.
135    #[must_use]
136    pub fn is_empty(&self) -> bool {
137        self.ring.is_empty()
138    }
139
140    /// Maximum capacity of the ring.
141    #[must_use]
142    pub const fn capacity(&self) -> usize {
143        self.capacity
144    }
145
146    /// First absolute index still in memory.
147    #[must_use]
148    pub fn first_index(&self) -> usize {
149        self.total_count.saturating_sub(self.ring.len())
150    }
151
152    /// Last absolute index (most recent item).
153    ///
154    /// Returns `None` if the ring is empty.
155    #[must_use]
156    pub fn last_index(&self) -> Option<usize> {
157        if self.total_count > 0 {
158            Some(self.total_count - 1)
159        } else {
160            None
161        }
162    }
163
164    /// Check if an absolute index is still in memory.
165    #[must_use]
166    pub fn is_in_memory(&self, absolute_idx: usize) -> bool {
167        absolute_idx >= self.first_index() && absolute_idx < self.total_count
168    }
169
170    /// Number of items that have been evicted.
171    #[must_use]
172    pub fn evicted_count(&self) -> usize {
173        self.first_index()
174    }
175
176    /// Clear all items.
177    ///
178    /// Note: `total_count` is preserved for consistency with absolute indexing.
179    pub fn clear(&mut self) {
180        self.ring.clear();
181    }
182
183    /// Clear all items and reset counters.
184    pub fn reset(&mut self) {
185        self.ring.clear();
186        self.total_count = 0;
187    }
188
189    /// Get the most recent item.
190    #[must_use]
191    pub fn back(&self) -> Option<&T> {
192        self.ring.back()
193    }
194
195    /// Get the oldest item still in memory.
196    #[must_use]
197    pub fn front(&self) -> Option<&T> {
198        self.ring.front()
199    }
200
201    /// Iterate over items currently in memory (oldest to newest).
202    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &T> {
203        self.ring.iter()
204    }
205
206    /// Iterate over items with their absolute indices.
207    pub fn iter_indexed(&self) -> impl DoubleEndedIterator<Item = (usize, &T)> {
208        let start = self.first_index();
209        self.ring
210            .iter()
211            .enumerate()
212            .map(move |(i, item)| (start + i, item))
213    }
214
215    /// Drain all items from the ring.
216    pub fn drain(&mut self) -> impl Iterator<Item = T> + '_ {
217        self.ring.drain(..)
218    }
219}
220
221impl<T> Default for LogRing<T> {
222    fn default() -> Self {
223        Self::new(1024) // Reasonable default capacity
224    }
225}
226
227impl<T> Extend<T> for LogRing<T> {
228    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
229        for item in iter {
230            self.push(item);
231        }
232    }
233}
234
235impl<T> FromIterator<T> for LogRing<T> {
236    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
237        let items: Vec<T> = iter.into_iter().collect();
238        let capacity = items.len().max(1);
239        let mut ring = Self::new(capacity);
240        ring.extend(items);
241        ring
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn new_creates_empty_ring() {
251        let ring: LogRing<i32> = LogRing::new(10);
252        assert!(ring.is_empty());
253        assert_eq!(ring.len(), 0);
254        assert_eq!(ring.total_count(), 0);
255        assert_eq!(ring.capacity(), 10);
256    }
257
258    #[test]
259    #[should_panic(expected = "capacity must be greater than 0")]
260    fn new_panics_on_zero_capacity() {
261        let _ring: LogRing<i32> = LogRing::new(0);
262    }
263
264    #[test]
265    fn push_adds_items() {
266        let mut ring = LogRing::new(5);
267        ring.push("a");
268        ring.push("b");
269        ring.push("c");
270
271        assert_eq!(ring.len(), 3);
272        assert_eq!(ring.total_count(), 3);
273        assert_eq!(ring.get(0), Some(&"a"));
274        assert_eq!(ring.get(1), Some(&"b"));
275        assert_eq!(ring.get(2), Some(&"c"));
276    }
277
278    #[test]
279    fn push_evicts_oldest_when_full() {
280        let mut ring = LogRing::new(3);
281        ring.push(1);
282        ring.push(2);
283        ring.push(3);
284        ring.push(4); // evicts 1
285        ring.push(5); // evicts 2
286
287        assert_eq!(ring.len(), 3);
288        assert_eq!(ring.total_count(), 5);
289        assert_eq!(ring.get(0), None); // evicted
290        assert_eq!(ring.get(1), None); // evicted
291        assert_eq!(ring.get(2), Some(&3));
292        assert_eq!(ring.get(3), Some(&4));
293        assert_eq!(ring.get(4), Some(&5));
294    }
295
296    #[test]
297    fn first_and_last_index() {
298        let mut ring = LogRing::new(3);
299        assert_eq!(ring.first_index(), 0);
300        assert_eq!(ring.last_index(), None);
301
302        ring.push("a");
303        ring.push("b");
304        assert_eq!(ring.first_index(), 0);
305        assert_eq!(ring.last_index(), Some(1));
306
307        ring.push("c");
308        ring.push("d"); // evicts "a"
309        assert_eq!(ring.first_index(), 1);
310        assert_eq!(ring.last_index(), Some(3));
311    }
312
313    #[test]
314    fn get_range_returns_available_items() {
315        let mut ring = LogRing::new(3);
316        ring.push("a");
317        ring.push("b");
318        ring.push("c");
319        ring.push("d"); // evicts "a"
320
321        let items: Vec<_> = ring.get_range(0..5).collect();
322        assert_eq!(items, vec![&"b", &"c", &"d"]);
323
324        let items: Vec<_> = ring.get_range(2..4).collect();
325        assert_eq!(items, vec![&"c", &"d"]);
326    }
327
328    #[test]
329    fn is_in_memory() {
330        let mut ring = LogRing::new(2);
331        ring.push(1);
332        ring.push(2);
333        ring.push(3); // evicts 1
334
335        assert!(!ring.is_in_memory(0));
336        assert!(ring.is_in_memory(1));
337        assert!(ring.is_in_memory(2));
338        assert!(!ring.is_in_memory(3));
339    }
340
341    #[test]
342    fn evicted_count() {
343        let mut ring = LogRing::new(2);
344        assert_eq!(ring.evicted_count(), 0);
345
346        ring.push(1);
347        ring.push(2);
348        assert_eq!(ring.evicted_count(), 0);
349
350        ring.push(3); // evicts 1
351        assert_eq!(ring.evicted_count(), 1);
352
353        ring.push(4); // evicts 2
354        assert_eq!(ring.evicted_count(), 2);
355    }
356
357    #[test]
358    fn clear_preserves_total_count() {
359        let mut ring = LogRing::new(5);
360        ring.push(1);
361        ring.push(2);
362        ring.push(3);
363
364        ring.clear();
365        assert!(ring.is_empty());
366        assert_eq!(ring.total_count(), 3);
367        assert_eq!(ring.first_index(), 3);
368    }
369
370    #[test]
371    fn reset_clears_everything() {
372        let mut ring = LogRing::new(5);
373        ring.push(1);
374        ring.push(2);
375        ring.push(3);
376
377        ring.reset();
378        assert!(ring.is_empty());
379        assert_eq!(ring.total_count(), 0);
380        assert_eq!(ring.first_index(), 0);
381    }
382
383    #[test]
384    fn front_and_back() {
385        let mut ring = LogRing::new(3);
386        assert_eq!(ring.front(), None);
387        assert_eq!(ring.back(), None);
388
389        ring.push("first");
390        ring.push("middle");
391        ring.push("last");
392
393        assert_eq!(ring.front(), Some(&"first"));
394        assert_eq!(ring.back(), Some(&"last"));
395
396        ring.push("newest"); // evicts "first"
397        assert_eq!(ring.front(), Some(&"middle"));
398        assert_eq!(ring.back(), Some(&"newest"));
399    }
400
401    #[test]
402    fn iter_yields_oldest_to_newest() {
403        let mut ring = LogRing::new(3);
404        ring.push(1);
405        ring.push(2);
406        ring.push(3);
407
408        let items: Vec<_> = ring.iter().copied().collect();
409        assert_eq!(items, vec![1, 2, 3]);
410    }
411
412    #[test]
413    fn iter_indexed_includes_absolute_indices() {
414        let mut ring = LogRing::new(2);
415        ring.push("a");
416        ring.push("b");
417        ring.push("c"); // evicts "a"
418
419        let indexed: Vec<_> = ring.iter_indexed().collect();
420        assert_eq!(indexed, vec![(1, &"b"), (2, &"c")]);
421    }
422
423    #[test]
424    fn extend_adds_multiple_items() {
425        let mut ring = LogRing::new(5);
426        ring.extend(vec![1, 2, 3]);
427
428        assert_eq!(ring.len(), 3);
429        assert_eq!(ring.total_count(), 3);
430    }
431
432    #[test]
433    fn from_iter_creates_ring() {
434        let ring: LogRing<i32> = vec![1, 2, 3, 4, 5].into_iter().collect();
435        assert_eq!(ring.len(), 5);
436        assert_eq!(ring.capacity(), 5);
437    }
438
439    #[test]
440    fn default_has_reasonable_capacity() {
441        let ring: LogRing<i32> = LogRing::default();
442        assert_eq!(ring.capacity(), 1024);
443    }
444
445    #[test]
446    fn get_mut_allows_modification() {
447        let mut ring = LogRing::new(3);
448        ring.push(1);
449        ring.push(2);
450
451        if let Some(item) = ring.get_mut(0) {
452            *item = 10;
453        }
454
455        assert_eq!(ring.get(0), Some(&10));
456    }
457
458    #[test]
459    fn drain_removes_all_items() {
460        let mut ring = LogRing::new(5);
461        ring.push(1);
462        ring.push(2);
463        ring.push(3);
464
465        let drained: Vec<_> = ring.drain().collect();
466        assert_eq!(drained, vec![1, 2, 3]);
467        assert!(ring.is_empty());
468        assert_eq!(ring.total_count(), 3); // preserved
469    }
470
471    #[test]
472    fn handles_large_total_count() {
473        let mut ring = LogRing::new(2);
474        for i in 0..1000 {
475            ring.push(i);
476        }
477
478        assert_eq!(ring.len(), 2);
479        assert_eq!(ring.total_count(), 1000);
480        assert_eq!(ring.first_index(), 998);
481        assert_eq!(ring.get(998), Some(&998));
482        assert_eq!(ring.get(999), Some(&999));
483    }
484}