Skip to main content

mini_docs/
watch.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::time::SystemTime;
4
5use crate::builder::{load_templates, Builder};
6use crate::error::DocError;
7use crate::walk;
8
9/// Incremental, mtime-polling rebuild state produced by [`Builder::watch`].
10///
11/// No background thread and no dependency on an async runtime or `notify` — a caller
12/// drives progress by calling [`Watcher::tick`] on whatever cadence it wants (a
13/// blocking loop with `std::thread::sleep`, a GUI's idle callback, a test). This
14/// matches `mini-static`'s own poller in spirit (mtime comparison, not OS file-events)
15/// while keeping the dependency budget at zero for the default case.
16pub struct Watcher<'b> {
17    builder: &'b Builder,
18    md_mtimes: HashMap<PathBuf, SystemTime>,
19    template_mtimes: HashMap<PathBuf, SystemTime>,
20}
21
22impl<'b> Watcher<'b> {
23    pub(crate) fn new(builder: &'b Builder) -> Result<Self, DocError> {
24        builder.build()?;
25        Ok(Self {
26            builder,
27            md_mtimes: snapshot_mtimes(builder.input_dir(), "md")?,
28            template_mtimes: snapshot_mtimes(builder.templates_dir(), "html")?,
29        })
30    }
31
32    /// Polls both watched trees once.
33    ///
34    /// If any template file's mtime changed (added, removed, or modified), every page
35    /// is rebuilt — mini-docs doesn't parse Tera's `{% extends %}`/`{% include %}`
36    /// graph, so a template-level change conservatively fans out to every dependent
37    /// rather than risking a stale page. Otherwise, only the `.md` files whose own
38    /// mtime changed are rebuilt.
39    ///
40    /// If the `.md` file set changed at all (any addition, removal, or modification —
41    /// not just what the render cache decided to rebuild), the `data.json` index (if
42    /// [`Builder::data_json`] is set) is regenerated too. A template-only change never
43    /// alters index content, so it doesn't trigger this.
44    ///
45    /// Returns the `.md` paths rebuilt this tick (empty if nothing changed).
46    pub fn tick(&mut self) -> Result<Vec<PathBuf>, DocError> {
47        let current_templates = snapshot_mtimes(self.builder.templates_dir(), "html")?;
48        let templates_changed = current_templates != self.template_mtimes;
49        self.template_mtimes = current_templates;
50
51        let current_md = snapshot_mtimes(self.builder.input_dir(), "md")?;
52        let md_set_changed = current_md != self.md_mtimes;
53
54        let changed: Vec<PathBuf> = if templates_changed {
55            current_md.keys().cloned().collect()
56        } else {
57            current_md
58                .iter()
59                .filter(|(path, mtime)| self.md_mtimes.get(path.as_path()) != Some(*mtime))
60                .map(|(path, _)| path.clone())
61                .collect()
62        };
63
64        if !changed.is_empty() {
65            let tera = load_templates(self.builder.templates_dir())?;
66            for md_path in &changed {
67                self.builder.build_one(&tera, md_path)?;
68            }
69        }
70
71        if md_set_changed {
72            self.builder.rebuild_data_json()?;
73        }
74
75        self.md_mtimes = current_md;
76        Ok(changed)
77    }
78}
79
80fn snapshot_mtimes(dir: &Path, extension: &str) -> Result<HashMap<PathBuf, SystemTime>, DocError> {
81    let mut mtimes = HashMap::new();
82    for path in walk::walk_files_with_extension(dir, extension)? {
83        let mtime = std::fs::metadata(&path)?.modified()?;
84        mtimes.insert(path, mtime);
85    }
86    Ok(mtimes)
87}