feat: allow restoring trashed items recursively (#4159)
Some checks failed
Cachix / Publish Flake (push) Has been cancelled
Check / clippy (push) Has been cancelled
Check / rustfmt (push) Has been cancelled
Check / stylua (push) Has been cancelled
Draft / build-unix (gcc-aarch64-linux-gnu, ubuntu-latest, aarch64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-i686-linux-gnu, ubuntu-latest, i686-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-riscv64-linux-gnu, ubuntu-latest, riscv64gc-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-sparc64-linux-gnu, ubuntu-latest, sparc64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (macos-latest, aarch64-apple-darwin) (push) Has been cancelled
Draft / build-unix (macos-latest, x86_64-apple-darwin) (push) Has been cancelled
Draft / build-unix (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-windows (windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
Draft / build-windows (windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
Draft / build-musl (aarch64-unknown-linux-musl) (push) Has been cancelled
Draft / build-musl (x86_64-unknown-linux-musl) (push) Has been cancelled
Draft / build-snap (amd64, ubuntu-latest) (push) Has been cancelled
Draft / build-snap (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Test / test (macos-latest) (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
Test / test (windows-latest) (push) Has been cancelled
Draft / snap (push) Has been cancelled
Draft / draft (push) Has been cancelled
Draft / nightly (push) Has been cancelled

This commit is contained in:
三咲雅 misaki masa 2026-07-25 11:39:19 +08:00 committed by GitHub
parent 58f1013348
commit 2c3f174eb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 1651 additions and 1062 deletions

45
AGENTS.md Normal file
View file

@ -0,0 +1,45 @@
# AGENTS.md
## Rules
- Applies repo-wide. Keep changes scoped; do not create issues or pull requests, or post comments.
## Project
- Rust 2024 Cargo workspace containing all `yazi-*` crates; default members are `yazi-fm` (`yazi`) and `yazi-cli` (`ya`).
## Style
- Follow nearby code and use idiomatic Rust and Lua. Rust uses `snake_case` for modules, functions, and fields and `PascalCase` for types, traits, and variants. Lua uses PascalCase component tables, `local M` plugin modules, `snake_case` methods/locals, and `_name` private fields.
- Preserve established terms and type families: `Url`/`UrlBuf`/`UrlCow`, `PathDyn`/`PathBufDyn`/`PathCow`, `*Ref`, `*Arc`, `*Opt`, `*State`, `*Job`, `*Prog`, `File`, `Folder`, `Tab`, `Mgr`, and `Task`. Use `Url` for logical locations and `Path` for filesystem paths.
- Reuse established plugin and event names (`fetch`, `preload`, `peek`, `seek`, `spot`, `entry`, `setup`, `yank`, `hover`, and `select`) across Rust, Lua, and configuration.
- Use Rust prefixes (`as_`, `to_`, `into_`, `try_`, `is_`, `has_`) according to their usual semantics; prefer descriptive names.
- Name variables, modules, methods, and other symbols simply, elegantly, and expressively. Be creative while keeping names clear, consistent with established terminology, and idiomatic.
- When passing arguments, use the parameter's conversion traits directly (such as `Into<_>` or `AsRef<_>`); avoid eager conversions like `.to_string()`, `.to_owned()`, and `.as_ref()` unless ownership, type inference, or semantics require them.
- Prefer general-purpose traits and conversion APIs already provided by the codebase or its dependencies over manual construction or adapter closures; for example, use `into_lua()` where applicable.
## Code Changes
- Search and reuse first. For new features, extend existing infrastructure or data structures with general, reusable capabilities when that keeps the final code concise.
- For refactors, inspect the whole target module and its callers first. Look for duplicated work, redundant I/O, underpowered return values, one-use wrappers, and reusable cross-platform abstractions; implement high-confidence, behavior-preserving simplifications while preserving error, fallback, and platform semantics.
- Keep diffs minimal and avoid unrelated refactors. Prefer clear code over custom patterns or comments; comment only behavior the code cannot explain.
- Put reusable code in the lowest suitable shared layer; avoid unnecessary dependencies and allocations. Prefer borrowed values and existing wrappers.
- Use stable Rust APIs; nightly is formatting-only. Use only `pub`, `pub(super)`, and `pub(crate)`—never `pub(in ...)`.
- Keep async I/O non-blocking, preserve platform/fork behavior, and follow existing error boundaries with `?`.
- For renames or refactors, update all related variables, functions, parameters, modules, methods, types, derived types, exports, tests, configuration keys, documentation, and Lua bindings.
- Do not add or modify tests unless requested.
## Validation
- Prefer targeted debug checks; use multiple `-p` flags for affected crates before the whole workspace.
```sh
cargo check -p <package>
cargo test -p <package>
cargo clippy -p <package>
find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} +
stylua --color always --check .
```
- Use `cargo check` instead of `cargo build` unless artifacts are needed. Do not use `--release` unless requested; use `scripts/build.sh <target>` for release or cross-target packaging.
- Run relevant existing tests when needed, then inspect `git diff` and verify that only intended files changed.

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
AGENTS.md

151
Cargo.lock generated
View file

@ -145,7 +145,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@ -156,7 +156,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@ -319,6 +319,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9"
[[package]]
name = "base64ct"
version = "1.8.3"
@ -573,9 +579,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
dependencies = [
"find-msvc-tools",
"jobserver",
@ -636,9 +642,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.6.2"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
dependencies = [
"clap_builder",
"clap_derive",
@ -687,14 +693,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.6.1"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.119",
"syn 3.0.3",
]
[[package]]
@ -921,15 +927,16 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
version = "5.0.0-rc.1"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c906a87e53a36ff795d72e06e8162a83c5436e3ea89e942a9cb9fc083f0a384f"
checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rand_core 0.10.1",
"rustc_version",
"subtle",
"zeroize",
@ -1076,7 +1083,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -1122,9 +1129,9 @@ dependencies = [
[[package]]
name = "ed25519-dalek"
version = "3.0.0-rc.1"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1685663e23882cd8517dcbcb1c23a6ebff4433c22dfb681d760219b62cd1b849"
checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"
dependencies = [
"curve25519-dalek",
"ed25519",
@ -1138,9 +1145,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.16.0"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
[[package]]
name = "elliptic-curve"
@ -1226,7 +1233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -1660,7 +1667,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.62.2",
"windows-core 0.56.0",
]
[[package]]
@ -1690,7 +1697,7 @@ dependencies = [
"exr",
"gif",
"image-webp",
"moxcms",
"moxcms 0.8.1",
"num-traits",
"png",
"qoi",
@ -1931,9 +1938,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libc"
version = "0.2.186"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libfuzzer-sys"
@ -2099,7 +2106,7 @@ dependencies = [
"module-lattice",
"pkcs8",
"rand_core 0.10.1",
"sha3",
"sha3 0.11.0",
]
[[package]]
@ -2170,6 +2177,16 @@ dependencies = [
"pxfm",
]
[[package]]
name = "moxcms"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aee701c0ffba6cc991c0fefda5ff5fca1bd2f2f4a928ffba9f825b9afceedbc3"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@ -2245,7 +2262,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -2436,9 +2453,9 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
[[package]]
name = "p256"
version = "0.14.0-rc.15"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bb40a5099e2c38a09dd29321a7a7f045f165a54317679c7cdfb0cbaf8f6b1e"
checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a"
dependencies = [
"ecdsa",
"elliptic-curve",
@ -2449,9 +2466,9 @@ dependencies = [
[[package]]
name = "p384"
version = "0.14.0-rc.15"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "492f329d7eb11d22dadc988626b9ea1f503b4ab043a8b1e4e2cc4ae45dabdd70"
checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f"
dependencies = [
"ecdsa",
"elliptic-curve",
@ -2463,9 +2480,9 @@ dependencies = [
[[package]]
name = "p521"
version = "0.14.0-rc.15"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff42e4ace5424e3b6d7cb82514be89866b85af87015f80341e37dcc21a66ce6e"
checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449"
dependencies = [
"base16ct",
"ecdsa",
@ -2662,7 +2679,7 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
dependencies = [
"base64",
"base64 0.22.1",
"indexmap 2.14.0",
"quick-xml",
"serde",
@ -3114,7 +3131,7 @@ checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.3",
]
[[package]]
@ -3197,9 +3214,9 @@ dependencies = [
[[package]]
name = "russh"
version = "0.62.2"
version = "0.62.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6c3c1dd0a9ad3a9915a2f6e907d158f9a7e9ac32ddc772b003faafc027d9a8"
checksum = "b8b67b5a0d8068c89dcbe9d95df986af7a851d1f3c604525274c37468e60464f"
dependencies = [
"aes",
"bitflags",
@ -3253,7 +3270,7 @@ dependencies = [
"sec1",
"sha1",
"sha2",
"sha3",
"sha3 0.12.0",
"signature",
"spki",
"ssh-encoding",
@ -3321,7 +3338,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -3463,7 +3480,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.3",
]
[[package]]
@ -3494,7 +3511,7 @@ version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
dependencies = [
"base64",
"base64 0.22.1",
"bs58",
"chrono",
"hex",
@ -3562,6 +3579,17 @@ dependencies = [
"keccak",
]
[[package]]
name = "sha3"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759"
dependencies = [
"digest",
"keccak",
"sponge-cursor",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -3659,7 +3687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@ -3672,6 +3700,12 @@ dependencies = [
"der",
]
[[package]]
name = "sponge-cursor"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620"
[[package]]
name = "ssh-cipher"
version = "0.3.0"
@ -3795,9 +3829,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.2"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
@ -3837,10 +3871,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -3880,7 +3914,7 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.3",
]
[[package]]
@ -3975,9 +4009,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.53.0"
version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"bytes",
"libc",
@ -4003,9 +4037,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
dependencies = [
"futures-core",
"pin-project-lite",
@ -4014,9 +4048,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.18"
version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
dependencies = [
"bytes",
"futures-core",
@ -4162,7 +4196,7 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9"
dependencies = [
"rand 0.10.2",
"rand 0.9.5",
]
[[package]]
@ -4191,7 +4225,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@ -4434,7 +4468,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -4850,10 +4884,10 @@ version = "26.5.6"
dependencies = [
"ansi-to-tui",
"anyhow",
"base64",
"base64 0.23.0",
"image",
"inventory",
"moxcms",
"moxcms 0.9.0",
"palette",
"quantette",
"ratatui-core",
@ -4956,7 +4990,7 @@ version = "26.5.6"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.3",
]
[[package]]
@ -5134,6 +5168,7 @@ dependencies = [
"anyhow",
"arc-swap",
"bitflags",
"cfg_aliases",
"core-foundation-sys",
"data-encoding",
"dirs",
@ -5146,6 +5181,7 @@ dependencies = [
"mlua",
"objc2",
"parking_lot",
"percent-encoding",
"rand 0.10.2",
"regex",
"scopeguard",
@ -5368,7 +5404,6 @@ version = "26.5.6"
dependencies = [
"anyhow",
"arc-swap",
"base64",
"hashbrown 0.17.1",
"mlua",
"percent-encoding",
@ -5389,7 +5424,7 @@ name = "yazi-term"
version = "26.5.6"
dependencies = [
"anyhow",
"base64",
"base64 0.23.0",
"bitflags",
"compact_str 0.10.0",
"futures",
@ -5411,7 +5446,7 @@ dependencies = [
name = "yazi-tty"
version = "26.5.9"
dependencies = [
"base64",
"base64 0.23.0",
"bitflags",
"libc",
"mlua",

View file

@ -38,16 +38,16 @@ debug = false
ansi-to-tui = "8.0.1"
anyhow = "1.0.104"
arc-swap = { version = "1.9.2", features = [ "serde" ] }
base64 = "0.22.1"
base64 = "0.23.0"
bitflags = { version = "2.13.1", features = [ "serde" ] }
chrono = "0.4.45"
clap = { version = "4.6.2", features = [ "derive" ] }
clap = { version = "4.6.4", 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" }
either = { version = "1.17.0" }
foldhash = "0.2.0"
futures = "0.3.33"
globset = "0.4.19"
@ -55,7 +55,7 @@ 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" ] }
inventory = "0.3.24"
libc = "0.2.186"
libc = "0.2.189"
lru = "0.18.1"
mlua = { version = "0.12.0", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] }
objc2 = "0.6.4"
@ -67,7 +67,7 @@ rand = { version = "0.10.2", default-features = false, features =
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.1"
russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] }
russh = { version = "0.62.4", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0"
serde = { version = "1.0.229", features = [ "derive" ] }
serde_json = "1.0.151"
@ -75,9 +75,9 @@ 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.19"
tokio = { version = "1.53.0", features = [ "full" ] }
tokio-stream = "0.1.18"
tokio-util = "0.7.18"
tokio = { version = "1.53.1", features = [ "full" ] }
tokio-stream = "0.1.19"
tokio-util = "0.7.19"
toml = { version = "1.1.3" }
tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] }

View file

@ -1 +1 @@
{"flagWords":[],"version":"0.2","language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm","mimetypes","Mimelist","renderables","redrawer","Padable"]}
{"language":"en","version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm","mimetypes","Mimelist","renderables","redrawer","Padable","ents"]}

View file

@ -28,7 +28,7 @@ anyhow = { workspace = true }
base64 = { workspace = true }
image = { workspace = true }
inventory = { workspace = true }
moxcms = "0.8.1"
moxcms = "0.9.0"
palette = { version = "0.7.6", default-features = false }
quantette = { version = "0.6.0", default-features = false }
ratatui-core = { workspace = true }

View file

@ -19,4 +19,4 @@ proc-macro = true
# External dependencies
proc-macro2 = "1"
quote = "1.0.47"
syn = { version = "3.0.2", features = [ "full" ] }
syn = { version = "3.0.3", features = [ "full" ] }

View file

@ -12,6 +12,9 @@ rust-version.workspace = true
[lints]
workspace = true
[build-dependencies]
cfg_aliases = "0.2.2"
[dependencies]
yazi-binding = { path = "../yazi-binding", version = "26.5.6" }
yazi-ffi = { path = "../yazi-ffi", version = "26.5.6" }
@ -45,8 +48,11 @@ typed-path = { workspace = true }
[target."cfg(unix)".dependencies]
uzers = { workspace = true }
[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))'.dependencies]
percent-encoding = { 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 = { version = "0.62.2", features = [ "Win32_Foundation", "Win32_Storage_EnhancedStorage", "Win32_System_Com_StructuredStorage", "Win32_System_SystemServices", "Win32_System_Variant", "Win32_UI_Shell_PropertiesSystem" ] }
windows-sys = { version = "0.61.2", features = [ "Win32_Storage_FileSystem" ] }
[target.'cfg(target_os = "macos")'.dependencies]

13
yazi-fs/build.rs Normal file
View file

@ -0,0 +1,13 @@
fn main() {
cfg_aliases::cfg_aliases! {
trash_unsupported: {
any(target_os = "android", target_os = "ios")
},
trash_unix: {
all(unix, not(trash_unsupported))
},
trash_freedesktop: {
all(unix, not(target_os = "macos"), not(trash_unsupported))
},
}
}

View file

@ -54,7 +54,7 @@ impl Cha {
where
T: AsStrand,
{
Self::from_bare(&meta).attach(ChaKind::hidden(name, &meta))
Self::from_bare(&meta).attach(ChaKind::hidden(name, &meta) | ChaKind::reparse(&meta))
}
pub fn from_dummy<U>(_url: U, r#type: Option<ChaType>) -> Self
@ -138,8 +138,8 @@ impl Cha {
return self;
}
let retain = self.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM) | ChaKind::FOLLOW;
followed.unwrap_or(self).attach(retain)
let retain = self.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM | ChaKind::REPARSE);
followed.unwrap_or(self).attach(retain | ChaKind::FOLLOW)
}
}
@ -165,6 +165,12 @@ impl Cha {
#[inline]
pub const fn is_dummy(self) -> bool { self.kind.contains(ChaKind::DUMMY) }
#[inline]
pub const fn is_reparse(self) -> bool { self.kind.contains(ChaKind::REPARSE) }
#[inline]
pub fn is_indirect(self) -> bool { self.is_link() || self.is_reparse() }
pub fn atime_dur(self) -> anyhow::Result<Duration> {
if let Some(atime) = self.atime {
Ok(atime.duration_since(UNIX_EPOCH)?)

View file

@ -7,10 +7,11 @@ use yazi_shared::strand::AsStrand;
bitflags! {
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct ChaKind: u8 {
const FOLLOW = 0b0000_0001;
const HIDDEN = 0b0000_0010;
const SYSTEM = 0b0000_0100;
const DUMMY = 0b0000_1000;
const FOLLOW = 0b0000_0001;
const HIDDEN = 0b0000_0010;
const SYSTEM = 0b0000_0100;
const DUMMY = 0b0000_1000;
const REPARSE = 0b0001_0000;
}
}
@ -43,4 +44,20 @@ impl ChaKind {
me
}
#[inline]
pub(super) fn reparse(_meta: &Metadata) -> Self {
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
if _meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Self::REPARSE;
}
}
Self::empty()
}
}

View file

@ -48,6 +48,7 @@ impl UserData for Cha {
fields.add_field_method_get("is_link", |_, me| Ok(me.is_link()));
fields.add_field_method_get("is_orphan", |_, me| Ok(me.is_orphan()));
fields.add_field_method_get("is_dummy", |_, me| Ok(me.is_dummy()));
fields.add_field_method_get("is_indirect", |_, me| Ok(me.is_indirect()));
fields.add_field_method_get("is_block", |_, me| Ok(me.is_block()));
fields.add_field_method_get("is_char", |_, me| Ok(me.is_char()));
fields.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo()));

View file

@ -17,7 +17,7 @@ impl SizeCalculator {
let p = path.to_owned();
tokio::task::spawn_blocking(move || {
let cha = Cha::new(p.file_name().unwrap_or_default(), std::fs::symlink_metadata(&p)?);
if !cha.is_dir() {
if !cha.is_dir() || cha.is_indirect() {
return Ok(Self::Idle((VecDeque::new(), Some(cha.len)), cha));
}
@ -101,11 +101,27 @@ impl SizeCalculator {
let Ok(ent) = next else { continue };
let Ok(ft) = ent.file_type() else { continue };
if ft.is_dir() {
buf.push_back(Either::Left(ent.path()));
} else if let Ok(meta) = ent.metadata() {
size += meta.len();
// If the entry is not a directory
if !ft.is_dir() {
size += ent.metadata().map_or(0, |meta| meta.len());
continue;
}
// The entry is a directory, but it may be a reparse point
#[cfg(windows)]
{
let Ok(cha) = ent.metadata().map(|meta| Cha::new(ent.file_name(), meta)) else {
continue;
};
if !cha.is_dir() || cha.is_indirect() {
size += cha.len;
continue;
}
}
// Now, we can safely assume the entry is a regular directory
buf.push_back(Either::Left(ent.path()));
}
Some(size)
}

View file

@ -0,0 +1,126 @@
use std::{io, path::Path};
#[cfg(unix)]
pub(super) fn remove_dir_clean_impl(path: &Path) -> io::Result<()> {
use std::{fs::OpenOptions, os::unix::fs::OpenOptionsExt};
use libc::{ELOOP, ENOENT, ENOTDIR, O_DIRECTORY, O_NOFOLLOW};
let dir = match OpenOptions::new().read(true).custom_flags(O_DIRECTORY | O_NOFOLLOW).open(path) {
Ok(dir) => dir,
Err(e) if e.raw_os_error().is_some_and(|e| matches!(e, ENOENT | ENOTDIR | ELOOP)) => {
return Ok(());
}
Err(e) => return Err(e),
};
clear_no_follow(dir);
std::fs::remove_dir(path)
}
#[cfg(unix)]
fn clear_no_follow(dir: std::fs::File) {
use std::{ffi::CStr, os::fd::{AsRawFd, FromRawFd, IntoRawFd}};
use libc::{AT_REMOVEDIR, DT_DIR, DT_UNKNOWN, O_CLOEXEC, O_DIRECTORY, O_NOFOLLOW, O_RDONLY};
struct Dropper(*mut libc::DIR);
impl Drop for Dropper {
fn drop(&mut self) { _ = unsafe { libc::closedir(self.0) }; }
}
let stream = unsafe { libc::fdopendir(dir.as_raw_fd()) };
if stream.is_null() {
return;
}
let _ = dir.into_raw_fd();
let stream = Dropper(stream);
let fd = unsafe { libc::dirfd(stream.0) };
loop {
let entry = unsafe { libc::readdir(stream.0) };
if entry.is_null() {
break;
}
let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) };
if matches!(name.to_bytes(), b"." | b"..") {
continue;
}
let ty = unsafe { (*entry).d_type };
if ty != DT_DIR && ty != DT_UNKNOWN {
continue;
}
let child =
unsafe { libc::openat(fd, name.as_ptr(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC) };
if child < 0 {
continue;
}
clear_no_follow(unsafe { std::fs::File::from_raw_fd(child) });
_ = unsafe { libc::unlinkat(fd, name.as_ptr(), AT_REMOVEDIR) };
}
}
// --- Windows
#[cfg(windows)]
pub(super) fn remove_dir_clean_impl(path: &Path) -> io::Result<()> {
match clear_no_reparse(path) {
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
result => result,
}
}
#[cfg(windows)]
fn clear_no_reparse(path: &Path) -> io::Result<()> {
use std::{fs::OpenOptions, os::windows::fs::{MetadataExt, OpenOptionsExt}};
use windows_sys::Win32::Storage::FileSystem::{DELETE, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE};
let dir = OpenOptions::new()
.access_mode(DELETE | FILE_READ_ATTRIBUTES)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let attrs = dir.metadata()?.file_attributes();
if attrs & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT) != FILE_ATTRIBUTE_DIRECTORY {
return Ok(());
}
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
if entry.file_type().is_ok_and(|t| t.is_dir()) {
clear_no_reparse(&entry.path()).ok();
}
}
}
remove_dir(&dir)
}
#[cfg(windows)]
fn remove_dir(dir: &std::fs::File) -> io::Result<()> {
use std::{mem, os::windows::io::AsRawHandle};
use windows_sys::Win32::{Foundation::HANDLE, Storage::FileSystem::{FILE_DISPOSITION_INFO, FileDispositionInfo, SetFileInformationByHandle}};
let info = FILE_DISPOSITION_INFO { DeleteFile: true };
if unsafe {
SetFileInformationByHandle(
dir.as_raw_handle() as HANDLE,
FileDispositionInfo,
&info as *const FILE_DISPOSITION_INFO as _,
mem::size_of::<FILE_DISPOSITION_INFO>() as u32,
)
} == 0
{
Err(io::Error::last_os_error())
} else {
Ok(())
}
}

View file

@ -119,6 +119,12 @@ impl<'a> Engine for Local<'a> {
#[inline]
async fn remove_dir_all(&self) -> io::Result<()> { tokio::fs::remove_dir_all(self.path).await }
#[inline]
async fn remove_dir_clean(&self) -> io::Result<()> {
let path = self.path.to_owned();
tokio::task::spawn_blocking(move || super::remove_dir_clean_impl(&path)).await?
}
#[inline]
async fn remove_file(&self) -> io::Result<()> { tokio::fs::remove_file(self.path).await }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(absolute calculator casefold copier dir_entry demand identical local read_dir);
yazi_macro::mod_flat!(absolute calculator casefold clear copier dir_entry demand identical local read_dir);

View file

@ -128,9 +128,11 @@ pub trait Engine: Sized {
{
let mut it = ok_or_not_found!(P::new(url).await?.read_dir().await, return Ok(()));
while let Some(child) = it.next().await? {
let ft = ok_or_not_found!(child.file_type().await, continue);
let result = if ft.is_dir() {
let cha = ok_or_not_found!(child.metadata().await, continue);
let result = if cha.is_dir() && !cha.is_indirect() {
Box::pin(remove_dir_all_impl::<P>(child.url().as_url())).await
} else if cha.is_dir() {
P::new(child.url().as_url()).await?.remove_dir().await
} else {
P::new(child.url().as_url()).await?.remove_file().await
};
@ -143,10 +145,12 @@ pub trait Engine: Sized {
async move {
let cha = ok_or_not_found!(self.symlink_metadata().await, return Ok(()));
if cha.is_link() {
self.remove_file().await
} else {
if !cha.is_indirect() {
remove_dir_all_impl::<Self>(self.url()).await
} else if cha.is_dir() {
self.remove_dir().await
} else {
self.remove_file().await
}
}
}
@ -159,18 +163,23 @@ pub trait Engine: Sized {
let mut result = Ok(());
while let Some((dir, visited)) = stack.pop() {
let Ok(engine) = Self::new(dir.as_url()).await else {
continue;
};
let Ok(engine) = Self::new(dir.as_url()).await else { continue };
if visited {
result = engine.remove_dir().await;
} else if let Ok(mut it) = engine.read_dir().await {
stack.push((dir, true));
while let Ok(Some(ent)) = it.next().await {
if ent.file_type().await.is_ok_and(|t| t.is_dir()) {
stack.push((ent.url(), false));
}
continue;
}
if !engine.symlink_metadata().await.is_ok_and(|c| c.is_dir() && !c.is_indirect()) {
continue;
}
let Ok(mut it) = engine.read_dir().await else { continue };
stack.push((dir, true));
while let Ok(Some(ent)) = it.next().await {
if ent.metadata().await.is_ok_and(|c| c.is_dir() && !c.is_indirect()) {
stack.push((ent.url(), false));
}
}
}

View file

@ -0,0 +1,18 @@
#[cfg(trash_unix)]
pub(super) fn restore_item(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> {
use std::{fs, io};
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),
}
}

View file

@ -0,0 +1,38 @@
use std::vec;
use mlua::{FromLua, Lua, Value};
use super::{TrashEntry, TrashId};
pub(crate) struct TrashEntries(Vec<TrashEntry>);
impl TrashEntries {
fn new(mut entries: Vec<TrashEntry>) -> Self {
entries.sort_unstable_by_key(|entry| entry.rel().components().count());
let mut seen = Vec::<TrashId>::with_capacity(entries.len());
entries.retain(|entry| {
if seen.iter().any(|id| id.top() == entry.top() && entry.rel().starts_with(id.rel())) {
false
} else {
seen.push(entry.id.clone());
true
}
});
Self(entries)
}
}
impl FromLua for TrashEntries {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let entries = Vec::<TrashEntry>::from_lua(value, lua)?;
Ok(Self::new(entries))
}
}
impl IntoIterator for TrashEntries {
type IntoIter = vec::IntoIter<TrashEntry>;
type Item = TrashEntry;
fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
}

View file

@ -1,31 +1,80 @@
use std::{ffi::OsString, path::PathBuf};
use std::{ffi::OsStr, ops::Deref, path::{Path, PathBuf}};
#[cfg(trash_unix)]
use std::{ffi::OsString, io};
use mlua::{IntoLua, Lua, Value};
use mlua::{AnyUserData, FromLua, Lua, UserData, UserDataFields, Value};
use yazi_shared::path::PathBufDyn;
use yazi_shim::mlua::UserDataFieldsExt;
use super::TrashId;
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,
pub(crate) struct TrashEntry {
pub(super) id: TrashId,
pub(super) cha: Cha,
pub(super) lcha: Cha,
pub(super) original: Option<PathBuf>,
pub(super) link_to: Option<PathBuf>,
pub(super) backing: PathBuf,
}
impl Deref for TrashEntry {
type Target = TrashId;
fn deref(&self) -> &Self::Target { &self.id }
}
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> {
#[cfg(trash_unix)]
pub(super) fn new<B>(id: TrashId, backing: B, original: Option<PathBuf>) -> io::Result<Self>
where
B: Into<PathBuf>,
{
use super::TrashCha;
let backing = backing.into();
let cha = Cha::from_trash(&path, &name, true)?;
let link_to = if cha.is_link() { std::fs::read_link(&path).ok() } else { None };
let name = id
.rel()
.file_name()
.or_else(|| original.as_deref().and_then(Path::file_name))
.or_else(|| backing.file_name())
.filter(|name| !name.is_empty())
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))?;
Ok(Self { name, key, cha, link_to, backing: path })
let (lcha, cha) = Cha::from_trash(&backing, name)?;
let link_to = if lcha.is_link() { std::fs::read_link(&backing).ok() } else { None };
Ok(Self { id, cha, lcha, original, link_to, backing })
}
#[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))]
#[cfg(trash_unix)]
pub(super) fn top<T, B>(top: T, backing: B, original: Option<PathBuf>) -> io::Result<Self>
where
T: Into<PathBuf>,
B: Into<PathBuf>,
{
Self::new(TrashId::new(top, PathBuf::new())?, backing, original)
}
#[cfg(trash_unix)]
pub(super) fn child(&self, name: OsString) -> io::Result<Self> {
let original = self.original.as_ref().map(|original| original.join(&name));
Self::new(self.id.child(&name)?, self.backing.join(&name), original)
}
pub(super) fn key(&self) -> &OsStr { self.rel().file_name().unwrap_or(self.top().as_os_str()) }
pub(super) fn name(&self) -> &OsStr {
self
.rel()
.file_name()
.or_else(|| self.original.as_deref().and_then(Path::file_name))
.or_else(|| self.backing.file_name())
.expect("trash entry must have a name")
}
#[cfg(trash_unix)]
pub(super) fn into_file(self, url: impl Into<yazi_shared::url::UrlBuf>) -> crate::file::File {
use crate::file::{File, FileExtra};
@ -37,17 +86,23 @@ impl TrashEntry {
}
}
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)
impl FromLua for TrashEntry {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
AnyUserData::from_lua(value, lua)?.take()
}
}
impl UserData for TrashEntry {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("key", |lua, me| lua.create_string(me.key().as_encoded_bytes()));
fields.add_cached_field("top", |lua, me| {
lua.create_string(me.top().as_os_str().as_encoded_bytes())
});
fields.add_cached_field("rel", |_, me| Ok(PathBufDyn::from(me.rel())));
fields.add_cached_field("name", |lua, me| lua.create_string(me.name().as_encoded_bytes()));
fields.add_cached_field("cha", |_, me| Ok(me.cha));
fields.add_cached_field("original", |_, me| Ok(me.original.clone().map(PathBufDyn::Os)));
fields.add_cached_field("link_to", |_, me| Ok(me.link_to.clone().map(PathBufDyn::Os)));
fields.add_cached_field("backing", |_, me| Ok(PathBufDyn::Os(me.backing.clone())));
}
}

View file

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

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(trash trash_info);

View file

@ -0,0 +1,150 @@
use std::{fs::{self}, hash::Hash, io, path::Path};
use trash::os_limited;
use yazi_macro::ok_or_not_found;
use yazi_shim::Twox128;
use super::{super::{TrashCha, TrashEntries, TrashEntry, TrashId, restore_item}, TrashInfo};
use crate::{cha::{Cha, ChaSig}, file::File};
pub struct Trash;
impl Trash {
pub(crate) fn new() -> io::Result<Self> { Ok(Self) }
pub(crate) fn list(&self, entry: Option<&TrashEntry>) -> io::Result<Vec<TrashEntry>> {
let Some(entry) = entry else {
return self.tops();
};
// TODO
if !entry.lcha.is_dir() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
}
fs::read_dir(&entry.backing)?
.map(|dirent| {
let dirent = dirent?;
entry.child(dirent.file_name())
})
.collect()
}
pub(crate) fn entry(&self, id: &TrashId) -> io::Result<TrashEntry> {
let info = TrashInfo::parse(id.top())?;
if !os_limited::trash_folders()
.map_err(io::Error::other)?
.iter()
.any(|folder| folder == &info.root)
{
return Err(io::Error::new(io::ErrorKind::NotFound, "trash item outside of trash folders"));
}
if id.has_rel() && !fs::symlink_metadata(&info.backing)?.file_type().is_dir() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
}
let backing = info.backing.join(id.rel());
TrashEntry::new(id.clone(), backing, Some(info.original.join(id.rel())))
}
pub(crate) fn metadata(&self, entry: &TrashEntry, follow: bool) -> io::Result<Cha> {
Ok(if follow { entry.cha } else { entry.lcha })
}
pub(crate) fn revalidate(
&self,
entry: Option<&TrashEntry>,
current: &File,
) -> io::Result<Option<File>> {
let latest = if let Some(entry) = entry {
entry.clone().into_file(&current.url)
} else {
let mut roots: Vec<_> = os_limited::trash_folders().unwrap_or_default().into_iter().collect();
roots.sort_unstable();
let mut h = Twox128::default();
for root in roots {
let meta = ok_or_not_found!(fs::metadata(root.join("info")), continue);
let cha = Cha::new(root.file_name().unwrap_or_default(), meta);
root.hash(&mut h);
ChaSig(cha).hash(&mut h);
}
let hash = h.finish_128();
File {
cha: Cha { len: hash as u64 ^ (hash >> 64) as u64, ..Cha::from_mold(true) },
..current.clone()
}
};
let changed = !latest.cha.hits(current.cha)
|| latest.extra.link_to() != current.extra.link_to()
|| latest.extra.backing() != current.extra.backing();
Ok(changed.then_some(latest))
}
pub(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> {
fs::remove_file(&entry.backing)?;
if !entry.has_rel() {
fs::remove_file(entry.top())?;
}
Ok(())
}
pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> {
fs::remove_dir(&entry.backing)?;
if !entry.has_rel() {
fs::remove_file(entry.top())?;
}
Ok(())
}
pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> {
for entry in entries {
let to = entry.original.clone().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location")
})?;
restore_item(&entry.backing, &to)?;
if !entry.has_rel() {
fs::remove_file(entry.top())?;
}
}
Ok(())
}
// FIXME: also rename or remove the .trashinfo file in the info folder
pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> {
fs::rename(&entry.backing, path)
}
pub(crate) fn empty(&self) -> io::Result<()> {
for entry in self.tops()? {
if entry.lcha.is_dir() {
fs::remove_dir_all(&entry.backing)?;
} else {
fs::remove_file(&entry.backing)?;
}
fs::remove_file(entry.top())?;
}
Ok(())
}
fn tops(&self) -> io::Result<Vec<TrashEntry>> {
let mut tops = Vec::new();
for root in os_limited::trash_folders().map_err(io::Error::other)? {
for entry in ok_or_not_found!(fs::read_dir(root.join("info")), continue) {
let entry = entry?;
let info = entry.path();
if let Ok(parsed) = TrashInfo::parse(&info) {
tops.push(TrashEntry::top(info, parsed.backing, Some(parsed.original))?);
}
}
}
Ok(tops)
}
}

View file

@ -0,0 +1,96 @@
use std::{borrow::Cow, ffi::OsStr, fs::File, io::{self, BufRead, BufReader}, os::unix::ffi::OsStrExt, path::{Path, PathBuf}};
use percent_encoding::percent_decode;
use uzers::Users;
use yazi_shared::USERS_CACHE;
use yazi_shim::path::PathExt;
pub(super) struct TrashInfo {
pub(super) root: PathBuf,
pub(super) backing: PathBuf,
pub(super) original: PathBuf,
}
impl TrashInfo {
// Parses from a trashinfo path, e.g.:
// /home/alice/.local/share/Trash/info/cat.jpg.trashinfo
pub(super) fn parse(info: &Path) -> io::Result<Self> {
if info.extension() != Some(OsStr::new("trashinfo")) {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash info path"));
}
// /home/alice/.local/share/Trash
let root = info
.parent()
.filter(|p| p.file_name() == Some(OsStr::new("info")))
.and_then(Path::parent)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash info path"))?;
// cat.jpg
let stem = info
.file_stem()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash info path"))?;
let original = Self::parse_original(info, root)?;
if original.file_name().is_none() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid original trash path"));
}
Ok(Self { root: root.to_owned(), backing: root.join("files").join(stem), original })
}
fn parse_original(info: &Path, root: &Path) -> io::Result<PathBuf> {
let mut reader = BufReader::new(File::open(info)?);
let mut line = Vec::new();
reader.read_until(b'\n', &mut line)?;
Self::trim_line(&mut line);
if line != b"[Trash Info]" {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash info header"));
}
loop {
line.clear();
if reader.read_until(b'\n', &mut line)? == 0 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "trash info has no Path"));
}
Self::trim_line(&mut line);
let Some(value) = line.strip_prefix(b"Path=") else { continue };
let decoded: Cow<[u8]> = percent_decode(value).into();
let path = Path::new(OsStr::from_bytes(decoded.as_ref()));
if path.as_os_str().is_empty() || !path.is_absolute() && path.has_parent_component() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid original trash path"));
}
return Ok(if path.is_absolute() {
path.to_owned()
} else {
Self::mount_point(root)?.join(path)
});
}
}
// /mnt/disk/.Trash/1000 => /mnt/disk
// /home/alice/.local/share/Trash => /home/alice/.local/share
fn mount_point(root: &Path) -> io::Result<&Path> {
let uid = USERS_CACHE.get_current_uid().to_string();
if root.file_name() == Some(OsStr::new(&uid))
&& let Some(parent) = root.parent()
&& parent.file_name() == Some(OsStr::new(".Trash"))
{
parent.parent()
} else {
root.parent()
}
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash mount point"))
}
fn trim_line(line: &mut Vec<u8>) {
while matches!(line.last(), Some(b'\n' | b'\r')) {
line.pop();
}
}
}

View file

@ -1,10 +1,11 @@
use std::io;
use mlua::{ExternalError, ExternalResult, IntoLuaMulti, LuaString, UserData, UserDataMethods, Value};
use mlua::{ExternalError, ExternalResult, FromLua, IntoLuaMulti, LuaString, UserData, UserDataMethods, Value};
use tokio::task::spawn_blocking;
use yazi_binding::Error;
use yazi_shared::path::{PathBufDyn, PathLike};
use super::{Trash, TrashNode, TrashNodes};
use super::{Trash, TrashEntries, TrashEntry, TrashId};
use crate::file::File;
impl UserData for Trash {
@ -16,31 +17,72 @@ impl UserData for Trash {
}
});
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, value: Value| async move {
if let Value::UserData(ud) = value {
return ud.borrow::<TrashEntry>()?.clone().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()? {
let id = TrashId::from_lua(value, &lua)?;
match spawn_blocking(move || Trash::new()?.entry(&id)).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()? {
methods.add_async_function("list", |lua, entry: Option<TrashEntry>| async move {
match spawn_blocking(move || Trash::new()?.list(entry.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("metadata", |lua, (entry, follow): (TrashEntry, bool)| async move {
match spawn_blocking(move || Trash::new()?.metadata(&entry, 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(
"remove",
|lua, (kind, entry): (LuaString, TrashEntry)| async move {
let f: fn(&Trash, &TrashEntry) -> 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()?, &entry)).await.into_lua_err()? {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
}
},
);
methods.add_async_function(
"rename",
|lua, (entry, path): (TrashEntry, PathBufDyn)| async move {
match spawn_blocking(move || Trash::new()?.rename(&entry, path.as_os()?))
.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, entries: TrashEntries| async move {
match spawn_blocking(move || Trash::new()?.restore(entries)).await.into_lua_err()? {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, 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))
|lua, (entry, file): (Option<TrashEntry>, File)| async move {
match spawn_blocking(move || Trash::new()?.revalidate(entry.as_ref(), &file))
.await
.into_lua_err()?
{
@ -49,25 +91,5 @@ impl UserData for Trash {
}
},
);
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),
}
});
}
}

View file

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

View file

@ -0,0 +1,57 @@
use std::{ffi::{OsStr, OsString}, fs, io, path::{Component, Path, PathBuf}};
use ds_parser::Value;
use hashbrown::HashMap;
use yazi_shim::path::PathExt;
#[derive(Default)]
pub(super) struct DsStore {
parent: Option<PathBuf>,
name: Option<OsString>,
}
impl DsStore {
pub(super) fn parse(path: &Path) -> io::Result<HashMap<OsString, DsStore>> {
let bytes = fs::read(path)?;
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)
}
pub(super) fn join(&self, rel: &Path) -> io::Result<PathBuf> {
if !rel.is_relative() || rel.has_parent_component() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path"));
}
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 rel.as_os_str().is_empty() { top_path } else { top_path.join(rel) })
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(trash ds_store);

View file

@ -0,0 +1,133 @@
use std::{fs, io, path::{Path, PathBuf}};
use yazi_macro::ok_or_not_found;
use super::{super::{TrashCha, TrashEntries, TrashEntry, TrashId, restore_item}, DsStore};
use crate::{cha::Cha, file::File};
pub struct Trash;
impl Trash {
pub(crate) fn new() -> io::Result<Self> { Ok(Self) }
pub(crate) fn list(&self, entry: Option<&TrashEntry>) -> io::Result<Vec<TrashEntry>> {
if entry.is_some_and(|entry| !entry.lcha.is_dir()) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
}
let root = self.root()?;
let store = if entry.is_none() {
DsStore::parse(&root.join(".DS_Store")).unwrap_or_default()
} else {
Default::default()
};
let path = entry.map_or(&root, |entry| &entry.backing);
let it = match fs::read_dir(path) {
Ok(it) => it,
Err(e) if e.kind() == io::ErrorKind::NotFound && entry.is_none() => return Ok(vec![]),
Err(e) => return Err(e),
};
it.map(|dirent| {
let dirent = dirent?;
if let Some(entry) = entry {
entry.child(dirent.file_name())
} else {
let path = dirent.path();
let original = store
.get(path.file_name().unwrap_or_default())
.and_then(|ds| ds.join(Path::new("")).ok());
TrashEntry::top(path.clone(), path, original)
}
})
.collect()
}
pub(crate) fn entry(&self, id: &TrashId) -> io::Result<TrashEntry> {
let root = self.root()?;
if id.top().parent() != Some(&root) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "item is not in the trash"));
}
if id.has_rel() && !fs::symlink_metadata(id.top())?.file_type().is_dir() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
}
let store = DsStore::parse(&root.join(".DS_Store")).unwrap_or_default();
let name = id.top().file_name().unwrap_or_default();
let original = store.get(name).and_then(|ds| ds.join(id.rel()).ok());
TrashEntry::new(id.clone(), id.path(), original)
}
pub(crate) fn metadata(&self, entry: &TrashEntry, follow: bool) -> io::Result<Cha> {
Ok(if follow { entry.cha } else { entry.lcha })
}
pub(crate) fn revalidate(
&self,
entry: Option<&TrashEntry>,
current: &File,
) -> io::Result<Option<File>> {
let latest = if let Some(entry) = entry {
entry.clone().into_file(&current.url)
} else {
let path = self.root()?;
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(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> {
fs::remove_file(&entry.backing)
}
pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> {
fs::remove_dir(&entry.backing)
}
pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> {
for entry in entries {
let to = entry.original.as_ref().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location")
})?;
restore_item(&entry.backing, &to)?;
}
Ok(())
}
pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> {
fs::rename(&entry.backing, path)
}
pub(crate) fn empty(&self) -> io::Result<()> {
let root = self.root()?;
for dirent in ok_or_not_found!(fs::read_dir(root), return Ok(())) {
let dirent = dirent?;
if dirent.file_type()?.is_dir() {
fs::remove_dir_all(dirent.path())?;
} else {
fs::remove_file(dirent.path())?;
}
}
Ok(())
}
fn root(&self) -> io::Result<PathBuf> {
dirs::home_dir()
.filter(|p| p.is_absolute())
.ok_or_else(|| io::Error::other("cannot determine home directory for trash root resolution"))
.map(|home| home.join(".Trash"))
}
}

View file

@ -1,4 +1,7 @@
yazi_macro::mod_flat!(entry lua node nodes);
yazi_macro::mod_flat!(entries entry lua trash_id);
#[cfg(trash_unix)]
yazi_macro::mod_flat!(common);
#[cfg(target_os = "macos")]
yazi_macro::mod_flat!(macos);
@ -6,11 +9,11 @@ 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")))]
#[cfg(trash_freedesktop)]
yazi_macro::mod_flat!(freedesktop);
#[cfg(any(target_os = "android", target_os = "ios"))]
#[cfg(trash_unsupported)]
yazi_macro::mod_flat!(unsupported);
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(trash_unsupported))]
yazi_macro::mod_flat!(traits);

View file

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

View file

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

View file

@ -1,14 +1,11 @@
use std::fs;
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>;
fn from_trash(path: &std::path::Path, name: &std::ffi::OsStr) -> std::io::Result<(Self, Self)>;
}
impl TrashCha for Cha {
@ -20,17 +17,13 @@ impl TrashCha for Cha {
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)))
fn from_trash(path: &std::path::Path, name: &std::ffi::OsStr) -> std::io::Result<(Self, Self)> {
let lcha = Cha::new(name, fs::symlink_metadata(path)?);
let cha = if lcha.is_link() {
lcha.follow(fs::metadata(path).ok().map(|meta| Cha::new(name, meta)))
} else {
cha
})
lcha
};
Ok((lcha, cha))
}
}

View file

@ -0,0 +1,59 @@
use std::{ffi::OsStr, io, path::{Path, PathBuf}};
use mlua::{BorrowedBytes, ExternalResult, FromLua, Lua, Table, Value};
use yazi_shared::{path::PathBufDyn, strand::AsStrand};
use yazi_shim::path::PathExt;
#[derive(Clone, Debug)]
pub(crate) struct TrashId {
top: PathBuf,
rel: PathBuf,
}
impl TrashId {
pub(super) fn new<T, R>(top: T, rel: R) -> io::Result<Self>
where
T: Into<PathBuf>,
R: Into<PathBuf>,
{
let top = top.into();
let rel = rel.into();
if top.as_os_str().is_empty() || !rel.is_relative() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry"));
}
if rel.has_parent_component() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path"));
}
Ok(Self { top, rel })
}
pub(super) fn top(&self) -> &Path { &self.top }
pub(super) fn rel(&self) -> &Path { &self.rel }
#[cfg(target_os = "macos")]
pub(super) fn path(&self) -> PathBuf { self.top.join(&self.rel) }
pub(super) fn child(&self, name: &OsStr) -> io::Result<Self> {
let rel = self.rel.join(name);
if !rel.is_relative() || rel.has_parent_component() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path"));
}
Ok(Self { top: self.top.clone(), rel })
}
pub(super) fn has_rel(&self) -> bool { !self.rel.as_os_str().is_empty() }
}
impl FromLua for TrashId {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let t = Table::from_lua(value, lua)?;
let top = t.raw_get::<BorrowedBytes>("top")?.as_strand().to_os_path()?;
let rel = t.raw_get::<PathBufDyn>("rel")?.into_os()?;
Self::new(top, rel).into_lua_err()
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(trash);

View file

@ -1,6 +1,6 @@
use std::io;
use std::{io, path::Path};
use super::{TrashEntry, TrashNode, TrashNodes};
use super::super::{TrashEntries, TrashEntry, TrashId};
use crate::{cha::Cha, file::File};
pub struct Trash;
@ -8,35 +8,39 @@ pub struct Trash;
impl Trash {
pub fn new() -> io::Result<Self> { Ok(Self) }
pub fn list(&self, _node: Option<&TrashNode>) -> io::Result<Vec<TrashEntry>> {
pub fn list(&self, _entry: Option<&TrashEntry>) -> 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> {
pub fn entry(&self, _id: &TrashId) -> 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> {
pub fn metadata(&self, _entry: &TrashEntry, _: bool) -> io::Result<Cha> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub(super) fn revalidate(
pub(crate) fn revalidate(
&self,
_node: Option<&TrashNode>,
_entry: Option<&TrashEntry>,
_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<()> {
pub fn remove_file(&self, _entry: &TrashEntry) -> 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<()> {
pub fn remove_dir(&self, _entry: &TrashEntry) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub fn restore(&self, _nodes: TrashNodes) -> io::Result<()> {
pub fn restore(&self, _entries: TrashEntries) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}
pub fn rename(&self, _entry: &TrashEntry, _path: &Path) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
}

View file

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

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(shell_item trash trash_sig);

View file

@ -0,0 +1,96 @@
use std::{ffi::{OsStr, OsString, c_void}, io, os::windows::ffi::{OsStrExt, OsStringExt}, path::{Path, PathBuf}};
use windows::{Win32::{Foundation::*, Storage::EnhancedStorage::*, System::{Com::{StructuredStorage::PropVariantToBSTR, *}, SystemServices::*}, UI::Shell::*}, core::{Interface, PCWSTR}};
use super::{super::{TrashCha, TrashEntry, TrashId}, trash::{error, operate, system_time}};
use crate::cha::Cha;
pub(super) struct ShellItem(pub(super) IShellItem);
impl ShellItem {
pub(super) fn new(name: impl AsRef<OsStr>) -> io::Result<Self> {
let name: Vec<u16> = name.as_ref().encode_wide().chain([0]).collect();
unsafe { SHCreateItemFromParsingName(PCWSTR(name.as_ptr()), None) }.map(Self).map_err(error)
}
pub(super) fn root() -> io::Result<Self> {
unsafe { SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, None) }
.map(Self)
.map_err(error)
}
pub(super) fn top(name: &Path) -> io::Result<Self> {
Self::root()?
.children()?
.into_iter()
.find(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING).is_ok_and(|s| s == name))
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "item is not in the recycle bin"))
}
pub(super) 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)
}
pub(super) fn cha(&self) -> io::Result<Cha> {
if let Ok(backing) = self.display_name(SIGDN_FILESYSPATH).map(PathBuf::from) {
let name = backing.file_name().unwrap_or_default();
match Cha::from_trash(&backing, name) {
Ok((_, cha)) => return Ok(cha),
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)
}
pub(super) fn original(&self) -> io::Result<PathBuf> {
let item: IShellItem2 = self.0.cast().map_err(error)?;
let value = unsafe {
item.GetProperty(&PROPERTYKEY { fmtid: PSGUID_DISPLACED, pid: PID_DISPLACED_FROM })
}
.map_err(error)?;
let parent = unsafe { PropVariantToBSTR(&value) }.map_err(error)?;
Ok(PathBuf::from(OsString::from_wide(&parent)).join(self.display_name(SIGDN_PARENTRELATIVE)?))
}
pub(super) fn entry(&self, id: TrashId, original: Option<PathBuf>) -> io::Result<TrashEntry> {
let backing: PathBuf = self.display_name(SIGDN_FILESYSPATH)?.into();
let (lcha, cha) = Cha::from_trash(&backing, backing.file_name().unwrap_or_default())?;
Ok(TrashEntry { id, cha, lcha, original, link_to: None, backing })
}
pub(super) fn delete(&self) -> io::Result<()> {
operate(FOF_NO_UI, |operation| unsafe { operation.DeleteItem(&self.0, None) })
}
pub(super) 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)
}
}
}

View file

@ -0,0 +1,199 @@
use std::{fs, io, os::windows::ffi::OsStrExt, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}};
use windows::{Win32::{Foundation::*, System::Com::*, UI::Shell::*}, core::PCWSTR};
use yazi_ffi::Com;
use super::{super::{TrashEntries, TrashEntry, TrashId}, shell_item::ShellItem, trash_sig::TrashSig};
use crate::{cha::Cha, file::File};
thread_local! {
static COM: io::Result<Com> = Com::new();
}
pub struct Trash;
impl Trash {
pub(crate) 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(crate) fn list(&self, entry: Option<&TrashEntry>) -> io::Result<Vec<TrashEntry>> {
let Some(entry) = entry else {
return self.tops();
};
if !entry.lcha.is_dir() || entry.lcha.is_indirect() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
}
let original = entry.original.as_deref().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location")
})?;
self
.resolve(entry)?
.children()?
.into_iter()
.map(|item| {
let name = item.display_name(SIGDN_PARENTRELATIVE)?;
item.entry(entry.id.child(&name)?, Some(original.join(&name)))
})
.collect()
}
pub(crate) fn entry(&self, id: &TrashId) -> io::Result<TrashEntry> {
let top = ShellItem::top(id.top())?;
let original = top.original()?.join(id.rel());
if !id.has_rel() {
return top.entry(id.clone(), Some(original));
}
let backing: PathBuf = top.display_name(SIGDN_FILESYSPATH)?.into();
let cha = Cha::new(backing.file_name().unwrap_or_default(), fs::symlink_metadata(&backing)?);
if cha.is_dir() && !cha.is_indirect() {
ShellItem::new(backing.join(id.rel()))?.entry(id.clone(), Some(original))
} else {
Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"))
}
}
pub(crate) fn metadata(&self, entry: &TrashEntry, follow: bool) -> io::Result<Cha> {
Ok(if follow { entry.cha } else { entry.lcha })
}
pub(crate) fn revalidate(
&self,
entry: Option<&TrashEntry>,
current: &File,
) -> io::Result<Option<File>> {
let cha = if let Some(entry) = entry {
TrashSig::item(&self.resolve(entry)?)?
} else {
TrashSig::root()?
};
Ok(if cha.hits(current.cha) { None } else { Some(File { cha, ..current.clone() }) })
}
pub(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> {
self.resolve(entry)?.delete()
}
pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> {
let item = self.resolve(entry)?;
if !entry.has_rel() {
item.delete()
} else if !item.children()?.is_empty() {
Err(io::Error::new(io::ErrorKind::DirectoryNotEmpty, "trash directory is not empty"))
} else {
item.delete()
}
}
pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> {
for entry in entries {
let to = entry.original.as_deref().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location")
})?;
self.restore_do(&self.resolve(&entry)?, &to)?;
}
Ok(())
}
pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> {
fs::rename(self.resolve(entry)?.display_name(SIGDN_FILESYSPATH)?, path)
}
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(crate) fn empty(&self) -> io::Result<()> {
unsafe {
SHEmptyRecycleBinW(
None,
PCWSTR::null(),
SHERB_NOCONFIRMATION | SHERB_NOPROGRESSUI | SHERB_NOSOUND,
)
}
.map_err(error)
}
fn resolve(&self, entry: &TrashEntry) -> io::Result<ShellItem> {
if entry.has_rel() { ShellItem::new(&entry.backing) } else { ShellItem::top(entry.top()) }
}
fn tops(&self) -> io::Result<Vec<TrashEntry>> {
ShellItem::root()?
.children()?
.into_iter()
.map(|item| {
let top = item.display_name(SIGDN_DESKTOPABSOLUTEPARSING)?;
let original = item.original()?;
item.entry(TrashId::new(top, PathBuf::new())?, Some(original))
})
.collect()
}
}
pub(super) 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(()) }
}
pub(super) 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)
}
}
pub(super) fn system_time(time: FILETIME) -> Option<SystemTime> {
let ticks = (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime);
let ticks = ticks.checked_sub(116_444_736_000_000_000)?;
Some(UNIX_EPOCH + Duration::new(ticks / 10_000_000, (ticks % 10_000_000) as u32 * 100))
}

View file

@ -0,0 +1,68 @@
use std::{ffi::OsString, hash::Hash, io, mem, time::{Duration, UNIX_EPOCH}};
use windows::{Win32::UI::Shell::*, core::PCWSTR};
use yazi_shim::Twox128;
use super::shell_item::ShellItem;
use crate::cha::Cha;
pub(super) struct TrashSig {
names: Option<Vec<OsString>>,
count_size: Option<(i64, i64)>,
}
impl TrashSig {
pub(super) 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))
}
pub(super) 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
}
}

View file

@ -7,18 +7,43 @@ local function top(url)
return url.name and url or nil
end
local function node(url)
local function entry(url)
local top = top(url)
if not top then
return
return -- We are at the root, no entry to return
end
return {
key = url.spec.domain,
return fs.trash.entry {
top = top.spec.domain,
rel = url:strip_prefix(top) or Path.os(""),
}
end
local function file(url, ent)
return File {
url = url,
cha = ent.cha,
link_to = ent.link_to,
backing = ent.backing,
}
end
local function files(parent, ents)
for i, ent in ipairs(ents) do
local url = parent:join(Path.os(ent.name)):into_domain(ent.key)
ents[i] = file(url, ent)
end
return ents
end
local function notify(action, err)
ya.notify {
title = "Trash",
content = string.format("Failed to %s: %s", action, err),
level = "error",
timeout = 10,
}
end
local function absolute(url)
if url.is_absolute then
return fs.clean_url(url)
@ -36,44 +61,72 @@ local function absolute(url)
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)
local restore_recursively
local function restore(ent)
local id = { top = ent.top, rel = ent.rel }
local ok, err = fs.trash.restore { ent }
if ok then
return true, 0
elseif err.kind ~= "AlreadyExists" then
return false, 1, err
end
return entries
return restore_recursively(id, err)
end
local function notify(action, err)
ya.notify {
title = "Trash",
content = string.format("Failed to %s: %s", action, err),
level = "error",
timeout = 10,
}
restore_recursively = function(id, collision)
local ent, err = fs.trash.entry(id)
if not ent then
return false, 1, err
elseif not ent.cha.is_dir or ent.cha.is_indirect then
return false, 1, collision
end
local target = ent.original and fs.cha(Url(ent.original), false)
if not target or not target.is_dir or target.is_indirect then
return false, 1, collision
end
local ents, err = fs.trash.list(fs.trash.entry(ent))
if not ents then
return false, 1, err
end
local failed, changed, first = 0, false, nil
for _, child in ipairs(ents) do
local a, b, c = restore(child)
changed, failed, first = changed or a, failed + b, first or c
end
if failed ~= 0 then
return changed, failed, first
end
local ok, err = fs.trash.remove("dir", ent)
return changed or ok, ok and 0 or 1, err
end
function M:setup()
ps.sub_remote("trash-restore", function(args)
ya.async(function()
local nodes = {}
local ents, err = {}, nil
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")
ents[i], err = entry(Url(arg))
if not ents[i] then
return notify("restore", err)
end
end
local ok, err = fs.trash.restore(nodes)
if ok then
local changed, failed, first = false, 0, nil
for _, ent in ipairs(ents) do
local a, b, c = restore(ent)
changed, failed, first = changed or a, failed + b, first or c
end
if changed then
ya.emit("refresh", {})
else
notify("restore", err)
end
if failed ~= 0 then
notify("restore", string.format("%d item(s) could not be restored: %s", failed, first))
end
end)
end)
@ -115,36 +168,46 @@ function M:provide(job)
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")
local ent, err = entry(job.url)
if ent then
return fs.trash.metadata(ent, op == "Metadata")
elseif err then
return nil, err
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)
local ent, err = entry(job.url)
if err then
return nil, err
end
local ents, err = fs.trash.list(ent)
if ents then
return files(job.url, ents)
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")
local ent, err = entry(job.file.url)
if err then
return nil, err
else
return fs.trash.revalidate(ent, job.file)
end
local entry, err = fs.trash.entry(n)
if entry then
return file(job.url, entry)
elseif op == "File" then
local ent, err = entry(job.url)
if ent then
return file(job.url, ent)
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))
elseif op == "RemoveFile" or op == "RemoveDir" then
local ent, err = entry(job.url)
if ent then
return fs.trash.remove(op == "RemoveDir" and "dir" or "file", ent)
else
return false, err
end
end
return false, Error.custom("Unsupported trash operation: " .. op)

View file

@ -43,7 +43,7 @@ impl File {
_ => Ok(()),
},
async |task, cha| {
Ok(if cha.is_orphan() || (cha.is_link() && !task.follow) {
Ok(if cha.is_orphan() || (cha.is_indirect() && !task.follow) {
self.ops.out(id, FileOutCopy::New(0));
self.requeue(task.into_link(), NORMAL);
} else {
@ -115,7 +115,7 @@ impl File {
_ => Ok(()),
},
|task, cha| {
let nofollow = cha.is_orphan() || (cha.is_link() && !task.follow);
let nofollow = cha.is_orphan() || (cha.is_indirect() && !task.follow);
self.ops.out(id, FileOutCut::New(if nofollow { 0 } else { cha.len }));
if nofollow {
@ -196,6 +196,7 @@ impl File {
}
pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> {
let mut cha = task.cha;
let mut src: PathCow = if task.resolve {
ok_or_not_found!(
task,
@ -216,17 +217,29 @@ impl File {
ctx!(
task,
engine::symlink(&task.to, src, async || {
Ok(match task.cha {
Ok(match cha {
Some(cha) => cha.is_dir(),
None => Self::cha(&task.from, task.resolve, None).await?.is_dir(),
None => {
cha = Some(Self::cha(&task.from, task.resolve, None).await?);
cha.unwrap().is_dir()
}
})
})
.await
)?;
if task.delete {
engine::remove_file(&task.from).await.ok();
let cha = match cha {
Some(cha) => cha,
None => ctx!(task, Self::cha(&task.from, task.resolve, None).await)?,
};
if cha.is_dir() && cha.is_indirect() {
engine::remove_dir(&task.from).await.ok();
} else {
engine::remove_file(&task.from).await.ok();
}
}
Ok(self.ops.out(task.id, FileOutLink::Succ))
}
@ -291,13 +304,20 @@ impl File {
}
pub(crate) async fn delete_do(&self, task: FileInDelete) -> Result<(), FileOutDeleteDo> {
match engine::remove_file(&task.target).await {
let cha = task.cha.unwrap();
let result = if cha.is_dir() && cha.is_indirect() {
engine::remove_dir(&task.target).await
} else {
engine::remove_file(&task.target).await
};
match result {
Ok(()) => {}
Err(e) if e.kind() == NotFound => {}
Err(_) if !maybe_exists(&task.target).await => {}
Err(e) => ctx!(task, Err(e))?,
}
Ok(self.ops.out(task.id, FileOutDeleteDo::Succ(task.cha.unwrap().len)))
Ok(self.ops.out(task.id, FileOutDeleteDo::Succ(cha.len)))
}
pub(crate) async fn trash(&self, task: FileInTrash) -> Result<(), FileOutTrash> {

View file

@ -34,7 +34,7 @@ impl Transaction {
let url = url.as_url();
let cha = ok_or_not_found!(engine::symlink_metadata(url).await, return Ok(()));
if cha.is_link() {
if cha.is_indirect() {
engine::rename(Self::tmp(url).await?, url).await?;
} else if !cha.contains(ChaMode::U_WRITE) {
engine::set_attrs(url, Attrs::mode(cha.mode | ChaMode::U_WRITE)).await?;

View file

@ -159,7 +159,8 @@ where
E: Fn(String),
{
let cha = ctx!(task, task.init().await)?;
if !cha.is_dir() {
let follow_symlink = cha.is_link() && task.follow();
if !cha.is_dir() || (!follow_symlink && cha.is_indirect()) {
return on_file(task, cha).await;
}
@ -198,7 +199,8 @@ where
"Cannot get metadata for {from:?}"
);
if cha.is_dir() {
let follow_symlink = cha.is_link() && task.follow();
if cha.is_dir() && (follow_symlink || !cha.is_indirect()) {
dirs.push_back(from);
continue;
}

View file

@ -1,4 +1,4 @@
use std::{fmt, hash::{Hash, Hasher}, sync::Arc};
use std::{fmt, sync::Arc};
use serde::Deserialize;
use yazi_shim::cell::RoCell;
@ -7,7 +7,7 @@ use crate::{auth::{AuthInventory, AuthKind, Domain, EncodeAuth, Scheme}, path::{
pub(super) static DEFAULT_ARC: RoCell<Arc<Auth>> = RoCell::new();
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq)]
pub struct Auth {
pub kind: AuthKind,
pub scheme: Scheme,
@ -16,22 +16,6 @@ pub struct Auth {
pub parent: Option<Arc<Auth>>,
}
impl PartialEq for Auth {
fn eq(&self, other: &Self) -> bool {
self.kind == other.kind && self.scheme == other.scheme && self.domain == other.domain
}
}
impl Eq for Auth {}
impl Hash for Auth {
fn hash<H: Hasher>(&self, state: &mut H) {
self.kind.hash(state);
self.scheme.hash(state);
self.domain.hash(state);
}
}
impl Default for Auth {
fn default() -> Self { Self::DEFAULT }
}

View file

@ -22,6 +22,10 @@ impl From<std::path::PathBuf> for PathBufDyn {
fn from(value: std::path::PathBuf) -> Self { Self::Os(value) }
}
impl From<&std::path::PathBuf> for PathBufDyn {
fn from(value: &std::path::PathBuf) -> Self { Self::Os(value.clone()) }
}
impl From<typed_path::UnixPathBuf> for PathBufDyn {
fn from(value: typed_path::UnixPathBuf) -> Self { Self::Unix(value) }
}

View file

@ -92,14 +92,19 @@ impl<'a> Strand<'a> {
}
}
#[inline]
pub fn to_os_string(self) -> Result<OsString, StrandError> { self.as_os().map(|s| s.to_owned()) }
#[inline]
pub fn as_os_path(self) -> Result<&'a std::path::Path, StrandError> {
self.as_os().map(std::path::Path::new)
}
#[inline]
pub fn to_os_path(self) -> Result<std::path::PathBuf, StrandError> {
self.as_os().map(std::path::PathBuf::from)
}
#[inline]
pub fn to_os_string(self) -> Result<OsString, StrandError> { self.as_os().map(|s| s.to_owned()) }
#[inline]
pub fn as_unix_path(self) -> &'a typed_path::UnixPath {
typed_path::UnixPath::new(self.encoded_bytes())

View file

@ -22,7 +22,6 @@ yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
# External dependencies
anyhow = { workspace = true }
arc-swap = { workspace = true }
base64 = { workspace = true }
hashbrown = { workspace = true }
mlua = { workspace = true }
percent-encoding = { workspace = true }

View file

@ -1,8 +0,0 @@
use base64::{alphabet::STANDARD, engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}};
pub const BASE64_SANE: GeneralPurpose = GeneralPurpose::new(
&STANDARD,
GeneralPurposeConfig::new()
.with_encode_padding(false)
.with_decode_padding_mode(DecodePaddingMode::Indifferent),
);

View file

@ -1,6 +1,6 @@
yazi_macro::mod_pub!(arc_swap cell fs mlua path ratatui serde strum toml vec wtf8);
yazi_macro::mod_flat!(base64 option percent_encoding sstr twox utf8);
yazi_macro::mod_flat!(option percent_encoding sstr twox utf8);
#[cfg(windows)]
yazi_macro::mod_flat!(win32);

View file

@ -1 +1 @@
yazi_macro::mod_flat!(separator);
yazi_macro::mod_flat!(separator traits);

View file

@ -0,0 +1,11 @@
use std::path::{Component, Path};
pub trait PathExt {
fn has_parent_component(&self) -> bool;
}
impl PathExt for Path {
fn has_parent_component(&self) -> bool {
self.components().any(|c| matches!(c, Component::ParentDir))
}
}

View file

@ -1,8 +1,7 @@
use std::str::SplitWhitespace;
use base64::Engine;
use base64::{Engine, engine::general_purpose::STANDARD_NO_PAD_INDIFFERENT};
use strum::{FromRepr, IntoStaticStr};
use yazi_shim::BASE64_SANE;
use crate::parser::StateOsc72;
@ -195,7 +194,7 @@ impl DndEvent {
}),
b'r' => Self::DropArrive(DndDropArrive {
idx: s.x?.try_into().ok()?,
data: BASE64_SANE.decode(&s.payload).ok()?,
data: STANDARD_NO_PAD_INDIFFERENT.decode(&s.payload).ok()?,
}),
b'R' => {
let (name, desc) = parse_error(s.payload)?;

View file

@ -1,7 +1,6 @@
use std::{fmt::{self, Display}, str};
use base64::Engine;
use yazi_shim::BASE64_SANE;
use base64::{Engine, engine::general_purpose::STANDARD_NO_PAD_INDIFFERENT};
use super::traits::Mimelist;
@ -92,7 +91,7 @@ pub struct PresentDrag<'a>(pub u8, pub &'a [u8]);
impl Display for PresentDrag<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let b64 = BASE64_SANE.encode(self.1).into_bytes();
let b64 = STANDARD_NO_PAD_INDIFFERENT.encode(self.1).into_bytes();
let chunks = b64.len().div_ceil(4096);
for (i, chunk) in b64.chunks(4096).enumerate() {
@ -120,7 +119,7 @@ pub struct PresentDragIcon<'a> {
impl Display for PresentDragIcon<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let b64 = BASE64_SANE.encode(self.data).into_bytes();
let b64 = STANDARD_NO_PAD_INDIFFERENT.encode(self.data).into_bytes();
let chunks = b64.len().div_ceil(4096);
for (i, chunk) in b64.chunks(4096).enumerate() {

View file

@ -48,7 +48,8 @@ impl Raterm {
If(TMUX.get(), EnterAlternateScreen),
PushKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| PushKeyboardFlags::REPORT_ALTERNATE_KEYS
| PushKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
| PushKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
| PushKeyboardFlags::REPORT_ASSOCIATED_TEXT,
EnableDrag(""),
EnableDrop(&["text/uri-list"]),
If(opt.mouse, EnableMouseCapture),

View file

@ -18,7 +18,7 @@ impl SizeCalculator {
{
let url = url.as_url();
let cha = super::symlink_metadata(url).await?;
Ok(if cha.is_dir() {
Ok(if cha.is_dir() && !cha.is_indirect() {
Self::Dir(VecDeque::from([Either::Left(url.to_owned())]), cha)
} else {
Self::File(Some(cha.len), cha)
@ -77,10 +77,10 @@ impl SizeCalculator {
pop_and_continue!();
};
let Ok(ft) = ent.file_type().await else { continue };
if ft.is_dir() {
let Ok(cha) = ent.metadata().await else { continue };
if cha.is_dir() && !cha.is_indirect() {
buf.push_back(Either::Left(ent.url()));
} else if let Ok(cha) = ent.metadata().await {
} else {
size += cha.len;
}
}

View file

@ -193,6 +193,14 @@ impl<'a> Engine for Engines<'a> {
}
}
async fn remove_dir_clean(&self) -> io::Result<()> {
match self {
Self::Local(p) => p.remove_dir_clean().await,
Self::Lua(p) => p.remove_dir_clean().await,
Self::Sftp(p) => p.remove_dir_clean().await,
}
}
async fn remove_file(&self) -> io::Result<()> {
match self {
Self::Local(p) => p.remove_file().await,