use colored::Colorize;
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher, EventKind};
use std::path::Path;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use crate::console::{icon_fail, icon_ok, icon_play};
use crate::detect::ProjectInfo;
pub fn watch_scss(input_dir: &str, output_dir: &str, minify: bool) {
let (tx, rx) = mpsc::channel();
let config = Config::default().with_poll_interval(Duration::from_secs(2));
let mut watcher: RecommendedWatcher =
Watcher::new(tx, config).expect("Failed to create watcher");
let input = Path::new(input_dir);
if input.exists() {
watcher
.watch(input, RecursiveMode::Recursive)
.expect("Failed to watch SCSS directory");
}
let mut last_compile = Instant::now();
loop {
match rx.recv() {
Ok(_event) => {
if last_compile.elapsed() < Duration::from_millis(500) {
continue;
}
last_compile = Instant::now();
println!(
"\n{} SCSS changed — recompiling...",
"♻".cyan()
);
crate::scss::compile_dir(input_dir, output_dir, minify);
}
Err(e) => {
eprintln!("{} Watcher error: {}", icon_fail().red(), e);
break;
}
}
}
}
pub fn watch_and_reload(
scss_dir: &str,
css_dir: &str,
info: &ProjectInfo,
port: u16,
host: &str,
server: &mut std::process::Child,
) {
let (tx, rx) = mpsc::channel();
let config = Config::default().with_poll_interval(Duration::from_secs(2));
let mut watcher: RecommendedWatcher =
Watcher::new(tx, config).expect("Failed to create watcher");
let watch_paths = ["src", "migrations"];
for p in &watch_paths {
let path = Path::new(p);
if path.exists() {
let _ = watcher.watch(path, RecursiveMode::Recursive);
}
}
let env_path = Path::new(".env");
if env_path.exists() {
let _ = watcher.watch(env_path, RecursiveMode::NonRecursive);
}
let mut last_event = Instant::now();
let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let r = running.clone();
ctrlc_handler(r);
while running.load(std::sync::atomic::Ordering::Relaxed) {
match rx.recv_timeout(Duration::from_secs(1)) {
Ok(Ok(event)) => {
match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {}
_ => continue,
}
if last_event.elapsed() < Duration::from_millis(500) {
continue;
}
last_event = Instant::now();
let paths: Vec<String> = event
.paths
.iter()
.map(|p| p.display().to_string())
.collect();
let dominated_by_noise = paths.iter().all(|p| {
let lower = p.to_lowercase();
lower.contains("__pycache__")
|| lower.ends_with(".pyc")
|| lower.ends_with(".pyo")
|| lower.ends_with(".db")
|| lower.ends_with(".db-journal")
|| lower.ends_with(".db-wal")
|| lower.ends_with(".db-shm")
|| lower.ends_with(".log")
|| lower.ends_with(".pid")
|| lower.ends_with(".tmp")
|| lower.ends_with(".swp")
|| lower.ends_with("~")
|| lower.ends_with(".css")
|| lower.contains("/data/")
|| lower.contains("\\data\\")
|| lower.contains("/logs/")
|| lower.contains("\\logs\\")
|| lower.contains("/secrets/")
|| lower.contains("\\secrets\\")
|| lower.contains("/public/")
|| lower.contains("\\public\\")
|| lower.contains("/node_modules/")
|| lower.contains("\\node_modules\\")
|| lower.contains("/.venv/")
|| lower.contains("\\.venv\\")
|| lower.contains("/vendor/")
|| lower.contains("\\vendor\\")
|| lower.contains("/.git/")
|| lower.contains("\\.git\\")
});
if dominated_by_noise {
continue;
}
let is_scss = paths
.iter()
.any(|p| p.ends_with(".scss"));
if is_scss {
println!("{} SCSS changed — recompiling", "♻".cyan());
crate::scss::compile_dir(scss_dir, css_dir, false);
} else {
let changed = paths
.first()
.map(|p| p.as_str())
.unwrap_or("file");
println!(
"{} {} changed — restarting server...",
"♻".cyan(),
changed.dimmed()
);
kill_server(server);
wait_for_port(port, host);
match crate::start_language_server(info, port, host) {
Some(child) => *server = child,
None => {
eprintln!("{} Failed to restart server", icon_fail().red());
}
}
}
}
Ok(Err(e)) => {
eprintln!("{} Watch error: {:?}", icon_fail().red(), e);
}
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
println!("\n{} Shutting down...", icon_play().yellow());
kill_server(server);
println!("{} Server stopped", icon_ok().green());
}
fn kill_server(server: &mut std::process::Child) {
#[cfg(unix)]
{
let pid = server.id() as i32;
unsafe {
libc::kill(-pid, libc::SIGTERM);
}
std::thread::sleep(Duration::from_millis(300));
unsafe {
libc::kill(-pid, libc::SIGKILL);
}
}
#[cfg(not(unix))]
{
let _ = server.kill();
}
let _ = server.wait();
}
fn wait_for_port(port: u16, _host: &str) {
use std::net::TcpStream;
let addr = format!("127.0.0.1:{}", port);
for i in 0..10 {
match TcpStream::connect_timeout(
&addr.parse().unwrap(),
Duration::from_millis(200),
) {
Ok(_) => {
if i == 0 {
println!(
" {} Waiting for port {} to be released...",
icon_play().yellow(),
port.to_string().cyan()
);
}
std::thread::sleep(Duration::from_millis(500));
}
Err(_) => {
return;
}
}
}
eprintln!(
" {} Port {} still in use — restarting anyway",
icon_fail().yellow(),
port
);
}
fn ctrlc_handler(running: std::sync::Arc<std::sync::atomic::AtomicBool>) {
let _ = ctrlc::set_handler(move || {
running.store(false, std::sync::atomic::Ordering::Relaxed);
});
}