Skip to main content

okf_studio/
watch.rs

1//! A std-only polling file watcher.
2//!
3//! One `metadata()` sweep of the `*.md` files under the root every interval,
4//! comparing `(mtime, size)`. Polling behaves identically on NFS and in
5//! containers where inotify does not, and a bundle is at most a few thousand
6//! small files, so a sweep costs single-digit milliseconds.
7
8use crate::app::Msg;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::mpsc::Sender;
14use std::time::{Duration, SystemTime};
15
16/// Stops the watcher thread when dropped.
17pub struct WatcherHandle {
18    stop: Arc<AtomicBool>,
19}
20
21impl Drop for WatcherHandle {
22    fn drop(&mut self) {
23        self.stop.store(true, Ordering::Relaxed);
24    }
25}
26
27type FileState = HashMap<PathBuf, (SystemTime, u64)>;
28
29fn sweep(root: &Path, out: &mut FileState) {
30    let Ok(entries) = std::fs::read_dir(root) else {
31        return;
32    };
33    for entry in entries.flatten() {
34        let path = entry.path();
35        let Ok(file_type) = entry.file_type() else {
36            continue;
37        };
38        if file_type.is_dir() {
39            sweep(&path, out);
40        } else if file_type.is_file()
41            && path.extension().is_some_and(|e| e == "md")
42            && let Ok(meta) = entry.metadata()
43        {
44            let mtime = meta.modified().unwrap_or(SystemTime::UNIX_EPOCH);
45            out.insert(path, (mtime, meta.len()));
46        }
47    }
48}
49
50/// Spawns the watcher thread. It sends [`Msg::FilesChanged`] whenever the
51/// `(mtime, size)` sweep differs from the previous one.
52#[must_use]
53pub fn spawn(root: PathBuf, interval: Duration, tx: Sender<Msg>) -> WatcherHandle {
54    let stop = Arc::new(AtomicBool::new(false));
55    let stop_flag = Arc::clone(&stop);
56    std::thread::spawn(move || {
57        let mut previous = FileState::new();
58        sweep(&root, &mut previous);
59        while !stop_flag.load(Ordering::Relaxed) {
60            std::thread::sleep(interval);
61            if stop_flag.load(Ordering::Relaxed) {
62                break;
63            }
64            let mut current = FileState::new();
65            sweep(&root, &mut current);
66            if current != previous {
67                previous = current;
68                if tx.send(Msg::FilesChanged).is_err() {
69                    break;
70                }
71            }
72        }
73    });
74    WatcherHandle { stop }
75}