cuj-tui 0.1.0

cui — a read-only TUI browser for cuj vaults
Documentation
//! The owned read model: everything the UI needs, copied out of a
//! transient `cuj::reads::Reads` (cuj SPEC §8.1) so no borrow of the
//! vault outlives the build. Content blobs are not part of the
//! snapshot — they are fetched lazily and cached by the app state.

use std::collections::HashMap;

use metatheca::Uuid;

use crate::error::Result;

/// One live jot, fully owned.
pub struct JotRow {
    pub entry: Uuid,
    pub profile: String,
    pub id: i64,
    pub ext: String,
    pub created_at_ns: Option<i64>,
    /// First non-blank content line, ≤72 chars (display-time
    /// summary; jots have no title).
    pub summary: String,
    pub effective: cuj::facts::EffectiveIndex,
    /// Attachment filenames, sorted.
    pub attachments: Vec<String>,
}

impl JotRow {
    /// `--profile--id`, shortened to `--id` inside a single-profile
    /// scope (mirrors the CLI's short_ref).
    pub fn short_ref(&self, scope: Option<&str>) -> String {
        match scope {
            Some(p) if p == self.profile => format!("--{}", self.id),
            _ => format!("--{}--{}", self.profile, self.id),
        }
    }
}

pub struct Snapshot {
    /// All live jots, ascending (profile, id).
    pub rows: Vec<JotRow>,
    pub by_entry: HashMap<Uuid, usize>,
    pub by_ident: HashMap<(String, i64), usize>,
    /// Attachment / extraction entry -> owning jot entry
    /// (cuj SPEC §8.3), for mapping search hits.
    owner: HashMap<Uuid, Uuid>,
    /// Profile names (sorted) with live jot counts.
    pub profiles: Vec<(String, usize)>,
    /// Profile configs, for the per-profile parser configuration
    /// the highlighter needs.
    pub configs: HashMap<String, cuj::facts::ProfileConfig>,
}

impl Snapshot {
    pub fn build(app: &cuj::App) -> Result<Snapshot> {
        let reads = app.reads(&cuj::Opts::default())?;
        let mut rows = Vec::new();
        for (entry, ident) in reads.list(None) {
            let summary = cuj::queries::summary_line(&reads, entry);
            let attachments = reads
                .attachments_of(entry)
                .iter()
                .map(|(_, a)| a.filename.clone())
                .collect();
            rows.push(JotRow {
                entry,
                profile: ident.profile.clone(),
                id: ident.id,
                ext: ident.ext.clone(),
                created_at_ns: reads.births.get(&entry).map(|b| b.created_at_ns),
                summary,
                effective: reads.effective(entry),
                attachments,
            });
        }
        let by_entry: HashMap<Uuid, usize> =
            rows.iter().enumerate().map(|(i, r)| (r.entry, i)).collect();
        let by_ident: HashMap<(String, i64), usize> = rows
            .iter()
            .enumerate()
            .map(|(i, r)| ((r.profile.clone(), r.id), i))
            .collect();

        let mut owner = HashMap::new();
        for (att_entry, att) in &reads.attachments {
            if let Ok(jot) = Uuid::parse_str(&att.jot) {
                owner.insert(*att_entry, jot);
            }
        }
        for (x_entry, body, _) in &reads.extractions {
            if let Some(jot) = Uuid::parse_str(&body.of)
                .ok()
                .and_then(|att| owner.get(&att).copied())
            {
                owner.insert(*x_entry, jot);
            }
        }

        // Every profile entry, even with zero live jots.
        let mut profiles: Vec<(String, usize)> = reads
            .profiles
            .keys()
            .map(|name| {
                let n = rows.iter().filter(|r| &r.profile == name).count();
                (name.clone(), n)
            })
            .collect();
        profiles.sort();
        let configs = reads
            .profiles
            .iter()
            .map(|(name, (_, cfg))| (name.clone(), cfg.clone()))
            .collect();

        Ok(Snapshot {
            rows,
            by_entry,
            by_ident,
            owner,
            profiles,
            configs,
        })
    }

    /// Map any hit entry (jot, attachment, extraction) to its live
    /// owning jot row index.
    pub fn owning_row(&self, entry: Uuid) -> Option<usize> {
        if let Some(i) = self.by_entry.get(&entry) {
            return Some(*i);
        }
        self.owner
            .get(&entry)
            .and_then(|jot| self.by_entry.get(jot))
            .copied()
    }
}