use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher, event::ModifyKind};
use tokio::sync::mpsc;
use super::AppState;
use crate::Error;
use crate::config::Config;
const DEBOUNCE: Duration = Duration::from_millis(200);
pub(crate) struct ContentWatcher {
_watcher: RecommendedWatcher,
rx: mpsc::Receiver<()>,
}
impl ContentWatcher {
pub fn new(config: &Config) -> Result<Self, Error> {
let (tx, rx) = mpsc::channel::<()>(1);
let mut watcher = RecommendedWatcher::new(
move |result: Result<notify::Event, notify::Error>| match result {
Ok(event) if is_content_change(&event.kind) => {
let _ = tx.try_send(());
}
Ok(_) => {}
Err(e) => tracing::error!("file watcher error: {e}"),
},
notify::Config::default(),
)?;
let dirs = [
Some(&config.source_dir),
Some(&config.static_dir),
config.theme_dir.as_ref(),
];
for dir in dirs.into_iter().flatten() {
if dir.is_dir() {
tracing::debug!(path = %dir.display(), "watching directory");
watcher.watch(dir, RecursiveMode::Recursive)?;
}
}
tracing::info!("file watcher started");
Ok(Self {
_watcher: watcher,
rx,
})
}
pub async fn run(&mut self, config_path: &Path, state: &Arc<AppState>) -> Result<(), Error> {
let shutdown = state.shutdown.notified();
tokio::pin!(shutdown);
loop {
tokio::select! {
biased;
_ = &mut shutdown => {
tracing::debug!("watcher received shutdown signal");
break;
}
result = self.rx.recv() => {
if result.is_none() {
break;
}
self.debounce().await;
tracing::info!("file change detected, rebuilding…");
if let Err(e) = state.rebuild(config_path).await {
tracing::error!("rebuild failed: {e}");
}
}
}
}
Ok(())
}
async fn debounce(&mut self) {
let mut deadline = tokio::time::Instant::now() + DEBOUNCE;
loop {
tokio::select! {
_ = tokio::time::sleep_until(deadline) => break,
result = self.rx.recv() => {
if result.is_none() { break; }
deadline = tokio::time::Instant::now() + DEBOUNCE;
}
}
}
}
}
fn is_content_change(kind: &EventKind) -> bool {
matches!(
kind,
EventKind::Create(_)
| EventKind::Remove(_)
| EventKind::Modify(ModifyKind::Data(_) | ModifyKind::Name(_) | ModifyKind::Any)
)
}