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)