use notify::{EventKind, RecursiveMode, Watcher};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
pub struct Watch {
_watcher: notify::RecommendedWatcher,
structural: Arc<AtomicBool>,
}
impl Watch {
pub fn start() -> Option<Self> {
let structural = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&structural);
let mut watcher = notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
if let Ok(event) = res
&& matches!(event.kind, EventKind::Create(_) | EventKind::Remove(_))
{
flag.store(true, Ordering::Relaxed);
}
})
.ok()?;
let mut watching = 0usize;
for root in roots() {
if watcher.watch(&root, RecursiveMode::Recursive).is_ok() {
watching += 1;
}
}
(watching > 0).then_some(Watch {
_watcher: watcher,
structural,
})
}
pub fn took_structural_change(&self) -> bool {
self.structural.swap(false, Ordering::Relaxed)
}
}
fn roots() -> Vec<PathBuf> {
let mut roots = vec![
crate::config::CLAUDE_PROJECTS_ROOT.clone(),
crate::config::CODEX_SESSIONS_ROOT.clone(),
crate::config::CURSOR_PROJECTS_ROOT.clone(),
crate::config::PI_SESSIONS_ROOT.clone(),
crate::config::OPENCODE_DATA_DIR.clone(),
];
roots.extend(crate::config::CLAUDE_MAC_COWORK_ROOT.clone());
roots.extend(crate::config::CLAUDE_MAC_CODE_ROOT.clone());
roots.retain(|r| r.is_dir());
roots
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_created_file_arms_the_flag_once() {
let dir = tempfile::tempdir().unwrap();
let structural = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&structural);
let mut watcher = notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
if let Ok(event) = res
&& matches!(event.kind, EventKind::Create(_) | EventKind::Remove(_))
{
flag.store(true, Ordering::Relaxed);
}
})
.unwrap();
watcher.watch(dir.path(), RecursiveMode::Recursive).unwrap();
let watch = Watch {
_watcher: watcher,
structural,
};
std::fs::write(dir.path().join("new-session.jsonl"), b"{}\n").unwrap();
let armed = (0..50).any(|_| {
std::thread::sleep(std::time::Duration::from_millis(100));
watch.took_structural_change()
});
assert!(armed, "a created file did not reach the flag");
assert!(
!watch.took_structural_change(),
"reading the flag must disarm it"
);
}
}