use crate::config::Config;
use crate::discovery::discover_site;
use crate::render::build_site;
use anyhow::Result;
use axum::Router;
use notify_debouncer_mini::new_debouncer;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::mpsc::channel;
use std::time::Duration;
use tower_http::services::ServeDir;
pub async fn serve(config: &Config) -> Result<()> {
tracing::info!("Building site...");
let site = discover_site(config)?;
build_site(&site, config)?;
let addr: SocketAddr = format!("{}:{}", config.server_host, config.server_port).parse()?;
let app = Router::new().fallback_service(ServeDir::new(&config.output_dir));
tracing::info!("Serving at http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
pub async fn serve_with_watch(config: &Config) -> Result<()> {
tracing::info!("Building site...");
let site = discover_site(config)?;
build_site(&site, config)?;
let config_clone = config.clone();
tokio::spawn(async move {
if let Err(e) = run_watcher(&config_clone) {
tracing::error!("Watcher error: {}", e);
}
});
let addr: SocketAddr = format!("{}:{}", config.server_host, config.server_port).parse()?;
let app = Router::new().fallback_service(ServeDir::new(&config.output_dir));
tracing::info!("Serving at http://{} (watching for changes)", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
fn run_watcher(config: &Config) -> Result<()> {
let (tx, rx) = channel();
let mut debouncer = new_debouncer(Duration::from_millis(500), tx)?;
debouncer
.watcher()
.watch(&config.source_dir, notify::RecursiveMode::Recursive)?;
loop {
match rx.recv() {
Ok(Ok(events)) => {
let relevant = events.iter().any(|e| !should_ignore(&e.path));
if !relevant {
continue;
}
tracing::info!("Changes detected, rebuilding...");
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();
const IGNORED: &[&str] = &[".git", ".obsidian", "node_modules", "target", "dist"];
for pattern in IGNORED {
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
}