ane/frontend/tui/
fs_watcher.rs1use 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}