#![no_std]
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use termstate_core::limits::{MAX_RECORDS, MIN_RECORDS};
use termstate_core::{Kind, Message, Report, State};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Record {
pub state: State,
pub kind: Option<Kind>,
pub progress: Option<u8>,
pub app: Option<String>,
pub title: Option<String>,
pub msg: Option<String>,
}
impl Record {
pub fn from_report(r: &Report<'_>) -> Self {
Record {
state: r.state,
kind: r.kind,
progress: r.progress,
app: r.app.map(|a| a.as_str().into()),
title: r.title.map(|t| t.to_string()),
msg: r.msg.map(|t| t.to_string()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Change {
Set(Option<String>),
Removed(Option<String>),
Evicted(Option<String>),
}
#[derive(Clone, Debug)]
struct Entry {
record: Record,
touched: u64,
}
#[derive(Clone, Debug)]
pub struct Store {
records: BTreeMap<String, Entry>,
capacity: usize,
clock: u64,
drop_idle: bool,
}
impl Default for Store {
fn default() -> Self {
Self::new()
}
}
fn key(id: Option<&str>) -> &str {
id.unwrap_or("")
}
fn id_of(key: &str) -> Option<String> {
(!key.is_empty()).then(|| key.into())
}
fn within(ancestor: &str, key: &str) -> bool {
ancestor.is_empty()
|| key == ancestor
|| (key.len() > ancestor.len()
&& key.starts_with(ancestor)
&& key.as_bytes()[ancestor.len()] == b'/')
}
impl Store {
pub fn new() -> Self {
Self::with_capacity(MAX_RECORDS)
}
pub fn with_capacity(capacity: usize) -> Self {
Store {
records: BTreeMap::new(),
capacity: capacity.clamp(MIN_RECORDS, MAX_RECORDS),
clock: 0,
drop_idle: false,
}
}
pub fn set_drop_idle_on_lifetime_events(&mut self, on: bool) {
self.drop_idle = on;
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn get(&self, id: Option<&str>) -> Option<&Record> {
self.records.get(key(id)).map(|e| &e.record)
}
pub fn iter(&self) -> impl Iterator<Item = (Option<&str>, &Record)> {
self.records
.iter()
.map(|(k, e)| ((!k.is_empty()).then_some(k.as_str()), &e.record))
}
pub fn effective_app(&self, id: Option<&str>) -> Option<&str> {
let mut k = key(id);
loop {
if let Some(app) = self.records.get(k).and_then(|e| e.record.app.as_deref()) {
return Some(app);
}
if k.is_empty() {
return None;
}
k = k.rfind('/').map_or("", |i| &k[..i]);
}
}
pub fn apply(&mut self, message: &Message<'_>) -> Vec<Change> {
match message {
Message::Report(r) => self.set(r),
Message::Clear { id } => self.clear(id.map(|i| i.as_str())),
Message::Feature(_) => Vec::new(),
}
}
pub fn set(&mut self, report: &Report<'_>) -> Vec<Change> {
let mut changes = Vec::new();
let k = key(report.id.map(|i| i.as_str()));
self.clock += 1;
let entry = Entry {
record: Record::from_report(report),
touched: self.clock,
};
if let Some(existing) = self.records.get_mut(k) {
*existing = entry;
} else {
if self.records.len() >= self.capacity {
let oldest = self
.records
.iter()
.min_by_key(|(_, e)| e.touched)
.map(|(k, _)| k.clone());
if let Some(oldest) = oldest {
self.records.remove(&oldest);
changes.push(Change::Evicted(id_of(&oldest)));
}
}
self.records.insert(k.to_string(), entry);
}
changes.push(Change::Set(id_of(k)));
changes
}
pub fn clear(&mut self, id: Option<&str>) -> Vec<Change> {
let ancestor = key(id);
self.remove_where(|k, _| within(ancestor, k))
}
pub fn process_exited(&mut self) -> Vec<Change> {
self.lifetime_event()
}
pub fn prompt_started(&mut self) -> Vec<Change> {
self.lifetime_event()
}
pub fn reset(&mut self) -> Vec<Change> {
self.clear(None)
}
fn lifetime_event(&mut self) -> Vec<Change> {
let drop_idle = self.drop_idle;
self.remove_where(|_, r| r.state.is_transient() || (drop_idle && r.state == State::Idle))
}
fn remove_where(&mut self, mut pred: impl FnMut(&str, &Record) -> bool) -> Vec<Change> {
let doomed: Vec<String> = self
.records
.iter()
.filter(|(k, e)| pred(k, &e.record))
.map(|(k, _)| k.clone())
.collect();
doomed
.into_iter()
.map(|k| {
self.records.remove(&k);
Change::Removed(id_of(&k))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use termstate_core::parse_body;
fn apply(s: &mut Store, body: &str) -> Vec<Change> {
s.apply(&parse_body(body.as_bytes()).unwrap())
}
#[test]
fn within_matches_segments_only() {
assert!(within("", "a"));
assert!(within("a", "a"));
assert!(within("a", "a/b"));
assert!(!within("a", "ab"));
assert!(!within("a/b", "a"));
}
#[test]
fn eviction_spares_updates_and_takes_least_recent() {
let mut s = Store::with_capacity(0);
assert_eq!(s.capacity(), MIN_RECORDS);
for i in 0..MIN_RECORDS {
apply(&mut s, &alloc::format!("state=idle:id=r{i}"));
}
assert_eq!(
apply(&mut s, "state=done:id=r0"),
[Change::Set(Some("r0".into()))]
);
assert_eq!(
apply(&mut s, "state=idle:id=new"),
[
Change::Evicted(Some("r1".into())),
Change::Set(Some("new".into()))
]
);
assert_eq!(s.len(), MIN_RECORDS);
}
}