osc7501_store/change.rs
1use crate::{
2 Error, MAX_RECORDS, Record, Store,
3 parser::{AppId, MAX_APP, Report},
4};
5
6/// A change to a current record or its inherited app.
7///
8/// Unchanged reports emit no change, even though they refresh eviction order.
9/// An ancestor update can change a child's effective app without a child report.
10/// Keep reports and lifecycle events to reproduce the store's full history.
11///
12/// Views borrow the store only for the callback. Copy any data you need to retain.
13#[derive(Clone, Copy, Debug)]
14#[non_exhaustive]
15pub enum Change<'a> {
16 /// A new record, changed explicit fields, or a changed effective app.
17 Set {
18 /// The record after the complete store operation.
19 record: Record<'a>,
20 /// The app resolved through the record and its remaining ancestors.
21 effective_app: Option<AppId<'a>>,
22 },
23 /// An old record, delivered before its removal.
24 Remove(Record<'a>),
25}
26
27#[derive(Clone, Copy, Eq, PartialEq)]
28struct AppSnapshot {
29 // App ids cannot contain NUL (0x00). Zero-padding lets us compare the full
30 // buffers without storing each id's length.
31 bytes: [u8; MAX_APP],
32}
33
34impl AppSnapshot {
35 fn new(app: AppId<'_>) -> Self {
36 let value = app.as_str().as_bytes();
37 let mut snapshot = Self {
38 bytes: [0; MAX_APP],
39 };
40 snapshot.bytes[..value.len()].copy_from_slice(value);
41 snapshot
42 }
43
44 fn matches(&self, app: AppId<'_>) -> bool {
45 self == &Self::new(app)
46 }
47}
48
49impl Store<'_> {
50 /// Applies a report and delivers all resulting record and app changes.
51 ///
52 /// Removals arrive before each record is removed. Sets arrive after the
53 /// complete operation, once per new or changed surviving record. Their
54 /// effective apps include ancestor replacement and eviction effects.
55 /// Order within removals or sets is unspecified; all removals precede sets.
56 ///
57 /// Equal decoded fields and effective apps produce no set notification.
58 /// The report still refreshes update order, just as [`apply`](Self::apply) does.
59 /// Does not allocate. Change detection copies effective apps into bounded
60 /// scratch storage instead of copying complete records.
61 ///
62 /// # Errors
63 /// Returns [`Error::Parser`] if field preparation fails, before any callback
64 /// or mutation. Callbacks are infallible and their effects are not rolled back.
65 pub fn apply_with_changes(
66 &mut self,
67 report: Report<'_>,
68 mut on_change: impl FnMut(Change<'_>),
69 ) -> Result<(), Error> {
70 let mut apps = [None; MAX_RECORDS];
71 self.snapshot_apps(&mut apps);
72 let changed = self.apply_inner(report, |record| on_change(Change::Remove(record)), true)?;
73 self.notify_sets(&apps, changed, &mut on_change);
74 Ok(())
75 }
76
77 /// Removes active records and reports inherited-app changes on survivors.
78 ///
79 /// Uses [`on_prompt`](Self::on_prompt) retention rules. All removals precede
80 /// sets, which contain the final records and effective apps. Does not allocate.
81 pub fn on_prompt_with_changes(&mut self, mut on_change: impl FnMut(Change<'_>)) {
82 let mut apps = [None; MAX_RECORDS];
83 self.snapshot_apps(&mut apps);
84 self.remove_active(&mut |record| on_change(Change::Remove(record)));
85 self.notify_sets(&apps, None, &mut on_change);
86 }
87
88 /// Applies prompt cleanup and change reporting when the attached process exits.
89 pub fn on_process_exit_with_changes(&mut self, on_change: impl FnMut(Change<'_>)) {
90 self.on_prompt_with_changes(on_change);
91 }
92
93 /// Removes every record on a full terminal reset, delivering only removals.
94 pub fn on_full_reset_with_changes(&mut self, mut on_change: impl FnMut(Change<'_>)) {
95 self.remove_matching(|_| true, &mut |record| on_change(Change::Remove(record)));
96 }
97
98 fn snapshot_apps(&self, apps: &mut [Option<AppSnapshot>; MAX_RECORDS]) {
99 for (index, slot) in self.slots.iter().enumerate() {
100 apps[index] = slot
101 .record
102 .as_ref()
103 .and_then(|record| self.effective_app(record.view().id()))
104 .map(AppSnapshot::new);
105 }
106 }
107
108 fn notify_sets(
109 &self,
110 apps: &[Option<AppSnapshot>; MAX_RECORDS],
111 changed: Option<usize>,
112 on_change: &mut impl FnMut(Change<'_>),
113 ) {
114 // Slots do not move; changed also marks a reused slot.
115 for (index, slot) in self.slots.iter().enumerate() {
116 let Some(record) = slot.record.as_ref().map(super::record::StoredRecord::view) else {
117 continue;
118 };
119 let effective_app = self.effective_app(record.id());
120 let same_app = match (&apps[index], effective_app) {
121 (Some(previous), Some(app)) => previous.matches(app),
122 (None, None) => true,
123 _ => false,
124 };
125 if changed == Some(index) || !same_app {
126 on_change(Change::Set {
127 record,
128 effective_app,
129 });
130 }
131 }
132 }
133}