use std::path::PathBuf;
use console::style;
use notify::{EventKind, RecursiveMode, Watcher, recommended_watcher};
use tokio::sync::mpsc;
use tokio::time::{Duration, timeout};
const DEBOUNCE: Duration = Duration::from_millis(50);
pub struct SourceWatcher {
_watcher: notify::RecommendedWatcher,
events: mpsc::UnboundedReceiver<()>,
pending: bool,
}
impl SourceWatcher {
pub async fn start() -> Self {
let (tx, events) = mpsc::unbounded_channel();
let mut watcher = recommended_watcher(move |event: notify::Result<notify::Event>| {
if let Ok(event) = &event
&& !matches!(event.kind, EventKind::Access(_))
{
let _ = tx.send(());
}
})
.expect("failed to create file watcher");
for dir in watch_dirs().await {
watcher
.watch(&dir, RecursiveMode::Recursive)
.unwrap_or_else(|error| {
eprintln!(
" {}",
style(format!("failed to watch {}: {error}", dir.display())).yellow()
);
});
}
Self {
_watcher: watcher,
events,
pending: false,
}
}
pub async fn changed(&mut self) {
if !self.pending {
self.events.recv().await.expect("watcher channel closed");
self.pending = true;
}
while let Ok(Some(())) = timeout(DEBOUNCE, self.events.recv()).await {}
self.pending = false;
}
}
async fn watch_dirs() -> Vec<PathBuf> {
let Some(metadata) = crate::cargo::metadata().await else {
eprintln!(
" {}",
style("cargo metadata failed; watching ./src").yellow()
);
return vec![PathBuf::from("./src")];
};
let dirs: Vec<PathBuf> = metadata["packages"]
.as_array()
.into_iter()
.flatten()
.filter_map(|package| {
let manifest = PathBuf::from(package["manifest_path"].as_str()?);
let src = manifest.parent()?.join("src");
src.is_dir().then_some(src)
})
.collect();
if dirs.is_empty() {
vec![PathBuf::from("./src")]
} else {
dirs
}
}