Refactor trash handling to improve portability

This commit is contained in:
Jeremy Soller 2026-04-17 12:01:28 -06:00
parent 33890633b5
commit 9c0eb63b82
7 changed files with 173 additions and 166 deletions

View file

@ -99,6 +99,7 @@ use crate::{
self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK,
SearchLocation, Tab,
},
trash::{Trash, TrashExt},
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
};
@ -1794,7 +1795,7 @@ impl App {
nav_model = nav_model.insert(|b| {
b.text(fl!("trash"))
.icon(icon::icon(tab::trash_helpers::trash_icon_symbolic(16)))
.icon(icon::icon(Trash::icon_symbolic(16)))
.data(Location::Trash)
.divider_above()
});
@ -2655,9 +2656,7 @@ impl Application for App {
));
}
if matches!(location_opt, Some(Location::Trash))
&& !trash::os_limited::is_empty().unwrap_or(true)
{
if matches!(location_opt, Some(Location::Trash)) && !Trash::is_empty() {
items.push(cosmic::widget::menu::Item::Button(
fl!("empty-trash"),
None,
@ -4237,10 +4236,8 @@ impl Application for App {
.is_some_and(|loc| matches!(loc, Location::Trash))
});
if let Some(entity) = maybe_entity {
self.nav_model.icon_set(
entity,
icon::icon(tab::trash_helpers::trash_icon_symbolic(16)),
);
self.nav_model
.icon_set(entity, icon::icon(Trash::icon_symbolic(16)));
}
return Task::batch([self.rescan_trash(), self.update_desktop()]);
@ -6803,14 +6800,7 @@ impl Application for App {
},
);
// TODO: Trash watching support for Windows, macOS, and other OSes
#[cfg(all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
))]
match (watcher_res, trash::os_limited::trash_folders()) {
match (watcher_res, Trash::folders()) {
(Ok(mut watcher), Ok(trash_bins)) => {
// Watch the "bins" themselves as well as the files folder where
// trashed items are placed. This allows us to avoid recursively

View file

@ -441,13 +441,8 @@ impl<M: Send + 'static> Dialog<M> {
#[derive(Clone, Debug)]
enum DialogPage {
NewFolder {
parent: PathBuf,
name: String,
},
Replace {
filename: String,
},
NewFolder { parent: PathBuf, name: String },
Replace { filename: String },
}
#[derive(Clone, Debug)]
@ -2042,7 +2037,7 @@ impl Application for App {
}
col = col.push(
self.tab
self.tab
.view(&self.key_binds, &self.modifiers, false, &[])
.map(Message::TabMessage),
);

View file

@ -5,14 +5,18 @@ use cosmic::{app::Settings, iced::Limits};
use std::{env, fs, path::PathBuf, process};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use app::{App, Flags};
use crate::{
app::{App, Flags},
config::{Config, State},
tab::Location,
};
pub mod app;
mod archive;
pub mod channel;
pub mod clipboard;
mod context_action;
use config::Config;
pub mod config;
mod context_action;
pub mod dialog;
mod key_bind;
pub(crate) mod large_image;
@ -25,13 +29,11 @@ mod mounter;
mod mouse_area;
pub mod operation;
mod spawn_detached;
use tab::Location;
mod zoom;
use crate::config::State;
pub mod tab;
mod thumbnail_cacher;
mod thumbnailer;
pub(crate) mod trash;
mod zoom;
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;

View file

@ -23,6 +23,7 @@ use crate::{
config::{Config, ContextActionPreset},
fl,
tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab},
trash::{Trash, TrashExt},
};
static MENU_ID: LazyLock<cosmic::widget::Id> =
@ -192,7 +193,7 @@ pub fn context_menu<'a>(
) => {
if selected_trash_only {
children.push(menu_item(fl!("open"), Action::Open).into());
if !trash::os_limited::is_empty().unwrap_or(true) {
if !Trash::is_empty() {
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
}
} else if let Some(entry) = selected_desktop_entry {

View file

@ -1125,7 +1125,9 @@ impl Operation {
#[cfg(target_os = "macos")]
Self::Restore { .. } => {
// TODO: add support for macos
return OperationError::from_msg("Restoring from trash is not supported on macos");
return Err(OperationError::from_msg(
"Restoring from trash is not supported on macos",
));
}
#[cfg(not(target_os = "macos"))]
Self::Restore { items } => {

View file

@ -39,7 +39,6 @@ use icu::{
use image::{DynamicImage, ImageReader};
use jiff_icu::ConvertFrom;
use mime_guess::{Mime, mime};
use regex::Regex;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::{
@ -84,6 +83,7 @@ use crate::{
operation::{Controller, OperationError},
thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize},
thumbnailer::thumbnailer,
trash::{Trash, TrashExt},
};
use uzers::{get_group_by_gid, get_user_by_uid};
@ -1168,133 +1168,7 @@ pub fn scan_search<F: Fn(SearchItem) -> bool + Sync>(
}
}
SearchLocation::Trash => {
trash_helpers::scan_search_trash(callback, &regex);
}
}
}
// This config statement is from trash::os_limited, inverted
#[cfg(not(any(
target_os = "windows",
all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
)
)))]
mod trash_helpers {
use super::*;
pub fn trash_entries() -> usize {
0
}
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name("user-trash")
.size(icon_size)
.handle()
}
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name("user-trash-symbolic")
.size(icon_size)
.handle()
}
pub fn scan_trash(_sizes: IconSizes) -> Vec<Item> {
log::warn!("viewing trash not supported on this platform");
Vec::new()
}
pub fn scan_search_trash<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {}
}
// This config statement is from trash::os_limited
#[cfg(any(
target_os = "windows",
all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
)
))]
pub mod trash_helpers {
use super::*;
pub fn trash_entries() -> usize {
match trash::os_limited::list() {
Ok(entries) => entries.len(),
Err(_err) => 0,
}
}
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
"user-trash"
} else {
"user-trash-full"
})
.size(icon_size)
.handle()
}
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
"user-trash-symbolic"
} else {
"user-trash-full-symbolic"
})
.size(icon_size)
.handle()
}
pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
let entries = match trash::os_limited::list() {
Ok(entry) => entry,
Err(err) => {
log::warn!("failed to read trash items: {err}");
return Vec::new();
}
};
let mut items: Vec<_> = entries
.into_iter()
.filter_map(|entry| {
let metadata = trash::os_limited::metadata(&entry)
.inspect_err(|err| {
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
})
.ok()?;
Some(item_from_trash_entry(entry, metadata, sizes))
})
.collect();
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
});
items
}
pub fn scan_search_trash<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
let entries = match trash::os_limited::list() {
Ok(entries) => entries,
Err(err) => {
log::warn!("failed to read trash items: {err}");
return;
}
};
for entry in entries {
if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| {
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
}) {
let name = entry.name.to_string_lossy();
if regex.is_match(&name) && !callback(SearchItem::Trash(entry, metadata)) {
break;
}
}
Trash::scan_search(callback, &regex);
}
}
}
@ -1432,15 +1306,15 @@ pub fn scan_desktop(
let display_name = Item::display_name(&name);
let metadata = ItemMetadata::SimpleDir {
entries: trash_helpers::trash_entries() as u64,
entries: Trash::entries() as u64,
};
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
(
"inode/directory".parse().unwrap(),
trash_helpers::trash_icon(sizes.grid()),
trash_helpers::trash_icon(sizes.list()),
trash_helpers::trash_icon(sizes.list_condensed()),
Trash::icon(sizes.grid()),
Trash::icon(sizes.list()),
Trash::icon(sizes.list_condensed()),
)
};
@ -1681,7 +1555,7 @@ impl Location {
// Search is done incrementally
Vec::new()
}
Self::Trash => trash_helpers::scan_trash(sizes),
Self::Trash => Trash::scan(sizes),
Self::Recents => scan_recents(sizes),
Self::Network(uri, _, _) => scan_network(uri, sizes),
};

143
src/trash.rs Normal file
View file

@ -0,0 +1,143 @@
use cosmic::widget;
use regex::Regex;
use std::{collections::HashSet, path::PathBuf};
use crate::{
config::IconSizes,
tab::{Item, SearchItem},
};
pub trait TrashExt {
fn is_empty() -> bool {
true
}
fn entries() -> usize {
0
}
fn folders() -> Result<HashSet<PathBuf>, trash::Error> {
Err(trash::Error::Unknown {
description: "reading trash folders not supported on this platform".into(),
})
}
fn scan(_sizes: IconSizes) -> Vec<Item> {
log::warn!("viewing trash not supported on this platform");
Vec::new()
}
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {}
fn icon(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(if Self::is_empty() {
"user-trash"
} else {
"user-trash-full"
})
.size(icon_size)
.handle()
}
fn icon_symbolic(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(if Self::is_empty() {
"user-trash-symbolic"
} else {
"user-trash-full-symbolic"
})
.size(icon_size)
.handle()
}
}
pub struct Trash;
// This config statement is from trash::os_limited
#[cfg(any(
target_os = "windows",
all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
)
))]
impl TrashExt for Trash {
fn is_empty() -> bool {
trash::os_limited::is_empty().unwrap_or(true)
}
fn entries() -> usize {
match trash::os_limited::list() {
Ok(entries) => entries.len(),
Err(_err) => 0,
}
}
fn folders() -> Result<HashSet<PathBuf>, trash::Error> {
trash::os_limited::trash_folders()
}
fn scan(sizes: IconSizes) -> Vec<Item> {
use crate::{localize::LANGUAGE_SORTER, tab::item_from_trash_entry};
use std::cmp::Ordering;
let entries = match trash::os_limited::list() {
Ok(entry) => entry,
Err(err) => {
log::warn!("failed to read trash items: {err}");
return Vec::new();
}
};
let mut items: Vec<_> = entries
.into_iter()
.filter_map(|entry| {
let metadata = trash::os_limited::metadata(&entry)
.inspect_err(|err| {
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
})
.ok()?;
Some(item_from_trash_entry(entry, metadata, sizes))
})
.collect();
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
});
items
}
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
let entries = match trash::os_limited::list() {
Ok(entries) => entries,
Err(err) => {
log::warn!("failed to read trash items: {err}");
return;
}
};
for entry in entries {
if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| {
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
}) {
let name = entry.name.to_string_lossy();
if regex.is_match(&name) && !callback(SearchItem::Trash(entry, metadata)) {
break;
}
}
}
}
}
// This config statement is from trash::os_limited, inverted
#[cfg(not(any(
target_os = "windows",
all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
)
)))]
impl TrashExt for Trash {}