Skip to main content

tree_space/fs/
watcher.rs

1//! Filesystem watching via `notify` (inotify on Linux).
2//!
3//! [`spawn`] registers a watch on the root directory only — intentionally
4//! **non-recursive** so that opening a large tree is instant (a recursive
5//! join on the inotify backend walks the whole subtree, which blocks the UI
6//! exactly when the user is waiting for the first listing). The tree adds
7//! watches for individual directories as they are expanded, so auto-updates
8//! still reach every directory the user can actually see.
9//!
10//! Every raw notify event passes through the pure [`event_to_changes`]
11//! translation onto the model's [`Change`] vocabulary.
12
13use std::path::{Path, PathBuf};
14
15use notify::Event;
16pub use notify::{Error, RecommendedWatcher, RecursiveMode, Watcher};
17
18use super::model::Change;
19
20/// Spawn a recursive watcher on `root`; every translated change is handed to
21/// `on_change` (which the caller typically fires into a component sender).
22///
23/// The returned [`RecommendedWatcher`] must be kept alive for events to flow.
24pub 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        // Drop watch errors (e.g. a permission-denied subdir); the tree
32        // just won't auto-update for that subtree.
33        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
43/// Translate one raw notify event into model changes.
44///
45/// Only structural events (create/remove/rename) are surfaced plus a coarse
46/// [rescan][Change::Rescan] on `need_rescan()`. Metadata and data rewrites are
47/// ignored (the tree shows names and directories only). Events outside `root`
48/// are dropped.
49pub 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        // Data rewrites and everything else do not change tree structure.
86        _ => {}
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}