use std::path::Path;
use std::time::Duration;
use console::style;
use notify::{EventKind, RecursiveMode, Watcher};
use tokio::process::Command;
use tokio::sync::mpsc;
use crate::tailwind;
pub async fn run() -> Result<(), String> {
verify_blixt_project()?;
println!(
" {} Starting Blixt dev server...",
style("▸").cyan().bold()
);
let tailwind_path = tailwind::ensure_tailwind().await?;
let mut tailwind_child = spawn_tailwind(&tailwind_path)?;
let (file_tx, mut file_rx) = mpsc::channel::<()>(1);
let _watcher = start_file_watcher(file_tx)?;
let mut cargo_child = spawn_cargo()?;
loop {
tokio::select! {
result = cargo_child.wait() => {
match result {
Ok(status) if status.success() => break,
Ok(status) => {
println!(" {} App exited ({}), waiting for file changes...",
style("!").yellow().bold(), status);
wait_for_change(&mut file_rx).await;
cargo_child = spawn_cargo()?;
}
Err(e) => return Err(format!("cargo run error: {e}")),
}
}
_ = file_rx.recv() => {
println!(" {} File changed, restarting...",
style("↻").cyan().bold());
kill_child(&mut cargo_child, "app");
let _ = cargo_child.wait().await;
debounce(&mut file_rx).await;
cargo_child = spawn_cargo()?;
}
_ = tokio::signal::ctrl_c() => {
println!("\n {} Shutting down...", style("▸").yellow().bold());
break;
}
}
}
kill_child(&mut tailwind_child, "Tailwind");
kill_child(&mut cargo_child, "cargo");
Ok(())
}
fn verify_blixt_project() -> Result<(), String> {
if !Path::new("Cargo.toml").exists() {
return Err(
"No Cargo.toml found. Run this command from a Blixt project directory.".into(),
);
}
Ok(())
}
fn spawn_tailwind(binary: &Path) -> Result<tokio::process::Child, String> {
println!(
" {} Tailwind CSS watching for changes...",
style("▸").cyan().bold()
);
Command::new(binary)
.args(["--input", "static/css/app.css", "--output", "static/css/output.css", "--watch"])
.kill_on_drop(true)
.spawn()
.map_err(|err| format!("Failed to start Tailwind watcher: {err}"))
}
fn spawn_cargo() -> Result<tokio::process::Child, String> {
println!(
" {} Compiling and starting application...",
style("▸").cyan().bold()
);
Command::new("cargo")
.arg("run")
.kill_on_drop(true)
.spawn()
.map_err(|err| format!("Failed to start cargo run: {err}"))
}
fn start_file_watcher(tx: mpsc::Sender<()>) -> Result<notify::RecommendedWatcher, String> {
let mut watcher = notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
let Ok(event) = event else { return };
if !is_relevant_change(&event) {
return;
}
let _ = tx.try_send(());
})
.map_err(|e| format!("Failed to create file watcher: {e}"))?;
for dir in ["src", "templates"] {
if Path::new(dir).exists() {
watcher.watch(Path::new(dir), RecursiveMode::Recursive)
.map_err(|e| format!("Failed to watch {dir}: {e}"))?;
}
}
println!(
" {} Watching src/ and templates/ for changes...",
style("▸").cyan().bold()
);
Ok(watcher)
}
fn is_relevant_change(event: ¬ify::Event) -> bool {
if !matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
return false;
}
event.paths.iter().any(|p| {
let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("");
matches!(ext, "rs" | "html" | "toml")
})
}
async fn wait_for_change(rx: &mut mpsc::Receiver<()>) {
rx.recv().await;
}
async fn debounce(rx: &mut mpsc::Receiver<()>) {
tokio::time::sleep(Duration::from_millis(300)).await;
while rx.try_recv().is_ok() {}
}
fn kill_child(child: &mut tokio::process::Child, label: &str) {
if let Err(err) = child.start_kill() {
eprintln!(
" {} Failed to stop {label}: {err}",
style("!").yellow().bold()
);
}
}