Support permanently deleting files and directories using Shift+Del

Add a confirmation dialog to limit risks of data lost.
This commit is contained in:
Tim Dengel 2024-08-12 05:23:04 +02:00 • committed by Gwen Lg
parent 8ced8b0551
commit 1a66d7b184
4 changed files with 97 additions and 1 deletions

View file

@ -83,6 +83,15 @@ browse-store = Browse {$store}
other-apps = Other applications other-apps = Other applications
related-apps = Related 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 Dialog
rename-file = Rename file rename-file = Rename file
rename-folder = Rename folder rename-folder = Rename folder
@ -223,6 +232,14 @@ moved = Moved {$items} {$items ->
[one] item [one] item
*[other] items *[other] items
} from "{$from}" to "{$to}" } 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}" renaming = Renaming "{$from}" to "{$to}"
renamed = Renamed "{$from}" to "{$to}" renamed = Renamed "{$from}" to "{$to}"
restoring = Restoring {$items} {$items -> restoring = Restoring {$items} {$items ->

View file

@ -33,7 +33,7 @@ use cosmic::{
widget::{ widget::{
self, self,
dnd_destination::DragId, dnd_destination::DragId,
horizontal_space, horizontal_space, icon,
menu::{action::MenuAction, key_bind::KeyBind}, menu::{action::MenuAction, key_bind::KeyBind},
segmented_button::{self, Entity}, segmented_button::{self, Entity},
vertical_space, vertical_space,
@ -138,6 +138,7 @@ pub enum Action {
OpenTerminal, OpenTerminal,
OpenWith, OpenWith,
Paste, Paste,
PermanentlyDelete,
Preview, Preview,
Rename, Rename,
RestoreFromTrash, RestoreFromTrash,
@ -205,6 +206,7 @@ impl Action {
Action::OpenTerminal => Message::OpenTerminal(entity_opt), Action::OpenTerminal => Message::OpenTerminal(entity_opt),
Action::OpenWith => Message::OpenWithDialog(entity_opt), Action::OpenWith => Message::OpenWithDialog(entity_opt),
Action::Paste => Message::Paste(entity_opt), Action::Paste => Message::Paste(entity_opt),
Action::PermanentlyDelete => Message::PermanentlyDelete(entity_opt),
Action::Preview => Message::Preview(entity_opt), Action::Preview => Message::Preview(entity_opt),
Action::Rename => Message::Rename(entity_opt), Action::Rename => Message::Rename(entity_opt),
Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt),
@ -346,6 +348,7 @@ pub enum Message {
PendingError(u64, OperationError), PendingError(u64, OperationError),
PendingPause(u64, bool), PendingPause(u64, bool),
PendingPauseAll(bool), PendingPauseAll(bool),
PermanentlyDelete(Option<Entity>),
Preview(Option<Entity>), Preview(Option<Entity>),
RescanTrash, RescanTrash,
Rename(Option<Entity>), Rename(Option<Entity>),
@ -481,6 +484,9 @@ pub enum DialogPage {
selected: usize, selected: usize,
store_opt: Option<MimeApp>, store_opt: Option<MimeApp>,
}, },
PermanentlyDelete {
paths: Vec<PathBuf>,
},
RenameItem { RenameItem {
from: PathBuf, from: PathBuf,
parent: PathBuf, parent: PathBuf,
@ -2461,6 +2467,9 @@ impl Application for App {
} }
} }
} }
DialogPage::PermanentlyDelete { paths } => {
return self.operation(Operation::PermanentlyDelete { paths });
}
DialogPage::RenameItem { DialogPage::RenameItem {
from, parent, name, .. 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) => { Message::Preview(entity_opt) => {
match self.mode { match self.mode {
Mode::App => { Mode::App => {
@ -4655,6 +4671,35 @@ impl Application for App {
dialog 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 { DialogPage::RenameItem {
from, from,
parent, parent,

View file

@ -68,6 +68,7 @@ pub fn key_binds(mode: &tab::Mode) -> HashMap<KeyBind, Action> {
bind!([Ctrl], Key::Character("c".into()), Copy); bind!([Ctrl], Key::Character("c".into()), Copy);
bind!([Ctrl], Key::Character("x".into()), Cut); bind!([Ctrl], Key::Character("x".into()), Cut);
bind!([], Key::Named(Named::Delete), Delete); bind!([], Key::Named(Named::Delete), Delete);
bind!([Shift], Key::Named(Named::Delete), PermanentlyDelete);
bind!([Shift], Key::Named(Named::Enter), OpenInNewWindow); bind!([Shift], Key::Named(Named::Enter), OpenInNewWindow);
bind!([Ctrl], Key::Character("v".into()), Paste); bind!([Ctrl], Key::Character("v".into()), Paste);
bind!([], Key::Named(Named::F2), Rename); bind!([], Key::Named(Named::F2), Rename);

View file

@ -490,6 +490,10 @@ pub enum Operation {
NewFolder { NewFolder {
path: PathBuf, path: PathBuf,
}, },
/// Permanently delete items, skipping the trash
PermanentlyDelete {
paths: Vec<PathBuf>,
},
Rename { Rename {
from: PathBuf, from: PathBuf,
to: PathBuf, to: PathBuf,
@ -593,6 +597,7 @@ impl Operation {
name = file_name(path), name = file_name(path),
parent = parent_name(path) parent = parent_name(path)
), ),
Self::PermanentlyDelete { paths } => fl!("permanently-deleting", items = paths.len()),
Self::Rename { from, to } => { Self::Rename { from, to } => {
fl!("renaming", from = file_name(from), to = file_name(to)) fl!("renaming", from = file_name(from), to = file_name(to))
} }
@ -651,6 +656,7 @@ impl Operation {
name = file_name(path), name = file_name(path),
parent = parent_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::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)),
Self::Restore { items } => fl!("restored", items = items.len()), Self::Restore { items } => fl!("restored", items = items.len()),
Self::SetExecutableAndLaunch { path } => { Self::SetExecutableAndLaunch { path } => {
@ -669,6 +675,7 @@ impl Operation {
| Self::EmptyTrash | Self::EmptyTrash
| Self::Extract { .. } | Self::Extract { .. }
| Self::Move { .. } | Self::Move { .. }
| Self::PermanentlyDelete { .. }
| Self::Restore { .. } => true, | Self::Restore { .. } => true,
Self::NewFile { .. } Self::NewFile { .. }
| Self::NewFolder { .. } | Self::NewFolder { .. }
@ -1054,6 +1061,32 @@ impl Operation {
.await .await
.map_err(wrap_compio_spawn_error)? .map_err(wrap_compio_spawn_error)?
.map_err(OperationError::from_str), .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 { Self::Rename { from, to } => compio::runtime::spawn(async move {
controller.check().await.map_err(OperationError::from_str)?; controller.check().await.map_err(OperationError::from_str)?;
compio::fs::rename(&from, &to) compio::fs::rename(&from, &to)