use std::path::Path;
use std::sync::Arc;
use anyhow::{Context, Result};
use notify_debouncer_mini::notify::{RecommendedWatcher, RecursiveMode};
use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer};
use tracing::{info, warn};
use crate::config::DevConfig;
use crate::worker_pool::WorkerPool;
pub type WatchGuard = Debouncer<RecommendedWatcher>;
pub fn spawn(dev: &DevConfig, pool: Arc<WorkerPool>) -> Result<WatchGuard> {
let handle = tokio::runtime::Handle::current();
let extensions: Vec<String> = dev
.watch_extensions
.iter()
.map(|e| e.trim_start_matches('.').to_ascii_lowercase())
.collect();
let mut debouncer = new_debouncer(dev.debounce, move |res: DebounceEventResult| match res {
Ok(events) => {
let changed = events
.iter()
.any(|e| matches_extension(&e.path, &extensions));
if changed {
let pool = pool.clone();
handle.spawn(async move { pool.trigger_reload().await });
}
},
Err(error) => {
warn!(%error, "file watcher error");
},
})
.context("failed to create file watcher")?;
let mut watched = 0usize;
for path in &dev.watch_paths {
let p = Path::new(path);
if !p.exists() {
warn!(path, "watch path does not exist, skipping");
continue;
}
debouncer
.watcher()
.watch(p, RecursiveMode::Recursive)
.with_context(|| format!("failed to watch {path}"))?;
watched += 1;
}
if watched == 0 {
warn!("hot reload enabled but no watch paths exist; watcher is idle");
} else {
info!(
paths = ?dev.watch_paths,
extensions = ?dev.watch_extensions,
debounce_ms = u64::try_from(dev.debounce.as_millis()).unwrap_or(u64::MAX),
"hot reload watcher started"
);
}
Ok(debouncer)
}
fn matches_extension(path: &Path, extensions: &[String]) -> bool {
path.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
.is_some_and(|ext| extensions.contains(&ext))
}