wavecraft_dev_server/reload/
watcher.rs1use anyhow::Result;
7use notify::RecursiveMode;
8use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, RecommendedCache};
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::sync::{mpsc, watch};
13
14#[derive(Debug, Clone)]
16pub enum WatchEvent {
17 RustFilesChanged(Vec<PathBuf>),
19}
20
21pub struct FileWatcher {
23 #[allow(dead_code)] debouncer: Debouncer<notify::RecommendedWatcher, RecommendedCache>,
25 #[allow(dead_code)] _shutdown_rx: watch::Receiver<bool>,
27}
28
29impl FileWatcher {
30 pub fn new(
40 engine_dir: &Path,
41 tx: mpsc::UnboundedSender<WatchEvent>,
42 shutdown_rx: watch::Receiver<bool>,
43 ) -> Result<Self> {
44 let engine_dir_clone = engine_dir.to_path_buf();
45 let engine_dir = Arc::new(engine_dir_clone.clone());
46 let shutdown_rx_for_events = shutdown_rx.clone();
47
48 let mut debouncer = new_debouncer(
49 Duration::from_millis(500),
50 None, move |result: DebounceEventResult| {
52 Self::handle_events(result, Arc::clone(&engine_dir), &tx, &shutdown_rx_for_events);
53 },
54 )?;
55
56 let src_path = engine_dir_clone.join("src");
58 debouncer.watch(&src_path, RecursiveMode::Recursive)?;
59
60 let cargo_toml = engine_dir_clone.join("Cargo.toml");
62 if cargo_toml.exists() {
63 debouncer.watch(&cargo_toml, RecursiveMode::NonRecursive)?;
64 }
65
66 Ok(Self {
67 debouncer,
68 _shutdown_rx: shutdown_rx,
69 })
70 }
71
72 fn handle_events(
74 result: DebounceEventResult,
75 engine_dir: Arc<PathBuf>,
76 tx: &mpsc::UnboundedSender<WatchEvent>,
77 shutdown_rx: &watch::Receiver<bool>,
78 ) {
79 if *shutdown_rx.borrow() {
80 return;
81 }
82
83 let events = match result {
84 Ok(events) => events,
85 Err(errors) => {
86 for error in errors {
87 eprintln!("File watcher error: {:?}", error);
88 }
89 return;
90 }
91 };
92
93 let mut changed_paths = Vec::new();
95 for event in events {
96 for path in &event.paths {
97 if Self::is_relevant_file(path, &engine_dir) {
98 changed_paths.push(path.clone());
99 }
100 }
101 }
102
103 if !changed_paths.is_empty() {
104 changed_paths.sort();
106 changed_paths.dedup();
107
108 if let Err(e) = tx.send(WatchEvent::RustFilesChanged(changed_paths)) {
109 eprintln!(
110 "Warning: File watcher failed to send event (channel closed): {:?}",
111 e
112 );
113 }
114 }
115 }
116
117 fn is_relevant_file(path: &Path, engine_dir: &Path) -> bool {
119 if path.starts_with(engine_dir.join("target")) {
121 return false;
122 }
123
124 if path
126 .components()
127 .any(|c| c.as_os_str().to_string_lossy().starts_with('.'))
128 {
129 return false;
130 }
131
132 let file_name = match path.file_name() {
133 Some(name) => name.to_string_lossy(),
134 None => return false,
135 };
136
137 if file_name.ends_with(".swp")
139 || file_name.ends_with(".swo")
140 || file_name.ends_with('~')
141 || file_name.starts_with(".#")
142 {
143 return false;
144 }
145
146 file_name.ends_with(".rs") || file_name == "Cargo.toml"
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_is_relevant_file() {
157 let engine_dir = PathBuf::from("/project/engine");
158
159 assert!(FileWatcher::is_relevant_file(
161 &engine_dir.join("src/lib.rs"),
162 &engine_dir
163 ));
164 assert!(FileWatcher::is_relevant_file(
165 &engine_dir.join("src/dsp/oscillator.rs"),
166 &engine_dir
167 ));
168
169 assert!(FileWatcher::is_relevant_file(
171 &engine_dir.join("Cargo.toml"),
172 &engine_dir
173 ));
174
175 assert!(!FileWatcher::is_relevant_file(
177 &engine_dir.join("target/debug/libfoo.dylib"),
178 &engine_dir
179 ));
180
181 assert!(!FileWatcher::is_relevant_file(
183 &engine_dir.join("src/.hidden.rs"),
184 &engine_dir
185 ));
186
187 assert!(!FileWatcher::is_relevant_file(
189 &engine_dir.join("src/lib.rs.swp"),
190 &engine_dir
191 ));
192 assert!(!FileWatcher::is_relevant_file(
193 &engine_dir.join("src/lib.rs~"),
194 &engine_dir
195 ));
196 assert!(!FileWatcher::is_relevant_file(
197 &engine_dir.join("src/.#lib.rs"),
198 &engine_dir
199 ));
200
201 assert!(!FileWatcher::is_relevant_file(
203 &engine_dir.join("src/data.json"),
204 &engine_dir
205 ));
206 }
207}