Skip to main content

cui/
snapshot.rs

1//! The owned read model: everything the UI needs, copied out of a
2//! transient `cuj::reads::Reads` (cuj SPEC §8.1) so no borrow of the
3//! vault outlives the build. Content blobs are not part of the
4//! snapshot — they are fetched lazily and cached by the app state.
5
6use std::collections::HashMap;
7
8use metatheca::Uuid;
9
10use crate::error::Result;
11
12/// One live jot, fully owned.
13pub struct JotRow {
14    pub entry: Uuid,
15    pub profile: String,
16    pub id: i64,
17    pub ext: String,
18    pub created_at_ns: Option<i64>,
19    /// First non-blank content line, ≤72 chars (display-time
20    /// summary; jots have no title).
21    pub summary: String,
22    pub effective: cuj::facts::EffectiveIndex,
23    /// Attachment filenames, sorted.
24    pub attachments: Vec<String>,
25}
26
27impl JotRow {
28    /// `--profile--id`, shortened to `--id` inside a single-profile
29    /// scope (mirrors the CLI's short_ref).
30    pub fn short_ref(&self, scope: Option<&str>) -> String {
31        match scope {
32            Some(p) if p == self.profile => format!("--{}", self.id),
33            _ => format!("--{}--{}", self.profile, self.id),
34        }
35    }
36}
37
38pub struct Snapshot {
39    /// All live jots, ascending (profile, id).
40    pub rows: Vec<JotRow>,
41    pub by_entry: HashMap<Uuid, usize>,
42    pub by_ident: HashMap<(String, i64), usize>,
43    /// Attachment / extraction entry -> owning jot entry
44    /// (cuj SPEC §8.3), for mapping search hits.
45    owner: HashMap<Uuid, Uuid>,
46    /// Profile names (sorted) with live jot counts.
47    pub profiles: Vec<(String, usize)>,
48    /// Profile configs, for the per-profile parser configuration
49    /// the highlighter needs.
50    pub configs: HashMap<String, cuj::facts::ProfileConfig>,
51}
52
53impl Snapshot {
54    pub fn build(app: &cuj::App) -> Result<Snapshot> {
55        let reads = app.reads(&cuj::Opts::default())?;
56        let mut rows = Vec::new();
57        for (entry, ident) in reads.list(None) {
58            let summary = cuj::queries::summary_line(&reads, entry);
59            let attachments = reads
60                .attachments_of(entry)
61                .iter()
62                .map(|(_, a)| a.filename.clone())
63                .collect();
64            rows.push(JotRow {
65                entry,
66                profile: ident.profile.clone(),
67                id: ident.id,
68                ext: ident.ext.clone(),
69                created_at_ns: reads.births.get(&entry).map(|b| b.created_at_ns),
70                summary,
71                effective: reads.effective(entry),
72                attachments,
73            });
74        }
75        let by_entry: HashMap<Uuid, usize> =
76            rows.iter().enumerate().map(|(i, r)| (r.entry, i)).collect();
77        let by_ident: HashMap<(String, i64), usize> = rows
78            .iter()
79            .enumerate()
80            .map(|(i, r)| ((r.profile.clone(), r.id), i))
81            .collect();
82
83        let mut owner = HashMap::new();
84        for (att_entry, att) in &reads.attachments {
85            if let Ok(jot) = Uuid::parse_str(&att.jot) {
86                owner.insert(*att_entry, jot);
87            }
88        }
89        for (x_entry, body, _) in &reads.extractions {
90            if let Some(jot) = Uuid::parse_str(&body.of)
91                .ok()
92                .and_then(|att| owner.get(&att).copied())
93            {
94                owner.insert(*x_entry, jot);
95            }
96        }
97
98        // Every profile entry, even with zero live jots.
99        let mut profiles: Vec<(String, usize)> = reads
100            .profiles
101            .keys()
102            .map(|name| {
103                let n = rows.iter().filter(|r| &r.profile == name).count();
104                (name.clone(), n)
105            })
106            .collect();
107        profiles.sort();
108        let configs = reads
109            .profiles
110            .iter()
111            .map(|(name, (_, cfg))| (name.clone(), cfg.clone()))
112            .collect();
113
114        Ok(Snapshot {
115            rows,
116            by_entry,
117            by_ident,
118            owner,
119            profiles,
120            configs,
121        })
122    }
123
124    /// Map any hit entry (jot, attachment, extraction) to its live
125    /// owning jot row index.
126    pub fn owning_row(&self, entry: Uuid) -> Option<usize> {
127        if let Some(i) = self.by_entry.get(&entry) {
128            return Some(*i);
129        }
130        self.owner
131            .get(&entry)
132            .and_then(|jot| self.by_entry.get(jot))
133            .copied()
134    }
135}
136