diff --git a/Cargo.lock b/Cargo.lock index 5d9b286..9198d90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1098,6 +1098,7 @@ dependencies = [ "lexical-sort", "libcosmic", "log", + "notify", "once_cell", "paste", "rust-embed", diff --git a/Cargo.toml b/Cargo.toml index 59c8b9c..4057982 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ env_logger = "0.11" once_cell = "1.19" lexical-sort = "0.3.1" log = "0.4" +notify = "6" paste = "1.0" serde = { version = "1", features = ["serde_derive"] } tokio = { version = "1" } diff --git a/src/main.rs b/src/main.rs index 3e177f9..310c096 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use cosmic::{ cosmic_theme, executor, iced::{ event, - futures::SinkExt, + futures::{self, SinkExt}, keyboard::{Event as KeyEvent, KeyCode, Modifiers}, subscription::{self, Subscription}, window, Event, Length, Point, @@ -16,9 +16,10 @@ use cosmic::{ widget::{self, segmented_button}, Application, ApplicationExt, Element, }; +use notify::Watcher; use std::{ any::TypeId, - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, env, fs, io, path::PathBuf, process, time, @@ -179,6 +180,8 @@ pub enum Message { MoveToTrash(Option), NewFile(Option), NewFolder(Option), + NotifyEvent(notify::Event), + NotifyWatcher(WatcherWrapper), Paste(Option), PendingComplete(u64), PendingError(u64, String), @@ -217,6 +220,23 @@ impl ContextPage { } } +#[derive(Debug)] +pub struct WatcherWrapper { + watcher_opt: Option, +} + +impl Clone for WatcherWrapper { + fn clone(&self) -> Self { + Self { watcher_opt: None } + } +} + +impl PartialEq for WatcherWrapper { + fn eq(&self, _other: &Self) -> bool { + false + } +} + /// The [`App`] stores application-specific state. pub struct App { core: Core, @@ -232,6 +252,7 @@ pub struct App { pending_operations: BTreeMap, complete_operations: BTreeMap, failed_operations: BTreeMap, + watcher_opt: Option<(notify::RecommendedWatcher, HashSet)>, } impl App { @@ -245,7 +266,11 @@ impl App { .closable() .activate() .id(); - Command::batch([self.update_title(), self.rescan_tab(entity, location)]) + Command::batch([ + self.update_title(), + self.update_watcher(), + self.rescan_tab(entity, location), + ]) } fn operation(&mut self, operation: Operation) { @@ -305,6 +330,53 @@ impl App { self.set_window_title(window_title) } + fn update_watcher(&mut self) -> Command { + if let Some((mut watcher, old_paths)) = self.watcher_opt.take() { + let mut new_paths = HashSet::new(); + for entity in self.tab_model.iter() { + if let Some(tab) = self.tab_model.data::(entity) { + if let Location::Path(path) = &tab.location { + new_paths.insert(path.clone()); + } + } + } + + // Unwatch paths no longer used + for path in old_paths.iter() { + if !new_paths.contains(path) { + match watcher.unwatch(path) { + Ok(()) => { + log::debug!("unwatching {:?}", path); + } + Err(err) => { + log::debug!("failed to unwatch {:?}: {}", path, err); + } + } + } + } + + // Watch new paths + for path in new_paths.iter() { + if !old_paths.contains(path) { + //TODO: should this be recursive? + match watcher.watch(path, notify::RecursiveMode::NonRecursive) { + Ok(()) => { + log::debug!("watching {:?}", path); + } + Err(err) => { + log::debug!("failed to watch {:?}: {}", path, err); + } + } + } + } + + self.watcher_opt = Some((watcher, new_paths)); + } + + //TODO: should any of this run in a command? + Command::none() + } + fn operations(&self) -> Element { let mut children = Vec::new(); @@ -463,6 +535,7 @@ impl Application for App { pending_operations: BTreeMap::new(), complete_operations: BTreeMap::new(), failed_operations: BTreeMap::new(), + watcher_opt: None, }; let mut commands = Vec::new(); @@ -603,6 +676,44 @@ impl Application for App { Message::NewFolder(entity_opt) => { log::warn!("TODO: NEW FOLDER"); } + Message::NotifyEvent(event) => { + log::debug!("{:?}", event); + + let mut needs_reload = Vec::new(); + for entity in self.tab_model.iter() { + if let Some(tab) = self.tab_model.data::(entity) { + //TODO: support reloading trash, somehow + if let Location::Path(path) = &tab.location { + let mut contains_change = false; + for event_path in event.paths.iter() { + if event_path.starts_with(&path) { + contains_change = true; + break; + } + } + if contains_change { + needs_reload.push((entity, tab.location.clone())); + } + } + } + } + + let mut commands = Vec::with_capacity(needs_reload.len()); + for (entity, location) in needs_reload { + commands.push(self.rescan_tab(entity, location)); + } + return Command::batch(commands); + } + Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take() + { + Some(mut watcher) => { + self.watcher_opt = Some((watcher, HashSet::new())); + return self.update_watcher(); + } + None => { + log::warn!("message did not contain notify watcher"); + } + }, Message::Paste(entity_opt) => { log::warn!("TODO: PASTE"); } @@ -719,7 +830,7 @@ impl Application for App { return window::close(window::Id::MAIN); } - return self.update_title(); + return Command::batch([self.update_title(), self.update_watcher()]); } Message::TabContextAction(entity, action) => { match self.tab_model.data_mut::(entity) { @@ -761,6 +872,7 @@ impl Application for App { self.tab_model.text_set(entity, tab_title); return Command::batch([ self.update_title(), + self.update_watcher(), self.rescan_tab(entity, tab_path), ]); } @@ -894,6 +1006,7 @@ impl Application for App { fn subscription(&self) -> Subscription { struct ConfigSubscription; struct ThemeSubscription; + struct WatcherSubscription; let mut subscriptions = vec![ event::listen_with(|event, _status| match event { @@ -936,6 +1049,68 @@ impl Application for App { } Message::SystemThemeModeChange(update.config) }), + subscription::channel( + TypeId::of::(), + 100, + |mut output| async move { + let watcher_res = { + let mut output = output.clone(); + //TODO: debounce + notify::recommended_watcher( + move |event_res: Result| match event_res { + Ok(event) => { + match &event.kind { + notify::EventKind::Access(_) + | notify::EventKind::Modify( + notify::event::ModifyKind::Metadata(_), + ) => { + // Data not mutated + return; + } + _ => {} + } + + match futures::executor::block_on(async { + output.send(Message::NotifyEvent(event)).await + }) { + Ok(()) => {} + Err(err) => { + log::warn!("failed to send notify event: {:?}", err); + } + } + } + Err(err) => { + log::warn!("failed to watch files: {:?}", err); + } + }, + ) + }; + + match watcher_res { + Ok(watcher) => { + match output + .send(Message::NotifyWatcher(WatcherWrapper { + watcher_opt: Some(watcher), + })) + .await + { + Ok(()) => {} + Err(err) => { + log::warn!("failed to send notify watcher: {:?}", err); + } + } + } + Err(err) => { + log::warn!("failed to create file watcher: {:?}", err); + } + } + + //TODO: how to properly kill this task? + loop { + tokio::time::sleep(time::Duration::new(1, 0)).await; + } + }, + ), ]; for (id, (pending_operation, _)) in self.pending_operations.iter() { diff --git a/src/tab.rs b/src/tab.rs index 0f72dc8..4617711 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -709,7 +709,7 @@ impl Tab { //TODO: allow editing other locations } } - } else { + } else if let Location::Path(_) = &self.location { row = row.push( widget::button(widget::icon::from_name("edit-symbolic").size(16)) .on_press(Message::EditLocation(Some(self.location.clone())))