use crate::config::Config;
use crate::discovery::discover_site;
use crate::render::build_site;
use anyhow::Result;
use notify::RecursiveMode;
use notify_debouncer_mini::new_debouncer;
use std::path::Path;
use std::sync::mpsc::channel;
use std::time::Duration;
const IGNORED_PATTERNS: &[&str] = &[
".git",
".obsidian",
"node_modules",
"target",
"dist",
".DS_Store",
];
pub fn watch_and_rebuild(config: &Config) -> Result<()> {
tracing::info!("Performing initial build...");
let site = discover_site(config)?;
build_site(&site, config)?;
tracing::info!("Initial build complete. Watching for changes...");
let (tx, rx) = channel();
let mut debouncer = new_debouncer(Duration::from_millis(500), tx)?;
debouncer
.watcher()
.watch(&config.source_dir, RecursiveMode::Recursive)?;
loop {
match rx.recv() {
Ok(Ok(events)) => {
let relevant_events: Vec<_> =
events.iter().filter(|e| !should_ignore(&e.path)).collect();
if relevant_events.is_empty() {
continue;
}
tracing::info!(
"Detected changes in {} file(s), rebuilding...",
relevant_events.len()
);
match discover_site(config) {
Ok(site) => match build_site(&site, config) {
Ok(()) => tracing::info!("Rebuild complete!"),
Err(e) => tracing::error!("Build error: {}", e),
},
Err(e) => tracing::error!("Discovery error: {}", e),
}
}
Ok(Err(e)) => {
tracing::warn!("Watch error: {:?}", e);
}
Err(e) => {
tracing::error!("Channel error: {}", e);
break;
}
}
}
Ok(())
}
fn should_ignore(path: &Path) -> bool {
let path_str = path.to_string_lossy();
for pattern in IGNORED_PATTERNS {
if path_str.contains(pattern) {
return true;
}
}
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with('.') {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_ignore() {
assert!(should_ignore(Path::new("/project/.git/config")));
assert!(should_ignore(Path::new("/project/node_modules/pkg")));
assert!(should_ignore(Path::new("/project/.DS_Store")));
assert!(!should_ignore(Path::new("/project/blog/post.md")));
}
}