use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use crate::error::Result;
pub type OnChange = Arc<dyn Fn(PathBuf) + Send + Sync>;
pub struct MemoWatcher {
_watcher: RecommendedWatcher,
}
impl MemoWatcher {
pub fn spawn(roots: Vec<PathBuf>, debounce: Duration, on_change: OnChange) -> Result<Self> {
let (tx, rx) = mpsc::channel::<PathBuf>();
std::thread::spawn(move || debounce_loop(rx, debounce, on_change));
let mut watcher =
notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
if let Ok(ev) = res {
let relevant = matches!(
ev.kind,
EventKind::Create(_)
| EventKind::Modify(_)
| EventKind::Remove(_)
| EventKind::Any
);
if !relevant {
return;
}
for p in ev.paths {
if is_markdown(&p) {
let _ = tx.send(p);
}
}
}
})?;
for root in &roots {
if root.exists() {
watcher.watch(root, RecursiveMode::Recursive)?;
}
}
Ok(Self { _watcher: watcher })
}
}
fn debounce_loop(rx: mpsc::Receiver<PathBuf>, debounce: Duration, on_change: OnChange) {
let mut pending: HashMap<PathBuf, Instant> = HashMap::new();
loop {
let now = Instant::now();
let next_due = pending.values().copied().min();
let timeout = match next_due {
Some(due) if due > now => Some(due - now),
_ => Some(Duration::from_millis(10)),
};
match rx.recv_timeout(timeout.unwrap_or(Duration::from_millis(10))) {
Ok(path) => {
pending.insert(path, Instant::now() + debounce);
}
Err(mpsc::RecvTimeoutError::Timeout) => {
let now = Instant::now();
let due: Vec<PathBuf> = pending
.iter()
.filter(|entry| *entry.1 <= now)
.map(|(p, _)| p.clone())
.collect();
for p in due {
pending.remove(&p);
on_change(p);
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
}
fn is_markdown(p: &Path) -> bool {
p.extension().is_some_and(|e| e == "md")
}