use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
const SUPPORTED_EXTENSIONS: &[&str] = &["scene", "json", "png", "tmx"];
fn is_supported_file(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| SUPPORTED_EXTENSIONS.contains(&ext))
}
pub struct ProjectWatcher {
_watcher: RecommendedWatcher,
rx: mpsc::Receiver<Result<Event, notify::Error>>,
seen: HashSet<PathBuf>,
}
impl ProjectWatcher {
pub fn new(dirs: &[PathBuf]) -> Result<Self, String> {
let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new(
move |res| {
let _ = tx.send(res);
},
Config::default(),
)
.map_err(|e| format!("failed to create file watcher: {e}"))?;
let mut watched_any = false;
for dir in dirs {
if dir.is_dir() {
watcher
.watch(dir, RecursiveMode::Recursive)
.map_err(|e| format!("failed to watch {}: {e}", dir.display()))?;
log::info!("hot-reload: watching {}", dir.display());
watched_any = true;
} else {
log::info!("hot-reload: skipping {} (not found)", dir.display());
}
}
if !watched_any {
return Err("no valid directories to watch".to_string());
}
Ok(Self {
_watcher: watcher,
rx,
seen: HashSet::new(),
})
}
pub fn poll_events(&mut self) -> Vec<PathBuf> {
self.seen.clear();
while let Ok(Ok(event)) = self.rx.try_recv() {
match event.kind {
EventKind::Modify(_) | EventKind::Create(_) => {
for path in event.paths {
if is_supported_file(&path) {
self.seen.insert(path);
}
}
}
_ => {}
}
}
self.seen.iter().cloned().collect()
}
}