1#![no_std]
18
19extern crate alloc;
20
21use alloc::collections::BTreeMap;
22use alloc::string::{String, ToString};
23use alloc::vec::Vec;
24
25use termstate_core::limits::{MAX_RECORDS, MIN_RECORDS};
26use termstate_core::{Kind, Message, Report, State};
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct Record {
31 pub state: State,
33 pub kind: Option<Kind>,
35 pub progress: Option<u8>,
37 pub app: Option<String>,
40 pub title: Option<String>,
42 pub msg: Option<String>,
44}
45
46impl Record {
47 pub fn from_report(r: &Report<'_>) -> Self {
49 Record {
50 state: r.state,
51 kind: r.kind,
52 progress: r.progress,
53 app: r.app.map(|a| a.as_str().into()),
54 title: r.title.map(|t| t.to_string()),
55 msg: r.msg.map(|t| t.to_string()),
56 }
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
62pub enum Change {
63 Set(Option<String>),
65 Removed(Option<String>),
67 Evicted(Option<String>),
69}
70
71#[derive(Clone, Debug)]
72struct Entry {
73 record: Record,
74 touched: u64,
75}
76
77#[derive(Clone, Debug)]
79pub struct Store {
80 records: BTreeMap<String, Entry>,
83 capacity: usize,
84 clock: u64,
85 drop_idle: bool,
86}
87
88impl Default for Store {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94fn key(id: Option<&str>) -> &str {
95 id.unwrap_or("")
96}
97
98fn id_of(key: &str) -> Option<String> {
99 (!key.is_empty()).then(|| key.into())
100}
101
102fn within(ancestor: &str, key: &str) -> bool {
104 ancestor.is_empty()
105 || key == ancestor
106 || (key.len() > ancestor.len()
107 && key.starts_with(ancestor)
108 && key.as_bytes()[ancestor.len()] == b'/')
109}
110
111impl Store {
112 pub fn new() -> Self {
114 Self::with_capacity(MAX_RECORDS)
115 }
116
117 pub fn with_capacity(capacity: usize) -> Self {
120 Store {
121 records: BTreeMap::new(),
122 capacity: capacity.clamp(MIN_RECORDS, MAX_RECORDS),
123 clock: 0,
124 drop_idle: false,
125 }
126 }
127
128 pub fn set_drop_idle_on_lifetime_events(&mut self, on: bool) {
131 self.drop_idle = on;
132 }
133
134 pub fn capacity(&self) -> usize {
136 self.capacity
137 }
138
139 pub fn len(&self) -> usize {
141 self.records.len()
142 }
143
144 pub fn is_empty(&self) -> bool {
146 self.records.is_empty()
147 }
148
149 pub fn get(&self, id: Option<&str>) -> Option<&Record> {
151 self.records.get(key(id)).map(|e| &e.record)
152 }
153
154 pub fn iter(&self) -> impl Iterator<Item = (Option<&str>, &Record)> {
156 self.records
157 .iter()
158 .map(|(k, e)| ((!k.is_empty()).then_some(k.as_str()), &e.record))
159 }
160
161 pub fn effective_app(&self, id: Option<&str>) -> Option<&str> {
165 let mut k = key(id);
166 loop {
167 if let Some(app) = self.records.get(k).and_then(|e| e.record.app.as_deref()) {
168 return Some(app);
169 }
170 if k.is_empty() {
171 return None;
172 }
173 k = k.rfind('/').map_or("", |i| &k[..i]);
174 }
175 }
176
177 pub fn apply(&mut self, message: &Message<'_>) -> Vec<Change> {
179 match message {
180 Message::Report(r) => self.set(r),
181 Message::Clear { id } => self.clear(id.map(|i| i.as_str())),
182 Message::Feature(_) => Vec::new(),
183 }
184 }
185
186 pub fn set(&mut self, report: &Report<'_>) -> Vec<Change> {
188 let mut changes = Vec::new();
189 let k = key(report.id.map(|i| i.as_str()));
190 self.clock += 1;
191 let entry = Entry {
192 record: Record::from_report(report),
193 touched: self.clock,
194 };
195 if let Some(existing) = self.records.get_mut(k) {
196 *existing = entry;
197 } else {
198 if self.records.len() >= self.capacity {
199 let oldest = self
200 .records
201 .iter()
202 .min_by_key(|(_, e)| e.touched)
203 .map(|(k, _)| k.clone());
204 if let Some(oldest) = oldest {
205 self.records.remove(&oldest);
206 changes.push(Change::Evicted(id_of(&oldest)));
207 }
208 }
209 self.records.insert(k.to_string(), entry);
210 }
211 changes.push(Change::Set(id_of(k)));
212 changes
213 }
214
215 pub fn clear(&mut self, id: Option<&str>) -> Vec<Change> {
218 let ancestor = key(id);
219 self.remove_where(|k, _| within(ancestor, k))
220 }
221
222 pub fn process_exited(&mut self) -> Vec<Change> {
225 self.lifetime_event()
226 }
227
228 pub fn prompt_started(&mut self) -> Vec<Change> {
231 self.lifetime_event()
232 }
233
234 pub fn reset(&mut self) -> Vec<Change> {
237 self.clear(None)
238 }
239
240 fn lifetime_event(&mut self) -> Vec<Change> {
241 let drop_idle = self.drop_idle;
242 self.remove_where(|_, r| r.state.is_transient() || (drop_idle && r.state == State::Idle))
243 }
244
245 fn remove_where(&mut self, mut pred: impl FnMut(&str, &Record) -> bool) -> Vec<Change> {
246 let doomed: Vec<String> = self
247 .records
248 .iter()
249 .filter(|(k, e)| pred(k, &e.record))
250 .map(|(k, _)| k.clone())
251 .collect();
252 doomed
253 .into_iter()
254 .map(|k| {
255 self.records.remove(&k);
256 Change::Removed(id_of(&k))
257 })
258 .collect()
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use termstate_core::parse_body;
266
267 fn apply(s: &mut Store, body: &str) -> Vec<Change> {
268 s.apply(&parse_body(body.as_bytes()).unwrap())
269 }
270
271 #[test]
272 fn within_matches_segments_only() {
273 assert!(within("", "a"));
274 assert!(within("a", "a"));
275 assert!(within("a", "a/b"));
276 assert!(!within("a", "ab"));
277 assert!(!within("a/b", "a"));
278 }
279
280 #[test]
281 fn eviction_spares_updates_and_takes_least_recent() {
282 let mut s = Store::with_capacity(0);
283 assert_eq!(s.capacity(), MIN_RECORDS);
284 for i in 0..MIN_RECORDS {
285 apply(&mut s, &alloc::format!("state=idle:id=r{i}"));
286 }
287 assert_eq!(
289 apply(&mut s, "state=done:id=r0"),
290 [Change::Set(Some("r0".into()))]
291 );
292 assert_eq!(
293 apply(&mut s, "state=idle:id=new"),
294 [
295 Change::Evicted(Some("r1".into())),
296 Change::Set(Some("new".into()))
297 ]
298 );
299 assert_eq!(s.len(), MIN_RECORDS);
300 }
301}