From 1a66d7b184045c664c5a87ce3ed511e0cb3cc10c Mon Sep 17 00:00:00 2001 From: Tim Dengel Date: Mon, 12 Aug 2024 05:23:04 +0200 Subject: [PATCH 1/7] Support permanently deleting files and directories using Shift+Del Add a confirmation dialog to limit risks of data lost. --- i18n/en/cosmic_files.ftl | 17 +++++++++++++++ src/app.rs | 47 +++++++++++++++++++++++++++++++++++++++- src/key_bind.rs | 1 + src/operation/mod.rs | 33 ++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index f31de44..3841ecb 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -83,6 +83,15 @@ browse-store = Browse {$store} other-apps = Other applications related-apps = Related applications +## Permanently delete Dialog +selected-items = the {$items} selected items +permanently-delete-question = Permanently delete? +delete = Delete +permanently-delete-warning = Permanently delete {$target}, {$nb_items -> + [one] he + *[other] they + } can not be restored + ## Rename Dialog rename-file = Rename file rename-folder = Rename folder @@ -223,6 +232,14 @@ moved = Moved {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" +permanently-deleting = Permanently deleting "{$items}" "{$items -> + [one] item + *[other] items + }" +permanently-deleted = Permanently deleted "{$items}" "{$items -> + [one] item + *[other] items + }" renaming = Renaming "{$from}" to "{$to}" renamed = Renamed "{$from}" to "{$to}" restoring = Restoring {$items} {$items -> diff --git a/src/app.rs b/src/app.rs index cace7ee..276383e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,7 +33,7 @@ use cosmic::{ widget::{ self, dnd_destination::DragId, - horizontal_space, + horizontal_space, icon, menu::{action::MenuAction, key_bind::KeyBind}, segmented_button::{self, Entity}, vertical_space, @@ -138,6 +138,7 @@ pub enum Action { OpenTerminal, OpenWith, Paste, + PermanentlyDelete, Preview, Rename, RestoreFromTrash, @@ -205,6 +206,7 @@ impl Action { Action::OpenTerminal => Message::OpenTerminal(entity_opt), Action::OpenWith => Message::OpenWithDialog(entity_opt), Action::Paste => Message::Paste(entity_opt), + Action::PermanentlyDelete => Message::PermanentlyDelete(entity_opt), Action::Preview => Message::Preview(entity_opt), Action::Rename => Message::Rename(entity_opt), Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), @@ -346,6 +348,7 @@ pub enum Message { PendingError(u64, OperationError), PendingPause(u64, bool), PendingPauseAll(bool), + PermanentlyDelete(Option), Preview(Option), RescanTrash, Rename(Option), @@ -481,6 +484,9 @@ pub enum DialogPage { selected: usize, store_opt: Option, }, + PermanentlyDelete { + paths: Vec, + }, RenameItem { from: PathBuf, parent: PathBuf, @@ -2461,6 +2467,9 @@ impl Application for App { } } } + DialogPage::PermanentlyDelete { paths } => { + return self.operation(Operation::PermanentlyDelete { paths }); + } DialogPage::RenameItem { from, parent, name, .. } => { @@ -3137,6 +3146,13 @@ impl Application for App { } } } + Message::PermanentlyDelete(entity_opt) => { + let paths = self.selected_paths(entity_opt); + if !paths.is_empty() { + self.dialog_pages + .push_back(DialogPage::PermanentlyDelete { paths }); + } + } Message::Preview(entity_opt) => { match self.mode { Mode::App => { @@ -4655,6 +4671,35 @@ impl Application for App { dialog } + DialogPage::PermanentlyDelete { paths } => { + let target = if paths.len() == 1 { + format!( + "« {} »", + paths[0] + .file_name() + .map(std::ffi::OsStr::to_string_lossy) + .unwrap_or_else(|| paths[0].to_string_lossy()) + ) + } else { + fl!("selected-items", items = paths.len()) + }; + + widget::dialog() + .title(fl!("permanently-delete-question")) + .icon(icon::from_name("dialog-warning").size(32)) + .primary_action( + widget::button::destructive(fl!("delete")) + .on_press(Message::DialogComplete), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ) + .control(widget::text(fl!( + "permanently-delete-warning", + nb_items = paths.len(), + target = target + ))) + } DialogPage::RenameItem { from, parent, diff --git a/src/key_bind.rs b/src/key_bind.rs index b139887..e0b528e 100644 --- a/src/key_bind.rs +++ b/src/key_bind.rs @@ -68,6 +68,7 @@ pub fn key_binds(mode: &tab::Mode) -> HashMap { bind!([Ctrl], Key::Character("c".into()), Copy); bind!([Ctrl], Key::Character("x".into()), Cut); bind!([], Key::Named(Named::Delete), Delete); + bind!([Shift], Key::Named(Named::Delete), PermanentlyDelete); bind!([Shift], Key::Named(Named::Enter), OpenInNewWindow); bind!([Ctrl], Key::Character("v".into()), Paste); bind!([], Key::Named(Named::F2), Rename); diff --git a/src/operation/mod.rs b/src/operation/mod.rs index 260d385..3ea4371 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -490,6 +490,10 @@ pub enum Operation { NewFolder { path: PathBuf, }, + /// Permanently delete items, skipping the trash + PermanentlyDelete { + paths: Vec, + }, Rename { from: PathBuf, to: PathBuf, @@ -593,6 +597,7 @@ impl Operation { name = file_name(path), parent = parent_name(path) ), + Self::PermanentlyDelete { paths } => fl!("permanently-deleting", items = paths.len()), Self::Rename { from, to } => { fl!("renaming", from = file_name(from), to = file_name(to)) } @@ -651,6 +656,7 @@ impl Operation { name = file_name(path), parent = parent_name(path) ), + Self::PermanentlyDelete { paths } => fl!("permanently-deleted", items = paths.len()), Self::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)), Self::Restore { items } => fl!("restored", items = items.len()), Self::SetExecutableAndLaunch { path } => { @@ -669,6 +675,7 @@ impl Operation { | Self::EmptyTrash | Self::Extract { .. } | Self::Move { .. } + | Self::PermanentlyDelete { .. } | Self::Restore { .. } => true, Self::NewFile { .. } | Self::NewFolder { .. } @@ -1054,6 +1061,32 @@ impl Operation { .await .map_err(wrap_compio_spawn_error)? .map_err(OperationError::from_str), + Self::PermanentlyDelete { paths } => { + let total = paths.len(); + for (idx, path) in paths.into_iter().enumerate() { + controller.check().await.map_err(OperationError::from_str)?; + + controller.set_progress((idx as f32) / (total as f32)); + + tokio::task::spawn_blocking(|| { + if path.is_symlink() || path.is_file() { + fs::remove_file(path) + } else if path.is_dir() { + fs::remove_dir_all(path) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "File to delete is not symlink, file or directory", + )) + } + }) + .await + .map_err(OperationError::from_str)? + .map_err(OperationError::from_str)?; + } + + Ok(OperationSelection::default()) + } Self::Rename { from, to } => compio::runtime::spawn(async move { controller.check().await.map_err(OperationError::from_str)?; compio::fs::rename(&from, &to) From d8a198e8367140e90997d44ca00a9f6f5196042e Mon Sep 17 00:00:00 2001 From: Gwen Lg Date: Wed, 26 Mar 2025 00:20:50 +0100 Subject: [PATCH 2/7] clean: rename `Modifiers` message to `ModifiersChanged` to be more explicit/accurate. --- src/app.rs | 6 +++--- src/dialog.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 276383e..ee21a03 100644 --- a/src/app.rs +++ b/src/app.rs @@ -315,7 +315,7 @@ pub enum Message { Key(Modifiers, Key, Option), LaunchUrl(String), MaybeExit, - Modifiers(Modifiers), + ModifiersChanged(Modifiers), MounterItems(MounterKey, MounterItems), MountResult(MounterKey, MounterItem, Result), NavBarClose(Entity), @@ -2627,7 +2627,7 @@ impl Application for App { log::warn!("failed to open {:?}: {}", url, err); } }, - Message::Modifiers(modifiers) => { + Message::ModifiersChanged(modifiers) => { self.modifiers = modifiers; } Message::MounterItems(mounter_key, mounter_items) => { @@ -5187,7 +5187,7 @@ impl Application for App { event::Status::Captured => None, }, Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { - Some(Message::Modifiers(modifiers)) + Some(Message::ModifiersChanged(modifiers)) } Event::Window(WindowEvent::Unfocused) => Some(Message::WindowUnfocus), #[cfg(all(feature = "desktop", feature = "wayland"))] diff --git a/src/dialog.rs b/src/dialog.rs index e226dea..e8b2925 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -382,7 +382,7 @@ enum Message { Filename(String), Filter(usize), Key(Modifiers, Key), - Modifiers(Modifiers), + ModifiersChanged(Modifiers), MounterItems(MounterKey, MounterItems), NewFolder, NotifyEvents(Vec), @@ -1290,7 +1290,7 @@ impl Application for App { } } } - Message::Modifiers(modifiers) => { + Message::ModifiersChanged(modifiers) => { self.modifiers = modifiers; } Message::MounterItems(mounter_key, mounter_items) => { @@ -1757,7 +1757,7 @@ impl Application for App { event::Status::Captured => None, }, Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { - Some(Message::Modifiers(modifiers)) + Some(Message::ModifiersChanged(modifiers)) } Event::Mouse(mouse::Event::CursorMoved { position: pos }) => { Some(Message::CursorMoved(pos)) From e2202689542f934f1e06c6ff9b5841d0c6853d2c Mon Sep 17 00:00:00 2001 From: Gwen Lg Date: Wed, 26 Mar 2025 00:23:41 +0100 Subject: [PATCH 3/7] Add ModifiersChanged message on Tab and keep value The message is forwarded from app/dialog Message, and used to keep the value of the status of modifiers in Tab. --- src/app.rs | 5 +++++ src/dialog.rs | 3 +++ src/tab.rs | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/src/app.rs b/src/app.rs index ee21a03..db22858 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2629,6 +2629,11 @@ impl Application for App { }, Message::ModifiersChanged(modifiers) => { self.modifiers = modifiers; + let entity = self.tab_model.active(); + return self.update(Message::TabMessage( + Some(entity), + tab::Message::ModifiersChanged(modifiers), + )); } Message::MounterItems(mounter_key, mounter_items) => { // Check for unmounted folders diff --git a/src/dialog.rs b/src/dialog.rs index e8b2925..442d4cc 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -1292,6 +1292,9 @@ impl Application for App { } Message::ModifiersChanged(modifiers) => { self.modifiers = modifiers; + return self.update(Message::TabMessage(tab::Message::ModifiersChanged( + modifiers, + ))); } Message::MounterItems(mounter_key, mounter_items) => { // Check for unmounted folders diff --git a/src/tab.rs b/src/tab.rs index f7ccdd8..40c8901 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1257,6 +1257,7 @@ pub enum Message { ItemUp, Location(Location), LocationUp, + ModifiersChanged(Modifiers), Open(Option), RightClick(Option), MiddleClick(usize), @@ -1898,6 +1899,7 @@ pub struct Tab { select_range: Option<(usize, usize)>, clicked: Option, selected_clicked: bool, + modifiers: Modifiers, last_right_click: Option, search_context: Option, global_cursor_position: Option, @@ -1994,6 +1996,7 @@ impl Tab { clicked: None, dnd_hovered: None, selected_clicked: false, + modifiers: Modifiers::default(), last_right_click: None, search_context: None, global_cursor_position: None, @@ -3034,6 +3037,9 @@ impl Tab { } } } + Message::ModifiersChanged(modifiers) => { + self.modifiers = modifiers; + } Message::Open(path_opt) => { match path_opt { Some(path) => { From 95aba7c74e484dc6ac7576f21bce6bb504938644 Mon Sep 17 00:00:00 2001 From: Gwen Lg Date: Mon, 12 Aug 2024 05:34:07 +0200 Subject: [PATCH 4/7] Add context menu management of permanently deleting files and folders The action replace move-to-trash when shift modifier is active, like on other desktop environement. Use modifiers value stored in Tab struct as needed to forward to context_menu creation. Started from work of Tim Dengel --- src/menu.rs | 11 +++++++++-- src/tab.rs | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/menu.rs b/src/menu.rs index 4182e83..7c7c99f 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -2,7 +2,7 @@ use cosmic::{ app::Core, - iced::{Alignment, Background, Border, Length}, + iced::{keyboard::Modifiers, Alignment, Background, Border, Length}, theme, widget::{ self, button, column, container, divider, horizontal_space, @@ -55,6 +55,7 @@ fn menu_button_optional( pub fn context_menu<'a>( tab: &Tab, key_binds: &HashMap, + modifiers: &Modifiers, ) -> Element<'a, tab::Message> { let find_key = |action: &Action| -> String { for (key_bind, key_action) in key_binds.iter() { @@ -216,7 +217,13 @@ pub fn context_menu<'a>( children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into()); } children.push(divider::horizontal::light().into()); - children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); + if modifiers.shift() && !modifiers.control() { + children.push( + menu_item(fl!("delete-permanently"), Action::PermanentlyDelete).into(), + ); + } else { + children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); + } } else { //TODO: need better designs for menu with no selection //TODO: have things like properties but they apply to the folder? diff --git a/src/tab.rs b/src/tab.rs index 40c8901..825b247 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -4855,10 +4855,12 @@ impl Tab { let mut popover = widget::popover(mouse_area); if let Some(point) = self.context_menu { + let context_menu = menu::context_menu(self, key_binds, &self.modifiers); popover = popover - .popup(menu::context_menu(self, key_binds)) + .popup(context_menu) .position(widget::popover::Position::Point(point)); } + let mut tab_column = widget::column::with_capacity(3); if let Some(location_view) = location_view_opt { tab_column = tab_column.push(location_view); From 160ef5f41416ea924803915225fc1b71cc558b66 Mon Sep 17 00:00:00 2001 From: Gwen Lg Date: Wed, 26 Mar 2025 18:50:25 +0100 Subject: [PATCH 5/7] feat: include permanent delete in menu_bar --- src/app.rs | 1 + src/menu.rs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index db22858..c8baa50 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5001,6 +5001,7 @@ impl Application for App { &self.core, self.tab_model.active_data::(), &self.config, + &self.modifiers, &self.key_binds, )] } diff --git a/src/menu.rs b/src/menu.rs index 7c7c99f..af53dd1 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -499,6 +499,7 @@ pub fn menu_bar<'a>( core: &Core, tab_opt: Option<&Tab>, config: &Config, + modifiers: &Modifiers, key_binds: &HashMap, ) -> Element<'a, Message> { let sort_options = tab_opt.map(|tab| tab.sort_options()); @@ -531,6 +532,12 @@ pub fn menu_bar<'a>( } }; + let (delete_item, delete_item_action) = if in_trash || modifiers.shift() { + (fl!("delete-permanently"), Action::Delete) + } else { + (fl!("move-to-trash"), Action::Delete) + }; + responsive_menu_bar() .item_height(ItemHeight::Dynamic(40)) .item_width(ItemWidth::Uniform(360)) @@ -569,14 +576,11 @@ pub fn menu_bar<'a>( ), menu::Item::Divider, menu_button_optional( - if in_trash { - fl!("delete-permanently") - } else { - fl!("move-to-trash") - }, - Action::Delete, - selected > 0, + fl!("restore-from-trash"), + Action::RestoreFromTrash, + selected > 0 && in_trash, ), + menu_button_optional(delete_item, delete_item_action, selected > 0), menu::Item::Divider, menu::Item::Button(fl!("close-tab"), None, Action::TabClose), menu::Item::Button(fl!("quit"), None, Action::WindowClose), From 8d313e7c1193effa029a8611fb895e4afb9cf44a Mon Sep 17 00:00:00 2001 From: Gwen Lg Date: Wed, 26 Mar 2025 01:46:50 +0100 Subject: [PATCH 6/7] Add `fr` translation for permanent delete --- i18n/fr/cosmic_files.ftl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/i18n/fr/cosmic_files.ftl b/i18n/fr/cosmic_files.ftl index 824a03a..94943bf 100644 --- a/i18n/fr/cosmic_files.ftl +++ b/i18n/fr/cosmic_files.ftl @@ -81,6 +81,15 @@ save-file = Enregistrer fichier open-with-title = Comment souhaitez-vous ouvrir "{$name}" ? browse-store = Parcourir {$store} +## Permanently delete Dialog +selected-items = les {$items} éléments sélectionnés +permanently-delete-question = Supprimer définitivement ? +delete = Supprimer +permanently-delete-warning = Supprimer définitivement {$target}, {$nb_items -> + [one] il ne pourras pas être restauré. + *[other] ils ne pourront pas être restaurés. +} + ## Rename Dialog rename-file = Renommer le fichier rename-folder = Renommer le dossier @@ -221,6 +230,14 @@ moved = {$items} {$items -> [one] élément déplacé *[other] éléments déplacés } depuis {$from} vers {$to} +permanently-deleting = Suppression définitive de "{$items}" "{$items -> + [one] item + *[other] items + }" +permanently-deleted = Supprimés définitivement "{$items}" "{$items -> + [one] item + *[other] items + }" renaming = Renommage de {$from} en {$to} renamed = {$from} renommé en {$to} restoring = Restauration de {$items} {$items -> From caa3d54e832c1924ec98fe0d8a8783b277262db3 Mon Sep 17 00:00:00 2001 From: Tim Dengel Date: Mon, 12 Aug 2024 05:23:04 +0200 Subject: [PATCH 7/7] Add `de` translation for permanently delete dialog --- i18n/de/cosmic_files.ftl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/i18n/de/cosmic_files.ftl b/i18n/de/cosmic_files.ftl index 640904b..1d32d3b 100644 --- a/i18n/de/cosmic_files.ftl +++ b/i18n/de/cosmic_files.ftl @@ -79,6 +79,15 @@ save-file = Datei speichern open-with-title = Wie möchtest du „{$name}“ öffnen? browse-store = {$store} durchsuchen +## Dauerhaft Löschen Dialog +selected-items = {$items} gewählte Objekte +permanently-delete-question = {$target} dauerhaft löschen? +delete = Löschen +permanently-delete-warning = {$target} dauerhaft löschen, {$nb_items -> + [one] es kann + *[other] Sie können + } nicht wiederhergestellt werden. + # Umbenennen-Dialog rename-file = Datei umbenennen rename-folder = Ordner umbenennen @@ -212,6 +221,14 @@ restored = {$items} {$items -> *[other] Elemente wurden } aus dem {trash} wiederhergestellt unknown-folder = unbekannter Ordner +permanently-deleting = Lösche {$items} {$items -> + [one] Objekt + *[other] Objekte + } dauerhaft +permanently-deleted = {$items} {$items -> + [one] Objekt + *[other] Objekte + } dauerhaft gelöscht ## Öffnen mit menu-open-with = Öffnen mit @@ -229,6 +246,7 @@ calculating = Wird berechnet... ## Einstellungen settings = Einstellungen +settings-show-delete-permanently = Dauerhaft löschen ### Aussehen appearance = Aussehen @@ -245,6 +263,7 @@ new-file = Neue Datei new-folder = Neuer Ordner open-in-terminal = Im Terminal öffnen move-to-trash = In den Papierkorb verschieben +delete-permanently = Dauerhaft löschen... restore-from-trash = Aus dem Papierkorb wiederherstellen remove-from-sidebar = Von der Seitenleiste entfernen sort-by-name = Nach Name sortieren