ailloli_ui_terminal_core/
scrollback.rs

1//! Bounded FIFO storage for terminal history lines.
2
3use std::collections::VecDeque;
4
5use serde::{Deserialize, Serialize};
6
7use crate::line::TerminalLine;
8
9/// Oldest-to-newest bounded terminal scrollback.
10///
11/// Normal mutation retains at most `limit` lines. Derived deserialization stores
12/// private fields verbatim and can bypass that relationship until the next
13/// [`Self::push`] or [`Self::replace_lines`].
14///
15/// # Examples
16///
17/// ```
18/// use ailloli_ui_terminal_core::TerminalScrollback;
19/// let history = TerminalScrollback::new(100);
20/// assert_eq!((history.limit(), history.len(), history.total_pushed()), (100, 0, 0));
21/// ```
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct TerminalScrollback {
24    /// Maximum retained line count; zero disables retention.
25    limit: usize,
26    /// Retained lines in oldest-to-newest order.
27    lines: VecDeque<TerminalLine>,
28    /// Saturating cumulative number of lines presented to [`Self::push`].
29    #[serde(default)]
30    total_pushed: u64,
31}
32
33impl TerminalScrollback {
34    /// Creates empty history with an exact retained-line limit.
35    ///
36    /// A zero limit still counts later pushes but retains no lines.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use ailloli_ui_terminal_core::TerminalScrollback;
42    /// assert_eq!(TerminalScrollback::new(0).limit(), 0);
43    /// ```
44    pub fn new(limit: usize) -> Self {
45        Self {
46            limit,
47            lines: VecDeque::new(),
48            total_pushed: 0,
49        }
50    }
51
52    /// Returns the configured maximum retained line count.
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use ailloli_ui_terminal_core::TerminalScrollback;
58    /// assert_eq!(TerminalScrollback::new(7).limit(), 7);
59    /// ```
60    pub fn limit(&self) -> usize {
61        self.limit
62    }
63
64    /// Returns the current retained line count.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use ailloli_ui_terminal_core::TerminalScrollback;
70    /// assert_eq!(TerminalScrollback::default().len(), 0);
71    /// ```
72    pub fn len(&self) -> usize {
73        self.lines.len()
74    }
75
76    /// Returns whether no history lines are retained.
77    ///
78    /// # Examples
79    ///
80    /// ```
81    /// use ailloli_ui_terminal_core::TerminalScrollback;
82    /// assert!(TerminalScrollback::default().is_empty());
83    /// ```
84    pub fn is_empty(&self) -> bool {
85        self.lines.is_empty()
86    }
87
88    /// Iterates retained lines from oldest to newest.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use ailloli_ui_terminal_core::{TerminalLine, TerminalScrollback, TerminalStyle};
94    /// let mut history = TerminalScrollback::new(1);
95    /// history.push(TerminalLine::blank(2, TerminalStyle::default()));
96    /// assert_eq!(history.iter().count(), 1);
97    /// ```
98    pub fn iter(&self) -> impl Iterator<Item = &TerminalLine> {
99        self.lines.iter()
100    }
101
102    /// Returns the saturating cumulative push count.
103    ///
104    /// Clearing retained lines does not reset this counter. [`Self::replace_lines`]
105    /// can explicitly replace it with any `u64`.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use ailloli_ui_terminal_core::{TerminalLine, TerminalScrollback, TerminalStyle};
111    /// let mut history = TerminalScrollback::new(0);
112    /// history.push(TerminalLine::blank(1, TerminalStyle::default()));
113    /// assert_eq!((history.len(), history.total_pushed()), (0, 1));
114    /// ```
115    pub fn total_pushed(&self) -> u64 {
116        self.total_pushed
117    }
118
119    /// Replaces retained lines and the cumulative push counter.
120    ///
121    /// Input is consumed oldest-to-newest; if it exceeds the limit, only the
122    /// newest `limit` lines remain. A zero limit consumes the supplied iterator
123    /// only up to construction of the iterator and retains none. `total_pushed`
124    /// is accepted verbatim and need not equal the input/retained count.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use ailloli_ui_terminal_core::{TerminalLine, TerminalScrollback, TerminalStyle};
130    /// let style = TerminalStyle::default();
131    /// let mut history = TerminalScrollback::new(1);
132    /// history.replace_lines([TerminalLine::blank(1, style), TerminalLine::blank(2, style)], 9);
133    /// assert_eq!((history.len(), history.iter().next().unwrap().len(), history.total_pushed()), (1, 2, 9));
134    /// ```
135    pub fn replace_lines(
136        &mut self,
137        lines: impl IntoIterator<Item = TerminalLine>,
138        total_pushed: u64,
139    ) {
140        self.lines.clear();
141        self.total_pushed = total_pushed;
142        if self.limit == 0 {
143            return;
144        }
145        for line in lines {
146            self.lines.push_back(line);
147            while self.lines.len() > self.limit {
148                self.lines.pop_front();
149            }
150        }
151    }
152
153    /// Counts and appends one line, evicting oldest lines above the limit.
154    ///
155    /// `total_pushed` saturates at [`u64::MAX`]. A zero limit increments the
156    /// counter but retains no line.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use ailloli_ui_terminal_core::{TerminalLine, TerminalScrollback, TerminalStyle};
162    /// let mut history = TerminalScrollback::new(1);
163    /// history.push(TerminalLine::blank(1, TerminalStyle::default()));
164    /// history.push(TerminalLine::blank(2, TerminalStyle::default()));
165    /// assert_eq!((history.len(), history.total_pushed()), (1, 2));
166    /// ```
167    pub fn push(&mut self, line: TerminalLine) {
168        self.total_pushed = self.total_pushed.saturating_add(1);
169        if self.limit == 0 {
170            return;
171        }
172        self.lines.push_back(line);
173        while self.lines.len() > self.limit {
174            self.lines.pop_front();
175        }
176    }
177
178    /// Removes retained lines without changing the limit or cumulative count.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use ailloli_ui_terminal_core::{TerminalLine, TerminalScrollback, TerminalStyle};
184    /// let mut history = TerminalScrollback::new(1);
185    /// history.push(TerminalLine::blank(1, TerminalStyle::default())); history.clear();
186    /// assert!(history.is_empty() && history.total_pushed() == 1);
187    /// ```
188    pub fn clear(&mut self) {
189        self.lines.clear();
190    }
191}
192
193impl Default for TerminalScrollback {
194    fn default() -> Self {
195        Self::new(10_000)
196    }
197}