use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, SystemTime};
const WATCH_FILES: &[&str] = &["Cargo.toml", "Cargo.lock"];
const WATCH_DIRS: &[&str] = &["src", "tests", "benches", "examples"];
const POLL: Duration = Duration::from_millis(200);
const DEBOUNCE: Duration = Duration::from_millis(150);
pub fn watch<F>(project_root: &Path, mut f: F)
where
F: FnMut(),
{
banner(project_root, "initial build");
f();
let mut last_seen = max_mtime(project_root).unwrap_or(SystemTime::UNIX_EPOCH);
loop {
thread::sleep(POLL);
let now = match max_mtime(project_root) {
Some(t) => t,
None => continue,
};
if now > last_seen {
thread::sleep(DEBOUNCE);
last_seen = max_mtime(project_root).unwrap_or(now);
banner(project_root, "change detected, rebuilding");
f();
}
}
}
fn banner(project_root: &Path, msg: &str) {
let mut stdout = io::stdout().lock();
let _ = writeln!(
stdout,
"\n[hopper --watch] {} ({})",
msg,
project_root.display()
);
let _ = stdout.flush();
}
fn max_mtime(root: &Path) -> Option<SystemTime> {
let mut best: Option<SystemTime> = None;
for name in WATCH_FILES {
if let Some(t) = mtime(&root.join(name)) {
best = Some(best.map(|b| b.max(t)).unwrap_or(t));
}
}
for dir in WATCH_DIRS {
walk_mtimes(&root.join(dir), &mut best);
}
best
}
fn walk_mtimes(dir: &Path, best: &mut Option<SystemTime>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with('.') || name == "target" {
continue;
}
}
walk_mtimes(&path, best);
continue;
}
if let Some(t) = mtime(&path) {
*best = Some(best.map(|b| b.max(t)).unwrap_or(t));
}
}
}
fn mtime(path: &Path) -> Option<SystemTime> {
std::fs::metadata(path).ok()?.modified().ok()
}
pub fn extract_watch_flag(args: &mut Vec<String>) -> bool {
let before = args.len();
args.retain(|a| a != "--watch" && a != "-w");
args.len() != before
}
#[allow(dead_code)]
pub fn project_watch_root(root: &Path) -> PathBuf {
root.to_path_buf()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_long_form() {
let mut args = vec![
"--release".to_string(),
"--watch".to_string(),
"--features".to_string(),
"foo".to_string(),
];
assert!(extract_watch_flag(&mut args));
assert_eq!(args, vec!["--release", "--features", "foo"]);
}
#[test]
fn extracts_short_form() {
let mut args = vec!["-w".to_string(), "--release".to_string()];
assert!(extract_watch_flag(&mut args));
assert_eq!(args, vec!["--release"]);
}
#[test]
fn extracts_both_forms_in_one_call() {
let mut args = vec![
"-w".to_string(),
"--release".to_string(),
"--watch".to_string(),
];
assert!(extract_watch_flag(&mut args));
assert_eq!(args, vec!["--release"]);
}
#[test]
fn returns_false_when_absent() {
let mut args = vec!["--release".to_string()];
assert!(!extract_watch_flag(&mut args));
assert_eq!(args, vec!["--release"]);
}
}