use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::broadcast;
use warp::Filter;
use notify::{Watcher, RecursiveMode, Event, EventKind};
use crate::generator::SiteGenerator;
use tracing::{info, error};
pub mod websocket;
pub mod static_files;
pub mod live_reload;
use websocket::*;
use live_reload::*;
pub struct DevServer {
input_dir: PathBuf,
output_dir: PathBuf,
theme_dir: Option<PathBuf>,
port: u16,
live_reload: bool,
reload_tx: broadcast::Sender<()>,
}
impl DevServer {
pub fn new(
input_dir: PathBuf,
output_dir: PathBuf,
theme_dir: Option<PathBuf>,
port: u16,
live_reload: bool,
) -> Self {
let (reload_tx, _) = broadcast::channel(100);
Self {
input_dir,
output_dir,
theme_dir,
port,
live_reload,
reload_tx,
}
}
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
self.generate_site()?;
self.start_file_watcher().await?;
let interfaces = get_network_interfaces();
let output_dir = self.output_dir.clone();
if self.live_reload {
let static_route = warp::fs::dir(output_dir.clone())
.or(warp::path::end().and(warp::fs::file(output_dir.join("index.html"))));
let reload_tx = self.reload_tx.clone();
let ws_route = warp::path("__krik_reload")
.and(warp::ws())
.map(move |ws: warp::ws::Ws| {
let tx = reload_tx.clone();
ws.on_upgrade(move |websocket| handle_websocket(websocket, tx))
});
let routes = ws_route.or(static_route);
info!("🚀 Krik development server started!");
info!("📁 Serving: {}", self.output_dir.display());
info!("👀 Watching: {}", self.input_dir.display());
if let Some(ref theme_dir) = self.theme_dir {
info!("👀 Watching theme: {}", theme_dir.display());
}
info!("🌐 Available on:");
for interface in &interfaces {
info!(" http://{}:{}", interface, self.port);
}
info!("✅ Live reload enabled");
info!("\n💡 Press Ctrl+C to stop");
warp::serve(routes)
.bind(([0, 0, 0, 0], self.port))
.await;
} else {
let static_route = warp::fs::dir(output_dir.clone())
.or(warp::path::end().and(warp::fs::file(output_dir.join("index.html"))));
info!("🚀 Krik development server started!");
info!("📁 Serving: {}", self.output_dir.display());
info!("👀 Watching: {}", self.input_dir.display());
if let Some(ref theme_dir) = self.theme_dir {
info!("👀 Watching theme: {}", theme_dir.display());
}
info!("🌐 Available on:");
for interface in &interfaces {
info!(" http://{}:{}", interface, self.port);
}
info!("❌ Live reload disabled");
info!("\n💡 Press Ctrl+C to stop");
warp::serve(static_route)
.bind(([0, 0, 0, 0], self.port))
.await;
}
Ok(())
}
fn generate_site(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut generator = SiteGenerator::new(&self.input_dir, &self.output_dir, self.theme_dir.as_ref())?;
generator.scan_files()?;
generator.generate_site()?;
if self.live_reload {
inject_live_reload_script(&self.output_dir, self.port)?;
}
Ok(())
}
async fn start_file_watcher(&self) -> Result<(), Box<dyn std::error::Error>> {
let input_dir = self.input_dir.clone();
let output_dir = self.output_dir.clone();
let theme_dir = self.theme_dir.clone();
let reload_tx = self.reload_tx.clone();
let port = self.port;
let live_reload = self.live_reload;
tokio::spawn(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(100);
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
if let Ok(event) = res {
if matches!(event.kind, EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)) {
let _ = tx.blocking_send(event);
}
}
}).expect("Failed to create file watcher");
watcher.watch(&input_dir, RecursiveMode::Recursive)
.expect("Failed to watch input directory");
if let Some(ref theme_dir) = theme_dir {
watcher.watch(theme_dir, RecursiveMode::Recursive)
.expect("Failed to watch theme directory");
}
let mut last_generation = std::time::Instant::now();
while let Some(_event) = rx.recv().await {
let now = std::time::Instant::now();
if now.duration_since(last_generation) < Duration::from_millis(100) {
continue;
}
last_generation = now;
info!("📝 File changed, regenerating site...");
if let Ok(mut generator) = SiteGenerator::new(&input_dir, &output_dir, theme_dir.as_ref()) {
if let Err(e) = generator.scan_files() {
error!("❌ Error scanning files: {}", e);
continue;
}
if let Err(e) = generator.generate_site() {
error!("❌ Error generating site: {}", e);
continue;
}
if live_reload {
if let Err(e) = inject_live_reload_script(&output_dir, port) {
error!("❌ Error injecting live reload script: {}", e);
continue;
}
}
info!("✅ Site regenerated");
let _ = reload_tx.send(());
}
}
});
Ok(())
}
}
fn get_network_interfaces() -> Vec<String> {
let mut interfaces = vec!["127.0.0.1".to_string()];
if let Ok(local_ip) = local_ip_address::local_ip() {
if local_ip.to_string() != "127.0.0.1" {
interfaces.push(local_ip.to_string());
}
}
if let Ok(network_interfaces) = local_ip_address::list_afinet_netifas() {
for (_name, ip) in network_interfaces {
let ip_str = ip.to_string();
if !ip_str.starts_with("127.") && !ip_str.starts_with("169.254.") && !interfaces.contains(&ip_str) {
interfaces.push(ip_str);
}
}
}
interfaces
}