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 = "use the returned item (if any)"]
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 = "use the returned item (if any)"]
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    #[inline]
130    #[must_use]
131    pub fn len(&self) -> usize {
132        self.ring.len()
133    }
134
135    /// Check if the ring is empty.
136    #[inline]
137    #[must_use]
138    pub fn is_empty(&self) -> bool {
139        self.ring.is_empty()
140    }
141
142    /// Maximum capacity of the ring.
143    #[must_use]
144    pub const fn capacity(&self) -> usize {
145        self.capacity
146    }
147
148    /// First absolute index still in memory.
149    #[must_use]
150    pub fn first_index(&self) -> usize {
151        self.total_count.saturating_sub(self.ring.len())
152    }
153
154    /// Last absolute index (most recent item).
155    ///
156    /// Returns `None` if the ring is empty.
157    #[must_use = "use the returned index (if any)"]
158    pub fn last_index(&self) -> Option<usize> {
159        if self.total_count > 0 {
160            Some(self.total_count - 1)
161        } else {
162            None
163        }
164    }
165
166    /// Check if an absolute index is still in memory.
167    #[must_use]
168    pub fn is_in_memory(&self, absolute_idx: usize) -> bool {
169        absolute_idx >= self.first_index() && absolute_idx < self.total_count
170    }
171
172    /// Number of items that have been evicted.
173    #[must_use]
174    pub fn evicted_count(&self) -> usize {
175        self.first_index()
176    }
177
178    /// Clear all items.
179    ///
180    /// Note: `total_count` is preserved for consistency with absolute indexing.
181    pub fn clear(&mut self) {
182        self.ring.clear();
183    }
184
185    /// Clear all items and reset counters.
186    pub fn reset(&mut self) {
187        self.ring.clear();
188        self.total_count = 0;
189    }
190
191    /// Get the most recent item.
192    #[must_use = "use the returned item (if any)"]
193    pub fn back(&self) -> Option<&T> {
194        self.ring.back()
195    }
196
197    /// Get the oldest item still in memory.
198    #[must_use = "use the returned item (if any)"]
199    pub fn front(&self) -> Option<&T> {
200        self.ring.front()
201    }
202
203    /// Iterate over items currently in memory (oldest to newest).
204    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &T> {
205        self.ring.iter()
206    }
207
208    /// Iterate over items with their absolute indices.
209    pub fn iter_indexed(&self) -> impl DoubleEndedIterator<Item = (usize, &T)> {
210        let start = self.first_index();
211        self.ring
212            .iter()
213            .enumerate()
214            .map(move |(i, item)| (start + i, item))
215    }
216
217    /// Drain all items from the ring.
218    pub fn drain(&mut self) -> impl Iterator<Item = T> + '_ {
219        self.ring.drain(..)
220    }
221}
222
223impl<T> Default for LogRing<T> {
224    fn default() -> Self {
225        Self::new(1024) // Reasonable default capacity
226    }
227}
228
229impl<T> Extend<T> for LogRing<T> {
230    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
231        for item in iter {
232            self.push(item);
233        }
234    }
235}
236
237impl<T> FromIterator<T> for LogRing<T> {
238    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
239        let items: Vec<T> = iter.into_iter().collect();
240        let capacity = items.len().max(1);
241        let mut ring = Self::new(capacity);
242        ring.extend(items);
243        ring
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn new_creates_empty_ring() {
253        let ring: LogRing<i32> = LogRing::new(10);
254        assert!(ring.is_empty());
255        assert_eq!(ring.len(), 0);
256        assert_eq!(ring.total_count(), 0);
257        assert_eq!(ring.capacity(), 10);
258    }
259
260    #[test]
261    #[should_panic(expected = "capacity must be greater than 0")]
262    fn new_panics_on_zero_capacity() {
263        let _ring: LogRing<i32> = LogRing::new(0);
264    }
265
266    #[test]
267    fn push_adds_items() {
268        let mut ring = LogRing::new(5);
269        ring.push("a");
270        ring.push("b");
271        ring.push("c");
272
273        assert_eq!(ring.len(), 3);
274        assert_eq!(ring.total_count(), 3);
275        assert_eq!(ring.get(0), Some(&"a"));
276        assert_eq!(ring.get(1), Some(&"b"));
277        assert_eq!(ring.get(2), Some(&"c"));
278    }
279
280    #[test]
281    fn push_evicts_oldest_when_full() {
282        let mut ring = LogRing::new(3);
283        ring.push(1);
284        ring.push(2);
285        ring.push(3);
286        ring.push(4); // evicts 1
287        ring.push(5); // evicts 2
288
289        assert_eq!(ring.len(), 3);
290        assert_eq!(ring.total_count(), 5);
291        assert_eq!(ring.get(0), None); // evicted
292        assert_eq!(ring.get(1), None); // evicted
293        assert_eq!(ring.get(2), Some(&3));
294        assert_eq!(ring.get(3), Some(&4));
295        assert_eq!(ring.get(4), Some(&5));
296    }
297
298    #[test]
299    fn first_and_last_index() {
300        let mut ring = LogRing::new(3);
301        assert_eq!(ring.first_index(), 0);
302        assert_eq!(ring.last_index(), None);
303
304        ring.push("a");
305        ring.push("b");
306        assert_eq!(ring.first_index(), 0);
307        assert_eq!(ring.last_index(), Some(1));
308
309        ring.push("c");
310        ring.push("d"); // evicts "a"
311        assert_eq!(ring.first_index(), 1);
312        assert_eq!(ring.last_index(), Some(3));
313    }
314
315    #[test]
316    fn get_range_returns_available_items() {
317        let mut ring = LogRing::new(3);
318        ring.push("a");
319        ring.push("b");
320        ring.push("c");
321        ring.push("d"); // evicts "a"
322
323        let items: Vec<_> = ring.get_range(0..5).collect();
324        assert_eq!(items, vec![&"b", &"c", &"d"]);
325
326        let items: Vec<_> = ring.get_range(2..4).collect();
327        assert_eq!(items, vec![&"c", &"d"]);
328    }
329
330    #[test]
331    fn is_in_memory() {
332        let mut ring = LogRing::new(2);
333        ring.push(1);
334        ring.push(2);
335        ring.push(3); // evicts 1
336
337        assert!(!ring.is_in_memory(0));
338        assert!(ring.is_in_memory(1));
339        assert!(ring.is_in_memory(2));
340        assert!(!ring.is_in_memory(3));
341    }
342
343    #[test]
344    fn evicted_count() {
345        let mut ring = LogRing::new(2);
346        assert_eq!(ring.evicted_count(), 0);
347
348        ring.push(1);
349        ring.push(2);
350        assert_eq!(ring.evicted_count(), 0);
351
352        ring.push(3); // evicts 1
353        assert_eq!(ring.evicted_count(), 1);
354
355        ring.push(4); // evicts 2
356        assert_eq!(ring.evicted_count(), 2);
357    }
358
359    #[test]
360    fn clear_preserves_total_count() {
361        let mut ring = LogRing::new(5);
362        ring.push(1);
363        ring.push(2);
364        ring.push(3);
365
366        ring.clear();
367        assert!(ring.is_empty());
368        assert_eq!(ring.total_count(), 3);
369        assert_eq!(ring.first_index(), 3);
370    }
371
372    #[test]
373    fn reset_clears_everything() {
374        let mut ring = LogRing::new(5);
375        ring.push(1);
376        ring.push(2);
377        ring.push(3);
378
379        ring.reset();
380        assert!(ring.is_empty());
381        assert_eq!(ring.total_count(), 0);
382        assert_eq!(ring.first_index(), 0);
383    }
384
385    #[test]
386    fn front_and_back() {
387        let mut ring = LogRing::new(3);
388        assert_eq!(ring.front(), None);
389        assert_eq!(ring.back(), None);
390
391        ring.push("first");
392        ring.push("middle");
393        ring.push("last");
394
395        assert_eq!(ring.front(), Some(&"first"));
396        assert_eq!(ring.back(), Some(&"last"));
397
398        ring.push("newest"); // evicts "first"
399        assert_eq!(ring.front(), Some(&"middle"));
400        assert_eq!(ring.back(), Some(&"newest"));
401    }
402
403    #[test]
404    fn iter_yields_oldest_to_newest() {
405        let mut ring = LogRing::new(3);
406        ring.push(1);
407        ring.push(2);
408        ring.push(3);
409
410        let items: Vec<_> = ring.iter().copied().collect();
411        assert_eq!(items, vec![1, 2, 3]);
412    }
413
414    #[test]
415    fn iter_indexed_includes_absolute_indices() {
416        let mut ring = LogRing::new(2);
417        ring.push("a");
418        ring.push("b");
419        ring.push("c"); // evicts "a"
420
421        let indexed: Vec<_> = ring.iter_indexed().collect();
422        assert_eq!(indexed, vec![(1, &"b"), (2, &"c")]);
423    }
424
425    #[test]
426    fn extend_adds_multiple_items() {
427        let mut ring = LogRing::new(5);
428        ring.extend(vec![1, 2, 3]);
429
430        assert_eq!(ring.len(), 3);
431        assert_eq!(ring.total_count(), 3);
432    }
433
434    #[test]
435    fn from_iter_creates_ring() {
436        let ring: LogRing<i32> = vec![1, 2, 3, 4, 5].into_iter().collect();
437        assert_eq!(ring.len(), 5);
438        assert_eq!(ring.capacity(), 5);
439    }
440
441    #[test]
442    fn default_has_reasonable_capacity() {
443        let ring: LogRing<i32> = LogRing::default();
444        assert_eq!(ring.capacity(), 1024);
445    }
446
447    #[test]
448    fn get_mut_allows_modification() {
449        let mut ring = LogRing::new(3);
450        ring.push(1);
451        ring.push(2);
452
453        if let Some(item) = ring.get_mut(0) {
454            *item = 10;
455        }
456
457        assert_eq!(ring.get(0), Some(&10));
458    }
459
460    #[test]
461    fn drain_removes_all_items() {
462        let mut ring = LogRing::new(5);
463        ring.push(1);
464        ring.push(2);
465        ring.push(3);
466
467        let drained: Vec<_> = ring.drain().collect();
468        assert_eq!(drained, vec![1, 2, 3]);
469        assert!(ring.is_empty());
470        assert_eq!(ring.total_count(), 3); // preserved
471    }
472
473    #[test]
474    fn handles_large_total_count() {
475        let mut ring = LogRing::new(2);
476        for i in 0..1000 {
477            ring.push(i);
478        }
479
480        assert_eq!(ring.len(), 2);
481        assert_eq!(ring.total_count(), 1000);
482        assert_eq!(ring.first_index(), 998);
483        assert_eq!(ring.get(998), Some(&998));
484        assert_eq!(ring.get(999), Some(&999));
485    }
486
487    // ── Edge-case tests (bd-2n2oo) ──────────────────────────
488
489    #[test]
490    fn capacity_one_ring() {
491        let mut ring = LogRing::new(1);
492        ring.push("a");
493        assert_eq!(ring.len(), 1);
494        assert_eq!(ring.get(0), Some(&"a"));
495
496        ring.push("b"); // evicts "a"
497        assert_eq!(ring.len(), 1);
498        assert_eq!(ring.total_count(), 2);
499        assert_eq!(ring.get(0), None);
500        assert_eq!(ring.get(1), Some(&"b"));
501        assert_eq!(ring.first_index(), 1);
502    }
503
504    #[test]
505    fn get_mut_evicted_returns_none() {
506        let mut ring = LogRing::new(2);
507        ring.push(1);
508        ring.push(2);
509        ring.push(3); // evicts 1
510        assert!(ring.get_mut(0).is_none());
511    }
512
513    #[test]
514    fn get_mut_beyond_total_returns_none() {
515        let mut ring = LogRing::new(5);
516        ring.push(1);
517        assert!(ring.get_mut(1).is_none());
518        assert!(ring.get_mut(100).is_none());
519    }
520
521    #[test]
522    fn get_beyond_total_count() {
523        let mut ring = LogRing::new(5);
524        ring.push(1);
525        assert_eq!(ring.get(1), None); // total_count=1, idx 1 is out of range
526        assert_eq!(ring.get(usize::MAX), None);
527    }
528
529    #[test]
530    fn get_range_empty_range() {
531        let mut ring = LogRing::new(5);
532        ring.push(1);
533        ring.push(2);
534        let items: Vec<_> = ring.get_range(1..1).collect();
535        assert!(items.is_empty());
536    }
537
538    #[test]
539    fn get_range_inverted_start_gt_end() {
540        let mut ring = LogRing::new(5);
541        ring.push(1);
542        ring.push(2);
543        // Range start > end is empty. Use black_box to avoid clippy's
544        // `reversed_empty_ranges` lint while still exercising the path.
545        let start = std::hint::black_box(5usize);
546        let end = std::hint::black_box(2usize);
547        let items: Vec<_> = ring.get_range(start..end).collect();
548        assert!(items.is_empty());
549    }
550
551    #[test]
552    fn get_range_fully_evicted() {
553        let mut ring = LogRing::new(2);
554        ring.push(1);
555        ring.push(2);
556        ring.push(3);
557        ring.push(4);
558        // indices 0..2 are evicted, first_index=2
559        let items: Vec<_> = ring.get_range(0..2).collect();
560        assert!(items.is_empty());
561    }
562
563    #[test]
564    fn get_range_fully_future() {
565        let mut ring = LogRing::new(5);
566        ring.push(1);
567        // total_count=1, range 5..10 is entirely future
568        let items: Vec<_> = ring.get_range(5..10).collect();
569        assert!(items.is_empty());
570    }
571
572    #[test]
573    fn get_range_partial_overlap() {
574        let mut ring = LogRing::new(3);
575        ring.push(10);
576        ring.push(20);
577        ring.push(30);
578        ring.push(40); // evicts 10, first_index=1
579        // Request range 0..5, only 1..4 is in memory
580        let items: Vec<_> = ring.get_range(0..5).collect();
581        assert_eq!(items, vec![&20, &30, &40]);
582    }
583
584    #[test]
585    fn iter_empty_ring() {
586        let ring: LogRing<i32> = LogRing::new(5);
587        assert_eq!(ring.iter().count(), 0);
588    }
589
590    #[test]
591    fn iter_indexed_empty_ring() {
592        let ring: LogRing<i32> = LogRing::new(5);
593        assert_eq!(ring.iter_indexed().count(), 0);
594    }
595
596    #[test]
597    fn iter_reverse() {
598        let mut ring = LogRing::new(3);
599        ring.push(1);
600        ring.push(2);
601        ring.push(3);
602        let rev: Vec<_> = ring.iter().rev().copied().collect();
603        assert_eq!(rev, vec![3, 2, 1]);
604    }
605
606    #[test]
607    fn iter_indexed_reverse() {
608        let mut ring = LogRing::new(2);
609        ring.push("a");
610        ring.push("b");
611        ring.push("c"); // evicts "a"
612        let rev: Vec<_> = ring.iter_indexed().rev().collect();
613        assert_eq!(rev, vec![(2, &"c"), (1, &"b")]);
614    }
615
616    #[test]
617    fn extend_empty_iterator() {
618        let mut ring = LogRing::new(5);
619        ring.push(1);
620        ring.extend(std::iter::empty::<i32>());
621        assert_eq!(ring.len(), 1);
622        assert_eq!(ring.total_count(), 1);
623    }
624
625    #[test]
626    fn extend_trait_impl() {
627        let mut ring = LogRing::new(5);
628        Extend::extend(&mut ring, vec![1, 2, 3]);
629        assert_eq!(ring.len(), 3);
630        assert_eq!(ring.total_count(), 3);
631    }
632
633    #[test]
634    fn from_iter_empty() {
635        let ring: LogRing<i32> = std::iter::empty().collect();
636        assert_eq!(ring.capacity(), 1); // max(0, 1)
637        assert!(ring.is_empty());
638    }
639
640    #[test]
641    fn from_iter_single() {
642        let ring: LogRing<i32> = std::iter::once(42).collect();
643        assert_eq!(ring.capacity(), 1);
644        assert_eq!(ring.len(), 1);
645        assert_eq!(ring.get(0), Some(&42));
646    }
647
648    #[test]
649    fn clone_independence() {
650        let mut ring = LogRing::new(5);
651        ring.push(1);
652        ring.push(2);
653        let mut cloned = ring.clone();
654        cloned.push(3);
655        assert_eq!(ring.len(), 2);
656        assert_eq!(cloned.len(), 3);
657        assert_eq!(ring.total_count(), 2);
658        assert_eq!(cloned.total_count(), 3);
659    }
660
661    #[test]
662    fn debug_format() {
663        let mut ring = LogRing::new(3);
664        ring.push(1);
665        let dbg = format!("{:?}", ring);
666        assert!(dbg.contains("LogRing"));
667    }
668
669    #[test]
670    fn drain_empty_ring() {
671        let mut ring: LogRing<i32> = LogRing::new(5);
672        let drained: Vec<_> = ring.drain().collect();
673        assert!(drained.is_empty());
674        assert_eq!(ring.total_count(), 0);
675    }
676
677    #[test]
678    fn clear_then_push_continues_absolute_index() {
679        let mut ring = LogRing::new(5);
680        ring.push("a");
681        ring.push("b");
682        ring.clear();
683        assert_eq!(ring.total_count(), 2);
684        assert_eq!(ring.first_index(), 2);
685
686        ring.push("c");
687        assert_eq!(ring.total_count(), 3);
688        assert_eq!(ring.first_index(), 2);
689        assert_eq!(ring.get(2), Some(&"c"));
690        assert_eq!(ring.get(0), None); // old indices gone
691        assert_eq!(ring.get(1), None);
692    }
693
694    #[test]
695    fn reset_then_push_starts_fresh() {
696        let mut ring = LogRing::new(5);
697        ring.push("a");
698        ring.push("b");
699        ring.reset();
700        ring.push("c");
701        assert_eq!(ring.total_count(), 1);
702        assert_eq!(ring.first_index(), 0);
703        assert_eq!(ring.get(0), Some(&"c"));
704    }
705
706    #[test]
707    fn last_index_after_clear() {
708        let mut ring = LogRing::new(5);
709        ring.push(1);
710        ring.clear();
711        assert_eq!(ring.last_index(), Some(0));
712        // total_count is 1, so last_index = 0
713    }
714
715    #[test]
716    fn last_index_after_reset() {
717        let mut ring = LogRing::new(5);
718        ring.push(1);
719        ring.reset();
720        assert_eq!(ring.last_index(), None);
721    }
722
723    #[test]
724    fn front_back_after_clear() {
725        let mut ring = LogRing::new(5);
726        ring.push(1);
727        ring.clear();
728        assert_eq!(ring.front(), None);
729        assert_eq!(ring.back(), None);
730    }
731
732    #[test]
733    fn is_in_memory_at_exact_boundaries() {
734        let mut ring = LogRing::new(3);
735        ring.push(10);
736        ring.push(20);
737        ring.push(30);
738        // first_index=0, total_count=3
739        assert!(ring.is_in_memory(0));
740        assert!(ring.is_in_memory(2));
741        assert!(!ring.is_in_memory(3)); // == total_count, out of range
742    }
743
744    #[test]
745    fn extend_causes_eviction() {
746        let mut ring = LogRing::new(3);
747        ring.extend(vec![1, 2, 3, 4, 5]);
748        assert_eq!(ring.len(), 3);
749        assert_eq!(ring.total_count(), 5);
750        assert_eq!(ring.get(2), Some(&3));
751        assert_eq!(ring.get(4), Some(&5));
752        assert_eq!(ring.get(0), None); // evicted
753    }
754
755    #[test]
756    fn get_range_exact_memory_window() {
757        let mut ring = LogRing::new(3);
758        ring.push(1);
759        ring.push(2);
760        ring.push(3);
761        ring.push(4); // first_index=1
762        let items: Vec<_> = ring.get_range(1..4).collect();
763        assert_eq!(items, vec![&2, &3, &4]);
764    }
765
766    #[test]
767    fn push_with_string_types() {
768        let mut ring = LogRing::new(2);
769        ring.push(String::from("hello"));
770        ring.push(String::from("world"));
771        ring.push(String::from("foo")); // evicts "hello"
772        assert_eq!(ring.get(1), Some(&String::from("world")));
773        assert_eq!(ring.get(2), Some(&String::from("foo")));
774    }
775}