2026-04-17 12:01:28 -06:00
|
|
|
use cosmic::widget;
|
|
|
|
|
use regex::Regex;
|
2026-04-28 15:05:08 +02:00
|
|
|
use std::collections::HashSet;
|
2026-06-29 18:39:12 +02:00
|
|
|
use std::path::{Path, PathBuf};
|
2026-07-18 19:13:12 +02:00
|
|
|
use std::sync::LazyLock;
|
2026-04-17 12:01:28 -06:00
|
|
|
|
2026-04-28 15:05:08 +02:00
|
|
|
use crate::config::IconSizes;
|
|
|
|
|
use crate::tab::{Item, SearchItem};
|
2026-04-17 12:01:28 -06:00
|
|
|
|
2026-06-29 18:39:12 +02:00
|
|
|
fn percent_decode(s: &str) -> Option<String> {
|
|
|
|
|
let (mut r, mut b) = (String::with_capacity(s.len()), s.bytes());
|
|
|
|
|
while let Some(c) = b.next() {
|
|
|
|
|
if c == b'%' {
|
|
|
|
|
let (hi, lo) = (b.next()?, b.next()?);
|
|
|
|
|
let h = |x: u8| match x {
|
|
|
|
|
b'0'..=b'9' => Some(x - b'0'),
|
|
|
|
|
b'a'..=b'f' => Some(x - b'a' + 10),
|
|
|
|
|
b'A'..=b'F' => Some(x - b'A' + 10),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
r.push((h(hi)? << 4 | h(lo)?) as char);
|
|
|
|
|
} else {
|
|
|
|
|
r.push(c as char);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Some(r)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 12:01:28 -06:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 14:45:01 +02:00
|
|
|
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(_callback: F, _regex: &Regex) {}
|
2026-04-17 12:01:28 -06:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 18:39:12 +02:00
|
|
|
/// Derive the actual filesystem path of a trashed item from its .trashinfo path
|
|
|
|
|
/// (or return the id directly if it's already a filesystem path).
|
|
|
|
|
pub fn trash_item_path(item: &trash::TrashItem) -> Option<PathBuf> {
|
|
|
|
|
let id_path = Path::new(&item.id);
|
|
|
|
|
if id_path.extension().map_or(false, |e| e == "trashinfo") {
|
|
|
|
|
let trash_root = id_path.parent()?.parent()?;
|
|
|
|
|
let file_name = id_path.file_stem()?;
|
|
|
|
|
Some(trash_root.join("files").join(file_name))
|
|
|
|
|
} else {
|
|
|
|
|
Some(PathBuf::from(&item.id))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// For a path inside a trash `files/` directory, reconstruct the original path
|
|
|
|
|
/// by reading the parent `.trashinfo` file.
|
|
|
|
|
///
|
|
|
|
|
/// Given `~/.local/share/Trash/files/folder/sub/file.txt`:
|
|
|
|
|
/// - The top-level trashed item is `folder`
|
|
|
|
|
/// - Read `~/.local/share/Trash/info/folder.trashinfo` to get the original path
|
|
|
|
|
/// - Compute: `<original_path>/sub/file.txt`
|
|
|
|
|
pub fn original_path_for_trash_child(p: &Path) -> Option<PathBuf> {
|
2026-07-18 19:13:12 +02:00
|
|
|
let files = trash_files_dir(p)?;
|
2026-06-29 18:39:12 +02:00
|
|
|
let root = files.parent()?;
|
|
|
|
|
let top = p.strip_prefix(files).ok()?.components().next()?;
|
2026-07-19 14:03:04 +02:00
|
|
|
let mut trashinfo_name = top.as_os_str().to_os_string();
|
|
|
|
|
trashinfo_name.push(".trashinfo");
|
|
|
|
|
let info = std::fs::read_to_string(root.join("info").join(trashinfo_name)).ok()?;
|
2026-06-29 18:39:12 +02:00
|
|
|
let orig = percent_decode(info.lines().find_map(|l| l.strip_prefix("Path="))?.trim())?;
|
|
|
|
|
let rel = p.strip_prefix(files.join(top)).ok()?;
|
|
|
|
|
let mut result = PathBuf::from(&orig);
|
|
|
|
|
if !rel.as_os_str().is_empty() {
|
|
|
|
|
result.push(rel);
|
|
|
|
|
}
|
|
|
|
|
Some(result)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 19:13:12 +02:00
|
|
|
static TRASH_FOLDERS: LazyLock<HashSet<PathBuf>> = LazyLock::new(|| {
|
|
|
|
|
Trash::folders().unwrap_or_else(|e| {
|
|
|
|
|
log::warn!("failed to list trash folders: {}", e);
|
|
|
|
|
HashSet::new()
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fn is_trash_root(root: &Path) -> bool {
|
|
|
|
|
if TRASH_FOLDERS.is_empty() {
|
|
|
|
|
root.join("info").is_dir()
|
|
|
|
|
} else {
|
|
|
|
|
TRASH_FOLDERS.contains(root)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn trash_files_dir(path: &Path) -> Option<&Path> {
|
|
|
|
|
path.ancestors()
|
|
|
|
|
.find(|a| a.ends_with("files") && a.parent().is_some_and(is_trash_root))
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 18:39:12 +02:00
|
|
|
/// Check whether a path is inside any trash `files/` directory.
|
|
|
|
|
pub fn is_trash_path(path: &Path) -> bool {
|
2026-07-18 19:13:12 +02:00
|
|
|
trash_files_dir(path).is_some()
|
2026-06-29 18:39:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 12:01:28 -06:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 13:31:55 -06:00
|
|
|
// Not available on Windows only
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
2026-04-17 12:01:28 -06:00
|
|
|
fn folders() -> Result<HashSet<PathBuf>, trash::Error> {
|
|
|
|
|
trash::os_limited::trash_folders()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn scan(sizes: IconSizes) -> Vec<Item> {
|
2026-04-28 15:05:08 +02:00
|
|
|
use crate::localize::LANGUAGE_SORTER;
|
|
|
|
|
use crate::tab::item_from_trash_entry;
|
2026-04-17 12:01:28 -06:00
|
|
|
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 {}
|