feat: trash bin

This commit is contained in:
三咲雅 misaki masa 2026-07-22 01:38:50 +08:00
parent 6aa75594db
commit aba0b5ad01
No known key found for this signature in database
143 changed files with 2778 additions and 948 deletions

View file

@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: 20

View file

@ -1,7 +1,7 @@
name: Validate PR
on:
pull_request_target:
pull_request:
types: [opened, edited, reopened, synchronize]
permissions:
@ -17,7 +17,7 @@ jobs:
ref: ${{ github.event.repository.default_branch }}
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: 20

View file

@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Added
- Drag and drop ([#4005])
- Trash bin ([#4144])
- Bulk create ([#3793])
- Make help menu a command palette ([#4074])
- Input history ([#4104])
@ -1781,3 +1782,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#4108]: https://github.com/sxyazi/yazi/pull/4108
[#4118]: https://github.com/sxyazi/yazi/pull/4118
[#4120]: https://github.com/sxyazi/yazi/pull/4120
[#4144]: https://github.com/sxyazi/yazi/pull/4144

380
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -36,20 +36,21 @@ debug = false
[workspace.dependencies]
ansi-to-tui = "8.0.1"
anyhow = "1.0.103"
anyhow = "1.0.104"
arc-swap = { version = "1.9.2", features = [ "serde" ] }
base64 = "0.22.1"
bitflags = { version = "2.13.0", features = [ "serde" ] }
bitflags = { version = "2.13.1", features = [ "serde" ] }
chrono = "0.4.45"
clap = { version = "4.6.1", features = [ "derive" ] }
compact_str = { version = "0.9.1", features = [ "serde" ] }
clap = { version = "4.6.2", features = [ "derive" ] }
compact_str = { version = "0.10.0", features = [ "serde" ] }
core-foundation-sys = "0.8.7"
data-encoding = "2.11.0"
dirs = "6.0.0"
dyn-clone = "1.0.20"
either = { version = "1.16.0" }
foldhash = "0.2.0"
futures = "0.3.32"
globset = "0.4.18"
futures = "0.3.33"
globset = "0.4.19"
hashbrown = { version = "0.17.1", features = [ "serde" ] }
image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] }
indexmap = { version = "2.14.0", features = [ "serde" ] }
@ -65,21 +66,21 @@ percent-encoding = "2.3.2"
rand = { version = "0.10.2", default-features = false, features = [ "std", "sys_rng" ] }
ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] }
ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] }
regex = "1.13.0"
regex = "1.13.1"
russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] }
serde_json = "1.0.150"
serde = { version = "1.0.229", features = [ "derive" ] }
serde_json = "1.0.151"
serde_with = "3.21.0"
strum = { version = "0.28.0", features = [ "derive" ] }
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.18"
tokio = { version = "1.52.3", features = [ "full" ] }
thiserror = "2.0.19"
tokio = { version = "1.53.0", features = [ "full" ] }
tokio-stream = "0.1.18"
tokio-util = "0.7.18"
toml = { version = "1.1.2" }
toml = { version = "1.1.3" }
tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
typed-path = "0.12.3"
unicode-width = { version = "0.2.2", default-features = false }
uzers = "0.12.2"

View file

@ -4,16 +4,16 @@ use anyhow::{Result, anyhow};
use scopeguard::defer;
use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRuleArc};
use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}, file::File};
use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}};
use yazi_macro::{succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt};
use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_shared::{data::Data, strand::Strand, url::{UrlBuf, UrlLike}};
use yazi_shim::path::CROSS_SEPARATOR;
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_tty::{TTY, sequence::EraseScreen};
use yazi_vfs::{VfsFile, engine};
use yazi_vfs::engine;
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
@ -31,7 +31,7 @@ impl Actor for BulkCreate {
let cwd = cx.cwd().clone();
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk-create");
engine::create_new(&tmp).await?;
let file = engine::create_new(&tmp).await?.file().await?;
defer! {
let tmp = tmp.clone();
@ -42,7 +42,7 @@ impl Actor for BulkCreate {
TasksProxy::process_exec(ShellOpt {
cwd: cwd.clone(),
cmd: Splatter::new(&[tmp.as_url()]).splat(&opener.run),
cmd: Splatter::new(&[file]).splat(&opener.run),
block: opener.block,
orphan: opener.orphan,
})
@ -86,7 +86,7 @@ impl BulkCreate {
if let Err(e) = result {
failed.push((entry, e.into()));
} else if let Ok(f) = File::new(dist).await {
} else if let Ok(f) = engine::file(dist).await {
succeeded.push(f);
} else {
failed.push((entry, anyhow!("Failed to retrieve file info")));

View file

@ -7,7 +7,7 @@ use tokio::io::AsyncWriteExt;
use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRuleArc};
use yazi_dds::Pubsub;
use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::{Demand, Local}}, file::File, max_common_root, path::skip_url};
use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::Local}, max_common_root, path::skip_url};
use yazi_macro::{err, succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
@ -15,7 +15,7 @@ use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt};
use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_tty::{TTY, sequence::EraseScreen};
use yazi_vfs::{VfsFile, engine, maybe_exists};
use yazi_vfs::{engine::{self, Demand}, maybe_exists};
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
@ -46,13 +46,8 @@ impl Actor for BulkRename {
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk-rename");
Demand::default()
.write(true)
.create_new(true)
.open(&tmp)
.await?
.write_all(old.join(Strand::Utf8("\n")).encoded_bytes())
.await?;
let mut rw = Demand::default().write(true).create_new(true).open(&tmp).await?;
rw.write_all(old.join(Strand::Utf8("\n")).encoded_bytes()).await?;
defer! {
let tmp = tmp.clone();
@ -65,7 +60,7 @@ impl Actor for BulkRename {
batcher.prime(&tmp);
TasksProxy::process_exec(ShellOpt {
cwd,
cmd: Splatter::new(&[tmp.as_url()]).splat(&opener.run),
cmd: Splatter::new(&[rw.into_file().await?]).splat(&opener.run),
block: opener.block,
orphan: opener.orphan,
})
@ -131,7 +126,7 @@ impl BulkRename {
failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = engine::rename(&old, &new).await {
failed.push((o, n, e.into()));
} else if let Ok(f) = File::new(new).await {
} else if let Ok(f) = engine::file(new).await {
succeeded.insert(old, f);
} else {
failed.push((o, n, anyhow!("Failed to retrieve file info")));

View file

@ -6,12 +6,12 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::YAZI;
use yazi_core::mgr::CdSource;
use yazi_dds::Pubsub;
use yazi_fs::{FilesOp, file::File, path::{clean_url, expand_url}};
use yazi_fs::{FilesOp, path::{clean_url, expand_url}};
use yazi_macro::{act, err, input, render, succ};
use yazi_parser::mgr::CdForm;
use yazi_proxy::{CmpProxy, MgrProxy};
use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, engine};
use yazi_vfs::engine;
use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx};
@ -76,7 +76,7 @@ impl Cd {
let Ok(url) = engine::absolute(&url).await else { return };
let url = clean_url(url);
let Ok(file) = File::new(&url).await else { return };
let Ok(file) = engine::file(&url).await else { return };
if file.is_dir() {
return MgrProxy::cd(&url, CdSource::Cd);
}

View file

@ -80,7 +80,7 @@ impl Create {
if let Ok(real) = engine::casefold(&new).await
&& let Some((parent, key)) = real.pair2()
{
let file = File::new(&real).await?;
let file = engine::file(&real).await?;
FilesOp::Upserting(parent.into(), [(key.into(), file)].into()).emit();
MgrProxy::reveal(&real);
}

View file

@ -4,12 +4,12 @@ use anyhow::Result;
use futures::{StreamExt, stream::FuturesUnordered};
use hashbrown::HashSet;
use yazi_core::mgr::OpenOpt;
use yazi_fs::{FsSpec, engine::{Engine, local::Local}, file::File};
use yazi_fs::{FsSpec, engine::{Engine, local::Local}};
use yazi_macro::succ;
use yazi_parser::mgr::DownloadForm;
use yazi_proxy::MgrProxy;
use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}};
use yazi_vfs::VfsFile;
use yazi_vfs::engine;
use crate::{Actor, Ctx};
@ -42,7 +42,7 @@ impl Actor for Download {
continue;
}
let Ok(f) = File::new(&url).await else { continue };
let Ok(f) = engine::file(&url).await else { continue };
urls.push(url);
files.push(f);

View file

@ -16,7 +16,7 @@ impl Actor for Follow {
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let Some(file) = cx.hovered() else { succ!() };
let Some(link_to) = &file.link_to else { succ!() };
let Some(link_to) = file.extra.link_to() else { succ!() };
let Some(parent) = file.url.parent() else { succ!() };
let Ok(joined) = parent.try_join(link_to) else { succ!() };
act!(mgr:reveal, cx, (clean_url(joined), CdSource::Follow))

View file

@ -4,6 +4,7 @@ use yazi_fs::FolderStage;
use yazi_macro::{act, render, render_and, succ};
use yazi_parser::{mgr::HiddenForm, spark::SparkKind};
use yazi_shared::{Source, data::Data};
use yazi_shim::OptionExt;
use crate::{Actor, Ctx};
@ -18,7 +19,7 @@ impl Actor for Hidden {
let state = form.state.bool(cx.tab().pref.show_hidden);
cx.tab_mut().pref.show_hidden = state;
let hovered = cx.hovered().map(|f| f.entry_key().to_owned());
let hovered = cx.hovered().map(|f| f.entry_key()).owned();
let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading {
render!();
@ -30,9 +31,7 @@ impl Actor for Hidden {
};
// Apply to CWD and parent
if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply))
&& (a | b)
{
if apply(cx.current_mut()) | cx.parent_mut().is_some_and(apply) {
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
}

View file

@ -3,12 +3,11 @@ use futures::StreamExt;
use hashbrown::HashSet;
use yazi_boot::ARGS;
use yazi_core::mgr::OpenDoOpt;
use yazi_fs::file::File;
use yazi_macro::{act, succ};
use yazi_parser::mgr::OpenForm;
use yazi_proxy::MgrProxy;
use yazi_shared::data::Data;
use yazi_vfs::VfsFile;
use yazi_vfs::engine;
use crate::{Actor, Ctx, mgr::Quit};
@ -59,7 +58,7 @@ impl Actor for Open {
let it = futures::stream::iter(opt.targets)
.enumerate()
.map(|(i, url)| async move { File::new(url).await.ok().map(|file| (i, file)) })
.map(|(i, url)| async move { engine::file(url).await.ok().map(|file| (i, file)) })
.buffered(3)
.filter_map(|item| async move { item });

View file

@ -42,10 +42,10 @@ impl Actor for OpenDo {
}
let pick = PickProxy::show(YAZI.pick.open(openers.iter().map(|o| o.desc()).collect()));
let urls: Vec<_> = targets.into_iter().map(|(file, _)| file.url).collect();
let files: Vec<_> = targets.into_iter().map(|(file, _)| file).collect();
tokio::spawn(async move {
if let Some(choice) = pick.await {
Self::open_with(&openers[choice], &opt.cwd, &urls);
Self::open_with(&openers[choice], &opt.cwd, &files);
}
});
succ!();
@ -59,20 +59,20 @@ impl OpenDo {
if let Some(open) = YAZI.open.matches(&file, mime)
&& let Some(opener) = YAZI.opener.first(&open)
{
openers.entry(opener).or_default().push(file.url);
openers.entry(opener).or_default().push(file);
}
}
for (opener, urls) in openers {
Self::open_with(&opener, &cwd, &urls);
for (opener, files) in openers {
Self::open_with(&opener, &cwd, &files);
}
}
fn open_with(opener: &OpenerRule, cwd: &UrlBuf, urls: &[UrlBuf]) {
let size = if opener.spread { urls.len().max(1) } else { 1 };
for urls in urls.chunks(size) {
fn open_with(opener: &OpenerRule, cwd: &UrlBuf, files: &[File]) {
let size = if opener.spread { files.len().max(1) } else { 1 };
for files in files.chunks(size) {
TasksProxy::process_open(ShellOpt {
cwd: cwd.clone(),
cmd: Splatter::new(urls).splat(&opener.run),
cmd: Splatter::new(files).splat(&opener.run),
block: opener.block,
orphan: opener.orphan,
});

View file

@ -21,18 +21,14 @@ impl Actor for Peek {
}
let mime = cx.mgr.mimetype.owned(&hovered.url).unwrap_or_default();
let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.cha));
let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.file.clone()));
if !cx.tab().preview.same_url(&hovered.url) {
cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_default();
cx.tab_mut().preview.skip = folder.as_ref().map(|f| f.0).unwrap_or_default();
}
if !cx.tab().preview.same_file(&hovered, &mime) {
cx.tab_mut().preview.reset();
}
if !cx.tab().preview.same_folder(&hovered.url) {
cx.tab_mut().preview.folder_lock = None;
}
if matches!(form.only_if, Some(u) if u != hovered.url) {
succ!();
}
@ -46,11 +42,13 @@ impl Actor for Peek {
}
}
if hovered.is_dir() {
cx.tab_mut().preview.go_folder(hovered, folder.map(|(_, cha)| cha), mime, form.force);
} else {
cx.tab_mut().preview.go(hovered, mime, form.force);
if let Some((_, file)) = folder {
cx.core.mgr.watcher.refresher.refresh([file]);
} else if hovered.is_dir() {
cx.core.mgr.watcher.refresher.load(&hovered);
}
cx.tab_mut().preview.go(hovered, mime, form.force);
succ!();
}
}

View file

@ -1,10 +1,8 @@
use anyhow::Result;
use yazi_core::tab::Folder;
use yazi_fs::{CWD, Entries, FilesOp, cha::Cha};
use yazi_fs::CWD;
use yazi_macro::{act, succ};
use yazi_parser::VoidForm;
use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsEntries, VfsFilesOp};
use yazi_shared::{data::Data, url::UrlLike};
use yazi_watcher::MgrProxy;
use crate::{Actor, Ctx};
@ -19,11 +17,13 @@ impl Actor for Refresh {
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
CWD.set(cx.cwd(), Self::cwd_changed);
if let Some(p) = cx.parent() {
Self::trigger_dirs(&[cx.current(), p]);
} else {
Self::trigger_dirs(&[cx.current()]);
}
cx.core.mgr.watcher.refresher.refresh(
[Some(cx.current()), cx.parent()]
.into_iter()
.flatten()
.filter(|f| f.url.is_absolute() && !f.url.is_search())
.map(|f| &f.file),
);
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
@ -40,26 +40,4 @@ impl Refresh {
MgrProxy::watch();
}
}
// TODO: performance improvement
fn trigger_dirs(folders: &[&Folder]) {
async fn go(dir: UrlBuf, cha: Cha) {
let Some(cha) = Entries::assert_stale(&dir, cha).await else { return };
match Entries::from_dir_bulk(&dir).await {
Ok(files) => FilesOp::Full(dir, files, cha).emit(),
Err(e) => FilesOp::issue_error(&dir, e).await,
}
}
let futs: Vec<_> = folders
.iter()
.filter(|&f| f.url.is_absolute() && !f.url.is_search())
.map(|&f| go(f.url.clone(), f.cha))
.collect();
if !futs.is_empty() {
tokio::spawn(futures::future::join_all(futs));
}
}
}

View file

@ -84,7 +84,7 @@ impl Rename {
let new = engine::casefold(&new).await?;
let Some((new_p, new_k)) = new.pair2() else { return Ok(()) };
let file = File::new(&new).await?;
let file = engine::file(&new).await?;
if new_p == old_p {
FilesOp::Upserting(old_p.into(), [(old_k.into(), file)].into()).emit();
} else {

View file

@ -5,7 +5,7 @@ use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::YAZI;
use yazi_core::mgr::{CdSource, SearchVia};
use yazi_fs::{FilesOp, cha::Cha};
use yazi_fs::{FilesOp, cha::ChaType, file::File};
use yazi_macro::{act, input, succ};
use yazi_parser::{VoidForm, mgr::SearchForm};
use yazi_plugin::external;
@ -89,7 +89,7 @@ impl Actor for SearchDo {
while let Some(chunk) = rx.next().await {
FilesOp::Part(cwd.clone(), chunk, ticket).emit();
}
FilesOp::Done(cwd, Cha::default(), ticket).emit();
FilesOp::Done(File::from_dummy(cwd, Some(ChaType::Dir)), ticket).emit();
Ok(())
}));

View file

@ -4,6 +4,7 @@ use yazi_fs::{FilesSorter, FolderStage};
use yazi_macro::{act, render, render_and, succ};
use yazi_parser::{mgr::SortForm, spark::SparkKind};
use yazi_shared::{Source, data::Data};
use yazi_shim::OptionExt;
use crate::{Actor, Ctx};
@ -24,7 +25,7 @@ impl Actor for Sort {
pref.sort_fallback = form.fallback.unwrap_or(pref.sort_fallback);
let sorter = FilesSorter::from(&*pref);
let hovered = cx.hovered().map(|f| f.entry_key().to_owned());
let hovered = cx.hovered().map(|f| f.entry_key()).owned();
let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading {
render!();
@ -36,9 +37,7 @@ impl Actor for Sort {
};
// Apply to CWD and parent
if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply))
&& (a | b)
{
if apply(cx.current_mut()) | cx.parent_mut().is_some_and(apply) {
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.entries);

View file

@ -15,9 +15,11 @@ impl Actor for Watch {
const NAME: &str = "watch";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let it = iter::once(cx.core.mgr.tabs.active().cwd())
.chain(cx.core.mgr.tabs.parent().map(|p| &p.url))
.chain(cx.core.mgr.tabs.hovered().filter(|h| h.is_dir()).map(|h| &h.url));
let tab = cx.core.mgr.tabs.active();
let it = iter::once(&tab.current.file)
.chain(tab.hovered_folder().map(|h| &h.file).or(tab.hovered().filter(|f| f.is_dir())))
.chain(tab.parent.as_ref().map(|p| &p.file));
cx.core.mgr.watcher.watch(it);
succ!();

View file

@ -209,15 +209,14 @@ macro_rules! impl_file_fields {
$fields.add_cached_field("cha", |_, me| Ok(me.cha));
$fields.add_cached_field("url", |_, me| Ok(me.url_owned()));
$fields.add_cached_field("link_to", |_, me| Ok(me.link_to.clone()));
$fields.add_cached_field("link_to", |_, me| Ok(me.extra.link_to().cloned()));
$fields.add_cached_field("name", |lua, me| {
me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose()
});
$fields.add_cached_field("path", |_, me| {
use yazi_fs::FsUrl;
use yazi_shared::{path::PathBufDyn, url::AsUrl};
Ok(PathBufDyn::from(me.url.as_url().unified_path()))
use yazi_shared::path::PathBufDyn;
Ok(PathBufDyn::from(me.content_path()))
});
$fields.add_cached_field("cache", |_, me| {
use yazi_fs::FsUrl;
@ -231,8 +230,8 @@ macro_rules! impl_file_fields {
macro_rules! impl_file_methods {
($methods:ident) => {
$methods.add_method("hash", |_, me, ()| {
use yazi_fs::FsHash64;
Ok(me.hash_u64())
use yazi_fs::{FsHash64, file::FileSig};
Ok(FileSig(me).hash_u64())
});
};
}

View file

@ -32,4 +32,4 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
clap = { workspace = true }
clap_complete = "4.6.7"
clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0"
clap_complete_nushell = "4.6.1"

View file

@ -21,6 +21,7 @@ yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" }
yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.6" }
yazi-version = { path = "../yazi-version", version = "26.5.6" }
@ -44,7 +45,7 @@ anyhow = { workspace = true }
clap = { workspace = true }
clap_complete = "4.6.7"
clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0"
clap_complete_nushell = "4.6.1"
serde = { workspace = true }
serde_json = { workspace = true }

View file

@ -5,6 +5,7 @@ use yazi_config::{THEME, YAZI};
use yazi_emulator::Mux;
use yazi_fs::Xdg;
use yazi_shared::timestamp_us;
use yazi_shim::OptionExt;
use yazi_term::TERM;
use crate::env::Env;
@ -156,7 +157,8 @@ impl Env {
Regex::new(r"\d+\.\d+(\.\d+-\d+|\.\d+|\b)")
.unwrap()
.find(&line)
.map(|m| m.as_str().to_owned())
.map(|m| m.as_str())
.owned()
.unwrap_or(line)
}
Ok(out) => format!("{:?}, {:?}", out.status, String::from_utf8_lossy(&out.stderr)),

View file

@ -18,5 +18,5 @@ proc-macro = true
[dependencies]
# External dependencies
proc-macro2 = "1"
quote = "1.0.46"
syn = { version = "2.0.118", features = [ "full" ] }
quote = "1.0.47"
syn = { version = "3.0.2", features = [ "full" ] }

View file

@ -128,8 +128,9 @@ keymap = [
# Goto
{ on = [ "g", "h" ], run = "cd ~", desc = "Go home" },
{ on = [ "g", "c" ], run = "cd ~/.config", desc = "Go ~/.config" },
{ on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go ~/Downloads" },
{ on = [ "g", "c" ], run = "cd ~/.config", desc = "Go to ~/.config" },
{ on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go to ~/Downloads" },
{ on = [ "g", "t" ], run = "plugin trash", desc = "Go to trash bin" },
{ on = [ "g", "<Space>" ], run = "cd --interactive", desc = "Jump interactively" },
{ on = [ "g", "f" ], run = "follow", desc = "Follow hovered symlink" },

View file

@ -0,0 +1,3 @@
[trash."*"]
kind = "hub"
run = "trash"

View file

@ -63,9 +63,16 @@ download = [
{ run = "ya emit download --open %S", desc = "Download and open" },
{ run = "ya emit download %S", desc = "Download" },
]
trash = [
{ run = "ya pub trash-restore --list %S", desc = "Restore selected files" },
{ run = "ya pub trash-empty --list %S", desc = "Empty trash bin" },
]
[open]
rules = [
# Trash
{ url = "trash://*", use = [ "open", "trash" ] },
{ url = "trash://*/", use = [ "edit", "trash" ] },
# Folder
{ url = "*/", use = [ "edit", "open", "reveal" ] },
# Text
@ -102,6 +109,7 @@ fetchers = [
# MIME-type
{ url = "*/", run = "mime.dir", prio = "high", group = "mime" },
{ url = "local://*", run = "mime.local", prio = "high", group = "mime" },
{ url = "trash://*", run = "mime.local", prio = "high", group = "mime" },
{ url = "remote://*", run = "mime.remote", prio = "high", group = "mime" },
]
spotters = [

View file

@ -1,12 +1,11 @@
use yazi_fs::Splatable;
use yazi_shared::url::{AsUrl, Url, UrlBuf};
use yazi_fs::{Splatable, file::File};
use crate::{mgr::Mgr, tab::TabSnap};
pub struct MgrSnap {
tab: usize,
tabs: Vec<TabSnap>,
yanked: Vec<UrlBuf>,
yanked: Vec<File>,
}
impl From<&Mgr> for MgrSnap {
@ -14,7 +13,7 @@ impl From<&Mgr> for MgrSnap {
Self {
tab: value.tabs.cursor,
tabs: value.tabs.iter().map(Into::into).collect(),
yanked: value.yanked.urls().cloned().collect(),
yanked: value.yanked.files().cloned().collect(),
}
}
}
@ -22,7 +21,7 @@ impl From<&Mgr> for MgrSnap {
impl Splatable for MgrSnap {
fn tab(&self) -> usize { self.tab + 1 }
fn selected(&self, tab: usize, mut idx: Option<usize>) -> impl Iterator<Item = Url<'_>> {
fn selected(&self, tab: usize, mut idx: Option<usize>) -> impl Iterator<Item = &File> {
idx = idx.and_then(|i| i.checked_sub(1));
tab
.checked_sub(1)
@ -31,24 +30,14 @@ impl Splatable for MgrSnap {
.iter()
.skip(idx.unwrap_or(0))
.take(if idx.is_some() { 1 } else { usize::MAX })
.map(AsUrl::as_url)
}
fn hovered(&self, tab: usize) -> Option<Url<'_>> {
tab
.checked_sub(1)
.and_then(|tab| self.tabs.get(tab))
.and_then(|tab| tab.hovered.as_ref())
.map(AsUrl::as_url)
fn hovered(&self, tab: usize) -> Option<&File> {
tab.checked_sub(1).and_then(|tab| self.tabs.get(tab)).and_then(|tab| tab.hovered.as_ref())
}
fn yanked(&self, mut idx: Option<usize>) -> impl Iterator<Item = Url<'_>> {
fn yanked(&self, mut idx: Option<usize>) -> impl Iterator<Item = &File> {
idx = idx.and_then(|i| i.checked_sub(1));
self
.yanked
.iter()
.skip(idx.unwrap_or(0))
.take(if idx.is_some() { 1 } else { usize::MAX })
.map(AsUrl::as_url)
self.yanked.iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX })
}
}

View file

@ -3,7 +3,7 @@ use std::ops::Deref;
use hashbrown::HashSet;
use indexmap::{IndexSet, set::MutableValues};
use yazi_dds::Pubsub;
use yazi_fs::{FilesOp, file::FileCov};
use yazi_fs::{FilesOp, file::{File, FileCov}};
use yazi_macro::err;
use yazi_shared::url::{Url, UrlBuf, UrlCov, UrlLike};
@ -27,6 +27,8 @@ impl Yanked {
Self { cut, files, ..Default::default() }
}
pub fn files(&self) -> impl Iterator<Item = &File> { self.files.iter().map(|f| &f.0) }
pub fn urls(&self) -> impl Iterator<Item = &UrlBuf> { self.files.iter().map(|f| &f.url) }
pub fn remove_many<'a, I, T>(&mut self, urls: I)

View file

@ -1,8 +1,8 @@
use std::mem;
use std::{mem, ops::Deref};
use yazi_config::{LAYOUT, YAZI};
use yazi_dds::Pubsub;
use yazi_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File};
use yazi_fs::{Entries, FilesOp, FolderStage, cha::ChaType, file::File};
use yazi_macro::err;
use yazi_shared::{id::Id, path::{DynPath, PathBufDyn, PathDyn}, url::UrlBuf};
use yazi_widgets::{Scrollable, Step};
@ -10,8 +10,7 @@ use yazi_widgets::{Scrollable, Step};
use crate::MgrProxy;
pub struct Folder {
pub url: UrlBuf,
pub cha: Cha,
pub file: File,
pub entries: Entries,
pub stage: FolderStage,
@ -22,11 +21,16 @@ pub struct Folder {
pub trace: Option<PathBufDyn>,
}
impl Deref for Folder {
type Target = File;
fn deref(&self) -> &Self::Target { &self.file }
}
impl Default for Folder {
fn default() -> Self {
Self {
url: Default::default(),
cha: Default::default(),
file: Default::default(),
entries: Entries::new(YAZI.mgr.show_hidden.get()),
stage: Default::default(),
offset: Default::default(),
@ -38,34 +42,36 @@ impl Default for Folder {
}
impl<T: Into<UrlBuf>> From<T> for Folder {
fn from(value: T) -> Self { Self { url: value.into(), ..Default::default() } }
fn from(value: T) -> Self {
Self { file: File::from_dummy(value, Some(ChaType::Dir)), ..Default::default() }
}
}
impl Folder {
pub fn update(&mut self, op: FilesOp) -> bool {
let (stage, revision) = (self.stage.clone(), self.entries.revision);
match op {
FilesOp::Full(_, _, cha) => {
(self.cha, self.stage) = (cha, FolderStage::Loaded);
FilesOp::Full(ref file, _) => {
(self.file, self.stage) = (file.clone(), FolderStage::Loaded);
}
FilesOp::Part(_, ref files, _) if files.is_empty() => {
(self.cha, self.stage) = (Cha::default(), FolderStage::Loading);
self.stage = FolderStage::Loading;
}
FilesOp::Part(_, _, ticket) if ticket == self.entries.ticket() => {
self.stage = FolderStage::Loading;
}
FilesOp::Done(_, cha, ticket) if ticket == self.entries.ticket() => {
(self.cha, self.stage) = (cha, FolderStage::Loaded);
FilesOp::Done(ref file, ticket) if ticket == self.entries.ticket() => {
(self.file, self.stage) = (file.clone(), FolderStage::Loaded);
}
FilesOp::IOErr(_, ref err) => {
(self.cha, self.stage) = (Cha::default(), FolderStage::Failed(err.clone()));
self.stage = FolderStage::Failed(err.clone());
}
_ => {}
}
let mut deleted = vec![];
match op {
FilesOp::Full(_, files, _) => self.entries.update_full(files),
FilesOp::Full(_, files) => self.entries.update_full(files),
FilesOp::Part(_, files, ticket) => self.entries.update_part(files, ticket),
FilesOp::Done(..) => {}
FilesOp::Size(_, sizes) => self.entries.update_size(sizes),

View file

@ -1,14 +1,10 @@
use std::time::Duration;
use tokio::{pin, task::JoinHandle};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use tokio::task::JoinHandle;
use yazi_adapter::ADAPTOR;
use yazi_config::{LAYOUT, YAZI};
use yazi_fs::{Entries, FilesOp, cha::Cha, file::File};
use yazi_fs::file::File;
use yazi_macro::render;
use yazi_runner::{RUNNER, previewer::{PeekError, PeekJob}};
use yazi_shared::{pool::Symbol, url::UrlBuf};
use yazi_vfs::{VfsEntries, VfsFilesOp};
use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock};
@ -17,9 +13,7 @@ pub struct Preview {
pub lock: Option<PreviewLock>,
pub skip: usize,
handle: Option<JoinHandle<()>>,
pub folder_lock: Option<UrlBuf>,
folder_loader: Option<JoinHandle<()>>,
handle: Option<JoinHandle<()>>,
}
impl Preview {
@ -47,36 +41,6 @@ impl Preview {
}));
}
pub fn go_folder(&mut self, file: File, dir: Option<Cha>, mime: Symbol<str>, force: bool) {
if self.folder_lock.as_ref() == Some(&file.url) {
return self.go(file, mime, force);
}
let wd = file.url_owned();
self.go(file, mime, force);
self.folder_lock = Some(wd.clone());
self.folder_loader.take().map(|h| h.abort());
self.folder_loader = Some(tokio::spawn(async move {
let Some(new) = Entries::assert_stale(&wd, dir.unwrap_or_default()).await else { return };
let rx = match Entries::from_dir(&wd).await {
Ok(rx) => rx,
Err(e) => return FilesOp::issue_error(&wd, e).await,
};
let stream =
UnboundedReceiverStream::new(rx).chunks_timeout(50000, Duration::from_millis(500));
pin!(stream);
let ticket = FilesOp::prepare(&wd);
while let Some(chunk) = stream.next().await {
FilesOp::Part(wd.clone(), chunk, ticket).emit();
}
FilesOp::Done(wd, new, ticket).emit();
}));
}
pub fn abort(&mut self) {
self.handle.take().map(|ct| ct.abort());
Highlighter::abort();
@ -103,6 +67,4 @@ impl Preview {
pub fn same_lock(&self, file: &File, mime: &str) -> bool {
self.same_file(file, mime) && matches!(&self.lock, Some(l) if l.skip == self.skip)
}
pub fn same_folder(&self, url: &UrlBuf) -> bool { self.folder_lock.as_ref() == Some(url) }
}

View file

@ -1,17 +1,14 @@
use yazi_shared::url::UrlBuf;
use yazi_fs::file::File;
use crate::tab::Tab;
pub struct TabSnap {
pub hovered: Option<UrlBuf>,
pub selected: Vec<UrlBuf>,
pub hovered: Option<File>,
pub selected: Vec<File>,
}
impl From<&Tab> for TabSnap {
fn from(value: &Tab) -> Self {
Self {
hovered: value.hovered_url().cloned(),
selected: value.selected.urls().cloned().collect(),
}
Self { hovered: value.hovered().cloned(), selected: value.selected.files().cloned().collect() }
}
}

View file

@ -1,5 +1,5 @@
use yazi_config::{YAZI, plugin::MAX_FETCHERS};
use yazi_fs::{Entries, FsHash64, SortBy, file::File};
use yazi_fs::{Entries, FsHash64, SortBy, file::{File, FileSig}};
use super::Tasks;
use crate::mgr::Mimetype;
@ -9,7 +9,7 @@ impl Tasks {
let mut loaded = self.scheduler.fetch.loaded.lock();
let mut tasks: [Vec<_>; MAX_FETCHERS as usize] = Default::default();
for f in paged {
let hash = f.hash_u64();
let hash = FileSig(f).hash_u64();
for g in YAZI.plugin.fetchers.matches(f, mimetype.get(&f.url).unwrap_or_default()) {
match loaded.get_mut(&hash) {
Some(n) if *n & (1 << g.idx) != 0 => continue,
@ -32,7 +32,7 @@ impl Tasks {
pub fn preload_paged(&self, paged: &[File], mimetype: &Mimetype) {
let mut loaded = self.scheduler.preload.loaded.lock();
for f in paged {
let hash = f.hash_u64();
let hash = FileSig(f).hash_u64();
for p in YAZI.plugin.preloaders.matches(f, mimetype.get(&f.url).unwrap_or_default()) {
match loaded.get_mut(&hash) {
Some(n) if *n & (1 << p.idx) != 0 => continue,

View file

@ -24,3 +24,6 @@ libc = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation-sys = { workspace = true }
objc2 = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.62.2", features = [ "Win32_System_Com" ] }

16
yazi-ffi/src/com.rs Normal file
View file

@ -0,0 +1,16 @@
use std::io;
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx, CoUninitialize};
pub struct Com;
impl Com {
pub fn new() -> io::Result<Self> {
unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok().map_err(io::Error::other)? };
Ok(Self)
}
}
impl Drop for Com {
fn drop(&mut self) { unsafe { CoUninitialize() } }
}

View file

@ -1,2 +1,5 @@
#[cfg(target_os = "macos")]
yazi_macro::mod_flat!(cf_dict cf_string disk_arbitration io_kit);
#[cfg(windows)]
yazi_macro::mod_flat!(com);

View file

@ -32,9 +32,9 @@ impl<'a> Input<'a> {
THEME.icon.matches(
&File {
url: Path::new(path).into(),
cha: Cha { kind: ChaKind::empty(), mode: mode.try_into().ok()?, ..Default::default() },
link_to: None,
url: Path::new(path).into(),
cha: Cha { kind: ChaKind::empty(), mode: mode.try_into().ok()?, ..Default::default() },
extra: Default::default(),
},
is_hovered,
)

View file

@ -23,6 +23,7 @@ yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
anyhow = { workspace = true }
arc-swap = { workspace = true }
bitflags = { workspace = true }
data-encoding = { workspace = true }
dirs = { workspace = true }
either = { workspace = true }
foldhash = { workspace = true }
@ -31,11 +32,11 @@ inventory = { workspace = true }
libc = { workspace = true }
mlua = { workspace = true }
parking_lot = { workspace = true }
percent-encoding = { workspace = true }
rand = { workspace = true }
regex = { workspace = true }
scopeguard = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
strum = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
@ -45,10 +46,12 @@ typed-path = { workspace = true }
uzers = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.62.2", features = [ "Win32_Foundation", "Win32_Storage_EnhancedStorage", "Win32_System_Com_StructuredStorage", "Win32_System_SystemServices", "Win32_UI_Shell_PropertiesSystem" ] }
windows-sys = { version = "0.61.2", features = [ "Win32_Storage_FileSystem" ] }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation-sys = { workspace = true }
ds_parser = "0.2.1"
objc2 = { workspace = true }
[target.'cfg(not(target_os = "android"))'.dependencies]

View file

@ -131,6 +131,16 @@ impl Cha {
self.kind |= kind;
self
}
#[inline]
pub fn follow(self, followed: Option<Self>) -> Self {
if !self.is_link() {
return self;
}
let retain = self.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM) | ChaKind::FOLLOW;
followed.unwrap_or(self).attach(retain)
}
}
impl Cha {

View file

@ -0,0 +1,21 @@
use std::{hash::{Hash, Hasher}, ops::Deref};
use crate::cha::Cha;
#[derive(Clone, Copy, Debug)]
pub struct ChaSig(pub Cha);
impl Deref for ChaSig {
type Target = Cha;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Hash for ChaSig {
fn hash<H: Hasher>(&self, state: &mut H) {
self.len.hash(state);
self.btime.hash(state);
self.ctime.hash(state);
self.mtime.hash(state);
}
}

View file

@ -67,17 +67,17 @@ impl UserData for Cha {
}
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("hash", |_, me, long: Option<bool>| {
Ok(if long.unwrap_or(false) {
methods.add_method("hash", |_, me, long: bool| {
Ok(if long {
format!("{:x}", me.hash_u128())
} else {
Err("Short hash not supported".into_lua_err())?
})
});
methods.add_method("perm", |lua, _me, ()| {
methods.add_method("perm", |_lua, _me, ()| {
Ok(
#[cfg(unix)]
lua.create_string(_me.mode.permissions(_me.is_dummy())),
_lua.create_string(_me.mode.permissions(_me.is_dummy())),
#[cfg(windows)]
Ok::<_, mlua::Error>(mlua::Value::Nil),
)

View file

@ -1 +1 @@
yazi_macro::mod_flat!(cha kind lua mode r#type);
yazi_macro::mod_flat!(cha cha_sig kind lua mode r#type);

View file

@ -30,7 +30,7 @@ impl Default for Cwd {
}
impl Cwd {
pub fn path(&self) -> PathBuf { self.0.load().as_url().unified_path().into_owned() }
pub fn path(&self) -> PathBuf { self.0.load().as_url().working_path().into_owned() }
pub fn set(&self, url: &UrlBuf, callback: fn()) -> bool {
if !url.is_absolute() {
@ -49,7 +49,7 @@ impl Cwd {
use std::{io::ErrorKind::{AlreadyExists, NotADirectory, NotFound}, path::Component as C};
let Some(cache) = url.cache() else {
return url.unified_path();
return url.working_path();
};
if !matches!(std::fs::create_dir_all(&cache), Err(e) if e.kind() == NotADirectory || e.kind() == AlreadyExists)

View file

@ -2,7 +2,7 @@ use std::{io, sync::Arc};
use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}};
use crate::{cha::{Cha, ChaType}, engine::FileHolder};
use crate::{cha::{Cha, ChaType}, engine::FileHolder, file::{File, FileExtra}};
pub enum DirEntry {
Regular(tokio::fs::DirEntry),
@ -10,6 +10,21 @@ pub enum DirEntry {
}
impl FileHolder for DirEntry {
async fn file(&self) -> io::Result<File> {
let cha = self.metadata().await?;
let url = self.url();
let (mut followed, mut link_to) = (None, None);
if cha.is_link() {
let path = url.as_local().expect("local entry path");
let name = path.file_name().unwrap_or_default();
followed = tokio::fs::metadata(path).await.ok().map(|m| Cha::new(name, m));
link_to = tokio::fs::read_link(path).await.ok().map(Into::into);
}
Ok(File { url, cha: cha.follow(followed), extra: FileExtra::new(link_to, None) })
}
async fn file_type(&self) -> io::Result<ChaType> {
match self {
Self::Regular(entry) | Self::Others { entry, .. } => entry.file_type().await.map(Into::into),

View file

@ -4,7 +4,7 @@ use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc};
use yazi_macro::ok_or_not_found;
use yazi_shared::{path::{DynPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}};
use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}};
use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}, file::{File, FileExtra}};
pub trait Engine: Sized {
type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin;
@ -78,6 +78,24 @@ pub trait Engine: Sized {
fn demand(&self) -> Self::Demand { Self::Demand::default() }
fn file(&self) -> impl Future<Output = io::Result<File>> {
async move {
let cha = self.symlink_metadata().await?;
let (mut followed, mut link_to) = (None, None);
if cha.is_link() {
followed = self.metadata().await.ok();
link_to = self.read_link().await.ok();
}
Ok(File {
url: self.url().to_owned(),
cha: cha.follow(followed),
extra: FileExtra::new(link_to, None),
})
}
}
fn hard_link<P>(&self, to: P) -> impl Future<Output = io::Result<()>>
where
P: DynPath;
@ -94,6 +112,25 @@ pub trait Engine: Sized {
fn read_link(&self) -> impl Future<Output = io::Result<PathBufDyn>>;
fn revalidate(&self, mut file: File) -> impl Future<Output = io::Result<Option<File>>> {
async move {
let cha = if !file.is_link() {
self.symlink_metadata().await?
} else if let Ok(new) = self.metadata().await {
file.cha.follow(Some(new))
} else {
self.symlink_metadata().await?
};
if cha.hits(file.cha) {
Ok(None)
} else {
file.cha = cha;
Ok(Some(file))
}
}
}
fn remove_dir(&self) -> impl Future<Output = io::Result<()>>;
fn remove_dir_all(&self) -> impl Future<Output = io::Result<()>> {
@ -204,6 +241,9 @@ pub trait DirReader {
// --- FileHolder
pub trait FileHolder {
#[must_use]
fn file(&self) -> impl Future<Output = io::Result<File>>;
#[must_use]
fn file_type(&self) -> impl Future<Output = io::Result<ChaType>>;

View file

@ -292,25 +292,20 @@ impl Entries {
// --- Show hidden
pub fn set_show_hidden(&mut self, state: bool) {
if self.show_hidden == state {
return;
}
self.show_hidden = state;
if self.show_hidden && self.hidden.is_empty() {
return;
} else if !self.show_hidden && self.items.is_empty() {
if mem::replace(&mut self.show_hidden, state) == state {
return;
}
let len = self.items.len();
let take =
if self.show_hidden { mem::take(&mut self.hidden) } else { mem::take(&mut self.items) };
let (hidden, items) = self.split_files(take);
self.hidden.extend(hidden);
if !items.is_empty() {
self.revision += 1;
self.items.extend(items);
if take.is_empty() {
return;
}
let (hidden, items) = self.split_files(take);
self.hidden.extend(hidden);
self.items.extend(items);
self.revision += (self.items.len() != len) as u64;
}
}

View file

@ -1,15 +1,16 @@
use std::{borrow::Cow, hash::{Hash, Hasher}, ops::Deref, path::Path};
use std::{borrow::Cow, ops::Deref, path::Path};
use serde::{Deserialize, Serialize};
use yazi_shared::{path::{PathBufDyn, PathDyn}, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}};
use yazi_shared::{path::PathDyn, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}};
use crate::cha::{Cha, ChaType};
use crate::{FsUrl, cha::{Cha, ChaType}, file::FileExtra};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct File {
pub url: UrlBuf,
pub cha: Cha,
pub link_to: Option<PathBufDyn>,
pub url: UrlBuf,
pub cha: Cha,
#[serde(flatten)]
pub extra: FileExtra,
}
impl Deref for File {
@ -43,12 +44,23 @@ impl File {
pub fn from_dummy(url: impl Into<UrlBuf>, r#type: Option<ChaType>) -> Self {
let url = url.into();
let cha = Cha::from_dummy(&url, r#type);
Self { url, cha, link_to: None }
Self { url, cha, extra: Default::default() }
}
#[inline]
pub fn chdir(&self, wd: &Path) -> Self {
Self { url: self.url.rebase(wd), cha: self.cha, link_to: self.link_to.clone() }
Self { url: self.url.rebase(wd), cha: self.cha, extra: self.extra.clone() }
}
#[inline]
pub fn content_path(&self) -> Cow<'_, Path> {
if let Some(backing) = self.extra.backing() {
backing.into()
} else if let Some(local) = self.url.as_local() {
local.into()
} else {
self.url.cache().expect("non-local URL should have a cache path").into()
}
}
}
@ -72,13 +84,3 @@ impl File {
#[inline]
pub fn stem(&self) -> Option<Strand<'_>> { self.url.stem() }
}
impl Hash for File {
fn hash<H: Hasher>(&self, state: &mut H) {
self.url.hash(state);
self.cha.len.hash(state);
self.cha.btime.hash(state);
self.cha.ctime.hash(state);
self.cha.mtime.hash(state);
}
}

View file

@ -0,0 +1,67 @@
use std::{path::{Path, PathBuf}, sync::Arc};
use mlua::{FromLua, Lua, Table, Value};
use serde::{Deserialize, Serialize};
use serde_with::{TryFromInto, serde_as};
use yazi_shared::path::PathBufDyn;
#[repr(transparent)]
#[derive(Clone, Debug, Default)]
pub struct FileExtra(Option<Arc<FileExtraInner>>);
#[serde_as]
#[derive(Debug, Default, Deserialize, Serialize)]
struct FileExtraInner {
link_to: Option<PathBufDyn>,
#[serde_as(as = "Option<TryFromInto<PathBufDyn>>")]
backing: Option<PathBuf>,
}
impl FileExtra {
#[inline]
pub fn new(link_to: Option<PathBufDyn>, backing: Option<PathBuf>) -> Self {
Self(
(link_to.is_some() || backing.is_some())
.then(|| Arc::new(FileExtraInner { link_to, backing })),
)
}
#[inline]
pub fn link_to(&self) -> Option<&PathBufDyn> { self.0.as_ref()?.link_to.as_ref() }
#[inline]
pub fn backing(&self) -> Option<&Path> { self.0.as_ref()?.backing.as_deref() }
}
impl TryFrom<Table> for FileExtra {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
Ok(Self::new(
value.raw_get("link_to")?,
value.raw_get::<Option<PathBufDyn>>("backing")?.map(PathBufDyn::into_os).transpose()?,
))
}
}
impl Serialize for FileExtra {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match &self.0 {
Some(inner) => inner.serialize(serializer),
None => FileExtraInner::default().serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for FileExtra {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let inner = FileExtraInner::deserialize(deserializer)?;
Ok(Self::new(inner.link_to, inner.backing))
}
}
impl FromLua for FileExtra {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
Table::from_lua(value, lua)?.try_into()
}
}

View file

@ -0,0 +1,26 @@
use std::{hash::{Hash, Hasher}, ops::Deref};
use yazi_shared::url::Url;
use crate::{cha::ChaSig, file::File};
#[derive(Clone, Copy, Debug)]
pub struct FileSig<'a>(pub &'a File);
impl Deref for FileSig<'_> {
type Target = File;
fn deref(&self) -> &Self::Target { self.0 }
}
impl Hash for FileSig<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
if let Some(backing) = self.extra.backing() {
Url::regular(backing).hash(state);
} else {
self.url.hash(state);
}
ChaSig(self.cha).hash(state);
}
}

View file

@ -15,7 +15,11 @@ impl TryFrom<Table> for File {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
Ok(Self { url: value.raw_get("url")?, cha: value.raw_get("cha")?, ..Default::default() })
Ok(Self {
url: value.raw_get("url")?,
cha: value.raw_get("cha")?,
extra: value.try_into()?,
})
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(data file_cov file file_ref files inventory lua);
yazi_macro::mod_flat!(data file file_cov file_extra file_ref file_sig files inventory lua);

View file

@ -1,26 +1,54 @@
use std::hash::{BuildHasher, Hash};
use data_encoding::BASE32_NOPAD;
use mlua::UserDataMethods;
use yazi_shared::url::{AsUrl, UrlBuf, UrlBufInventory};
use yazi_shared::{auth::Domain, url::{AsUrl, Url, UrlBuf, UrlBufInventory}};
use yazi_shim::Twox128;
use crate::{cha::Cha, file::File};
use crate::{cha::Cha, file::FileSig};
pub trait FsHash64 {
fn hash_u64(&self) -> u64;
}
impl FsHash64 for File {
fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
}
impl FsHash64 for UrlBuf {
fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self.as_url()) }
}
impl FsHash64 for FileSig<'_> {
fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
}
// Hash128
pub trait FsHash128 {
fn hash_u128(&self) -> u128;
fn hash_base32<'a>(&self, buf: &'a mut [u8; 26]) -> &'a str {
BASE32_NOPAD.encode_mut_str(&self.hash_u128().to_be_bytes(), buf)
}
}
impl FsHash128 for Url<'_> {
fn hash_u128(&self) -> u128 {
let mut h = Twox128::default();
self.auth().hash(&mut h);
for c in self.loc().components() {
c.hash(&mut h);
}
h.finish_128()
}
}
impl FsHash128 for UrlBuf {
fn hash_u128(&self) -> u128 { self.as_url().hash_u128() }
}
impl FsHash128 for Domain<'_> {
fn hash_u128(&self) -> u128 {
let mut h = Twox128::default();
self.hash(&mut h);
h.finish_128()
}
}
impl FsHash128 for Cha {
@ -35,10 +63,10 @@ impl FsHash128 for Cha {
}
}
impl<T: AsUrl> FsHash128 for T {
impl FsHash128 for FileSig<'_> {
fn hash_u128(&self) -> u128 {
let mut h = Twox128::default();
self.as_url().hash(&mut h);
self.hash(&mut h);
h.finish_128()
}
}

View file

@ -1,6 +1,6 @@
extern crate self as yazi_fs;
yazi_macro::mod_pub!(cha file mounts path engine);
yazi_macro::mod_pub!(cha file mounts path engine trash);
yazi_macro::mod_flat!(cwd spec entries filter fns hash op sorter sorting splatter stage url xdg);

View file

@ -4,15 +4,15 @@ use hashbrown::{HashMap, HashSet};
use yazi_macro::{impl_data_any, relay};
use yazi_shared::{id::{Id, Ids}, path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlLike, UrlMapExt}};
use crate::{cha::Cha, file::File};
use crate::file::File;
pub static FILES_TICKET: Ids = Ids::new();
#[derive(Clone, Debug)]
pub enum FilesOp {
Full(UrlBuf, Vec<File>, Cha),
Full(File, Vec<File>),
Part(UrlBuf, Vec<File>, Id),
Done(UrlBuf, Cha, Id),
Done(File, Id),
Size(UrlBuf, HashMap<PathBufDyn, u64>),
IOErr(UrlBuf, yazi_shim::fs::Error),
@ -27,9 +27,9 @@ impl_data_any!(FilesOp);
impl FilesOp {
pub fn cwd(&self) -> &UrlBuf {
match self {
Self::Full(u, ..) => u,
Self::Full(f, ..) => &f.url,
Self::Part(u, ..) => u,
Self::Done(u, ..) => u,
Self::Done(f, ..) => &f.url,
Self::Size(u, _) => u,
Self::IOErr(u, _) => u,
@ -42,7 +42,7 @@ impl FilesOp {
pub fn files(&self) -> Box<dyn Iterator<Item = &File> + '_> {
match self {
Self::Full(_, files, _) | Self::Part(_, files, _) | Self::Creating(_, files) => {
Self::Full(_, files) | Self::Part(_, files, _) | Self::Creating(_, files) => {
Box::new(files.iter().filter(|f| !f.entry_key().is_empty()))
}
Self::Done(..) => Box::new(iter::empty()),
@ -141,9 +141,9 @@ impl FilesOp {
let w = UrlBuf::from(wd);
match self {
Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha),
Self::Full(file, files) => Self::Full(file.chdir(wd), files!(files)),
Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket),
Self::Done(_, cha, ticket) => Self::Done(w, *cha, *ticket),
Self::Done(file, ticket) => Self::Done(file.chdir(wd), *ticket),
Self::Size(_, map) => Self::Size(w, map.iter().map(|(urn, &s)| (urn.clone(), s)).collect()),
Self::IOErr(_, err) => Self::IOErr(w, err.clone()),

View file

@ -1 +1 @@
yazi_macro::mod_flat!(clean expand normalize path percent relative);
yazi_macro::mod_flat!(clean expand normalize path relative);

View file

@ -1,40 +0,0 @@
use std::{borrow::Cow, path::{Path, PathBuf}};
use anyhow::Result;
use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode};
use yazi_shared::path::{PathCow, PathDyn, PathKind};
const SET: &AsciiSet = &CONTROLS
.add(b'"')
.add(b'*')
.add(b':')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'\\')
.add(b'|')
.add(b'%');
pub trait PercentEncoding<'a> {
fn percent_encode(self) -> Cow<'a, Path>;
fn percent_decode<K>(self, kind: K) -> Result<PathCow<'a>>
where
K: Into<PathKind>;
}
impl<'a> PercentEncoding<'a> for PathDyn<'a> {
fn percent_encode(self) -> Cow<'a, Path> {
match percent_encode(self.encoded_bytes(), SET).into() {
Cow::Borrowed(s) => Path::new(s).into(),
Cow::Owned(s) => PathBuf::from(s).into(),
}
}
fn percent_decode<K>(self, kind: K) -> Result<PathCow<'a>>
where
K: Into<PathKind>,
{
PathCow::with(kind, percent_decode(self.encoded_bytes()))
}
}

View file

@ -1,9 +1,9 @@
use std::path::PathBuf;
use yazi_shared::{auth::{Auth, AuthKind, EncodeAuth}, path::PathBufDyn, spec::SpecInventory};
use yazi_shared::{auth::{Auth, AuthKind}, path::PathBufDyn, spec::SpecInventory};
use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr};
use crate::Xdg;
use crate::{FsHash128, Xdg};
pub trait FsSpec {
fn cache(&self) -> Option<PathBuf>;
@ -18,7 +18,7 @@ impl FsSpec for Auth {
"{}_{}_{}",
self.kind.into_str(),
self.scheme,
EncodeAuth::domain(&self.domain)
self.domain.hash_base32(&mut [0; 26])
)))
}
}

View file

@ -2,11 +2,11 @@
use std::os::unix::ffi::{OsStrExt, OsStringExt};
#[cfg(windows)]
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::{cell::Cell, ffi::{OsStr, OsString}, iter::{self, Peekable}, mem};
use std::{cell::Cell, ffi::{OsStr, OsString}, iter::{self, Peekable}, mem, path::Path};
use yazi_shared::url::{AsUrl, Url};
use yazi_shared::url::UrlLike;
use crate::FsUrl;
use crate::file::File;
#[cfg(unix)]
type Iter<'a> = Peekable<std::iter::Copied<std::slice::Iter<'a, u8>>>;
@ -27,11 +27,11 @@ pub struct Splatter<T> {
pub trait Splatable {
fn tab(&self) -> usize;
fn selected(&self, tab: usize, idx: Option<usize>) -> impl Iterator<Item = Url<'_>>;
fn selected(&self, tab: usize, idx: Option<usize>) -> impl Iterator<Item = &File>;
fn hovered(&self, tab: usize) -> Option<Url<'_>>;
fn hovered(&self, tab: usize) -> Option<&File>;
fn yanked(&self, idx: Option<usize>) -> impl Iterator<Item = Url<'_>>;
fn yanked(&self, idx: Option<usize>) -> impl Iterator<Item = &File>;
}
#[cfg(unix)]
@ -91,15 +91,15 @@ where
let idx = self.consume_digit(it);
let mut first = true;
for url in self.src.selected(self.tab, idx) {
for file in self.src.selected(self.tab, idx) {
if !mem::replace(&mut first, false) {
buf.push(b' ' as _);
}
if c == Some('S') {
cue(buf, url.os_str());
cue(buf, file.url.os_str());
} else {
cue(buf, url.unified_path_str());
cue(buf, &*file.content_path());
}
}
if first && idx.is_some() {
@ -110,10 +110,10 @@ where
fn visit_hovered(&mut self, it: &mut Iter, buf: &mut Buf) {
match it.next().and_then(b2c) {
Some('h') => {
cue(buf, self.src.hovered(self.tab).map(|u| u.unified_path_str()).unwrap_or_default());
cue(buf, &*self.src.hovered(self.tab).map(|f| f.content_path()).unwrap_or_default());
}
Some('H') => {
cue(buf, self.src.hovered(self.tab).map(|u| u.os_str()).unwrap_or_default());
cue(buf, self.src.hovered(self.tab).map(|f| f.url.os_str()).unwrap_or_default());
}
_ => unreachable!(),
}
@ -124,15 +124,15 @@ where
let idx = self.consume_digit(it);
let mut first = true;
for url in self.src.selected(self.tab, idx) {
for file in self.src.selected(self.tab, idx) {
if !mem::replace(&mut first, false) {
buf.push(b' ' as _);
}
if c == Some('D') {
cue(buf, url.parent().map(|p| p.os_str()).unwrap_or_default());
cue(buf, file.url.parent().map(|p| p.os_str()).unwrap_or_default());
} else {
cue(buf, url.parent().map(|p| p.unified_path_str()).unwrap_or_default());
cue(buf, file.content_path().parent().unwrap_or(Path::new("")));
}
}
if first && idx.is_some() {
@ -157,15 +157,15 @@ where
let idx = self.consume_digit(it);
let mut first = true;
for url in self.src.yanked(idx) {
for file in self.src.yanked(idx) {
if !mem::replace(&mut first, false) {
buf.push(b' ' as _);
}
if c == Some('Y') {
cue(buf, url.os_str());
cue(buf, file.url.os_str());
} else {
cue(buf, url.unified_path_str());
cue(buf, &*file.content_path());
}
}
if first && idx.is_some() {
@ -204,16 +204,16 @@ impl<T> Splatter<T> {
impl Splatable for &Source {
fn tab(&self) -> usize { 0 }
fn selected(&self, _tab: usize, idx: Option<usize>) -> impl Iterator<Item = Url<'_>> {
fn selected(&self, _tab: usize, idx: Option<usize>) -> impl Iterator<Item = &File> {
if idx.is_none() {
self.0.set(true);
}
iter::empty()
}
fn hovered(&self, _tab: usize) -> Option<Url<'_>> { None }
fn hovered(&self, _tab: usize) -> Option<&File> { None }
fn yanked(&self, _idx: Option<usize>) -> impl Iterator<Item = Url<'_>> { iter::empty() }
fn yanked(&self, _idx: Option<usize>) -> impl Iterator<Item = &File> { iter::empty() }
}
let src = Source(Cell::new(false));
@ -222,68 +222,64 @@ impl<T> Splatter<T> {
}
}
impl<'a, I, T> Splatable for &'a I
impl<I> Splatable for &I
where
I: ?Sized,
&'a I: IntoIterator<Item = &'a T>,
T: AsUrl + 'a,
for<'a> &'a I: IntoIterator<Item = &'a File>,
{
fn tab(&self) -> usize { 1 }
fn selected(&self, tab: usize, mut idx: Option<usize>) -> impl Iterator<Item = Url<'_>> {
fn selected(&self, tab: usize, mut idx: Option<usize>) -> impl Iterator<Item = &File> {
idx = idx.and_then(|i| i.checked_sub(1));
(*self)
.into_iter()
.filter(move |_| tab == 1)
.map(|u| u.as_url())
.skip(idx.unwrap_or(0))
.take(if idx.is_some() { 1 } else { usize::MAX })
(*self).into_iter().filter(move |_| tab == 1).skip(idx.unwrap_or(0)).take(if idx.is_some() {
1
} else {
usize::MAX
})
}
fn hovered(&self, _tab: usize) -> Option<Url<'_>> { None }
fn hovered(&self, _tab: usize) -> Option<&File> { None }
fn yanked(&self, _idx: Option<usize>) -> impl Iterator<Item = Url<'_>> { iter::empty() }
fn yanked(&self, _idx: Option<usize>) -> impl Iterator<Item = &File> { iter::empty() }
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use super::*;
struct Source(usize);
static SELECTED: LazyLock<[Vec<File>; 2]> =
LazyLock::new(|| [vec![file("t1/s1"), file("t1/s2")], vec![file("t 2/s 1"), file("t 2/s 2")]]);
static HOVERED: LazyLock<[File; 2]> = LazyLock::new(|| [file("hovered"), file("hover ed")]);
static YANKED: LazyLock<[File; 3]> = LazyLock::new(|| [file("y1"), file("y 2"), file("y3")]);
fn file(path: &'static str) -> File { File::from_dummy(Path::new(path), None) }
impl Splatable for Source {
fn tab(&self) -> usize { self.0 }
fn selected(&self, tab: usize, mut idx: Option<usize>) -> impl Iterator<Item = Url<'_>> {
let urls = if tab == 1 {
vec![Url::regular("t1/s1"), Url::regular("t1/s2")]
} else if tab == 2 {
vec![Url::regular("t 2/s 1"), Url::regular("t 2/s 2")]
} else {
vec![]
};
fn selected(&self, tab: usize, mut idx: Option<usize>) -> impl Iterator<Item = &File> {
idx = idx.and_then(|i| i.checked_sub(1));
urls.into_iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX })
}
fn hovered(&self, tab: usize) -> Option<Url<'_>> {
if tab == 1 {
Some(Url::regular("hovered"))
} else if tab == 2 {
Some(Url::regular("hover ed"))
} else {
None
}
}
fn yanked(&self, mut idx: Option<usize>) -> impl Iterator<Item = Url<'_>> {
idx = idx.and_then(|i| i.checked_sub(1));
[Url::regular("y1"), Url::regular("y 2"), Url::regular("y3")]
tab
.checked_sub(1)
.and_then(|tab| SELECTED.get(tab))
.into_iter()
.flatten()
.skip(idx.unwrap_or(0))
.take(if idx.is_some() { 1 } else { usize::MAX })
}
fn hovered(&self, tab: usize) -> Option<&File> {
tab.checked_sub(1).and_then(|tab| HOVERED.get(tab))
}
fn yanked(&self, mut idx: Option<usize>) -> impl Iterator<Item = &File> {
idx = idx.and_then(|i| i.checked_sub(1));
YANKED.iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX })
}
}
#[test]
@ -332,4 +328,19 @@ mod tests {
assert_eq!(s, OsStr::new(expected), "{cmd}");
}
}
#[test]
#[cfg(unix)]
fn test_content_path() {
use crate::file::FileExtra;
let file = File {
url: Path::new("/logical/file").into(),
cha: Default::default(),
extra: FileExtra::new(None, Some("/real/file".into())),
};
let s = Splatter::new(&[file]).splat(OsStr::new("%s %S %d %D"));
assert_eq!(s, OsStr::new("/real/file /logical/file /real /logical"));
}
}

View file

@ -0,0 +1,53 @@
use std::{ffi::OsString, path::PathBuf};
use mlua::{IntoLua, Lua, Value};
use yazi_shared::path::PathBufDyn;
use crate::cha::Cha;
#[derive(Clone, Debug)]
pub struct TrashEntry {
pub name: OsString,
pub key: OsString,
pub cha: Cha,
pub link_to: Option<PathBuf>,
pub backing: PathBuf,
}
impl TrashEntry {
#[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))]
pub(super) fn new(path: PathBuf, name: OsString, key: OsString) -> std::io::Result<Self> {
use super::TrashCha;
let cha = Cha::from_trash(&path, &name, true)?;
let link_to = if cha.is_link() { std::fs::read_link(&path).ok() } else { None };
Ok(Self { name, key, cha, link_to, backing: path })
}
#[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))]
pub(super) fn into_file(self, url: impl Into<yazi_shared::url::UrlBuf>) -> crate::file::File {
use crate::file::{File, FileExtra};
File {
url: url.into(),
cha: self.cha,
extra: FileExtra::new(self.link_to.map(Into::into), Some(self.backing)),
}
}
}
impl IntoLua for TrashEntry {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
let Self { name, key, cha, link_to, backing } = self;
lua
.create_table_from([
("name", lua.create_external_string(name.into_encoded_bytes())?.into_lua(lua)?),
("key", lua.create_external_string(key.into_encoded_bytes())?.into_lua(lua)?),
("cha", cha.into_lua(lua)?),
("link_to", link_to.map(PathBufDyn::Os).into_lua(lua)?),
("backing", PathBufDyn::Os(backing).into_lua(lua)?),
])?
.into_lua(lua)
}
}

View file

@ -0,0 +1,171 @@
use std::{ffi::OsStr, fs, hash::Hash, io, path::{Path, PathBuf}};
use hashbrown::HashMap;
use trash::{TrashItem, os_limited};
use yazi_macro::ok_or_not_found;
use yazi_shim::Twox128;
use super::{TrashCha, TrashEntry, TrashNode, TrashNodes};
use crate::{cha::{Cha, ChaSig}, file::File};
pub struct Trash;
impl Trash {
pub fn new() -> io::Result<Self> { Ok(Self) }
pub fn list(&self, node: Option<&TrashNode>) -> io::Result<Vec<TrashEntry>> {
let Some(node) = node else {
return os_limited::list()
.map_err(io::Error::other)?
.into_iter()
.map(|item| TrashEntry::new(Self::path(&item.id)?, item.name, item.id))
.collect();
};
fs::read_dir(self.resolve(node)?)?
.map(|entry| {
let entry = entry?;
let name = entry.file_name();
let path = entry.path();
TrashEntry::new(path, name.clone(), name)
})
.collect()
}
pub fn entry(&self, node: &TrashNode) -> io::Result<TrashEntry> {
let path = self.resolve(node)?;
let name = path
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))?
.to_owned();
TrashEntry::new(path, name, node.key.clone())
}
pub fn metadata(&self, node: &TrashNode, follow: bool) -> io::Result<Cha> {
let path = self.resolve(node)?;
if let Some(name) = node.rel.file_name() {
Cha::from_trash(&path, name, follow)
} else {
let item = self.top_item(&node.top)?;
Cha::from_trash(&path, &item.name, follow)
}
}
pub(super) fn revalidate(
&self,
node: Option<&TrashNode>,
current: &File,
) -> io::Result<Option<File>> {
let latest = if let Some(node) = node {
self.entry(node)?.into_file(&current.url)
} else {
let mut roots: Vec<_> = os_limited::trash_folders().unwrap_or_default().into_iter().collect();
roots.sort_unstable();
let mut h = Twox128::default();
for root in roots {
let meta = ok_or_not_found!(fs::metadata(root.join("info")), continue);
let cha = Cha::new(root.file_name().unwrap_or_default(), meta);
root.hash(&mut h);
ChaSig(cha).hash(&mut h);
}
let hash = h.finish_128();
File {
cha: Cha { len: hash as u64 ^ (hash >> 64) as u64, ..Cha::from_mold(true) },
..current.clone()
}
};
let changed = !latest.cha.hits(current.cha)
|| latest.extra.link_to() != current.extra.link_to()
|| latest.extra.backing() != current.extra.backing();
Ok(changed.then_some(latest))
}
pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> {
if node.rel.as_os_str().is_empty() {
os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other)
} else {
fs::remove_file(self.resolve(node)?)
}
}
pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> {
if node.rel.as_os_str().is_empty() {
os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other)
} else {
fs::remove_dir(self.resolve(node)?)
}
}
pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> {
let mut tops = Vec::new();
let items: HashMap<_, _> = os_limited::list()
.map_err(io::Error::other)?
.into_iter()
.map(|item| (item.id.clone(), item))
.collect();
for node in nodes {
let item = items
.get(&node.top)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))?;
if node.rel.as_os_str().is_empty() {
tops.push(item.clone());
continue;
}
let from = self.resolve(&node)?;
let to = item.original_path().join(node.rel);
let is_dir = fs::symlink_metadata(&from)?.is_dir();
if let Some(parent) = to.parent() {
fs::create_dir_all(parent)?;
}
match if is_dir { fs::create_dir(&to) } else { fs::File::create_new(&to).map(|_| ()) } {
Ok(()) => fs::rename(from, to),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("restore target already exists: {to:?}"),
)),
Err(e) => Err(e),
}?;
}
os_limited::restore_all(tops).map_err(io::Error::other)
}
pub fn empty(&self) -> io::Result<()> {
os_limited::purge_all(os_limited::list().map_err(io::Error::other)?).map_err(io::Error::other)
}
fn resolve(&self, node: &TrashNode) -> io::Result<PathBuf> {
let path = Self::path(&node.top)?;
Ok(if node.rel.as_os_str().is_empty() { path } else { path.join(&node.rel) })
}
fn top_item(&self, key: &OsStr) -> io::Result<TrashItem> {
os_limited::list()
.map_err(io::Error::other)?
.into_iter()
.find(|item| item.id == key)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))
}
fn path(key: &OsStr) -> io::Result<PathBuf> {
let info = Path::new(key); // Path of the trash info file, e.g. "~/.local/share/Trash/info/filename.txt.trashinfo"
let parent = info
.parent()
.and_then(|parent| parent.parent())
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash item path"))?;
let stem = info
.file_stem()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash item path"))?;
Ok(parent.join("files").join(stem))
}
}

73
yazi-fs/src/trash/lua.rs Normal file
View file

@ -0,0 +1,73 @@
use std::io;
use mlua::{ExternalError, ExternalResult, IntoLuaMulti, LuaString, UserData, UserDataMethods, Value};
use tokio::task::spawn_blocking;
use yazi_binding::Error;
use super::{Trash, TrashNode, TrashNodes};
use crate::file::File;
impl UserData for Trash {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_function("empty", |lua, ()| async move {
match spawn_blocking(|| Trash::new()?.empty()).await.into_lua_err()? {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
}
});
methods.add_async_function("list", |lua, node: Option<TrashNode>| async move {
match spawn_blocking(move || Trash::new()?.list(node.as_ref())).await.into_lua_err()? {
Ok(items) => lua.create_sequence_from(items)?.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
});
methods.add_async_function("entry", |lua, node: TrashNode| async move {
match spawn_blocking(move || Trash::new()?.entry(&node)).await.into_lua_err()? {
Ok(entry) => entry.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
});
methods.add_async_function("metadata", |lua, (node, follow): (TrashNode, bool)| async move {
match spawn_blocking(move || Trash::new()?.metadata(&node, follow)).await.into_lua_err()? {
Ok(cha) => cha.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
});
methods.add_async_function(
"revalidate",
|lua, (node, file): (Option<TrashNode>, File)| async move {
match spawn_blocking(move || Trash::new()?.revalidate(node.as_ref(), &file))
.await
.into_lua_err()?
{
Ok(file) => file.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
},
);
methods.add_async_function("remove", |lua, (kind, node): (LuaString, TrashNode)| async move {
let f: fn(&Trash, &TrashNode) -> io::Result<()> = match &*kind.as_bytes() {
b"file" => Trash::remove_file,
b"dir" => Trash::remove_dir,
_ => Err("Removal type must be 'file' or 'dir'".into_lua_err())?,
};
match spawn_blocking(move || f(&Trash::new()?, &node)).await.into_lua_err()? {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
}
});
methods.add_async_function("restore", |lua, nodes: TrashNodes| async move {
match spawn_blocking(move || Trash::new()?.restore(nodes)).await.into_lua_err()? {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
}
});
}
}

188
yazi-fs/src/trash/macos.rs Normal file
View file

@ -0,0 +1,188 @@
use std::{ffi::{OsStr, OsString}, fs, io, path::{Component, Path, PathBuf}};
use ds_parser::Value;
use hashbrown::HashMap;
use yazi_macro::ok_or_not_found;
use super::{TrashCha, TrashEntry, TrashNode, TrashNodes};
use crate::{cha::Cha, file::File};
pub struct Trash;
impl Trash {
pub fn new() -> io::Result<Self> { Ok(Self) }
pub fn list(&self, node: Option<&TrashNode>) -> io::Result<Vec<TrashEntry>> {
let it = match fs::read_dir(self.resolve(node)?) {
Ok(it) => it,
Err(e) if e.kind() == io::ErrorKind::NotFound && node.is_none() => return Ok(vec![]),
Err(e) => return Err(e),
};
it.map(|entry| {
let entry = entry?;
let name = entry.file_name();
let path = entry.path();
let key = if node.is_some() { name.clone() } else { path.as_os_str().to_owned() };
TrashEntry::new(path, name, key)
})
.collect()
}
pub fn entry(&self, node: &TrashNode) -> io::Result<TrashEntry> {
let path = self.resolve(Some(node))?;
let name = path
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))?
.to_owned();
TrashEntry::new(path, name, node.key.clone())
}
pub fn metadata(&self, node: &TrashNode, follow: bool) -> io::Result<Cha> {
let path = self.resolve(Some(node))?;
Cha::from_trash(&path, path.file_name().unwrap_or_default(), follow)
}
pub(super) fn revalidate(
&self,
node: Option<&TrashNode>,
current: &File,
) -> io::Result<Option<File>> {
let latest = if let Some(node) = node {
self.entry(node)?.into_file(&current.url)
} else {
let path = self.resolve(None)?;
let cha = match fs::symlink_metadata(&path) {
Ok(meta) => Cha::new(path.file_name().unwrap_or_default(), meta),
Err(e) if e.kind() == io::ErrorKind::NotFound => Cha::from_mold(true),
Err(e) => return Err(e),
};
File { cha, ..current.clone() }
};
let changed = !latest.cha.hits(current.cha)
|| latest.extra.link_to() != current.extra.link_to()
|| latest.extra.backing() != current.extra.backing();
Ok(changed.then_some(latest))
}
pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> {
fs::remove_file(self.resolve(Some(node))?)
}
pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> {
fs::remove_dir(self.resolve(Some(node))?)
}
pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> {
let root = self.resolve(None)?;
let locations = OriginalLocation::parse(&fs::read(root.join(".DS_Store"))?)?;
for node in nodes {
let name = Path::new(&node.top).file_name().unwrap_or_default();
let location = locations.get(name).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("original location is unavailable for trash item: {:?}", node.top),
)
})?;
let from = self.resolve(Some(&node))?;
let to = location.join(&node)?;
let is_dir = fs::symlink_metadata(&from)?.is_dir();
if let Some(parent) = to.parent() {
fs::create_dir_all(parent)?;
}
match if is_dir { fs::create_dir(&to) } else { fs::File::create_new(&to).map(|_| ()) } {
Ok(()) => fs::rename(from, to),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("restore target already exists: {to:?}"),
)),
Err(e) => Err(e),
}?;
}
Ok(())
}
pub fn empty(&self) -> io::Result<()> {
let root = self.resolve(None)?;
for entry in ok_or_not_found!(fs::read_dir(root), return Ok(())) {
let entry = entry?;
if entry.file_type()?.is_dir() {
fs::remove_dir_all(entry.path())?;
} else {
fs::remove_file(entry.path())?;
}
}
Ok(())
}
fn resolve(&self, node: Option<&TrashNode>) -> io::Result<PathBuf> {
if let Some(node) = node {
let top = Path::new(&node.top);
return Ok(if node.rel.as_os_str().is_empty() { top.into() } else { top.join(&node.rel) });
}
Ok(
dirs::home_dir()
.filter(|p| p.is_absolute())
.ok_or_else(|| {
io::Error::other("cannot determine home directory for trash root resolution")
})?
.join(".Trash"),
)
}
}
// --- OriginalLocation
#[derive(Default)]
struct OriginalLocation {
parent: Option<PathBuf>,
name: Option<OsString>,
}
impl OriginalLocation {
fn parse(bytes: &[u8]) -> io::Result<HashMap<OsString, OriginalLocation>> {
let store = ds_parser::parse(bytes).map_err(io::Error::other)?;
let mut locations = HashMap::<OsString, Self>::new();
for record in store.records {
let Value::Ustr(value) = record.value else { continue };
if value.is_empty() {
continue;
}
let location = locations.entry_ref(OsStr::new(&record.name)).or_default();
match &record.field.fourcc().bytes() {
b"ptbL" => location.parent = Some(value.into()),
b"ptbN" => location.name = Some(value.into()),
_ => {}
}
}
Ok(locations)
}
fn join(&self, node: &TrashNode) -> io::Result<PathBuf> {
let parent = self.parent.as_deref().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back location")
})?;
let name = self.name.as_deref().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back name")
})?;
let mut components = Path::new(name).components();
if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash put-back name"));
}
let top_path = Path::new("/").join(parent).join(name);
Ok(if node.rel.as_os_str().is_empty() { top_path } else { top_path.join(&node.rel) })
}
}

16
yazi-fs/src/trash/mod.rs Normal file
View file

@ -0,0 +1,16 @@
yazi_macro::mod_flat!(entry lua node nodes);
#[cfg(target_os = "macos")]
yazi_macro::mod_flat!(macos);
#[cfg(windows)]
yazi_macro::mod_flat!(windows);
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))]
yazi_macro::mod_flat!(freedesktop);
#[cfg(any(target_os = "android", target_os = "ios"))]
yazi_macro::mod_flat!(unsupported);
#[cfg(not(any(target_os = "android", target_os = "ios")))]
yazi_macro::mod_flat!(traits);

49
yazi-fs/src/trash/node.rs Normal file
View file

@ -0,0 +1,49 @@
use std::{ffi::OsString, io, path::{Component, PathBuf}};
use mlua::{BorrowedBytes, FromLua, IntoLua, Lua, Table, Value};
use yazi_shared::{path::PathBufDyn, strand::AsStrand};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrashNode {
pub(super) key: OsString,
pub(super) top: OsString,
pub(super) rel: PathBuf,
}
impl TrashNode {
pub(super) fn validate(&self) -> io::Result<()> {
if self.key.is_empty() || self.top.is_empty() || !self.rel.is_relative() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node"));
}
if self.rel.components().any(|c| matches!(c, Component::ParentDir)) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node path"));
}
Ok(())
}
}
impl FromLua for TrashNode {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let t = Table::from_lua(value, lua)?;
let node = Self {
key: t.raw_get::<BorrowedBytes>("key")?.as_strand().to_os_string()?,
top: t.raw_get::<BorrowedBytes>("top")?.as_strand().to_os_string()?,
rel: t.raw_get::<PathBufDyn>("rel")?.into_os()?,
};
node.validate().map_err(mlua::Error::external)?;
Ok(node)
}
}
impl IntoLua for TrashNode {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
lua
.create_table_from([
("key", lua.create_external_string(self.key.into_encoded_bytes())?.into_lua(lua)?),
("top", lua.create_external_string(self.top.into_encoded_bytes())?.into_lua(lua)?),
("rel", PathBufDyn::from(self.rel).into_lua(lua)?),
])?
.into_lua(lua)
}
}

View file

@ -0,0 +1,36 @@
use std::vec;
use mlua::{FromLua, Lua, Value};
use super::TrashNode;
pub struct TrashNodes(Vec<TrashNode>);
impl TrashNodes {
fn new(mut nodes: Vec<TrashNode>) -> Self {
nodes.sort_unstable_by_key(|node| node.rel.components().count());
let mut seen = Vec::<TrashNode>::with_capacity(nodes.len());
for node in nodes {
if seen.iter().any(|parent| parent.top == node.top && node.rel.starts_with(&parent.rel)) {
continue;
}
seen.push(node);
}
Self(seen)
}
}
impl FromLua for TrashNodes {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let nodes = Vec::<TrashNode>::from_lua(value, lua)?;
Ok(Self::new(nodes))
}
}
impl IntoIterator for TrashNodes {
type IntoIter = vec::IntoIter<TrashNode>;
type Item = TrashNode;
fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
}

View file

@ -0,0 +1,36 @@
use crate::cha::{Cha, ChaKind, ChaMode};
pub(super) trait TrashCha: Sized {
fn from_mold(is_dir: bool) -> Self;
#[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))]
fn from_trash(
path: &std::path::Path,
name: &std::ffi::OsStr,
follow: bool,
) -> std::io::Result<Self>;
}
impl TrashCha for Cha {
fn from_mold(is_dir: bool) -> Self {
let mut cha = Self::default();
cha.kind.remove(ChaKind::DUMMY);
cha.mode = if is_dir { ChaMode::T_DIR | ChaMode::U_EXEC } else { ChaMode::T_FILE };
cha.mode |= ChaMode::U_READ | ChaMode::U_WRITE;
cha
}
#[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))]
fn from_trash(
path: &std::path::Path,
name: &std::ffi::OsStr,
follow: bool,
) -> std::io::Result<Self> {
let cha = Cha::new(name, std::fs::symlink_metadata(path)?);
Ok(if cha.is_link() && follow {
cha.follow(std::fs::metadata(path).ok().map(|meta| Cha::new(name, meta)))
} else {
cha
})
}
}

View file

@ -0,0 +1,46 @@
use std::io;
use super::{TrashEntry, TrashNode, TrashNodes};
use crate::{cha::Cha, file::File};
pub struct Trash;
impl Trash {
pub fn new() -> io::Result<Self> { Ok(Self) }
pub fn list(&self, _node: Option<&TrashNode>) -> io::Result<Vec<TrashEntry>> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub fn entry(&self, _node: &TrashNode) -> io::Result<TrashEntry> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub fn metadata(&self, _node: &TrashNode, _: bool) -> io::Result<Cha> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub(super) fn revalidate(
&self,
_node: Option<&TrashNode>,
_current: &File,
) -> io::Result<Option<File>> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub fn remove_file(&self, _node: &TrashNode) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub fn remove_dir(&self, _node: &TrashNode) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub fn restore(&self, _nodes: TrashNodes) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub fn empty(&self) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
}

View file

@ -0,0 +1,330 @@
use std::{ffi::{OsStr, OsString, c_void}, fs, hash::Hash, io, mem, os::windows::ffi::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}};
use hashbrown::HashMap;
use trash::{TrashItem, os_limited};
use windows::{Win32::{Foundation::*, Storage::EnhancedStorage::*, System::{Com::*, SystemServices::*}, UI::Shell::*}, core::{Interface, PCWSTR}};
use yazi_ffi::Com;
use yazi_shim::Twox128;
use super::{TrashCha, TrashEntry, TrashNode, TrashNodes};
use crate::{cha::Cha, file::File};
thread_local! {
static COM: io::Result<Com> = Com::new();
}
pub struct Trash;
impl Trash {
pub fn new() -> io::Result<Self> {
COM.with(|result| {
result.as_ref().map(|_| Self).map_err(|e| io::Error::new(e.kind(), e.to_string()))
})
}
pub fn list(&self, node: Option<&TrashNode>) -> io::Result<Vec<TrashEntry>> {
let Some(node) = node else {
return os_limited::list()
.map_err(io::Error::other)?
.into_iter()
.map(|item| ShellItem::new(&item.id)?.entry(item.name, item.id))
.collect();
};
self
.resolve(node)?
.children()?
.into_iter()
.map(|item| {
let name = item.display_name(SIGDN_PARENTRELATIVE)?;
item.entry(name.clone(), name)
})
.collect()
}
pub fn entry(&self, node: &TrashNode) -> io::Result<TrashEntry> {
let item = self.resolve(node)?;
item.entry(item.display_name(SIGDN_PARENTRELATIVE)?, &node.key)
}
pub fn metadata(&self, node: &TrashNode, _: bool) -> io::Result<Cha> { self.resolve(node)?.cha() }
pub(super) fn revalidate(
&self,
node: Option<&TrashNode>,
current: &File,
) -> io::Result<Option<File>> {
let cha =
if let Some(node) = node { TrashSig::item(&self.resolve(node)?)? } else { TrashSig::root()? };
Ok(if cha.hits(current.cha) { None } else { Some(File { cha, ..current.clone() }) })
}
pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> {
if node.rel.as_os_str().is_empty() {
os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other)
} else {
self.resolve(node)?.delete()
}
}
pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> {
if node.rel.as_os_str().is_empty() {
return os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other);
}
let item = self.resolve(node)?;
if !item.children()?.is_empty() {
return Err(io::Error::new(io::ErrorKind::DirectoryNotEmpty, "trash directory is not empty"));
}
item.delete()
}
pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> {
let mut tops = Vec::new();
let items: HashMap<_, _> = os_limited::list()
.map_err(io::Error::other)?
.into_iter()
.map(|item| (item.id.clone(), item))
.collect();
for node in nodes {
let item = items
.get(&node.top)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))?;
if node.rel.as_os_str().is_empty() {
tops.push(item.clone());
} else {
let to = item.original_path().join(&node.rel);
self.restore_do(&self.resolve(&node)?, &to)?;
}
}
os_limited::restore_all(tops).map_err(io::Error::other)
}
fn restore_do(&self, item: &ShellItem, to: &Path) -> io::Result<()> {
match fs::symlink_metadata(to) {
Ok(_) => {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("restore target already exists: {to:?}"),
));
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
// Create parent directories
let parent = to
.parent()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))?;
fs::create_dir_all(parent)?;
let parent = ShellItem::new(parent.as_os_str())?;
let name: Vec<u16> = to
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))?
.encode_wide()
.chain([0])
.collect();
operate(FOF_NO_UI | FOFX_EARLYFAILURE, |operation| unsafe {
operation.MoveItem(&item.0, &parent.0, PCWSTR(name.as_ptr()), None)
})
}
pub fn empty(&self) -> io::Result<()> {
os_limited::purge_all(os_limited::list().map_err(io::Error::other)?).map_err(io::Error::other)
}
fn resolve(&self, node: &TrashNode) -> io::Result<ShellItem> {
let top = ShellItem::new(&node.top)?;
if node.rel.as_os_str().is_empty() {
return Ok(top);
}
let path = PathBuf::from(top.display_name(SIGDN_FILESYSPATH)?).join(&node.rel);
ShellItem::new(path.as_os_str())
}
fn top_item(&self, key: &OsStr) -> io::Result<TrashItem> {
os_limited::list()
.map_err(io::Error::other)?
.into_iter()
.find(|item| item.id == key)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))
}
}
// --- TrashSig
struct TrashSig {
names: Option<Vec<OsString>>,
count_size: Option<(i64, i64)>,
}
impl TrashSig {
fn root() -> io::Result<Cha> {
let root = ShellItem::root()?;
let cha = root.cha()?;
let mut info =
SHQUERYRBINFO { cbSize: mem::size_of::<SHQUERYRBINFO>() as u32, ..Default::default() };
let sig = if unsafe { SHQueryRecycleBinW(PCWSTR::null(), &mut info) }.is_ok() {
Self { names: None, count_size: Some((info.i64NumItems, info.i64Size)) }
} else if cha.mtime.is_some() {
Self { names: None, count_size: None }
} else {
Self { names: Some(Self::names(&root)?), count_size: None }
};
Ok(sig.into_cha(cha))
}
fn item(item: &ShellItem) -> io::Result<Cha> {
let cha = item.cha()?;
let sig = if cha.mtime.is_none() && cha.is_dir() {
Self { names: Some(Self::names(item)?), count_size: None }
} else {
Self { names: None, count_size: None }
};
Ok(sig.into_cha(cha))
}
fn names(item: &ShellItem) -> io::Result<Vec<OsString>> {
let mut names: Vec<_> = item
.children()?
.into_iter()
.map(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING))
.collect::<io::Result<_>>()?;
names.sort_unstable();
Ok(names)
}
fn into_cha(self, mut cha: Cha) -> Cha {
let mut h = Twox128::default();
if let Some((count, size)) = self.count_size {
(size, count).hash(&mut h);
} else if let Some(names) = self.names {
names.hash(&mut h);
} else {
return cha;
}
let hash = h.finish_128();
cha.ctime = UNIX_EPOCH.checked_add(Duration::from_nanos(hash as u64 ^ (hash >> 64) as u64));
cha
}
}
// --- ShellItem
struct ShellItem(IShellItem);
impl ShellItem {
fn new(name: &OsStr) -> io::Result<Self> {
let name: Vec<u16> = name.encode_wide().chain([0]).collect();
unsafe { SHCreateItemFromParsingName(PCWSTR(name.as_ptr()), None) }.map(Self).map_err(error)
}
fn root() -> io::Result<Self> {
unsafe { SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, None) }
.map(Self)
.map_err(error)
}
fn children(&self) -> io::Result<Vec<Self>> {
let items: IEnumShellItems =
unsafe { self.0.BindToHandler(None, &BHID_EnumItems).map_err(error)? };
let mut result = Vec::new();
loop {
let mut fetched = 0;
let mut next = [None];
unsafe { items.Next(&mut next, Some(&mut fetched)).map_err(error)? };
if fetched == 0 {
break;
} else if let Some(item) = next[0].take() {
result.push(Self(item));
}
}
Ok(result)
}
fn cha(&self) -> io::Result<Cha> {
if let Ok(backing) = self.display_name(SIGDN_FILESYSPATH).map(PathBuf::from) {
match fs::metadata(&backing) {
Ok(meta) => return Ok(Cha::new(backing.file_name().unwrap_or_default(), meta)),
Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(e),
Err(_) => {}
}
}
let item: IShellItem2 = self.0.cast().map_err(error)?;
let is_dir = unsafe { self.0.GetAttributes(SFGAO_FOLDER) }.map_err(error)? == SFGAO_FOLDER;
let mut cha = Cha::from_mold(is_dir);
cha.len = unsafe { item.GetUInt64(&PKEY_Size) }.unwrap_or_default();
cha.mtime = unsafe { item.GetFileTime(&PKEY_DateModified) }.ok().and_then(system_time);
Ok(cha)
}
fn entry<N, K>(&self, name: N, key: K) -> io::Result<TrashEntry>
where
N: Into<OsString>,
K: Into<OsString>,
{
Ok(TrashEntry {
name: name.into(),
key: key.into(),
cha: self.cha()?,
link_to: None,
backing: self.display_name(SIGDN_FILESYSPATH)?.into(),
})
}
fn delete(&self) -> io::Result<()> {
operate(FOF_NO_UI, |operation| unsafe { operation.DeleteItem(&self.0, None) })
}
fn display_name(&self, kind: SIGDN) -> io::Result<OsString> {
unsafe {
let name = self.0.GetDisplayName(kind).map_err(error)?;
let result = OsString::from_wide(name.as_wide());
CoTaskMemFree(Some(name.0.cast::<c_void>()));
Ok(result)
}
}
}
fn operate<F>(flags: FILEOPERATION_FLAGS, f: F) -> io::Result<()>
where
F: FnOnce(&IFileOperation) -> windows::core::Result<()>,
{
let aborted = unsafe {
let operation: IFileOperation =
CoCreateInstance(&FileOperation, None, CLSCTX_ALL).map_err(error)?;
operation.SetOperationFlags(flags).map_err(error)?;
f(&operation).map_err(error)?;
operation.PerformOperations().map_err(error)?;
operation.GetAnyOperationsAborted().map_err(error)?.as_bool()
};
if aborted { Err(io::Error::other("trash operation was aborted")) } else { Ok(()) }
}
fn error(error: windows::core::Error) -> io::Error {
if error.code() == ERROR_FILE_NOT_FOUND.to_hresult()
|| error.code() == ERROR_PATH_NOT_FOUND.to_hresult()
{
io::Error::new(io::ErrorKind::NotFound, error)
} else {
io::Error::other(error)
}
}
fn system_time(time: FILETIME) -> Option<SystemTime> {
let ticks = (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime);
let ticks = ticks.checked_sub(116_444_736_000_000_000)?;
Some(UNIX_EPOCH + Duration::new(ticks / 10_000_000, (ticks % 10_000_000) as u32 * 100))
}

View file

@ -1,54 +1,36 @@
use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}};
use std::{borrow::Cow, path::{Path, PathBuf}};
use mlua::UserDataFields;
use yazi_shared::{path::{DynPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}};
use yazi_shared::url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike};
use yazi_shim::mlua::UserDataFieldsExt;
use crate::{FsHash128, FsSpec, path::PercentEncoding};
use crate::{FsHash128, FsSpec};
pub trait FsUrl<'a> {
fn cache(&self) -> Option<PathBuf>;
fn cache_lock(&self) -> Option<PathBuf>;
fn unified_path(self) -> Cow<'a, Path>;
fn unified_path_str(self) -> Cow<'a, OsStr>
where
Self: Sized,
{
match self.unified_path() {
Cow::Borrowed(p) => p.as_os_str().into(),
Cow::Owned(p) => p.into_os_string().into(),
}
}
fn working_path(self) -> Cow<'a, Path>;
}
impl<'a> FsUrl<'a> for Url<'a> {
fn cache(&self) -> Option<PathBuf> {
fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf {
let mut it = loc.components();
if it.next() == Some(yazi_shared::path::Component::RootDir) {
root.push(it.dyn_path().percent_encode());
} else {
root.push(".%2F");
root.push(loc.percent_encode());
}
self.auth().cache().map(|mut root| {
root.push(self.hash_base32(&mut [0; 26]));
root
}
self.auth().cache().map(|root| with_loc(self.loc(), root))
})
}
fn cache_lock(&self) -> Option<PathBuf> {
self.auth().cache().map(|mut root| {
root.push("%lock");
root.push(format!("{:x}", self.hash_u128()));
root.push(self.hash_base32(&mut [0; 26]));
root
})
}
fn unified_path(self) -> Cow<'a, Path> {
fn working_path(self) -> Cow<'a, Path> {
match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(),
Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
@ -63,7 +45,7 @@ impl FsUrl<'_> for UrlBuf {
fn cache_lock(&self) -> Option<PathBuf> { self.as_url().cache_lock() }
fn unified_path(self) -> Cow<'static, Path> {
fn working_path(self) -> Cow<'static, Path> {
match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(),
Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
@ -78,7 +60,7 @@ impl<'a> FsUrl<'a> for UrlCow<'a> {
fn cache_lock(&self) -> Option<PathBuf> { self.as_url().cache_lock() }
fn unified_path(self) -> Cow<'a, Path> {
fn working_path(self) -> Cow<'a, Path> {
match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(),
Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {

View file

@ -1,13 +1,13 @@
#[macro_export]
macro_rules! emit {
(Call($action:expr)) => {
yazi_shared::event::Event::Call(yazi_shared::event::ActionCow::from($action)).emit();
yazi_shared::event::Event::Call(yazi_shared::event::ActionCow::from($action)).emit()
};
(Seq($actions:expr)) => {
yazi_shared::event::Event::Seq($actions).emit();
yazi_shared::event::Event::Seq($actions).emit()
};
($event:ident) => {
yazi_shared::event::Event::$event.emit();
yazi_shared::event::Event::$event.emit()
};
}

View file

@ -1,7 +1,7 @@
#[macro_export]
macro_rules! render {
() => {
yazi_shared::event::NEED_RENDER.store(1, std::sync::atomic::Ordering::Relaxed);
yazi_shared::event::NEED_RENDER.store(1, std::sync::atomic::Ordering::Relaxed)
};
($cond:expr) => {
if $cond {

View file

@ -317,7 +317,7 @@ function M.treelize(items, tops, parents)
end
local buf, it = {}, f.path.parent
while it and it ~= tops[#tops] do
while it and #it ~= 0 and it ~= tops[#tops] do
buf[#buf + 1], it = it, it.parent
end
for i = #buf, 1, -1 do

View file

@ -3,7 +3,7 @@ local M = {}
function M:peek(job)
local start, url = os.clock(), ya.file_cache(job)
if not url or not fs.cha(url) then
url = job.file.url
url = Url(job.file.path)
end
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
@ -20,7 +20,7 @@ function M:preload(job)
return true
end
return ya.image_precache(job.file.url, cache)
return ya.image_precache(Url(job.file.path), cache)
end
function M:spot(job)
@ -41,7 +41,7 @@ function M:spot(job)
end
function M:spot_base(job)
local info = ya.image_info(job.file.url)
local info = ya.image_info(Url(job.file.path))
if not info then
return {}
end

View file

@ -6,11 +6,7 @@ local M = {}
function M:fetch(job)
local urls, paths = {}, {}
for i, file in ipairs(job.files) do
if file.cache then
urls[i], paths[i] = tostring(file.url), tostring(file.cache)
else
paths[i] = tostring(file.path)
end
urls[i], paths[i] = file.url, tostring(file.path)
end
local child, err = M.spawn_file1(paths)
@ -42,7 +38,7 @@ function M:fetch(job)
match, ignore = M.match_mimetype(line)
if match then
updates[urls[i] or paths[i]], state[i], i = match, true, i + 1
updates[urls[i]], state[i], i = match, true, i + 1
flush(false)
elseif not ignore then
state[i], i = false, i + 1
@ -103,7 +99,7 @@ function M.placeholder(err, urls, paths)
local updates = {}
for i = 1, #paths do
updates[urls[i] or paths[i]] = "null/file1-not-found"
updates[urls[i]] = "null/file1-not-found"
end
ya.emit("update_mimes", { updates = updates })

View file

@ -0,0 +1,153 @@
local M = {}
local function top(url)
while url.parent and url.parent.name do
url = url.parent
end
return url.name and url or nil
end
local function node(url)
local top = top(url)
if not top then
return
end
return {
key = url.spec.domain,
top = top.spec.domain,
rel = url:strip_prefix(top) or Path.os(""),
}
end
local function absolute(url)
if url.is_absolute then
return fs.clean_url(url)
end
local cwd, err = fs.cwd()
if not cwd then
return nil, err
end
local root = cwd.path
while root.parent do
root = root.parent
end
return fs.clean_url(url:join(root:join(url.path)))
end
local function file(url, entry)
entry.url = url
return File(entry)
end
local function files(parent, entries)
for i, entry in ipairs(entries) do
local url = parent:join(Path.os(entry.name)):into_domain(entry.key)
entries[i] = file(url, entry)
end
return entries
end
local function notify(action, err)
ya.notify {
title = "Trash",
content = string.format("Failed to %s: %s", action, err),
level = "error",
timeout = 10,
}
end
function M:setup()
ps.sub_remote("trash-restore", function(args)
ya.async(function()
local nodes = {}
for i, arg in ipairs(args) do
nodes[i] = node(Url(arg))
if not nodes[i] then
return notify("restore", "Cannot restore the trash root")
end
end
local ok, err = fs.trash.restore(nodes)
if ok then
ya.emit("refresh", {})
else
notify("restore", err)
end
end)
end)
ps.sub_remote("trash-empty", function()
ya.async(function()
local confirmed = ya.confirm {
pos = { "center", w = 60, h = 10 },
title = ui.Line("Empty trash?"):style(th.confirm.title),
body = ui.Text("All items in the trash will be permanently deleted."),
}
if not confirmed then
return
end
local ok, err = fs.trash.empty()
if ok then
ya.emit("refresh", {})
else
notify("empty", err)
end
end)
end)
end
function M:entry()
local url, err = absolute(Url("trash:///@/."))
if url then
ya.emit("cd", { url })
else
notify("open", err)
end
end
function M:provide(job)
local op = job.op
if op == "Absolute" or op == "Canonicalize" then
return absolute(job.url)
elseif op == "Casefold" then
return job.url
elseif op == "Metadata" or op == "SymlinkMetadata" then
local n = node(job.url)
if n then
return fs.trash.metadata(n, op == "Metadata")
else
return Cha { mode = tonumber("40700", 8) }
end
elseif op == "ReadDir" then
local entries, err = fs.trash.list(node(job.url))
if entries then
return files(job.url, entries)
else
return nil, err
end
elseif op == "Revalidate" then
return fs.trash.revalidate(node(job.file.url), job.file)
elseif op == "File" then
local n = node(job.url)
if not n then
return nil, Error.custom("Cannot construct a file for the trash root")
end
local entry, err = fs.trash.entry(n)
if entry then
return file(job.url, entry)
else
return nil, err
end
elseif op == "RemoveFile" then
return fs.trash.remove("file", node(job.url))
elseif op == "RemoveDir" then
return fs.trash.remove("dir", node(job.url))
end
return false, Error.custom("Unsupported trash operation: " .. op)
end
return M

View file

@ -2,3 +2,4 @@ os.setlocale("")
require("dds"):setup()
require("extract"):setup()
require("trash"):setup()

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use tokio::{io::{AsyncBufReadExt, BufReader}, process::{Child, Command}, sync::mpsc::{self, UnboundedReceiver}};
use yazi_fs::{FsUrl, file::File};
use yazi_shared::url::{AsUrl, UrlBuf, UrlLike};
use yazi_vfs::VfsFile;
use yazi_vfs::engine;
pub struct FdOpt {
pub cwd: UrlBuf,
@ -27,7 +27,7 @@ pub fn fd(opt: FdOpt) -> Result<UnboundedReceiver<File>> {
let Ok(url) = opt.cwd.try_join(line) else {
continue;
};
if let Ok(file) = File::new(url).await {
if let Ok(file) = engine::file(url).await {
tx.send(file).ok();
}
}
@ -39,7 +39,7 @@ pub fn fd(opt: FdOpt) -> Result<UnboundedReceiver<File>> {
fn spawn(program: &str, opt: &FdOpt) -> std::io::Result<Child> {
Command::new(program)
.arg("--base-directory")
.arg(&*opt.cwd.as_url().unified_path())
.arg(&*opt.cwd.as_url().working_path())
.arg("--regex")
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
.args(&opt.args)

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}};
use yazi_fs::{FsUrl, file::File};
use yazi_shared::url::{AsUrl, UrlBuf, UrlLike};
use yazi_vfs::VfsFile;
use yazi_vfs::engine;
pub struct RgOpt {
pub cwd: UrlBuf,
@ -19,7 +19,7 @@ pub fn rg(opt: RgOpt) -> Result<UnboundedReceiver<File>> {
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
.args(opt.args)
.arg(opt.subject)
.current_dir(&*opt.cwd.as_url().unified_path())
.current_dir(opt.cwd.as_url().working_path())
.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::null())
@ -36,7 +36,7 @@ pub fn rg(opt: RgOpt) -> Result<UnboundedReceiver<File>> {
let Ok(url) = opt.cwd.try_join(line) else {
continue;
};
if let Ok(file) = File::new(url).await {
if let Ok(file) = engine::file(url).await {
tx.send(file).ok();
}
}

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}};
use yazi_fs::{FsUrl, file::File};
use yazi_shared::url::{AsUrl, UrlBuf, UrlLike};
use yazi_vfs::VfsFile;
use yazi_vfs::engine;
pub struct RgaOpt {
pub cwd: UrlBuf,
@ -19,7 +19,7 @@ pub fn rga(opt: RgaOpt) -> Result<UnboundedReceiver<File>> {
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
.args(opt.args)
.arg(opt.subject)
.current_dir(&*opt.cwd.as_url().unified_path())
.current_dir(opt.cwd.as_url().working_path())
.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::null())
@ -36,7 +36,7 @@ pub fn rga(opt: RgaOpt) -> Result<UnboundedReceiver<File>> {
let Ok(url) = opt.cwd.try_join(line) else {
continue;
};
if let Ok(file) = File::new(url).await {
if let Ok(file) = engine::file(url).await {
tx.send(file).ok();
}
}

View file

@ -3,7 +3,7 @@ use yazi_binding::Error;
pub enum SizeCalculator {
Local(yazi_fs::engine::local::SizeCalculator),
Remote(yazi_vfs::engine::SizeCalculator),
Virtual(yazi_vfs::engine::SizeCalculator),
}
impl UserData for SizeCalculator {
@ -11,7 +11,7 @@ impl UserData for SizeCalculator {
fields.add_field_method_get("cha", |_, me| {
Ok(match me {
Self::Local(c) => c.cha(),
Self::Remote(c) => c.cha(),
Self::Virtual(c) => c.cha(),
})
});
}
@ -20,7 +20,7 @@ impl UserData for SizeCalculator {
methods.add_async_method_mut("recv", |lua, mut me, ()| async move {
let next = match &mut *me {
Self::Local(c) => c.next().await,
Self::Remote(c) => c.next().await,
Self::Virtual(c) => c.next().await,
};
match next {

View file

@ -5,7 +5,7 @@ use yazi_binding::{Composer, ComposerGet, ComposerSet, Error};
use yazi_config::Pattern;
use yazi_fs::{engine::{Attrs, DirReader, FileHolder}, file::File, mounts::PARTITIONS};
use yazi_shared::url::{UrlBuf, UrlCow, UrlLike, UrlRef};
use yazi_vfs::{VfsFile, engine};
use yazi_vfs::engine;
use crate::fs::SizeCalculator;
@ -15,6 +15,7 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
b"access" => access(lua)?,
b"calc_size" => calc_size(lua)?,
b"cha" => cha(lua)?,
b"clean_url" => clean_url(lua)?,
b"copy" => copy(lua)?,
b"create" => create(lua)?,
b"cwd" => cwd(lua)?,
@ -25,6 +26,7 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
b"read_dir" => read_dir(lua)?,
b"remove" => remove(lua)?,
b"rename" => rename(lua)?,
b"trash" => return yazi_fs::trash::Trash.into_lua(lua),
b"unique" => unique(lua)?,
b"write" => write(lua)?,
_ => return Ok(Value::Nil),
@ -46,7 +48,7 @@ fn calc_size(lua: &Lua) -> mlua::Result<Function> {
let it = if let Some(path) = url.as_local() {
yazi_fs::engine::local::SizeCalculator::new(path).await.map(SizeCalculator::Local)
} else {
yazi_vfs::engine::SizeCalculator::new(&*url).await.map(SizeCalculator::Remote)
yazi_vfs::engine::SizeCalculator::new(&*url).await.map(SizeCalculator::Virtual)
};
match it {
@ -57,12 +59,9 @@ fn calc_size(lua: &Lua) -> mlua::Result<Function> {
}
fn cha(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, follow): (UrlRef, Option<bool>)| async move {
let cha = if follow.unwrap_or(false) {
engine::metadata(&*url).await
} else {
engine::symlink_metadata(&*url).await
};
lua.create_async_function(|lua, (url, follow): (UrlRef, bool)| async move {
let cha =
if follow { engine::metadata(&*url).await } else { engine::symlink_metadata(&*url).await };
match cha {
Ok(c) => c.into_lua_multi(&lua),
@ -71,6 +70,10 @@ fn cha(lua: &Lua) -> mlua::Result<Function> {
})
}
fn clean_url(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, url: UrlRef| Ok(yazi_fs::path::clean_url(&*url)))
}
fn copy(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (from, to): (UrlRef, UrlRef)| async move {
match engine::copy(&*from, &*to, Attrs::default()).await {
@ -124,7 +127,7 @@ fn expand_url(lua: &Lua) -> mlua::Result<Function> {
fn file(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, url: UrlRef| async move {
match File::new(&*url).await {
match engine::file(&*url).await {
Ok(file) => file.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
@ -185,8 +188,8 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
let file = if !resolve {
File::from_dummy(url, next.file_type().await.ok())
} else if let Ok(cha) = next.metadata().await {
File::from_follow(url, cha).await
} else if let Ok(file) = next.file().await {
file
} else {
File::from_dummy(url, next.file_type().await.ok())
};

View file

@ -31,10 +31,9 @@ impl FilesOp {
pub(super) fn done(_: &Lua, t: Table) -> mlua::Result<Self> {
let id = t.raw_get("id")?;
let cha = t.raw_get("cha")?;
let url = t.raw_get("url")?;
let file = t.raw_get("file")?;
Ok(Self(yazi_fs::FilesOp::Done(url, cha, id)))
Ok(Self(yazi_fs::FilesOp::Done(file, id)))
}
pub(super) fn size(_: &Lua, t: Table) -> mlua::Result<Self> {

View file

@ -2,29 +2,34 @@ use std::hash::Hash;
use mlua::{Function, Lua, Table};
use yazi_config::YAZI;
use yazi_fs::file::FileRef;
use yazi_shared::url::{UrlBuf, UrlLike};
use yazi_fs::{FsHash128, file::{FileRef, FileSig}};
use yazi_shared::url::{Url, UrlBuf, UrlLike};
use yazi_shim::Twox128;
use super::Utils;
impl Utils {
pub(super) fn file_cache(lua: &Lua) -> mlua::Result<Function> {
struct Sig<'a>(FileSig<'a>, usize);
impl FsHash128 for Sig<'_> {
fn hash_u128(&self) -> u128 {
let mut h = Twox128::default();
self.0.hash(&mut h);
self.1.hash(&mut h);
h.finish_128()
}
}
lua.create_function(|_, t: Table| {
let file: FileRef = t.raw_get("file")?;
file.borrow(|f| {
if f.url.parent() == Some(yazi_shared::url::Url::regular(&YAZI.preview.cache_dir)) {
if f.url.parent() == Some(Url::regular(&YAZI.preview.cache_dir)) {
return Ok(None);
}
let hex = {
let mut h = Twox128::default();
f.hash(&mut h);
t.raw_get("skip").unwrap_or(0usize).hash(&mut h);
format!("{:x}", h.finish_128())
};
Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(hex))))
let sig = Sig(FileSig(f), t.raw_get("skip").unwrap_or_default());
Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(sig.hash_base32(&mut [0; 26])))))
})
})
}

View file

@ -1,15 +1,18 @@
use mlua::{Function, IntoLuaMulti, Lua, Value};
use yazi_adapter::{ADAPTOR, Image};
use yazi_binding::{Error, ImageInfo, elements::Rect};
use yazi_fs::FsUrl;
use yazi_shared::url::{AsUrl, UrlLike, UrlRef};
use yazi_shared::url::{UrlLike, UrlRef};
use yazi_shim::OptionExt;
use super::Utils;
impl Utils {
pub(super) fn image_info(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, url: UrlRef| async move {
let path = url.as_url().unified_path().into_owned();
let Some(path) = url.as_local().owned() else {
return (Value::Nil, Error::custom("Source must be a local path")).into_lua_multi(&lua);
};
match ImageInfo::new(path).await {
Ok(info) => info.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua),
@ -19,7 +22,10 @@ impl Utils {
pub(super) fn image_show(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move {
let path = url.as_url().unified_path();
let Some(path) = url.as_local() else {
return (Value::Nil, Error::custom("Source must be a local path")).into_lua_multi(&lua);
};
match ADAPTOR.image_show(path, *rect).await {
Ok(area) => Rect::from(area).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua),
@ -29,11 +35,13 @@ impl Utils {
pub(super) fn image_precache(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (src, dist): (UrlRef, UrlRef)| async move {
let Some(dist) = dist.as_local() else {
return (Value::Nil, Error::custom("Destination must be a local path"))
.into_lua_multi(&lua);
let Some(src) = src.as_local().owned() else {
return (false, Error::custom("Source must be a local path")).into_lua_multi(&lua);
};
let src = src.as_url().unified_path().into_owned();
let Some(dist) = dist.as_local() else {
return (false, Error::custom("Destination must be a local path")).into_lua_multi(&lua);
};
match Image::precache(src, dist).await {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua),

View file

@ -1,9 +1,8 @@
use mlua::{ExternalError, Function, IntoLuaMulti, Lua, Table, Value};
use yazi_binding::{Error, elements::Area};
use yazi_core::{Highlighter, MgrProxy, tab::PreviewLock};
use yazi_fs::FsUrl;
use yazi_fs::file::FileRef;
use yazi_runner::previewer::PeekError;
use yazi_shared::url::AsUrl;
use yazi_widgets::Renderable;
use super::Utils;
@ -15,9 +14,9 @@ impl Utils {
pub(super) fn preview_code(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, t: Table| async move {
let area: Area = t.raw_get("area")?;
let mut lock = PreviewLock::try_from(t)?;
let path = t.raw_get::<FileRef>("file")?.borrow(|f| Ok(f.content_path().into_owned()))?;
let path = lock.url.as_url().unified_path();
let mut lock = PreviewLock::try_from(t)?;
let inner = match Highlighter::oneshot(path, lock.skip, area.size()).await {
Ok(text) => text,
Err(e @ PeekError::Exceeded(max)) => return (e, max).into_lua_multi(&lua),

View file

@ -51,6 +51,7 @@ impl Default for Loader {
("pdf".to_owned(), preset!("plugins/pdf").into()),
("session".to_owned(), preset!("plugins/session").into()),
("svg".to_owned(), preset!("plugins/svg").into()),
("trash".to_owned(), preset!("plugins/trash").into()),
("vfs".to_owned(), preset!("plugins/vfs").into()),
("video".to_owned(), preset!("plugins/video").into()),
("zoxide".to_owned(), preset!("plugins/zoxide").into()),

View file

@ -3,7 +3,7 @@ use std::io;
use mlua::{IntoLua, Lua, Value};
use strum::AsRefStr;
use yazi_binding::MpscTx;
use yazi_fs::engine::{Attrs, Demand};
use yazi_fs::{engine::{Attrs, Demand}, file::File};
use yazi_shared::{path::PathBufDyn, url::UrlBuf};
#[derive(AsRefStr)]
@ -28,6 +28,12 @@ pub enum ProviderJob {
ReadDir {
url: UrlBuf,
},
Revalidate {
file: File,
},
File {
url: UrlBuf,
},
Open {
url: UrlBuf,
attrs: Attrs,
@ -46,6 +52,9 @@ pub enum ProviderJob {
RemoveDir {
url: UrlBuf,
},
RemoveDirAll {
url: UrlBuf,
},
RemoveFile {
url: UrlBuf,
},
@ -99,6 +108,7 @@ impl IntoLua for ProviderJob {
match self {
Self::Capabilities => {}
Self::Revalidate { file } => t.raw_set("file", file)?,
Self::Absolute { url }
| Self::Canonicalize { url }
@ -106,9 +116,11 @@ impl IntoLua for ProviderJob {
| Self::SymlinkMetadata { url }
| Self::Metadata { url }
| Self::ReadDir { url }
| Self::File { url }
| Self::CreateDir { url }
| Self::ReadLink { url }
| Self::RemoveDir { url }
| Self::RemoveDirAll { url }
| Self::RemoveFile { url }
| Self::Trash { url } => t.raw_set("url", url)?,

View file

@ -6,7 +6,7 @@ use parking_lot::Mutex;
use tokio::sync::mpsc;
use tracing::error;
use yazi_config::Priority;
use yazi_fs::FsHash64;
use yazi_fs::{FsHash64, file::FileSig};
use yazi_runner::RUNNER;
use crate::{HIGH, LOW, TaskOp, TaskOps, fetch::{FetchIn, FetchOutFetch}};
@ -32,7 +32,7 @@ impl Fetch {
pub(crate) async fn fetch(&self, task: FetchIn) -> Result<(), FetchOutFetch> {
let (id, fetcher) = (task.id, task.fetcher.clone());
let hashes: Vec<_> = task.targets.iter().map(|f| f.hash_u64()).collect();
let hashes: Vec<_> = task.targets.iter().map(|f| FileSig(f).hash_u64()).collect();
let (state, err) = RUNNER.fetch(task.into()).await?;
let mut loaded = self.loaded.lock();

View file

@ -6,7 +6,7 @@ use parking_lot::Mutex;
use tokio::sync::mpsc;
use tracing::error;
use yazi_config::Priority;
use yazi_fs::FsHash64;
use yazi_fs::{FsHash64, file::FileSig};
use yazi_runner::{RUNNER, preloader::{PreloadError, PreloadJob}};
use yazi_shared::id::Id;
@ -35,7 +35,7 @@ impl Preload {
}
pub(crate) async fn preload(&self, task: PreloadIn) -> Result<(), PreloadOut> {
let hash = task.target.hash_u64();
let hash = FileSig(&task.target).hash_u64();
let mut rx =
RUNNER.preload(PreloadJob { preloader: task.preloader.clone(), file: task.target }).await;

View file

@ -2,7 +2,7 @@ use std::{ops::Deref, sync::Arc, time::Duration};
use tokio::task::JoinHandle;
use yazi_config::{YAZI, plugin::{FetcherArc, PreloaderArc}};
use yazi_fs::FsHash64;
use yazi_fs::{FsHash64, file::FileSig};
use yazi_shared::{CompletionToken, Throttle, id::Id, url::{UrlBuf, UrlLike}};
use crate::{Behavior, HIGH, LOW, NORMAL, Task, TaskIn, TaskProg, Worker, fetch::FetchIn, file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDownload, FileOutHardlink, FileOutUpload}, hook::{HookIn, HookInDelete, HookInDownload, HookInPreload, HookInTrash, HookInUpload}, plugin::PluginInEntry, preload::PreloadIn, process::{ProcessIn, ProcessInBg, ProcessInBlock, ProcessInOrphan, ShellOpt}, size::SizeIn};
@ -204,7 +204,7 @@ impl Scheduler {
}
pub fn preload_paged(&self, preloader: PreloaderArc, target: &yazi_fs::file::File) {
let hook = HookInPreload::new(preloader.idx, target.hash_u64());
let hook = HookInPreload::new(preloader.idx, FileSig(target).hash_u64());
let mut r#in = PreloadIn { id: Id::ZERO, preloader, target: target.clone() };
self.add_hooked(&mut r#in, hook, |_| ());

View file

@ -14,6 +14,7 @@ workspace = true
[dependencies]
bitflags = { workspace = true }
hashbrown = { workspace = true }
parking_lot = { workspace = true }
russh = { workspace = true }
serde = { workspace = true }

View file

@ -1,5 +1,6 @@
use std::{collections::HashMap, fmt};
use std::fmt;
use hashbrown::HashMap;
use serde::{Deserialize, Deserializer, Serialize, de::Visitor, ser::SerializeStruct};
#[derive(Clone, Debug, Default, Eq, PartialEq)]

View file

@ -1,5 +1,4 @@
use std::collections::HashMap;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]

View file

@ -1,5 +1,4 @@
use std::collections::HashMap;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]

View file

@ -1,5 +1,6 @@
use std::{any::TypeId, collections::HashMap, io::{self, ErrorKind}, sync::Arc, time::Duration};
use std::{any::TypeId, io::{self, ErrorKind}, sync::Arc, time::Duration};
use hashbrown::HashMap;
use parking_lot::Mutex;
use russh::{ChannelStream, client::Msg};
use serde::Serialize;

View file

@ -1,7 +1,5 @@
use std::{fmt, hash::{Hash, Hasher}, sync::Arc};
use anyhow::{Result, anyhow};
use percent_encoding::percent_decode_str;
use serde::Deserialize;
use yazi_shim::cell::RoCell;
@ -145,11 +143,4 @@ impl Auth {
self.covariant(other)
|| self.kind == AuthKind::Hub && other.kind == AuthKind::Hub && self.scheme == other.scheme
}
pub fn parse_cache(cache: &str) -> Result<Arc<Self>> {
let (kind, rest) = cache.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?;
let (scheme, domain) = rest.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?;
Ok(Self::new(kind.parse()?, scheme.parse()?, percent_decode_str(domain).decode_utf8()?))
}
}

109
yazi-shared/src/auth/de.rs Normal file
View file

@ -0,0 +1,109 @@
use std::sync::Arc;
use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}};
use super::{Auth, Domain};
use crate::pool::{InternStr, Symbol};
pub(crate) struct AuthDeserializer(pub(crate) Arc<Auth>);
impl<'de> Deserializer<'de> for AuthDeserializer {
type Error = de::value::Error;
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes byte_buf
unit unit_struct struct newtype_struct seq tuple tuple_struct enum identifier ignored_any
}
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: de::Visitor<'de>,
{
self.deserialize_map(visitor)
}
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: de::Visitor<'de>,
{
visitor.visit_map(MapDeserializer::new(self.0))
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: de::Visitor<'de>,
{
visitor.visit_some(self)
}
}
// --- Map
struct MapDeserializer {
kind: Option<&'static str>,
scheme: Option<Symbol<str>>,
domain: Option<Domain<'static>>,
parent: Option<Arc<Auth>>,
}
impl MapDeserializer {
fn new(auth: Arc<Auth>) -> Self {
Self {
kind: Some(auth.kind.into()),
scheme: Some(auth.scheme.intern()),
domain: Some(auth.domain.clone()),
parent: auth.parent.clone(),
}
}
}
impl<'de> MapAccess<'de> for MapDeserializer {
type Error = de::value::Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: de::DeserializeSeed<'de>,
{
let key = if self.kind.is_some() {
Some("kind")
} else if self.scheme.is_some() {
Some("scheme")
} else if self.domain.is_some() {
Some("domain")
} else if self.parent.is_some() {
Some("parent")
} else {
None
};
key.map(|key| seed.deserialize(key.into_deserializer())).transpose()
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: de::DeserializeSeed<'de>,
{
if let Some(kind) = self.kind.take() {
return seed.deserialize(kind.into_deserializer());
}
if let Some(scheme) = self.scheme.take() {
return seed.deserialize(scheme.into_deserializer());
}
if let Some(domain) = self.domain.take() {
return seed.deserialize(domain.into_deserializer());
}
if let Some(parent) = self.parent.take() {
return seed.deserialize(AuthDeserializer(parent));
}
Err(de::Error::custom("value missing for key"))
}
fn size_hint(&self) -> Option<usize> {
Some(
self.kind.is_some() as usize
+ self.scheme.is_some() as usize
+ self.domain.is_some() as usize
+ self.parent.is_some() as usize,
)
}
}

View file

@ -1,69 +1,99 @@
use std::{borrow::{Borrow, Cow}, fmt::{self, Display, Formatter}, ops::Deref};
use std::{borrow::{Borrow, Cow}, fmt::{self, Display, Formatter}, ops::Deref, str};
use serde::{Deserialize, Deserializer, Serialize, de::{self, IntoDeserializer, value::CowStrDeserializer}};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::{self, IntoDeserializer, Visitor}};
#[derive(Default, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct Domain<'a>(Cow<'a, str>);
use crate::{BytesExt, data::BytesDeserializer};
#[derive(Default, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Domain<'a>(Cow<'a, [u8]>);
impl Domain<'static> {
pub const CATCHALL: Self = Self(Cow::Borrowed("*"));
pub const EMPTY: Self = Self(Cow::Borrowed(""));
}
impl Domain<'_> {
#[inline]
pub fn into_owned(self) -> Domain<'static> { Domain(Cow::Owned(self.0.into_owned())) }
#[inline]
pub fn is_catchall(&self) -> bool { *self == Domain::CATCHALL }
pub const CATCHALL: Self = Self(Cow::Borrowed(b"*"));
pub const EMPTY: Self = Self(Cow::Borrowed(b""));
}
impl Deref for Domain<'_> {
type Target = str;
type Target = [u8];
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Borrow<str> for Domain<'_> {
fn borrow(&self) -> &str { &self.0 }
impl Borrow<[u8]> for Domain<'_> {
fn borrow(&self) -> &[u8] { &self.0 }
}
impl AsRef<str> for Domain<'_> {
fn as_ref(&self) -> &str { &self.0 }
impl AsRef<[u8]> for Domain<'_> {
fn as_ref(&self) -> &[u8] { &self.0 }
}
impl<'a> From<Vec<u8>> for Domain<'a> {
fn from(value: Vec<u8>) -> Self { Self(Cow::Owned(value)) }
}
impl<'a> From<Cow<'a, [u8]>> for Domain<'a> {
fn from(value: Cow<'a, [u8]>) -> Self { Self(value) }
}
impl<'a> From<String> for Domain<'a> {
fn from(value: String) -> Self { Self(Cow::Owned(value)) }
fn from(value: String) -> Self { Self(Cow::Owned(value.into_bytes())) }
}
impl<'a> From<&'a str> for Domain<'a> {
fn from(value: &'a str) -> Self { Self(Cow::Borrowed(value)) }
}
impl<'a> From<Cow<'a, str>> for Domain<'a> {
fn from(value: Cow<'a, str>) -> Self { Self(value) }
}
impl<'a> From<&'a Domain<'_>> for Domain<'a> {
fn from(value: &'a Domain<'_>) -> Self { Self(Cow::Borrowed(&value.0)) }
impl<'a, T> From<&'a T> for Domain<'a>
where
T: AsRef<[u8]> + ?Sized,
{
fn from(value: &'a T) -> Self { Self(Cow::Borrowed(value.as_ref())) }
}
impl Display for Domain<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self.0, f) }
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.display().fmt(f) }
}
impl<'a> Domain<'a> {
pub fn into_owned(self) -> Domain<'static> { Domain(Cow::Owned(self.0.into_owned())) }
pub fn is_catchall(&self) -> bool { *self == Domain::CATCHALL }
pub fn to_str(&self) -> Result<&str, str::Utf8Error> { str::from_utf8(&self.0) }
}
impl<'de, 'a: 'de> IntoDeserializer<'de, de::value::Error> for Domain<'a> {
type Deserializer = BytesDeserializer<'a>;
fn into_deserializer(self) -> Self::Deserializer { BytesDeserializer(self.0) }
}
impl Serialize for Domain<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self.to_str() {
Ok(s) => serializer.serialize_str(s),
Err(_) => serializer.serialize_bytes(&self.0),
}
}
}
impl<'de> Deserialize<'de> for Domain<'_> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self(Cow::Owned(String::deserialize(deserializer)?)))
struct V;
impl Visitor<'_> for V {
type Value = Domain<'static>;
fn expecting(&self, f: &mut Formatter) -> fmt::Result { f.write_str("a string or bytes") }
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(v.to_owned().into())
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> { Ok(v.into()) }
fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
Ok(v.to_owned().into())
}
fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> { Ok(v.into()) }
}
deserializer.deserialize_any(V)
}
}
impl<'de, 'a, E> IntoDeserializer<'de, E> for Domain<'a>
where
E: de::Error,
{
type Deserializer = CowStrDeserializer<'a, E>;
fn into_deserializer(self) -> Self::Deserializer { self.0.into_deserializer() }
}

View file

@ -1,15 +1,16 @@
use std::fmt;
use std::fmt::{self, Display};
use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode};
use percent_encoding::{AsciiSet, CONTROLS, percent_encode};
use super::{Auth, AuthKind, Domain};
// --- EncodeAuth
pub struct EncodeAuth<'a>(pub &'a Auth, pub bool);
impl EncodeAuth<'_> {
pub fn domain<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> {
pub fn domain<'a>(s: &'a Domain<'_>) -> EncodeDomain<'a> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':').add(b'%');
percent_encode(s.as_bytes(), SET)
EncodeDomain(s, SET)
}
}
@ -29,9 +30,9 @@ impl fmt::Display for EncodeAuth<'_> {
pub struct EncodePrefix<'a>(pub &'a Auth);
impl EncodePrefix<'_> {
pub fn parent<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> {
pub fn parent<'a>(s: &'a Domain<'_>) -> EncodeDomain<'a> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b',').add(b'@').add(b'%');
percent_encode(s.as_bytes(), SET)
EncodeDomain(s, SET)
}
}
@ -53,3 +54,22 @@ impl fmt::Display for EncodePrefix<'_> {
f.write_str("/")
}
}
// --- EncodeDomain
pub struct EncodeDomain<'a>(&'a [u8], &'static AsciiSet);
impl Display for EncodeDomain<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for chunk in self.0.utf8_chunks() {
for c in chunk.valid().chars() {
if c.is_ascii() {
percent_encode(&[c as u8], self.1).fmt(f)?;
} else {
c.fmt(f)?;
}
}
percent_encode(chunk.invalid(), self.1).fmt(f)?;
}
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show more