Watch folders for changes
This commit is contained in:
parent
12a2a39a9f
commit
12d087abd9
4 changed files with 182 additions and 5 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1098,6 +1098,7 @@ dependencies = [
|
|||
"lexical-sort",
|
||||
"libcosmic",
|
||||
"log",
|
||||
"notify",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"rust-embed",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
183
src/main.rs
183
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<segmented_button::Entity>),
|
||||
NewFile(Option<segmented_button::Entity>),
|
||||
NewFolder(Option<segmented_button::Entity>),
|
||||
NotifyEvent(notify::Event),
|
||||
NotifyWatcher(WatcherWrapper),
|
||||
Paste(Option<segmented_button::Entity>),
|
||||
PendingComplete(u64),
|
||||
PendingError(u64, String),
|
||||
|
|
@ -217,6 +220,23 @@ impl ContextPage {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WatcherWrapper {
|
||||
watcher_opt: Option<notify::RecommendedWatcher>,
|
||||
}
|
||||
|
||||
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<u64, (Operation, f32)>,
|
||||
complete_operations: BTreeMap<u64, Operation>,
|
||||
failed_operations: BTreeMap<u64, (Operation, String)>,
|
||||
watcher_opt: Option<(notify::RecommendedWatcher, HashSet<PathBuf>)>,
|
||||
}
|
||||
|
||||
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<Message> {
|
||||
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::<Tab>(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<Message> {
|
||||
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::<Tab>(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::<Tab>(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<Self::Message> {
|
||||
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::<WatcherSubscription>(),
|
||||
100,
|
||||
|mut output| async move {
|
||||
let watcher_res = {
|
||||
let mut output = output.clone();
|
||||
//TODO: debounce
|
||||
notify::recommended_watcher(
|
||||
move |event_res: Result<notify::Event, notify::Error>| 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() {
|
||||
|
|
|
|||
|
|
@ -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())))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue