file manager
git clone https://git.lucas.co/cce-files.git
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);