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",
|
"lexical-sort",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
"log",
|
"log",
|
||||||
|
"notify",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ env_logger = "0.11"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
lexical-sort = "0.3.1"
|
lexical-sort = "0.3.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
notify = "6"
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
serde = { version = "1", features = ["serde_derive"] }
|
serde = { version = "1", features = ["serde_derive"] }
|
||||||
tokio = { version = "1" }
|
tokio = { version = "1" }
|
||||||
|
|
|
||||||
183
src/main.rs
183
src/main.rs
|
|
@ -7,7 +7,7 @@ use cosmic::{
|
||||||
cosmic_theme, executor,
|
cosmic_theme, executor,
|
||||||
iced::{
|
iced::{
|
||||||
event,
|
event,
|
||||||
futures::SinkExt,
|
futures::{self, SinkExt},
|
||||||
keyboard::{Event as KeyEvent, KeyCode, Modifiers},
|
keyboard::{Event as KeyEvent, KeyCode, Modifiers},
|
||||||
subscription::{self, Subscription},
|
subscription::{self, Subscription},
|
||||||
window, Event, Length, Point,
|
window, Event, Length, Point,
|
||||||
|
|
@ -16,9 +16,10 @@ use cosmic::{
|
||||||
widget::{self, segmented_button},
|
widget::{self, segmented_button},
|
||||||
Application, ApplicationExt, Element,
|
Application, ApplicationExt, Element,
|
||||||
};
|
};
|
||||||
|
use notify::Watcher;
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
env, fs, io,
|
env, fs, io,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process, time,
|
process, time,
|
||||||
|
|
@ -179,6 +180,8 @@ pub enum Message {
|
||||||
MoveToTrash(Option<segmented_button::Entity>),
|
MoveToTrash(Option<segmented_button::Entity>),
|
||||||
NewFile(Option<segmented_button::Entity>),
|
NewFile(Option<segmented_button::Entity>),
|
||||||
NewFolder(Option<segmented_button::Entity>),
|
NewFolder(Option<segmented_button::Entity>),
|
||||||
|
NotifyEvent(notify::Event),
|
||||||
|
NotifyWatcher(WatcherWrapper),
|
||||||
Paste(Option<segmented_button::Entity>),
|
Paste(Option<segmented_button::Entity>),
|
||||||
PendingComplete(u64),
|
PendingComplete(u64),
|
||||||
PendingError(u64, String),
|
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.
|
/// The [`App`] stores application-specific state.
|
||||||
pub struct App {
|
pub struct App {
|
||||||
core: Core,
|
core: Core,
|
||||||
|
|
@ -232,6 +252,7 @@ pub struct App {
|
||||||
pending_operations: BTreeMap<u64, (Operation, f32)>,
|
pending_operations: BTreeMap<u64, (Operation, f32)>,
|
||||||
complete_operations: BTreeMap<u64, Operation>,
|
complete_operations: BTreeMap<u64, Operation>,
|
||||||
failed_operations: BTreeMap<u64, (Operation, String)>,
|
failed_operations: BTreeMap<u64, (Operation, String)>,
|
||||||
|
watcher_opt: Option<(notify::RecommendedWatcher, HashSet<PathBuf>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
|
@ -245,7 +266,11 @@ impl App {
|
||||||
.closable()
|
.closable()
|
||||||
.activate()
|
.activate()
|
||||||
.id();
|
.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) {
|
fn operation(&mut self, operation: Operation) {
|
||||||
|
|
@ -305,6 +330,53 @@ impl App {
|
||||||
self.set_window_title(window_title)
|
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> {
|
fn operations(&self) -> Element<Message> {
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
|
@ -463,6 +535,7 @@ impl Application for App {
|
||||||
pending_operations: BTreeMap::new(),
|
pending_operations: BTreeMap::new(),
|
||||||
complete_operations: BTreeMap::new(),
|
complete_operations: BTreeMap::new(),
|
||||||
failed_operations: BTreeMap::new(),
|
failed_operations: BTreeMap::new(),
|
||||||
|
watcher_opt: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut commands = Vec::new();
|
let mut commands = Vec::new();
|
||||||
|
|
@ -603,6 +676,44 @@ impl Application for App {
|
||||||
Message::NewFolder(entity_opt) => {
|
Message::NewFolder(entity_opt) => {
|
||||||
log::warn!("TODO: NEW FOLDER");
|
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) => {
|
Message::Paste(entity_opt) => {
|
||||||
log::warn!("TODO: PASTE");
|
log::warn!("TODO: PASTE");
|
||||||
}
|
}
|
||||||
|
|
@ -719,7 +830,7 @@ impl Application for App {
|
||||||
return window::close(window::Id::MAIN);
|
return window::close(window::Id::MAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.update_title();
|
return Command::batch([self.update_title(), self.update_watcher()]);
|
||||||
}
|
}
|
||||||
Message::TabContextAction(entity, action) => {
|
Message::TabContextAction(entity, action) => {
|
||||||
match self.tab_model.data_mut::<Tab>(entity) {
|
match self.tab_model.data_mut::<Tab>(entity) {
|
||||||
|
|
@ -761,6 +872,7 @@ impl Application for App {
|
||||||
self.tab_model.text_set(entity, tab_title);
|
self.tab_model.text_set(entity, tab_title);
|
||||||
return Command::batch([
|
return Command::batch([
|
||||||
self.update_title(),
|
self.update_title(),
|
||||||
|
self.update_watcher(),
|
||||||
self.rescan_tab(entity, tab_path),
|
self.rescan_tab(entity, tab_path),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -894,6 +1006,7 @@ impl Application for App {
|
||||||
fn subscription(&self) -> Subscription<Self::Message> {
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
struct ConfigSubscription;
|
struct ConfigSubscription;
|
||||||
struct ThemeSubscription;
|
struct ThemeSubscription;
|
||||||
|
struct WatcherSubscription;
|
||||||
|
|
||||||
let mut subscriptions = vec![
|
let mut subscriptions = vec![
|
||||||
event::listen_with(|event, _status| match event {
|
event::listen_with(|event, _status| match event {
|
||||||
|
|
@ -936,6 +1049,68 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
Message::SystemThemeModeChange(update.config)
|
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() {
|
for (id, (pending_operation, _)) in self.pending_operations.iter() {
|
||||||
|
|
|
||||||
|
|
@ -709,7 +709,7 @@ impl Tab {
|
||||||
//TODO: allow editing other locations
|
//TODO: allow editing other locations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if let Location::Path(_) = &self.location {
|
||||||
row = row.push(
|
row = row.push(
|
||||||
widget::button(widget::icon::from_name("edit-symbolic").size(16))
|
widget::button(widget::icon::from_name("edit-symbolic").size(16))
|
||||||
.on_press(Message::EditLocation(Some(self.location.clone())))
|
.on_press(Message::EditLocation(Some(self.location.clone())))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue