1use std::path::{Path, PathBuf};
14
15use notify::Event;
16pub use notify::{Error, RecommendedWatcher, RecursiveMode, Watcher};
17
18use super::model::Change;
19
20pub fn spawn(
25 root: &Path,
26 mut on_change: impl FnMut(Change) + Send + 'static,
27) -> Result<RecommendedWatcher, Error> {
28 let root = root.to_path_buf();
29 let watched_root = root.clone();
30 let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
31 if let Ok(event) = res {
34 for change in event_to_changes(&event, &root) {
35 on_change(change);
36 }
37 }
38 })?;
39 watcher.watch(&watched_root, RecursiveMode::NonRecursive)?;
40 Ok(watcher)
41}
42
43pub fn event_to_changes(event: &Event, root: &Path) -> Vec<Change> {
50 if event.need_rescan() {
51 return vec![Change::Rescan];
52 }
53
54 use notify::event::{EventKind, ModifyKind, RenameMode};
55
56 let mut changes = Vec::new();
57 match &event.kind {
58 EventKind::Create(_) => push_change(&mut changes, event.paths.first(), root, |p| {
59 Change::Created { path: p }
60 }),
61 EventKind::Remove(_) => push_change(&mut changes, event.paths.first(), root, |p| {
62 Change::Removed { path: p }
63 }),
64 EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
65 if let (Some(from), Some(to)) = (event.paths.first(), event.paths.get(1))
66 && under_root(root, from) {
67 changes.push(Change::Renamed { from: from.clone(), to: to.clone() });
68 }
69 }
70 EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
71 push_change(&mut changes, event.paths.first(), root, |p| Change::Created { path: p });
72 }
73 EventKind::Modify(ModifyKind::Name(mode))
74 if !matches!(mode, RenameMode::To | RenameMode::Both) =>
75 {
76 push_change(&mut changes, event.paths.first(), root, |p| Change::Removed {
77 path: p,
78 });
79 }
80 EventKind::Modify(ModifyKind::Metadata(_)) => {
81 push_change(&mut changes, event.paths.first(), root, |p| Change::Modified {
82 path: p,
83 });
84 }
85 _ => {}
87 }
88 changes
89}
90
91fn push_change(
92 changes: &mut Vec<Change>,
93 path: Option<&PathBuf>,
94 root: &Path,
95 build: impl Fn(PathBuf) -> Change,
96) {
97 if let Some(path) = path
98 && under_root(root, path)
99 {
100 changes.push(build(path.to_path_buf()));
101 }
102}
103
104fn under_root(root: &Path, path: &Path) -> bool {
105 path.starts_with(root)
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use notify::event::{CreateKind, EventKind, ModifyKind, RemoveKind, RenameMode};
112
113 fn ev(kind: EventKind, paths: &[&str]) -> Event {
114 let mut event = Event::new(kind);
115 for p in paths {
116 event = event.add_path(PathBuf::from(p));
117 }
118 event
119 }
120
121 const ROOT: &str = "/home/u/src";
122
123 #[test]
124 fn creates_and_removes_map_directly() {
125 let e = ev(EventKind::Create(CreateKind::File), &["/home/u/src/a.txt"]);
126 assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![Change::Created {
127 path: "/home/u/src/a.txt".into()
128 }]);
129
130 let e = ev(EventKind::Remove(RemoveKind::Folder), &["/home/u/src/dir"]);
131 assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![Change::Removed {
132 path: "/home/u/src/dir".into()
133 }]);
134 }
135
136 #[test]
137 fn rename_both_preserves_from_and_to() {
138 let e = ev(
139 EventKind::Modify(ModifyKind::Name(RenameMode::Both)),
140 &["/home/u/src/old.txt", "/home/u/src/new.txt"],
141 );
142 assert_eq!(
143 event_to_changes(&e, Path::new(ROOT)),
144 vec![Change::Renamed {
145 from: "/home/u/src/old.txt".into(),
146 to: "/home/u/src/new.txt".into()
147 }]
148 );
149 }
150
151 #[test]
152 fn rename_single_sides_become_removed_and_created() {
153 let from_only =
154 ev(EventKind::Modify(ModifyKind::Name(RenameMode::From)), &["/home/u/src/x"]);
155 assert_eq!(event_to_changes(&from_only, Path::new(ROOT)), vec![Change::Removed {
156 path: "/home/u/src/x".into()
157 }]);
158
159 let to_only =
160 ev(EventKind::Modify(ModifyKind::Name(RenameMode::To)), &["/home/u/src/y"]);
161 assert_eq!(event_to_changes(&to_only, Path::new(ROOT)), vec![Change::Created {
162 path: "/home/u/src/y".into()
163 }]);
164 }
165
166 #[test]
167 fn metadata_change_becomes_modified() {
168 let e = ev(
169 EventKind::Modify(ModifyKind::Metadata(notify::event::MetadataKind::WriteTime)),
170 &["/home/u/src/a.txt"],
171 );
172 assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![Change::Modified {
173 path: "/home/u/src/a.txt".into()
174 }]);
175 }
176
177 #[test]
178 fn data_writes_are_ignored() {
179 let e = ev(
180 EventKind::Modify(ModifyKind::Data(notify::event::DataChange::Content)),
181 &["/home/u/src/log.txt"],
182 );
183 assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
184 }
185
186 #[test]
187 fn events_outside_root_are_dropped() {
188 let e = ev(EventKind::Create(CreateKind::File), &["/tmp/elsewhere/f.txt"]);
189 assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
190 }
191
192 #[test]
193 fn root_itself_is_in_scope() {
194 let e = ev(EventKind::Create(CreateKind::Folder), &[ROOT]);
195 assert_eq!(event_to_changes(&e, Path::new(ROOT)).len(), 1);
196 }
197
198 #[test]
199 fn events_without_paths_are_silently_ignored() {
200 let e = ev(EventKind::Create(CreateKind::File), &[]);
201 assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
202 }
203
204 #[test]
205 fn generic_kinds_are_ignored() {
206 let e = ev(EventKind::Any, &["/home/u/src/a"]);
207 assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
208 let e = ev(EventKind::Access(notify::event::AccessKind::Open(notify::event::AccessMode::Any)), &["/home/u/src/a"]);
209 assert_eq!(event_to_changes(&e, Path::new(ROOT)), vec![]);
210 }
211
212 #[test]
213 fn rescan_flag_dominates_everything() {
214 let e = Event::new(EventKind::Create(CreateKind::File))
215 .add_path("/home/u/src/a.txt".into());
216 let needs_rescan = Event {
217 attrs: {
218 let mut a = notify::event::EventAttributes::new();
219 a.set_flag(notify::event::Flag::Rescan);
220 a
221 },
222 ..e
223 };
224 assert_eq!(event_to_changes(&needs_rescan, Path::new(ROOT)), vec![Change::Rescan]);
225 }
226}