git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commit5a49c1ad91583c8f8b6fd494e40174b09656a409
parent4c59a90eff
authorLucas Galante <[email protected]>
date2026-06-15 14:30
Implement active filesystem watching using notify

 Cargo.toml          |  1 +
 src/main.rs         | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/pages/browse.rs | 12 +++++++++++
 3 files changed, 75 insertions(+)

diff --git a/Cargo.toml b/Cargo.toml
index e6580f3..ce53d62 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,4 +12,5 @@ glyphon = "0.8"
 calloop = "0.13.0"
 chrono = "0.4.44"
 tokio = { version = "1", features = ["full"] }
+notify = "8.2.0"
 
diff --git a/src/main.rs b/src/main.rs
index e14df49..e285e88 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,6 +7,8 @@ use clear_ui::engine::{Application, LogicalPosition, LogicalSize, WindowSettings
 use clear_ui::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element};
 use clear_ui::widget::{GraphController, PathController};
 
+use notify::{Watcher, RecommendedWatcher, RecursiveMode, Config};
+
 use pages::Page;
 use pages::browse::is_project_dir;
 
@@ -50,6 +52,7 @@ struct FilesystemApp {
     paginator: clear_ui::widget::Paginator,
     just_initialized: bool,
     ui_context: clear_ui::context::UiContext,
+    watcher: Option<notify::RecommendedWatcher>,
 }
 
 // ── Messages ────────────────────────────────────────────────────────
@@ -89,6 +92,55 @@ fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<&st
 // ── Layout Rebuild ──────────────────────────────────────────────────
 
 impl FilesystemApp {
+    fn start_watching(&mut self, path: std::path::PathBuf) {
+        use tokio::sync::mpsc;
+        use std::time::Duration;
+
+        let (tx, mut rx) = mpsc::channel::<()>(100);
+        let sender_clone = self.sender.clone();
+        let path_clone = path.clone();
+
+        tokio::spawn(async move {
+            while rx.recv().await.is_some() {
+                tokio::time::sleep(Duration::from_millis(150)).await;
+                while rx.try_recv().is_ok() {}
+
+                let entries = pages::browse::read_directory(&path_clone);
+                let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryRefreshed(
+                    path_clone.clone(),
+                    entries,
+                )));
+            }
+        });
+
+        let mut watcher = match RecommendedWatcher::new(
+            move |res: Result<notify::Event, notify::Error>| {
+                if let Ok(event) = res {
+                    match event.kind {
+                        notify::EventKind::Create(_) | notify::EventKind::Modify(_) | notify::EventKind::Remove(_) => {
+                            let _ = tx.try_send(());
+                        }
+                        _ => {}
+                    }
+                }
+            },
+            Config::default(),
+        ) {
+            Ok(w) => w,
+            Err(e) => {
+                eprintln!("Failed to create watcher: {:?}", e);
+                return;
+            }
+        };
+
+        if let Err(e) = watcher.watch(&path, RecursiveMode::NonRecursive) {
+            eprintln!("Failed to watch path {}: {:?}", path.display(), e);
+            return;
+        }
+
+        self.watcher = Some(watcher);
+    }
+
     fn rebuild_layout(&mut self) {
         self.browse.save_name_box.prepare_text(&mut self.font_system);
         self.browse.search_box.prepare_text(&mut self.font_system);
@@ -384,6 +436,7 @@ impl Application for FilesystemApp {
             paginator,
             just_initialized: true,
             ui_context: clear_ui::context::UiContext::new(),
+            watcher: None,
         };
 
         // Start initial directory loading
@@ -450,6 +503,11 @@ impl Application for FilesystemApp {
                     pages::browse::BrowseMessage::NavigateToPath(_) => true,
                     _ => false,
                 };
+                let is_directory_loaded = match &msg {
+                    pages::browse::BrowseMessage::DirectoryLoaded(path, _) => Some(path.clone()),
+                    _ => None,
+                };
+
                 let (target_path, handle) = pages::browse::update(&mut self.browse, msg);
                 if is_navigation {
                     let sender_clone = self.sender.clone();
@@ -459,6 +517,10 @@ impl Application for FilesystemApp {
                     });
                 }
 
+                if let Some(path) = is_directory_loaded {
+                    self.start_watching(path);
+                }
+
                 // If NavigateTo or SelectEntry happened, update Preview path
                 let selected_path = if let Some(idx) = self.browse.selected {
                     self.browse.entries.get(idx).map(|e| e.path.clone())
diff --git a/src/pages/browse.rs b/src/pages/browse.rs
index ee32eab..b2804fd 100644
--- a/src/pages/browse.rs
+++ b/src/pages/browse.rs
@@ -102,6 +102,7 @@ pub enum BrowseMessage {
     NavigateTo(usize),
     NavigateToPath(PathBuf),
     DirectoryLoaded(PathBuf, Vec<DirEntry>),
+    DirectoryRefreshed(PathBuf, Vec<DirEntry>),
     ToggleHidden,
 }
 
@@ -456,6 +457,17 @@ pub fn update(state: &mut BrowseState, msg: BrowseMessage) -> (PathBuf, tokio::t
             state.update_breadcrumb();
             (state.current_dir.clone(), tokio::spawn(async { Vec::new() }))
         }
+        BrowseMessage::DirectoryRefreshed(path, entries) => {
+            if state.current_dir == path {
+                let selected_path = state.selected.and_then(|idx| state.entries.get(idx).map(|e| e.path.clone()));
+                state.all_entries = entries;
+                apply_filters(state);
+                if let Some(path) = selected_path {
+                    state.selected = state.entries.iter().position(|e| e.path == path);
+                }
+            }
+            (state.current_dir.clone(), tokio::spawn(async { Vec::new() }))
+        }
         BrowseMessage::ToggleHidden => {
             state.show_hidden = !state.show_hidden;
             apply_filters(state);