Skip to main content

tilo_app/framework/
record_index.rs

1//! An app's records read without its window: what the system's search index
2//! asks of an app that has not opened the record.
3//!
4//! Search is a record's to answer, not a window's: a message, a conversation
5//! or an agent is found on a device that never ran the app, because whoever
6//! keeps the record reads it here and publishes what it holds to the same
7//! index a running window publishes to. The reader is the app's own binary in
8//! its `--index` mode: it links the app's model types, so what it derives is
9//! exactly what the app's window would, and the session that asks links none
10//! of them (`docs/architecture/apps.md`).
11//!
12//! The document arrives on standard input and leaves as the entries on
13//! standard output, as JSON: no path, no store and no window is handed in, so
14//! a reader cannot reach anything but the bytes it was asked about.
15
16use std::io::{Read as _, Write as _};
17
18use tilo_resource::{Doc, Kinds};
19use ui_core::search::index_publication::IndexEntry;
20
21use super::app::ResourceApp;
22use super::kinds::resource_kind;
23
24/// Every kind `A` declares, hosted by `A`: its resource, and the parts of the
25/// chains it keeps beside its record.
26///
27/// The same set [`super::kinds::register`] hosts for the process, built
28/// without depending on a definition having been created: a host that reads a
29/// record of an app that is not running reads it by these.
30fn kinds_of<A: ResourceApp>() -> Kinds {
31    let mut declared = super::Records::declaring();
32    A::records(&mut declared);
33    let mut kinds = Kinds::new();
34    kinds.insert(resource_kind::<A>());
35    for chain in declared.into_chains() {
36        kinds.insert(chain.hosted_by(A::ID, A::TITLE));
37    }
38    kinds
39}
40
41/// The flag that makes an app's binary print what a record holds for search
42/// instead of running: its document is read from standard input.
43pub const INDEX_FLAG: &str = "--index";
44
45/// What search finds in `doc`, as the index keeps it: every kind of `A`'s
46/// that the document holds, read the way its window reads the record.
47///
48/// A record kept before the app changed its storage is read at this build's
49/// version, in memory: the document itself is the app's to migrate.
50#[must_use]
51pub fn entries<A: ResourceApp>(doc: &Doc) -> Vec<IndexEntry> {
52    let mut found = Vec::new();
53    let mut keys = std::collections::BTreeSet::new();
54    for kind in kinds_of::<A>().iter() {
55        let mut read = doc.clone();
56        if kind.needs_migrate(&read) && kind.migrate(&mut read).is_err() {
57            continue;
58        }
59        let Ok(entries) = kind.search(&read) else {
60            // The document holds another kind: its own type answers for it.
61            continue;
62        };
63        for entry in entries {
64            let Some(entry) = super::root::index_entry(&entry).and_then(IndexEntry::bounded) else {
65                continue;
66            };
67            if keys.insert(entry.key.clone()) {
68                found.push(entry);
69            }
70        }
71    }
72    found
73}
74
75/// Answer the `--index` mode when the command line asks for it: read the
76/// record's document from standard input and print its entries. `None` for
77/// a command line that starts the app as usual.
78pub fn serve<A: ResourceApp>() -> Option<i32> {
79    let asked = std::env::args().skip(1).any(|arg| arg == INDEX_FLAG);
80    if !asked {
81        return None;
82    }
83    #[allow(
84        clippy::exit,
85        reason = "the record reader is the whole run of the process"
86    )]
87    Some(match ask::<A>() {
88        Ok(()) => 0,
89        Err(error) => {
90            eprintln!("Error: {error}");
91            1
92        }
93    })
94}
95
96/// [`serve`] once it is known to be asked: the document in, the entries out.
97fn ask<A: ResourceApp>() -> Result<(), String> {
98    let mut bytes = Vec::new();
99    std::io::stdin()
100        .lock()
101        .read_to_end(&mut bytes)
102        .map_err(|error| format!("the record is not read: {error}"))?;
103    let doc =
104        Doc::load(&bytes).map_err(|error| format!("the record is not a document: {error}"))?;
105    let entries = entries::<A>(&doc);
106    let mut out = Vec::new();
107    serde_json::to_writer(&mut out, &entries)
108        .map_err(|error| format!("the entries are not written: {error}"))?;
109    std::io::stdout()
110        .lock()
111        .write_all(&out)
112        .map_err(|error| format!("the entries do not leave: {error}"))
113}