1use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::sync::mpsc;
13use std::time::{Duration, Instant};
14
15use crate::error::Result;
16
17pub type OnChange = Arc<dyn Fn(PathBuf) + Send + Sync>;
20
21pub struct MemoWatcher {
23 _watcher: RecommendedWatcher,
24}
25
26impl MemoWatcher {
27 pub fn spawn(roots: Vec<PathBuf>, debounce: Duration, on_change: OnChange) -> Result<Self> {
30 let (tx, rx) = mpsc::channel::<PathBuf>();
31 std::thread::spawn(move || debounce_loop(rx, debounce, on_change));
32
33 let mut watcher =
34 notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
35 if let Ok(ev) = res {
36 let relevant = matches!(
39 ev.kind,
40 EventKind::Create(_)
41 | EventKind::Modify(_)
42 | EventKind::Remove(_)
43 | EventKind::Any
44 );
45 if !relevant {
46 return;
47 }
48 for p in ev.paths {
49 if is_markdown(&p) {
50 let _ = tx.send(p);
51 }
52 }
53 }
54 })?;
55
56 for root in &roots {
57 if root.exists() {
58 watcher.watch(root, RecursiveMode::Recursive)?;
59 }
60 }
61 Ok(Self { _watcher: watcher })
62 }
63}
64
65fn debounce_loop(rx: mpsc::Receiver<PathBuf>, debounce: Duration, on_change: OnChange) {
68 let mut pending: HashMap<PathBuf, Instant> = HashMap::new();
69
70 loop {
71 let now = Instant::now();
72 let next_due = pending.values().copied().min();
74
75 let timeout = match next_due {
76 Some(due) if due > now => Some(due - now),
77 _ => Some(Duration::from_millis(10)),
78 };
79
80 match rx.recv_timeout(timeout.unwrap_or(Duration::from_millis(10))) {
81 Ok(path) => {
82 pending.insert(path, Instant::now() + debounce);
83 }
84 Err(mpsc::RecvTimeoutError::Timeout) => {
85 let now = Instant::now();
87 let due: Vec<PathBuf> = pending
88 .iter()
89 .filter(|entry| *entry.1 <= now)
90 .map(|(p, _)| p.clone())
91 .collect();
92 for p in due {
93 pending.remove(&p);
94 on_change(p);
95 }
96 }
97 Err(mpsc::RecvTimeoutError::Disconnected) => break,
98 }
99 }
100}
101
102fn is_markdown(p: &Path) -> bool {
103 p.extension().is_some_and(|e| e == "md")
104}