use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::time::{Duration, Instant, SystemTime};
const VERBS: [&str; 3] = ["check", "test", "run"];
const POLL: Duration = Duration::from_millis(300);
pub fn parse(targets: &[String]) -> Result<(&str, Vec<PathBuf>), String> {
let (verb, paths) = match targets.split_first() {
Some((v, rest)) if VERBS.contains(&v.as_str()) => (v.as_str(), rest),
_ => ("check", targets),
};
if paths.is_empty() {
return Err(format!("`blue watch {verb}` needs a PATH to watch"));
}
Ok((verb, paths.iter().map(PathBuf::from).collect()))
}
pub fn watch(verb: &str, paths: &[PathBuf], args: &[String]) -> Result<ExitCode, String> {
let me = std::env::current_exe().map_err(|e| format!("cannot find this blue: {e}"))?;
loop {
for path in paths {
let mut cmd = Command::new(&me);
cmd.arg(verb).arg(path);
if verb == "run" && !args.is_empty() {
cmd.arg("--").args(args);
}
println!("── blue {verb} {}", path.display());
let started = Instant::now();
let verdict = match cmd.status() {
Ok(s) if s.success() => "ok".to_string(),
Ok(s) => format!("failed ({s})"),
Err(e) => format!("could not start: {e}"),
};
println!("── {verdict} in {} ms", started.elapsed().as_millis());
}
let before = snapshot(paths);
println!(
"── watching {} file(s); a save reruns, Ctrl-C stops",
before.len()
);
let mut seen = before.clone();
loop {
std::thread::sleep(POLL);
let now = snapshot(paths);
if now != before && now == seen {
break;
}
seen = now;
}
}
}
fn snapshot(paths: &[PathBuf]) -> BTreeMap<PathBuf, SystemTime> {
let mut files = BTreeMap::new();
for path in paths {
if path.is_dir() {
sources(path, &mut files);
} else {
stamp(path, &mut files);
for root in blue_lang_pkg::load_path::project_roots(path) {
sources(&root, &mut files);
}
}
}
files
}
fn stamp(path: &Path, files: &mut BTreeMap<PathBuf, SystemTime>) {
let time = std::fs::metadata(path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
files.insert(path.to_path_buf(), time);
}
fn sources(dir: &Path, files: &mut BTreeMap<PathBuf, SystemTime>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
if path.is_dir() {
if !name.starts_with('.') && name != "target" && name != "result" {
sources(&path, files);
}
} else if name == "Bluefile" || path.extension().is_some_and(|e| e == "b") {
stamp(&path, files);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_verb_is_optional_and_defaults_to_check() {
let s = |v: &[&str]| v.iter().map(ToString::to_string).collect::<Vec<_>>();
assert_eq!(
parse(&s(&["x.b"])).unwrap(),
("check", vec![PathBuf::from("x.b")])
);
assert_eq!(
parse(&s(&["test", "a.b", "d"])).unwrap(),
("test", vec![PathBuf::from("a.b"), PathBuf::from("d")])
);
assert_eq!(
parse(&s(&["./test"])).unwrap(),
("check", vec![PathBuf::from("./test")])
);
assert!(parse(&s(&["run"])).is_err());
}
}