Add operation to permanently delete trash items, fixes #841

This commit is contained in:
Jeremy Soller 2025-03-03 13:44:06 -07:00
parent 3cce822ffc
commit c8aa80fb2f
No known key found for this signature in database
GPG key ID: 670FDFB5428E05CA
6 changed files with 101 additions and 18 deletions

View file

@ -181,6 +181,14 @@ copied = Copied {$items} {$items ->
[one] item
*[other] items
} from "{$from}" to "{$to}"
deleting = Deleting {$items} {$items ->
[one] item
*[other] items
} from {trash} ({$progress})...
deleted = Deleted {$items} {$items ->
[one] item
*[other] items
} from {trash}
emptying-trash = Emptying {trash} ({$progress})...
emptied-trash = Emptied {trash}
extracting = Extracting {$items} {$items ->
@ -240,6 +248,7 @@ light = Light
# Context menu
add-to-sidebar = Add to sidebar
compress = Compress
delete-permanently = Delete permanently
extract-here = Extract
new-file = New file...
new-folder = New folder...

View file

@ -100,6 +100,7 @@ pub enum Action {
CosmicSettingsDisplays,
CosmicSettingsWallpaper,
DesktopViewOptions,
Delete,
EditHistory,
EditLocation,
EmptyTrash,
@ -114,7 +115,6 @@ pub enum Action {
ItemRight,
ItemUp,
LocationUp,
MoveToTrash,
NewFile,
NewFolder,
Open,
@ -161,6 +161,7 @@ impl Action {
Action::CosmicSettingsAppearance => Message::CosmicSettings("appearance"),
Action::CosmicSettingsDisplays => Message::CosmicSettings("displays"),
Action::CosmicSettingsWallpaper => Message::CosmicSettings("wallpaper"),
Action::Delete => Message::Delete(entity_opt),
Action::DesktopViewOptions => Message::DesktopViewOptions,
Action::EditHistory => Message::ToggleContextPage(ContextPage::EditHistory),
Action::EditLocation => {
@ -180,7 +181,6 @@ impl Action {
Action::ItemRight => Message::TabMessage(entity_opt, tab::Message::ItemRight),
Action::ItemUp => Message::TabMessage(entity_opt, tab::Message::ItemUp),
Action::LocationUp => Message::TabMessage(entity_opt, tab::Message::LocationUp),
Action::MoveToTrash => Message::MoveToTrash(entity_opt),
Action::NewFile => Message::NewItem(entity_opt, false),
Action::NewFolder => Message::NewItem(entity_opt, true),
Action::Open => Message::TabMessage(entity_opt, tab::Message::Open(None)),
@ -281,6 +281,7 @@ pub enum Message {
CosmicSettings(&'static str),
CursorMoved(Point),
Cut(Option<Entity>),
Delete(Option<Entity>),
DesktopConfig(DesktopConfig),
DesktopViewOptions,
DialogCancel,
@ -295,7 +296,6 @@ pub enum Message {
LaunchUrl(String),
MaybeExit,
Modifiers(Modifiers),
MoveToTrash(Option<Entity>),
MounterItems(MounterKey, MounterItems),
MountResult(MounterKey, MounterItem, Result<bool, String>),
NavBarClose(Entity),
@ -1966,6 +1966,39 @@ impl Application for App {
}
}
}
Message::Delete(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
match &tab.location {
Location::Trash => {
if let Some(items) = tab.items_opt() {
let mut trash_items = Vec::new();
for item in items.iter() {
if item.selected {
match &item.metadata {
ItemMetadata::Trash { entry, .. } => {
trash_items.push(entry.clone());
}
_ => {
//TODO: error on trying to permanently delete non-trash file?
}
}
}
}
if !trash_items.is_empty() {
self.operation(Operation::DeleteTrash { items: trash_items });
}
}
}
_ => {
let paths = dbg!(self.selected_paths(entity_opt));
if !paths.is_empty() {
self.operation(Operation::Delete { paths });
}
}
}
}
}
Message::DesktopConfig(config) => {
if config != self.config.desktop {
config_set!(desktop, config);
@ -2177,12 +2210,6 @@ impl Application for App {
Message::Modifiers(modifiers) => {
self.modifiers = modifiers;
}
Message::MoveToTrash(entity_opt) => {
let paths = self.selected_paths(entity_opt);
if !paths.is_empty() {
self.operation(Operation::Delete { paths });
}
}
Message::MounterItems(mounter_key, mounter_items) => {
// Check for unmounted folders
let mut unmounted = Vec::new();
@ -2972,6 +2999,9 @@ impl Application for App {
self.update_tab(entity, tab_path, selection_paths),
]));
}
tab::Command::Delete(paths) => {
self.operation(Operation::Delete { paths });
}
tab::Command::DropFiles(to, from) => {
commands.push(self.update(Message::PasteContents(to, from)));
}
@ -2989,9 +3019,6 @@ impl Application for App {
}),
);
}
tab::Command::MoveToTrash(paths) => {
self.operation(Operation::Delete { paths });
}
tab::Command::OpenFile(path) => self.open_file(&path),
tab::Command::OpenInNewTab(path) => {
commands.push(self.open_tab(Location::Path(path.clone()), false, None));

View file

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

View file

@ -151,7 +151,7 @@ pub fn context_menu<'a>(
children.push(menu_item(fl!("cut"), Action::Cut).into());
children.push(menu_item(fl!("copy"), Action::Copy).into());
// Should this simply bypass trash and remove the shortcut?
children.push(menu_item(fl!("move-to-trash"), Action::MoveToTrash).into());
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
} else if selected > 0 {
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
children.push(menu_item(fl!("open"), Action::Open).into());
@ -211,7 +211,7 @@ 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::MoveToTrash).into());
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?
@ -311,6 +311,8 @@ pub fn context_menu<'a>(
children.push(divider::horizontal::light().into());
children
.push(menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into());
children.push(divider::horizontal::light().into());
children.push(menu_item(fl!("delete-permanently"), Action::Delete).into());
} else {
// TODO: Nested menu
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
@ -537,7 +539,15 @@ pub fn menu_bar<'a>(
menu::Item::Divider,
menu_button_optional(fl!("add-to-sidebar"), Action::AddToSidebar, selected > 0),
menu::Item::Divider,
menu_button_optional(fl!("move-to-trash"), Action::MoveToTrash, selected > 0),
menu_button_optional(
if in_trash {
fl!("delete-permanently")
} else {
fl!("move-to-trash")
},
Action::Delete,
selected > 0,
),
menu::Item::Divider,
menu::Item::Button(fl!("close-tab"), None, Action::TabClose),
menu::Item::Button(fl!("quit"), None, Action::WindowClose),

View file

@ -460,6 +460,10 @@ pub enum Operation {
Delete {
paths: Vec<PathBuf>,
},
/// Delete a path from the trash
DeleteTrash {
items: Vec<trash::TrashItem>,
},
/// Empty the trash
EmptyTrash,
/// Uncompress files
@ -550,6 +554,9 @@ impl Operation {
to = fl!("trash"),
progress = progress()
),
Self::DeleteTrash { items } => {
fl!("deleting", items = items.len(), progress = progress())
}
Self::EmptyTrash => fl!("emptying-trash", progress = progress()),
Self::Extract {
paths,
@ -609,6 +616,7 @@ impl Operation {
from = paths_parent_name(paths),
to = fl!("trash")
),
Self::DeleteTrash { items } => fl!("deleted", items = items.len()),
Self::EmptyTrash => fl!("emptied-trash"),
Self::Extract {
paths,
@ -650,6 +658,7 @@ impl Operation {
Self::Compress { .. }
| Self::Copy { .. }
| Self::Delete { .. }
| Self::DeleteTrash { .. }
| Self::EmptyTrash
| Self::Extract { .. }
| Self::Move { .. }
@ -846,6 +855,34 @@ impl Operation {
}
Ok(OperationSelection::default())
}
Self::DeleteTrash { items } => {
#[cfg(any(
target_os = "windows",
all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
)
))]
{
tokio::task::spawn_blocking(move || -> Result<(), OperationError> {
let count = items.len();
for (i, item) in items.into_iter().enumerate() {
controller.check().map_err(OperationError::from_str)?;
controller.set_progress(i as f32 / count as f32);
trash::os_limited::purge_all([item])
.map_err(OperationError::from_str)?;
}
Ok(())
})
.await
.map_err(OperationError::from_str)??;
}
Ok(OperationSelection::default())
}
Self::EmptyTrash => {
#[cfg(any(
target_os = "windows",

View file

@ -1127,12 +1127,12 @@ pub enum Command {
AddToSidebar(PathBuf),
AutoScroll(Option<f32>),
ChangeLocation(String, Location, Option<Vec<PathBuf>>),
Delete(Vec<PathBuf>),
DropFiles(PathBuf, ClipboardPaste),
EmptyTrash,
#[cfg(feature = "desktop")]
ExecEntryAction(cosmic::desktop::DesktopEntryData, usize),
Iced(TaskWrapper),
MoveToTrash(Vec<PathBuf>),
OpenFile(PathBuf),
OpenInNewTab(PathBuf),
OpenInNewWindow(PathBuf),
@ -3188,7 +3188,7 @@ impl Tab {
commands.push(Command::DropFiles(to, from))
}
Location::Trash if matches!(from.kind, ClipboardKind::Cut) => {
commands.push(Command::MoveToTrash(from.paths))
commands.push(Command::Delete(from.paths))
}
_ => {
log::warn!("{:?} to {:?} is not supported.", from.kind, to);