use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::thread;
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
pub fn watch_file(path: PathBuf, changed: Arc<Mutex<bool>>) {
let canonical_target = path.canonicalize().unwrap_or_else(|_| path.clone());
let watch_dir = canonical_target
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
thread::spawn(move || {
let changed_inner = Arc::clone(&changed);
let target = canonical_target.clone();
let root = watch_dir.clone();
let mut watcher = match recommended_watcher(move |res: notify::Result<Event>| match res {
Ok(event) => {
if !is_relevant_kind(&event.kind) {
return;
}
if !event_touches_relevant_path(&event, &target, &root) {
return;
}
if let Ok(mut flag) = changed_inner.lock() {
*flag = true;
}
}
Err(e) => {
eprintln!("[crepuscularity-dev] watcher error: {e}");
}
}) {
Ok(w) => w,
Err(e) => {
eprintln!("[crepuscularity-dev] could not create watcher: {e}");
return;
}
};
if let Err(e) = watcher.watch(&watch_dir, RecursiveMode::Recursive) {
eprintln!(
"[crepuscularity-dev] failed to watch {}: {e}",
watch_dir.display()
);
return;
}
loop {
thread::sleep(std::time::Duration::from_secs(3600));
}
});
}
fn is_relevant_kind(kind: &EventKind) -> bool {
matches!(
kind,
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
)
}
pub(crate) fn event_touches_relevant_path(event: &Event, target: &Path, watch_root: &Path) -> bool {
for path in &event.paths {
if path_matches_target(path, target) {
return true;
}
if path_is_relevant_sibling(path, watch_root) {
return true;
}
}
false
}
fn path_matches_target(path: &Path, target: &Path) -> bool {
if path == target {
return true;
}
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
canon == *target
}
fn path_is_relevant_sibling(path: &Path, watch_root: &Path) -> bool {
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let canon_root = watch_root
.canonicalize()
.unwrap_or_else(|_| watch_root.to_path_buf());
if !canon.starts_with(&canon_root) {
return false;
}
match canon.extension().and_then(|e| e.to_str()) {
Some("crepus") => true,
Some("toml") if canon.file_name().and_then(|n| n.to_str()) == Some("context.toml") => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use notify::event::{CreateKind, ModifyKind, RemoveKind};
use std::fs;
fn ev(kind: EventKind, paths: Vec<PathBuf>) -> Event {
Event {
kind,
paths,
attrs: Default::default(),
}
}
#[test]
fn relevant_kinds_include_modify_create_remove() {
assert!(is_relevant_kind(&EventKind::Modify(ModifyKind::Any)));
assert!(is_relevant_kind(&EventKind::Create(CreateKind::Any)));
assert!(is_relevant_kind(&EventKind::Remove(RemoveKind::Any)));
assert!(!is_relevant_kind(&EventKind::Access(
notify::event::AccessKind::Any
)));
}
#[test]
fn matches_when_event_path_equals_target() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("ui.crepus");
fs::write(&target, "div\n \"x\"").unwrap();
let target = target.canonicalize().unwrap();
let e = ev(
EventKind::Modify(ModifyKind::Data(notify::event::DataChange::Content)),
vec![target.clone()],
);
assert!(event_touches_relevant_path(&e, &target, dir.path()));
}
#[test]
fn matches_sibling_crepus_file_for_include_changes() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("ui.crepus");
let included = dir.path().join("card.crepus");
fs::write(&target, "div\n \"x\"").unwrap();
fs::write(&included, "div\n \"y\"").unwrap();
let target = target.canonicalize().unwrap();
let included = included.canonicalize().unwrap();
let e = ev(EventKind::Modify(ModifyKind::Any), vec![included.clone()]);
assert!(event_touches_relevant_path(&e, &target, dir.path()));
}
#[test]
fn matches_nested_include_in_subdirectory() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join("components")).unwrap();
let target = dir.path().join("ui.crepus");
let nested = dir.path().join("components").join("button.crepus");
fs::write(&target, "div").unwrap();
fs::write(&nested, "button").unwrap();
let target = target.canonicalize().unwrap();
let nested = nested.canonicalize().unwrap();
let e = ev(EventKind::Modify(ModifyKind::Any), vec![nested]);
assert!(event_touches_relevant_path(&e, &target, dir.path()));
}
#[test]
fn matches_context_toml_changes() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("ui.crepus");
let toml = dir.path().join("context.toml");
fs::write(&target, "div").unwrap();
fs::write(&toml, "name = \"a\"").unwrap();
let target = target.canonicalize().unwrap();
let toml = toml.canonicalize().unwrap();
let e = ev(EventKind::Modify(ModifyKind::Any), vec![toml]);
assert!(event_touches_relevant_path(&e, &target, dir.path()));
}
#[test]
fn ignores_unrelated_files_in_dir() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("ui.crepus");
let unrelated = dir.path().join("notes.txt");
fs::write(&target, "div").unwrap();
fs::write(&unrelated, "...").unwrap();
let target = target.canonicalize().unwrap();
let unrelated = unrelated.canonicalize().unwrap();
let e = ev(EventKind::Modify(ModifyKind::Any), vec![unrelated]);
assert!(!event_touches_relevant_path(&e, &target, dir.path()));
}
#[test]
fn ignores_random_toml_that_is_not_context_toml() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("ui.crepus");
let other = dir.path().join("Cargo.toml");
fs::write(&target, "div").unwrap();
fs::write(&other, "[package]").unwrap();
let target = target.canonicalize().unwrap();
let other = other.canonicalize().unwrap();
let e = ev(EventKind::Modify(ModifyKind::Any), vec![other]);
assert!(!event_touches_relevant_path(&e, &target, dir.path()));
}
#[test]
fn matches_target_via_remove_event_for_atomic_save() {
let dir = tempfile::tempdir().unwrap();
let target_path = dir.path().join("ui.crepus");
fs::write(&target_path, "div").unwrap();
let target_canon = target_path.canonicalize().unwrap();
fs::remove_file(&target_path).unwrap();
let e = ev(
EventKind::Remove(RemoveKind::File),
vec![target_canon.clone()],
);
assert!(event_touches_relevant_path(&e, &target_canon, dir.path()));
}
}