Continue operations in the background if the window is closed

This commit is contained in:
Jeremy Soller 2024-08-09 09:59:25 -06:00
parent 190029aa27
commit da329004aa
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
6 changed files with 166 additions and 6 deletions

58
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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<Entity>),
Key(Modifiers, Key),
LaunchUrl(String),
MaybeExit,
Modifiers(Modifiers),
MoveToTrash(Option<Entity>),
MounterItems(MounterKey, MounterItems),
@ -221,6 +224,7 @@ pub enum Message {
NavBarContext(Entity),
NavMenuAction(NavMenuAction),
NewItem(Option<Entity>, bool),
Notification(Arc<Mutex<notify_rust::NotificationHandle>>),
NotifyEvents(Vec<DebouncedEvent>),
NotifyWatcher(WatcherWrapper),
OpenTerminal(Option<Entity>),
@ -355,6 +359,7 @@ pub struct App {
modifiers: Modifiers,
mounters: Mounters,
mounter_items: HashMap<MounterKey, MounterItems>,
notification_opt: Option<Arc<Mutex<notify_rust::NotificationHandle>>>,
pending_operation_id: u64,
pending_operations: BTreeMap<u64, (Operation, f32)>,
complete_operations: BTreeMap<u64, Operation>,
@ -364,6 +369,7 @@ pub struct App {
search_input: String,
toasts: widget::toaster::Toasts<Message>,
watcher_opt: Option<(Debouncer<RecommendedWatcher, FileIdMap>, HashSet<PathBuf>)>,
window_id_opt: Option<window::Id>,
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<Message> {
// 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<Message> {
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<Message> {
Some(Message::WindowClose)
}
fn on_escape(&mut self) -> Command<Self::Message> {
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::<NotificationSubscription>(),
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;

View file

@ -72,6 +72,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
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,

View file

@ -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;
}