Skip to main content

ane/frontend/tui/
fs_watcher.rs

1use std::path::{Path, PathBuf};
2use std::sync::mpsc;
3
4use anyhow::Result;
5use notify::{RecommendedWatcher, RecursiveMode, Watcher};
6
7pub struct FsWatcher {
8    watcher: RecommendedWatcher,
9    pub rx: mpsc::Receiver<notify::Result<notify::Event>>,
10    watched_file: Option<PathBuf>,
11    watched_tree: Option<PathBuf>,
12}
13
14impl FsWatcher {
15    pub fn new() -> Result<Self> {
16        let (tx, rx) = mpsc::channel();
17        let watcher = RecommendedWatcher::new(tx, notify::Config::default())?;
18        Ok(Self {
19            watcher,
20            rx,
21            watched_file: None,
22            watched_tree: None,
23        })
24    }
25
26    pub fn watch_file(&mut self, path: &Path) -> Result<()> {
27        self.unwatch_file();
28        let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
29        self.watcher
30            .watch(&canonical, RecursiveMode::NonRecursive)?;
31        self.watched_file = Some(canonical);
32        Ok(())
33    }
34
35    pub fn unwatch_file(&mut self) {
36        if let Some(path) = self.watched_file.take() {
37            let _ = self.watcher.unwatch(&path);
38        }
39    }
40
41    pub fn watch_tree(&mut self, root: &Path) -> Result<()> {
42        self.unwatch_tree();
43        let canonical = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
44        self.watcher.watch(&canonical, RecursiveMode::Recursive)?;
45        self.watched_tree = Some(canonical);
46        Ok(())
47    }
48
49    pub fn unwatch_tree(&mut self) {
50        if let Some(path) = self.watched_tree.take() {
51            let _ = self.watcher.unwatch(&path);
52        }
53    }
54
55    pub fn watched_file(&self) -> Option<&Path> {
56        self.watched_file.as_deref()
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use std::io::Write;
64    use std::time::Duration;
65    use tempfile::NamedTempFile;
66
67    #[test]
68    fn watch_file_unwatch_file_round_trip_receives_event() {
69        let mut f = NamedTempFile::new().unwrap();
70        f.write_all(b"initial\n").unwrap();
71        f.flush().unwrap();
72
73        let mut watcher = FsWatcher::new().unwrap();
74        watcher.watch_file(f.path()).unwrap();
75
76        let expected_canonical = f.path().canonicalize().unwrap();
77        assert_eq!(
78            watcher.watched_file().map(|p| p.to_path_buf()),
79            Some(expected_canonical),
80            "watched_file should be set to the canonical path after watch_file"
81        );
82
83        std::thread::sleep(Duration::from_millis(50));
84        std::fs::write(f.path(), b"modified\n").unwrap();
85
86        let result = watcher.rx.recv_timeout(Duration::from_secs(1));
87        assert!(
88            result.is_ok(),
89            "should receive an FS event within 1 second after writing to watched file"
90        );
91
92        watcher.unwatch_file();
93        assert!(
94            watcher.watched_file().is_none(),
95            "watched_file should be None after unwatch_file"
96        );
97    }
98}