diff --git a/Cargo.lock b/Cargo.lock index 6c545e5..4c0f753 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1161,6 +1161,7 @@ dependencies = [ "log", "mime_guess", "notify-debouncer-full", + "notify-rust", "once_cell", "open", "paste", @@ -3541,6 +3542,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "mac-notification-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64" +dependencies = [ + "cc", + "dirs-next", + "objc-foundation", + "objc_id", + "time", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -3826,6 +3840,19 @@ dependencies = [ "walkdir", ] +[[package]] +name = "notify-rust" +version = "4.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a1d03b6305ecefdd9c6c60150179bb8d9f0cd4e64bbcad1e41419e7bf5e414" +dependencies = [ + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus 4.4.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4460,6 +4487,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.34.0" @@ -5329,6 +5365,17 @@ version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" +[[package]] +name = "tauri-winrt-notification" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89f5fb70d6f62381f5d9b2ba9008196150b40b75f3068eb24faeddf1c686871" +dependencies = [ + "quick-xml 0.31.0", + "windows 0.56.0", + "windows-version", +] + [[package]] name = "temp-dir" version = "0.1.13" @@ -6221,7 +6268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.34.0", "quote", ] @@ -6609,6 +6656,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-version" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 28d661c..f89032c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ gio = { version = "0.19", optional = true } glob = "0.3" ignore = "0.4" image = "0.24" +notify-rust = "4" once_cell = "1.19" open = "5.0.2" icu_collator = "1.5" diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 26e308b..bf5fee4 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -3,6 +3,7 @@ empty-folder = Empty folder empty-folder-hidden = Empty folder (has hidden items) filesystem = Filesystem home = Home +notification-in-progress = File operations are in progress. trash = Trash undo = Undo diff --git a/src/app.rs b/src/app.rs index e2d0709..39980d8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,7 +11,8 @@ use cosmic::{ keyboard::{Event as KeyEvent, Key, Modifiers}, subscription::{self, Subscription}, widget::scrollable, - window, Alignment, Event, Length, + window::{self, Event as WindowEvent}, + Alignment, Event, Length, }, iced_runtime::clipboard, style, theme, @@ -33,10 +34,11 @@ use std::{ any::TypeId, collections::{BTreeMap, HashMap, HashSet, VecDeque}, env, fmt, fs, + future::pending, num::NonZeroU16, path::PathBuf, process, - sync::Arc, + sync::{Arc, Mutex}, time::{self, Instant}, }; use tokio::sync::mpsc; @@ -214,6 +216,7 @@ pub enum Message { EditLocation(Option), Key(Modifiers, Key), LaunchUrl(String), + MaybeExit, Modifiers(Modifiers), MoveToTrash(Option), MounterItems(MounterKey, MounterItems), @@ -221,6 +224,7 @@ pub enum Message { NavBarContext(Entity), NavMenuAction(NavMenuAction), NewItem(Option, bool), + Notification(Arc>), NotifyEvents(Vec), NotifyWatcher(WatcherWrapper), OpenTerminal(Option), @@ -355,6 +359,7 @@ pub struct App { modifiers: Modifiers, mounters: Mounters, mounter_items: HashMap, + notification_opt: Option>>, pending_operation_id: u64, pending_operations: BTreeMap, complete_operations: BTreeMap, @@ -364,6 +369,7 @@ pub struct App { search_input: String, toasts: widget::toaster::Toasts, watcher_opt: Option<(Debouncer, HashSet)>, + window_id_opt: Option, nav_dnd_hover: Option<(Location, Instant)>, tab_dnd_hover: Option<(Entity, Instant)>, nav_drag_id: DragId, @@ -539,6 +545,30 @@ impl App { self.nav_model = nav_model.build(); } + fn update_notification(&mut self) -> Command { + // Handle closing notification if there are no operations + if self.pending_operations.is_empty() { + if let Some(notification_arc) = self.notification_opt.take() { + return Command::perform( + async move { + tokio::task::spawn_blocking(move || { + //TODO: this is nasty + let notification_mutex = Arc::try_unwrap(notification_arc).unwrap(); + let notification = notification_mutex.into_inner().unwrap(); + notification.close(); + }) + .await + .unwrap(); + message::app(Message::MaybeExit) + }, + |x| x, + ); + } + } + + Command::none() + } + fn update_title(&mut self) -> Command { let window_title = match self.tab_model.text(self.tab_model.active()) { Some(tab_title) => format!("{tab_title} — {}", fl!("cosmic-files")), @@ -981,6 +1011,7 @@ impl Application for App { modifiers: Modifiers::empty(), mounters: mounters(), mounter_items: HashMap::new(), + notification_opt: None, pending_operation_id: 0, pending_operations: BTreeMap::new(), complete_operations: BTreeMap::new(), @@ -990,6 +1021,7 @@ impl Application for App { search_input: String::new(), toasts: widget::toaster::Toasts::new(Message::CloseToast), watcher_opt: None, + window_id_opt: Some(window::Id::MAIN), nav_dnd_hover: None, tab_dnd_hover: None, nav_drag_id: DragId::new(), @@ -1024,6 +1056,10 @@ impl Application for App { (app, Command::batch(commands)) } + fn main_window_id(&self) -> window::Id { + self.window_id_opt.unwrap_or(window::Id::MAIN) + } + fn nav_context_menu( &self, id: widget::nav_bar::Id, @@ -1090,6 +1126,10 @@ impl Application for App { Command::none() } + fn on_app_exit(&mut self) -> Option { + Some(Message::WindowClose) + } + fn on_escape(&mut self) -> Command { let entity = self.tab_model.active(); @@ -1256,6 +1296,12 @@ impl Application for App { } } } + Message::MaybeExit => { + if self.window_id_opt.is_none() && self.pending_operations.is_empty() { + // Exit if window is closed and there are no pending operations + process::exit(0); + } + } Message::LaunchUrl(url) => match open::that_detached(&url) { Ok(()) => {} Err(err) => { @@ -1347,6 +1393,9 @@ impl Application for App { } } } + Message::Notification(notification) => { + self.notification_opt = Some(notification); + } Message::NotifyEvents(events) => { log::debug!("{:?}", events); @@ -1542,7 +1591,7 @@ impl Application for App { } } Message::PendingComplete(id) => { - let mut commands = Vec::new(); + let mut commands = Vec::with_capacity(3); if let Some((op, _)) = self.pending_operations.remove(&id) { if let Some(description) = op.toast() { @@ -1563,6 +1612,8 @@ impl Application for App { self.complete_operations.insert(id, op); } } + // Potentially show a notification + commands.push(self.update_notification()); // Manually rescan any trash tabs after any operation is completed commands.push(self.rescan_trash()); return Command::batch(commands); @@ -1579,6 +1630,7 @@ impl Application for App { if let Some((_, progress)) = self.pending_operations.get_mut(&id) { *progress = new_progress; } + return self.update_notification(); } Message::RescanTrash => { // Update trash icon if empty/full @@ -1946,7 +1998,12 @@ impl Application for App { self.operation(Operation::Restore { paths }); } Message::WindowClose => { - return window::close(window::Id::MAIN); + if let Some(window_id) = self.window_id_opt.take() { + return Command::batch([ + window::close(window_id), + Command::perform(async move { message::app(Message::MaybeExit) }, |x| x), + ]); + } } Message::WindowNew => match env::current_exe() { Ok(exe) => match process::Command::new(&exe).spawn() { @@ -2483,6 +2540,7 @@ impl Application for App { Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { Some(Message::Modifiers(modifiers)) } + Event::Window(_id, WindowEvent::CloseRequested) => Some(Message::WindowClose), _ => None, }), cosmic_config::config_subscription( @@ -2669,6 +2727,45 @@ impl Application for App { ); } + if !self.pending_operations.is_empty() { + //TODO: inhibit suspend/shutdown? + + if self.window_id_opt.is_none() { + struct NotificationSubscription; + subscriptions.push(subscription::channel( + TypeId::of::(), + 1, + move |msg_tx| async move { + let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx)); + tokio::task::spawn_blocking(move || match notify_rust::Notification::new() + .summary(&fl!("notification-in-progress")) + .timeout(notify_rust::Timeout::Never) + .show() + { + Ok(notification) => { + let _ = futures::executor::block_on(async { + msg_tx + .lock() + .await + .send(Message::Notification(Arc::new(Mutex::new( + notification, + )))) + .await + }); + } + Err(err) => { + log::warn!("failed to create notification: {}", err); + } + }) + .await + .unwrap(); + + pending().await + }, + )); + } + } + for (id, (pending_operation, _)) in self.pending_operations.iter() { //TODO: use recipe? let id = *id; diff --git a/src/lib.rs b/src/lib.rs index b69a2b5..6f29c89 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,7 @@ pub fn main() -> Result<(), Box> { let mut settings = Settings::default(); settings = settings.theme(config.app_theme.theme()); settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0)); + settings = settings.exit_on_close(false); let flags = Flags { config_handler, diff --git a/src/tab.rs b/src/tab.rs index 5540509..f80056c 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -2651,7 +2651,11 @@ impl Tab { ))); if count > 0 { - children.push(container(horizontal_rule(1)).padding([0, space_xxxs]).into()); + children.push( + container(horizontal_rule(1)) + .padding([0, space_xxxs]) + .into(), + ); y += 1; }