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}