mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: trash bin
This commit is contained in:
parent
6aa75594db
commit
aba0b5ad01
143 changed files with 2778 additions and 948 deletions
2
.github/workflows/validate-issue.yml
vendored
2
.github/workflows/validate-issue.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
4
.github/workflows/validate-pr.yml
vendored
4
.github/workflows/validate-pr.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
380
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
27
Cargo.toml
27
Cargo.toml
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")));
|
||||
|
|
|
|||
|
|
@ -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")));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
4
yazi-cli/src/env/env.rs
vendored
4
yazi-cli/src/env/env.rs
vendored
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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" ] }
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
[trash."*"]
|
||||
kind = "hub"
|
||||
run = "trash"
|
||||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
16
yazi-ffi/src/com.rs
Normal 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() } }
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
21
yazi-fs/src/cha/cha_sig.rs
Normal file
21
yazi-fs/src/cha/cha_sig.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(cha kind lua mode r#type);
|
||||
yazi_macro::mod_flat!(cha cha_sig kind lua mode r#type);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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>>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
yazi-fs/src/file/file_extra.rs
Normal file
67
yazi-fs/src/file/file_extra.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
26
yazi-fs/src/file/file_sig.rs
Normal file
26
yazi-fs/src/file/file_sig.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(clean expand normalize path percent relative);
|
||||
yazi_macro::mod_flat!(clean expand normalize path relative);
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
@ -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])
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
yazi-fs/src/trash/entry.rs
Normal file
53
yazi-fs/src/trash/entry.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
171
yazi-fs/src/trash/freedesktop.rs
Normal file
171
yazi-fs/src/trash/freedesktop.rs
Normal 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(¤t.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
73
yazi-fs/src/trash/lua.rs
Normal 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
188
yazi-fs/src/trash/macos.rs
Normal 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(¤t.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
16
yazi-fs/src/trash/mod.rs
Normal 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
49
yazi-fs/src/trash/node.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
36
yazi-fs/src/trash/nodes.rs
Normal file
36
yazi-fs/src/trash/nodes.rs
Normal 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() }
|
||||
}
|
||||
36
yazi-fs/src/trash/traits.rs
Normal file
36
yazi-fs/src/trash/traits.rs
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
46
yazi-fs/src/trash/unsupported.rs
Normal file
46
yazi-fs/src/trash/unsupported.rs
Normal 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"))
|
||||
}
|
||||
}
|
||||
330
yazi-fs/src/trash/windows.rs
Normal file
330
yazi-fs/src/trash/windows.rs
Normal 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))
|
||||
}
|
||||
|
|
@ -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 { .. } => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
153
yazi-plugin/preset/plugins/trash.lua
Normal file
153
yazi-plugin/preset/plugins/trash.lua
Normal 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
|
||||
|
|
@ -2,3 +2,4 @@ os.setlocale("")
|
|||
|
||||
require("dds"):setup()
|
||||
require("extract"):setup()
|
||||
require("trash"):setup()
|
||||
|
|
|
|||
6
yazi-plugin/src/external/fd.rs
vendored
6
yazi-plugin/src/external/fd.rs
vendored
|
|
@ -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)
|
||||
|
|
|
|||
6
yazi-plugin/src/external/rg.rs
vendored
6
yazi-plugin/src/external/rg.rs
vendored
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
yazi-plugin/src/external/rga.rs
vendored
6
yazi-plugin/src/external/rga.rs
vendored
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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])))))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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)?,
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, |_| ());
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
bitflags = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
russh = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
109
yazi-shared/src/auth/de.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue