From 9c0eb63b8259813503ccd4ed72830553b8263ff8 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 17 Apr 2026 12:01:28 -0600 Subject: [PATCH] Refactor trash handling to improve portability --- src/app.rs | 22 ++----- src/dialog.rs | 11 +--- src/lib.rs | 16 ++--- src/menu.rs | 3 +- src/operation/mod.rs | 4 +- src/tab.rs | 140 +++--------------------------------------- src/trash.rs | 143 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 173 insertions(+), 166 deletions(-) create mode 100644 src/trash.rs diff --git a/src/app.rs b/src/app.rs index 2ab62a6..af0ecbe 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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 diff --git a/src/dialog.rs b/src/dialog.rs index f9894ac..cac5907 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -441,13 +441,8 @@ impl Dialog { #[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), ); diff --git a/src/lib.rs b/src/lib.rs index 9214a7f..644b64d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 = ordermap::OrderMap; diff --git a/src/menu.rs b/src/menu.rs index 8db61d7..89e4a02 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -23,6 +23,7 @@ use crate::{ config::{Config, ContextActionPreset}, fl, tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab}, + trash::{Trash, TrashExt}, }; static MENU_ID: LazyLock = @@ -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 { diff --git a/src/operation/mod.rs b/src/operation/mod.rs index f9fae0d..b4a66e9 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -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 } => { diff --git a/src/tab.rs b/src/tab.rs index 9d9d2a2..a350f08 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -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 bool + Sync>( } } SearchLocation::Trash => { - trash_helpers::scan_search_trash(callback, ®ex); - } - } -} - -// 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 { - log::warn!("viewing trash not supported on this platform"); - Vec::new() - } - - pub fn scan_search_trash 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 { - 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 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, ®ex); } } } @@ -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), }; diff --git a/src/trash.rs b/src/trash.rs new file mode 100644 index 0000000..0262ba9 --- /dev/null +++ b/src/trash.rs @@ -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, trash::Error> { + Err(trash::Error::Unknown { + description: "reading trash folders not supported on this platform".into(), + }) + } + + fn scan(_sizes: IconSizes) -> Vec { + log::warn!("viewing trash not supported on this platform"); + Vec::new() + } + + fn scan_search 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, trash::Error> { + trash::os_limited::trash_folders() + } + + fn scan(sizes: IconSizes) -> Vec { + 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 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 {}