Skip to main content

osc7501_store/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3
4#[cfg(feature = "alloc")]
5extern crate alloc;
6
7mod change;
8mod record;
9
10pub use change::Change;
11/// Parse and validate reports before applying them to the store.
12pub use osc7501_parser as parser;
13pub use record::Record;
14
15use core::ops::{Deref, DerefMut};
16use parser::{Action, AppId, RecordId, Report, State};
17use record::StoredRecord;
18
19/// Minimum record capacity required of a conforming terminal host.
20pub const MIN_HOST_CAPACITY: usize = 64;
21/// Maximum records per terminal, including root.
22pub const MAX_RECORDS: usize = 256;
23
24/// Caller-owned storage for one record. Fields cannot be mutated directly.
25#[derive(Debug)]
26pub struct Slot {
27    record: Option<StoredRecord>,
28    rank: u16,
29}
30
31impl Slot {
32    /// An unused slot, suitable for `[const { Slot::EMPTY }; capacity]`.
33    pub const EMPTY: Self = Self {
34        record: None,
35        rank: 0,
36    };
37}
38
39enum Storage<'a> {
40    Borrowed(&'a mut [Slot]),
41    #[cfg(feature = "alloc")]
42    Owned(alloc::vec::Vec<Slot>),
43}
44
45impl Storage<'_> {
46    const fn as_slice(&self) -> &[Slot] {
47        match self {
48            Self::Borrowed(slots) => slots,
49            #[cfg(feature = "alloc")]
50            Self::Owned(slots) => slots.as_slice(),
51        }
52    }
53}
54
55impl Deref for Storage<'_> {
56    type Target = [Slot];
57
58    fn deref(&self) -> &Self::Target {
59        self.as_slice()
60    }
61}
62
63impl DerefMut for Storage<'_> {
64    fn deref_mut(&mut self) -> &mut Self::Target {
65        match self {
66            Self::Borrowed(slots) => slots,
67            #[cfg(feature = "alloc")]
68            Self::Owned(slots) => slots,
69        }
70    }
71}
72
73/// Current records for one terminal with a fixed capacity.
74///
75/// Use [`Store::from_slots`] to supply storage without an allocator.
76/// With the `alloc` feature, `Store::new(capacity)` owns heap storage instead.
77/// Updates replace every explicit field. Lookup does not affect eviction order.
78/// Change callbacks borrow records only for the duration of the call.
79/// Records belong to the terminal, so screen switches require no store action.
80pub struct Store<'a> {
81    slots: Storage<'a>,
82    length: u16,
83}
84
85#[cfg(feature = "alloc")]
86impl Store<'static> {
87    /// Creates an empty store with heap storage for `capacity` records.
88    /// Requires the `alloc` feature. Storage is allocated once and never grows.
89    /// Capacities below 64 serve tests but do not meet the host requirement.
90    ///
91    /// # Errors
92    /// Returns [`Error::InvalidCapacity`] for zero or more than 256 records,
93    /// before allocating. Returns [`Error::Allocation`] if storage cannot be reserved.
94    pub fn new(capacity: usize) -> Result<Self, Error> {
95        validate_capacity(capacity)?;
96        let mut slots = alloc::vec::Vec::new();
97        slots.try_reserve_exact(capacity)?;
98        slots.resize_with(capacity, || Slot::EMPTY);
99        Ok(Self {
100            slots: Storage::Owned(slots),
101            length: 0,
102        })
103    }
104}
105
106impl<'a> Store<'a> {
107    /// Borrows caller storage and discards any records previously in the slots.
108    /// This constructor does not allocate, including with the `alloc` feature.
109    /// Capacities below 64 serve tests but do not meet the host requirement.
110    ///
111    /// # Errors
112    /// Returns [`Error::InvalidCapacity`] for zero slots or more than 256 slots,
113    /// without changing the supplied storage.
114    pub fn from_slots(slots: &'a mut [Slot]) -> Result<Self, Error> {
115        validate_capacity(slots.len())?;
116        for slot in &mut *slots {
117            slot.record = None;
118            slot.rank = 0;
119        }
120        Ok(Self {
121            slots: Storage::Borrowed(slots),
122            length: 0,
123        })
124    }
125
126    /// Returns the number of current records, including root.
127    #[must_use]
128    pub fn len(&self) -> usize {
129        usize::from(self.length)
130    }
131
132    /// Returns whether there are no current records.
133    #[must_use]
134    pub const fn is_empty(&self) -> bool {
135        self.length == 0
136    }
137
138    /// Returns the caller-selected record capacity.
139    #[must_use]
140    pub const fn capacity(&self) -> usize {
141        self.slots.as_slice().len()
142    }
143
144    /// Looks up a root (`None`) or child record without updating its age.
145    #[must_use]
146    pub fn get(&self, id: Option<RecordId<'_>>) -> Option<Record<'_>> {
147        self.find(id.map(RecordId::as_str)).map(StoredRecord::view)
148    }
149
150    /// Iterates current records. Iteration order is unspecified.
151    pub fn iter(&self) -> impl Iterator<Item = Record<'_>> {
152        self.slots
153            .iter()
154            .filter_map(|slot| slot.record.as_ref().map(StoredRecord::view))
155    }
156
157    /// Resolves a stored record's app through its nearest ancestor with an app.
158    /// Returns `None` when the addressed record does not exist or no app applies.
159    #[must_use]
160    pub fn effective_app(&self, id: Option<RecordId<'_>>) -> Option<AppId<'_>> {
161        let mut path = id.map(RecordId::as_str);
162        self.find(path)?;
163        loop {
164            if let Some(app) = self.find(path).and_then(|record| record.view().app()) {
165                return Some(app);
166            }
167            path = path?.rsplit_once('/').map(|(parent, _)| parent);
168        }
169    }
170
171    /// Applies a validated report as a complete replacement or subtree clear.
172    /// A new record at capacity evicts the least recently updated record.
173    /// Existing-record updates never evict another record.
174    /// Every set refreshes the record's update order, even when all fields match.
175    ///
176    /// Does not allocate or collect change notifications.
177    /// Use [`apply_with_changes`](Self::apply_with_changes) to observe removals,
178    /// replacements, and inherited-app changes.
179    ///
180    /// # Errors
181    /// Returns [`Error::Parser`] if copying or decoding a field fails. All fields
182    /// are prepared before any slot mutation.
183    pub fn apply(&mut self, report: Report<'_>) -> Result<(), Error> {
184        self.apply_inner(report, |_| {}, false).map(|_| ())
185    }
186
187    fn apply_inner(
188        &mut self,
189        report: Report<'_>,
190        mut on_remove: impl FnMut(Record<'_>),
191        track_changes: bool,
192    ) -> Result<Option<usize>, Error> {
193        let id = report.id().map(RecordId::as_str);
194        let Action::Set(state) = report.action() else {
195            self.remove_matching(
196                |record| match id {
197                    None => true,
198                    Some(parent) => record.id().is_some_and(|child| {
199                        let child = child.as_str();
200                        child == parent
201                            || child
202                                .strip_prefix(parent)
203                                .is_some_and(|suffix| suffix.starts_with('/'))
204                    }),
205                },
206                &mut on_remove,
207            );
208            return Ok(None);
209        };
210
211        let prepared = StoredRecord::prepare(state, report)?;
212        let existing = self.slots.iter().position(|slot| {
213            slot.record
214                .as_ref()
215                .is_some_and(|record| record.matches_id(id))
216        });
217        let index = existing
218            .or_else(|| self.slots.iter().position(|slot| slot.record.is_none()))
219            .unwrap_or_else(|| {
220                self.slots
221                    .iter()
222                    .enumerate()
223                    .min_by_key(|(_, slot)| slot.rank)
224                    .map_or(0, |(index, _)| index)
225            });
226        let changed = track_changes
227            && self.slots[index]
228                .record
229                .as_ref()
230                .is_none_or(|record| !record.same_fields(&prepared));
231        if let Some(record) = self.slots[index].record.as_ref() {
232            if existing.is_none() {
233                on_remove(record.view());
234            }
235            self.discard(index);
236        }
237        self.slots[index].record = Some(prepared);
238        self.slots[index].rank = self.length;
239        self.length += 1;
240        Ok(changed.then_some(index))
241    }
242
243    /// Removes working and blocked records when a new shell prompt begins.
244    /// Retains idle, done, and error records. Use
245    /// [`on_prompt_with_changes`](Self::on_prompt_with_changes) for notifications.
246    pub fn on_prompt(&mut self) {
247        self.remove_active(&mut |_| {});
248    }
249
250    /// Applies prompt cleanup when the process attached to this terminal exits.
251    /// This event is not inferred from record ids or child process ids.
252    /// Use [`on_process_exit_with_changes`](Self::on_process_exit_with_changes)
253    /// for notifications.
254    pub fn on_process_exit(&mut self) {
255        self.on_prompt();
256    }
257
258    /// Removes all records on a full terminal reset (RIS).
259    /// Use [`on_full_reset_with_changes`](Self::on_full_reset_with_changes)
260    /// for notifications.
261    pub fn on_full_reset(&mut self) {
262        self.remove_matching(|_| true, &mut |_| {});
263    }
264
265    fn find(&self, id: Option<&str>) -> Option<&StoredRecord> {
266        self.slots
267            .iter()
268            .filter_map(|slot| slot.record.as_ref())
269            .find(|record| record.matches_id(id))
270    }
271
272    fn remove_active(&mut self, on_remove: &mut impl FnMut(Record<'_>)) {
273        self.remove_matching(
274            |record| matches!(record.state(), State::Working | State::Blocked),
275            on_remove,
276        );
277    }
278
279    fn remove_matching(
280        &mut self,
281        matches: impl Fn(Record<'_>) -> bool,
282        on_remove: &mut impl FnMut(Record<'_>),
283    ) {
284        for index in 0..self.slots.len() {
285            if let Some(record) = self.slots[index].record.as_ref()
286                && matches(record.view())
287            {
288                on_remove(record.view());
289                self.discard(index);
290            }
291        }
292    }
293
294    fn discard(&mut self, index: usize) {
295        let rank = self.slots[index].rank;
296        self.slots[index].record = None;
297        self.length -= 1;
298        // Active ranks stay dense in 0..length; repeated updates cannot overflow.
299        for slot in &mut *self.slots {
300            if slot.record.is_some() && slot.rank > rank {
301                slot.rank -= 1;
302            }
303        }
304    }
305}
306
307fn validate_capacity(capacity: usize) -> Result<(), Error> {
308    if capacity == 0 || capacity > MAX_RECORDS {
309        return Err(Error::InvalidCapacity);
310    }
311    Ok(())
312}
313
314/// Store capacity, allocation, or field preparation failure.
315#[derive(Debug, thiserror::Error)]
316#[non_exhaustive]
317pub enum Error {
318    /// Requested capacity is zero or exceeds the protocol record cap.
319    #[error("record capacity must be between 1 and 256")]
320    InvalidCapacity,
321    /// Heap storage could not be reserved. Requires the `alloc` feature.
322    #[cfg(feature = "alloc")]
323    #[error("{0}")]
324    Allocation(#[from] alloc::collections::TryReserveError),
325    /// Copying or decoding a validated report field failed.
326    #[error("{0}")]
327    Parser(#[from] parser::Error),
328}