1use crate::config::WatchConfig;
2use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
3use std::path::Path;
4use std::sync::mpsc::{Receiver, channel};
5use std::time::Duration;
6
7pub struct FileWatcher {
8 watcher: RecommendedWatcher,
9 receiver: Receiver<Result<notify::Event, notify::Error>>,
10 debounce_duration: Duration,
11}
12
13impl FileWatcher {
14 pub fn new() -> Result<Self, notify::Error> {
16 Self::with_config(&WatchConfig::default())
17 }
18
19 pub fn with_config(config: &WatchConfig) -> Result<Self, notify::Error> {
21 let (tx, rx) = channel();
22
23 let watcher = RecommendedWatcher::new(
24 move |res| {
25 let _ = tx.send(res);
26 },
27 Config::default().with_poll_interval(Duration::from_millis(config.poll_interval_ms)),
28 )?;
29
30 Ok(Self {
31 watcher,
32 receiver: rx,
33 debounce_duration: Duration::from_millis(config.debounce_ms),
34 })
35 }
36
37 pub fn watch(&mut self, path: &Path) -> Result<(), notify::Error> {
38 self.watcher.watch(path, RecursiveMode::Recursive)
39 }
40
41 pub fn wait_for_change(&self) -> bool {
42 let mut has_change = false;
44
45 loop {
46 match self.receiver.recv_timeout(if has_change {
47 self.debounce_duration
48 } else {
49 Duration::from_secs(60 * 60) }) {
51 Ok(Ok(event)) => {
52 if matches!(
54 event.kind,
55 EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
56 ) {
57 has_change = true;
58 }
59 }
60 Ok(Err(e)) => {
61 eprintln!("Watch error: {}", e);
62 }
63 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
64 if has_change {
66 return true;
67 }
68 }
70 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
71 return false;
72 }
73 }
74 }
75 }
76}
77
78impl Default for FileWatcher {
79 fn default() -> Self {
80 Self::new().expect("Failed to create file watcher")
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use std::fs;
88 use tempfile::TempDir;
89
90 #[test]
91 fn test_file_watcher_creation() {
92 let watcher = FileWatcher::new();
93 assert!(watcher.is_ok());
94 }
95
96 #[test]
97 fn test_file_watcher_with_config() {
98 let config = WatchConfig {
99 debounce_ms: 500,
100 poll_interval_ms: 1000,
101 };
102 let watcher = FileWatcher::with_config(&config);
103 assert!(watcher.is_ok());
104 let watcher = watcher.unwrap();
105 assert_eq!(watcher.debounce_duration, Duration::from_millis(500));
106 }
107
108 #[test]
109 fn test_file_watcher_with_custom_debounce() {
110 let config = WatchConfig {
111 debounce_ms: 100,
112 poll_interval_ms: 200,
113 };
114 let watcher = FileWatcher::with_config(&config).unwrap();
115 assert_eq!(watcher.debounce_duration, Duration::from_millis(100));
116 }
117
118 #[test]
119 fn test_watch_directory() {
120 let temp_dir = TempDir::new().unwrap();
121 let mut watcher = FileWatcher::new().unwrap();
122 let result = watcher.watch(temp_dir.path());
123 assert!(result.is_ok());
124 }
125
126 #[test]
127 fn test_watch_nonexistent_directory() {
128 let mut watcher = FileWatcher::new().unwrap();
129 let result = watcher.watch(Path::new("/nonexistent/path/12345"));
130 assert!(result.is_err());
131 }
132
133 #[test]
134 fn test_default_trait() {
135 let _watcher = FileWatcher::default();
137 }
138
139 #[test]
140 fn test_watch_file_change() {
141 use std::thread;
142 use std::time::Duration;
143
144 let temp_dir = TempDir::new().unwrap();
145 let test_file = temp_dir.path().join("test.md");
146 fs::write(&test_file, "initial content").unwrap();
147
148 let mut watcher = FileWatcher::new().unwrap();
149 watcher.watch(temp_dir.path()).unwrap();
150
151 let test_file_clone = test_file.clone();
153 let handle = thread::spawn(move || {
154 thread::sleep(Duration::from_millis(100));
155 fs::write(&test_file_clone, "modified content").unwrap();
156 });
157
158 let (tx, _rx) = channel();
161 let watcher_receiver = watcher.receiver;
162 thread::spawn(move || {
163 let result = watcher_receiver.recv_timeout(Duration::from_secs(2));
164 let _ = tx.send(result.is_ok());
165 });
166
167 handle.join().unwrap();
168
169 thread::sleep(Duration::from_millis(500));
171 }
172
173 #[test]
174 fn test_wait_for_change_with_create_event() {
175 use std::thread;
176 use std::time::Duration;
177
178 let temp_dir = TempDir::new().unwrap();
179 let mut watcher = FileWatcher::new().unwrap();
180 watcher.watch(temp_dir.path()).unwrap();
181
182 let test_file = temp_dir.path().join("new_file.txt");
184 thread::spawn(move || {
185 thread::sleep(Duration::from_millis(100));
186 fs::write(&test_file, "new content").unwrap();
187 });
188
189 let result = watcher.wait_for_change();
191 assert!(result);
192 }
193
194 #[test]
195 fn test_wait_for_change_with_remove_event() {
196 use std::thread;
197 use std::time::Duration;
198
199 let temp_dir = TempDir::new().unwrap();
200 let test_file = temp_dir.path().join("to_remove.txt");
201 fs::write(&test_file, "content").unwrap();
202
203 let mut watcher = FileWatcher::new().unwrap();
204 watcher.watch(temp_dir.path()).unwrap();
205
206 let test_file_clone = test_file.clone();
208 thread::spawn(move || {
209 thread::sleep(Duration::from_millis(100));
210 fs::remove_file(&test_file_clone).unwrap();
211 });
212
213 let result = watcher.wait_for_change();
215 assert!(result);
216 }
217
218 #[test]
219 fn test_wait_for_change_disconnected() {
220 use std::thread;
221 use std::time::Duration;
222
223 let (tx, rx) = channel::<Result<notify::Event, notify::Error>>();
225
226 let watcher_handle = thread::spawn(move || {
229 drop(tx);
232 });
233
234 thread::sleep(Duration::from_millis(50));
236
237 let result = rx.recv_timeout(Duration::from_millis(100));
239 assert!(result.is_err());
240
241 watcher_handle.join().unwrap();
242 }
243
244 #[test]
245 fn test_debounce_duration() {
246 let watcher = FileWatcher::new().unwrap();
247 assert_eq!(watcher.debounce_duration, Duration::from_millis(300));
248 }
249
250 #[test]
251 fn test_wait_for_change_with_modify_event() {
252 use std::thread;
253 use std::time::Duration;
254
255 let temp_dir = TempDir::new().unwrap();
256 let test_file = temp_dir.path().join("existing.txt");
257 fs::write(&test_file, "initial").unwrap();
258
259 let mut watcher = FileWatcher::new().unwrap();
260 watcher.watch(temp_dir.path()).unwrap();
261
262 let test_file_clone = test_file.clone();
264 thread::spawn(move || {
265 thread::sleep(Duration::from_millis(100));
266 fs::write(&test_file_clone, "modified").unwrap();
267 });
268
269 let result = watcher.wait_for_change();
271 assert!(result);
272 }
273
274 #[test]
275 fn test_receiver_fields() {
276 let watcher = FileWatcher::new().unwrap();
278 assert_eq!(watcher.debounce_duration, Duration::from_millis(300));
279 }
281}