use anyhow::Result;
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver};
#[derive(Debug, Clone)]
pub enum FileEvent {
Created(Vec<PathBuf>),
Modified(Vec<PathBuf>),
Deleted(Vec<PathBuf>),
}
pub struct FileSystemWatcher {
_watcher: RecommendedWatcher,
rx: Receiver<Result<Event, notify::Error>>,
}
impl FileSystemWatcher {
pub fn new(path: &Path) -> Result<Self> {
let (tx, rx) = mpsc::channel();
let mut watcher = notify::recommended_watcher(move |event| {
let _ = tx.send(event);
})?;
watcher.watch(path, RecursiveMode::Recursive)?;
Ok(Self {
_watcher: watcher,
rx,
})
}
pub fn check_events(&self) -> Vec<FileEvent> {
let mut events = Vec::new();
while let Ok(Ok(event)) = self.rx.try_recv() {
match event.kind {
EventKind::Create(_) => {
if !event.paths.is_empty() {
events.push(FileEvent::Created(event.paths));
}
},
EventKind::Modify(modify_kind) => {
use notify::event::ModifyKind;
match modify_kind {
ModifyKind::Data(_) | ModifyKind::Any => {
if !event.paths.is_empty() {
events.push(FileEvent::Modified(event.paths));
}
},
_ => {}, }
},
EventKind::Remove(_) => {
if !event.paths.is_empty() {
events.push(FileEvent::Deleted(event.paths));
}
},
_ => {}, }
}
events
}
pub fn should_ignore_path(path: &Path) -> bool {
if let Some(name) = path.file_name() {
if let Some(name_str) = name.to_str() {
if name_str.starts_with('.') {
return true;
}
}
}
if let Some(parent) = path.parent() {
if let Some(parent_name) = parent.file_name() {
if let Some(parent_str) = parent_name.to_str() {
match parent_str {
"target" | "node_modules" | "__pycache__" | ".git" | "dist" | "build"
| ".venv" | "venv" => return true,
_ => {},
}
}
}
}
if let Some(ext) = path.extension() {
if let Some(ext_str) = ext.to_str() {
match ext_str {
"txt" | "md" | "rs" | "toml" | "yaml" | "yml" | "json" | "js" | "ts"
| "jsx" | "tsx" | "py" | "go" | "java" | "c" | "cpp" | "h" | "hpp" | "sh"
| "bash" | "zsh" | "fish" | "html" | "css" | "scss" | "xml" | "vue"
| "svelte" => false,
_ => true,
}
} else {
true }
} else {
false }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_should_ignore_path() {
assert!(FileSystemWatcher::should_ignore_path(Path::new(
".gitignore"
)));
assert!(FileSystemWatcher::should_ignore_path(Path::new(
"node_modules/package.json"
)));
assert!(FileSystemWatcher::should_ignore_path(Path::new(
"image.png"
)));
assert!(!FileSystemWatcher::should_ignore_path(Path::new("main.rs")));
assert!(!FileSystemWatcher::should_ignore_path(Path::new(
"README.md"
)));
assert!(!FileSystemWatcher::should_ignore_path(Path::new(
"config.toml"
)));
}
#[tokio::test]
async fn test_file_watcher_events() {
let temp_dir = TempDir::new().unwrap();
let watcher = FileSystemWatcher::new(temp_dir.path()).unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "hello").unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let events = watcher.check_events();
assert!(!events.is_empty(), "Should have detected file creation");
}
}