use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::thread;
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
pub fn create_watcher(
path: PathBuf,
changed: Arc<Mutex<bool>>,
) -> Result<Box<dyn Watcher + Send>, String> {
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("."));
let target = canonical_target.clone();
let root = watch_dir.clone();
let mut watcher = 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.lock() {
*flag = true;
}
}
Err(e) => {
eprintln!("[crepuscularity-runtime] watcher error: {e}");
}
})
.map_err(|e| format!("could not create watcher: {e}"))?;
watcher
.watch(&watch_dir, RecursiveMode::Recursive)
.map_err(|e| format!("failed to watch {}: {e}", watch_dir.display()))?;
Ok(Box::new(watcher))
}
#[deprecated(
since = "0.4.3",
note = "Use `create_watcher` and own the returned `Watcher` so it drops with your hot-reload state."
)]
pub fn watch_file(path: PathBuf, changed: Arc<Mutex<bool>>) {
let watcher = match create_watcher(path, changed) {
Ok(w) => w,
Err(e) => {
eprintln!("[crepuscularity-runtime] {e}");
return;
}
};
thread::spawn(move || {
let _keep_alive = watcher;
thread::park();
});
}
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()));
}
#[test]
fn create_watcher_is_droppable_without_leaking_a_thread() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("ui.crepus");
fs::write(&target, "div").unwrap();
for _ in 0..16 {
let flag = Arc::new(Mutex::new(false));
let watcher =
create_watcher(target.clone(), Arc::clone(&flag)).expect("watcher should start");
drop(watcher);
}
}
#[test]
fn create_watcher_observes_modifications_through_the_flag() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("ui.crepus");
fs::write(&target, "div\n \"x\"").unwrap();
let flag = Arc::new(Mutex::new(false));
let _watcher =
create_watcher(target.clone(), Arc::clone(&flag)).expect("watcher should start");
thread::sleep(std::time::Duration::from_millis(150));
fs::write(&target, "div\n \"y\"").unwrap();
let mut saw_change = false;
for _ in 0..40 {
if *flag.lock().unwrap() {
saw_change = true;
break;
}
thread::sleep(std::time::Duration::from_millis(50));
}
assert!(saw_change, "watcher never flipped the changed flag");
}
}