use anyhow::{anyhow, Result};
use console::style;
use notify::RecursiveMode;
use notify_debouncer_mini::new_debouncer;
use std::path::Path;
use std::sync::mpsc::channel;
use std::time::Duration;
use crate::registry::Registry;
pub fn run(name: String, debounce_ms: u64) -> Result<()> {
let reg = Registry::load()?;
let project = reg
.find(&name)
.ok_or_else(|| anyhow!("Project not found: {}", name))?
.clone();
if !project.path.exists() {
return Err(anyhow!(
"Project directory missing: {}",
project.path.display()
));
}
println!("{}", style("devist watch").bold());
println!(
" watching {} (debounce {}ms)",
style(project.path.display()).cyan(),
debounce_ms
);
println!(" press Ctrl+C to stop");
println!();
let (tx, rx) = channel();
let mut debouncer = new_debouncer(Duration::from_millis(debounce_ms), tx)?;
debouncer
.watcher()
.watch(&project.path, RecursiveMode::Recursive)?;
for res in rx {
match res {
Ok(events) => {
let interesting: Vec<¬ify_debouncer_mini::DebouncedEvent> =
events.iter().filter(|e| !is_ignored(&e.path)).collect();
if interesting.is_empty() {
continue;
}
print_change_block(&interesting, &project.path);
}
Err(error) => {
eprintln!(" {} {:?}", style("[WATCH-ERR]").red(), error);
}
}
}
Ok(())
}
fn print_change_block(events: &[¬ify_debouncer_mini::DebouncedEvent], project_root: &Path) {
let now = chrono::Local::now().format("%H:%M:%S");
println!(
"{} {} {}",
style(format!("[{}]", now)).dim(),
style("CHANGE").yellow(),
style(format!("{} file(s)", events.len())).bold()
);
for e in events {
let rel = e
.path
.strip_prefix(project_root)
.map(|p| p.to_path_buf())
.unwrap_or_else(|_| e.path.clone());
println!(" {} {}", style("ยท").dim(), rel.display());
}
println!();
}
const IGNORED_DIRS: &[&str] = &[
"node_modules",
"target",
"dist",
"build",
".next",
".turbo",
".cache",
".venv",
"venv",
"__pycache__",
".pytest_cache",
".git",
".idea",
".vscode",
".expo",
".dart_tool",
];
const IGNORED_FILES: &[&str] = &[".DS_Store"];
fn is_ignored(path: &Path) -> bool {
let s = path.to_string_lossy();
for d in IGNORED_DIRS {
if s.contains(&format!("/{}/", d))
|| s.contains(&format!("\\{}\\", d))
|| s.ends_with(&format!("/{}", d))
{
return true;
}
}
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if IGNORED_FILES.contains(&name) {
return true;
}
}
false
}