diff --git a/.github/workflows/validate-issue.yml b/.github/workflows/validate-issue.yml index 7e997036..2fb7bc4b 100644 --- a/.github/workflows/validate-issue.yml +++ b/.github/workflows/validate-issue.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 20 diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 17d9009e..a4640a92 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -1,7 +1,7 @@ name: Validate PR on: - pull_request_target: + pull_request: types: [opened, edited, reopened, synchronize] permissions: @@ -17,7 +17,7 @@ jobs: ref: ${{ github.event.repository.default_branch }} - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 20 diff --git a/CHANGELOG.md b/CHANGELOG.md index 09209fa6..0c6b4322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Added - Drag and drop ([#4005]) +- Trash bin ([#4144]) - Bulk create ([#3793]) - Make help menu a command palette ([#4074]) - Input history ([#4104]) @@ -1781,3 +1782,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4108]: https://github.com/sxyazi/yazi/pull/4108 [#4118]: https://github.com/sxyazi/yazi/pull/4118 [#4120]: https://github.com/sxyazi/yazi/pull/4120 +[#4144]: https://github.com/sxyazi/yazi/pull/4144 diff --git a/Cargo.lock b/Cargo.lock index cd76337b..b4c657e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,7 +106,7 @@ dependencies = [ "ratatui-core", "simdutf8", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "approx" @@ -198,7 +198,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -213,6 +213,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "arrayvec" version = "0.7.8" @@ -258,7 +264,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "y4m", ] @@ -349,6 +355,30 @@ dependencies = [ "serde", ] +[[package]] +name = "binrw" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53195f985e88ab94d1cc87e80049dd2929fd39e4a772c5ae96a7e5c4aad3642" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5910da05ee556b789032c8ff5a61fb99239580aa3fd0bfaa8f4d094b2aee00ad" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "bit_field" version = "0.10.3" @@ -357,9 +387,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -445,7 +475,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.119", ] [[package]] @@ -459,9 +489,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "serde_core", @@ -487,9 +517,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.25.1" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" dependencies = [ "bytemuck_derive", ] @@ -502,7 +532,7 @@ checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -543,9 +573,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -561,9 +591,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -606,9 +636,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" dependencies = [ "clap_builder", "clap_derive", @@ -616,9 +646,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -647,9 +677,9 @@ dependencies = [ [[package]] name = "clap_complete_nushell" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb9e9715d29a754b468591be588f6b926f5b0a1eb6a8b62acabeb66ff84d897" +checksum = "933b05d5d83ff65fd7eaf5d106c792f2264908790a2642aca57429767b762ce2" dependencies = [ "clap", "clap_complete", @@ -664,7 +694,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -715,6 +745,20 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "compact_str" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "serde", + "static_assertions", + "zmij", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -899,7 +943,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -922,7 +966,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] @@ -933,7 +977,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -970,7 +1014,7 @@ checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1035,6 +1079,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ds_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d632a0239fbe380ec79e8f80f5d2643da25ecfd61cde10d8c9e927d1c035acb" +dependencies = [ + "binrw", + "thiserror 2.0.19", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1125,7 +1179,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1145,7 +1199,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1216,9 +1270,9 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fax" @@ -1306,9 +1360,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1321,9 +1375,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1331,15 +1385,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1348,38 +1402,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -1477,9 +1531,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -1735,7 +1789,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1758,7 +1812,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1820,7 +1874,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -2024,9 +2078,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -2092,7 +2146,7 @@ checksum = "0232231813b214d44b57c7678e7dbbcfaac35bd1a01acef0dad7c5dfdc1b1973" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2228,7 +2282,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2374,6 +2428,12 @@ dependencies = [ "serde", ] +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + [[package]] name = "p256" version = "0.14.0-rc.15" @@ -2429,7 +2489,7 @@ dependencies = [ "log", "rand 0.10.2", "sha2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "windows 0.62.2", "windows-strings", @@ -2457,7 +2517,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2635,9 +2695,9 @@ dependencies = [ [[package]] name = "polyval" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b20f20e954175de5f463f67781b35583397d916b1d148738923711b2ad16bee8" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ "cpubits", "cpufeatures", @@ -2646,9 +2706,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "powerfmt" @@ -2672,7 +2732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.119", ] [[package]] @@ -2704,9 +2764,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -2727,7 +2787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2801,9 +2861,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2898,7 +2958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ "bitflags", - "compact_str", + "compact_str 0.9.1", "critical-section", "hashbrown 0.17.1", "itertools", @@ -2907,7 +2967,7 @@ dependencies = [ "palette", "serde", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -2963,7 +3023,7 @@ dependencies = [ "rand 0.9.5", "rand_chacha", "simd_helpers", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "wasm-bindgen", ] @@ -3034,34 +3094,34 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3071,9 +3131,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3199,7 +3259,7 @@ dependencies = [ "ssh-encoding", "ssh-key", "subtle", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "typenum", "universal-hash", @@ -3278,9 +3338,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "safe_arch" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed" +checksum = "3a52ec151f024d703f9fd65abb7cbe81e7cdb39f18917a3a37e3014470dc7c59" dependencies = [ "bytemuck", ] @@ -3368,9 +3428,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -3388,29 +3448,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -3457,7 +3517,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3561,9 +3621,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_helpers" @@ -3594,9 +3654,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3707,7 +3767,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3724,9 +3784,20 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", @@ -3749,7 +3820,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", ] @@ -3783,11 +3854,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -3798,18 +3869,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -3857,9 +3928,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "libc", @@ -3879,9 +3950,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -3904,9 +3975,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", @@ -3921,13 +3992,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3956,9 +4027,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -3989,9 +4060,9 @@ dependencies = [ [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tracing" @@ -4012,7 +4083,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tracing-subscriber", ] @@ -4025,7 +4096,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4087,11 +4158,11 @@ dependencies = [ [[package]] name = "twox-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" dependencies = [ - "rand 0.9.5", + "rand 0.10.2", ] [[package]] @@ -4319,7 +4390,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -4340,9 +4411,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "which" -version = "8.0.4" +version = "8.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" +checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" dependencies = [ "libc", ] @@ -4441,7 +4512,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4452,7 +4523,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4463,7 +4534,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4474,7 +4545,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4696,9 +4767,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" @@ -4873,6 +4944,7 @@ dependencies = [ "yazi-fs", "yazi-macro", "yazi-shared", + "yazi-shim", "yazi-term", "yazi-tty", "yazi-version", @@ -4884,7 +4956,7 @@ version = "26.5.6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -5007,6 +5079,7 @@ dependencies = [ "core-foundation-sys", "libc", "objc2", + "windows 0.62.2", "yazi-macro", ] @@ -5061,7 +5134,9 @@ dependencies = [ "arc-swap", "bitflags", "core-foundation-sys", + "data-encoding", "dirs", + "ds_parser", "either", "foldhash", "hashbrown 0.17.1", @@ -5070,17 +5145,18 @@ dependencies = [ "mlua", "objc2", "parking_lot", - "percent-encoding", "rand 0.10.2", "regex", "scopeguard", "serde", + "serde_with", "strum", "tokio", "tracing", "trash", "typed-path", "uzers", + "windows 0.62.2", "windows-sys 0.61.2", "yazi-binding", "yazi-ffi", @@ -5198,7 +5274,7 @@ dependencies = [ "mlua", "parking_lot", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tracing", @@ -5245,6 +5321,7 @@ name = "yazi-sftp" version = "26.5.6" dependencies = [ "bitflags", + "hashbrown 0.17.1", "parking_lot", "russh", "serde", @@ -5273,7 +5350,7 @@ dependencies = [ "serde", "serde_with", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "typed-path", "uzers", @@ -5297,8 +5374,9 @@ dependencies = [ "ratatui-core", "ratatui-widgets", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml", + "trash", "twox-hash", "unicode-segmentation", "unicode-width", @@ -5312,7 +5390,7 @@ dependencies = [ "anyhow", "base64", "bitflags", - "compact_str", + "compact_str 0.10.0", "futures", "mlua", "parking_lot", @@ -5320,7 +5398,7 @@ dependencies = [ "serde", "signal-hook", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "windows-sys 0.61.2", "yazi-macro", @@ -5401,9 +5479,9 @@ version = "26.5.6" dependencies = [ "futures", "hashbrown 0.17.1", + "indexmap 2.14.0", "notify", "parking_lot", - "percent-encoding", "tokio", "tokio-stream", "tracing", @@ -5460,7 +5538,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5471,9 +5549,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zmij" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2f034a4bebf216c9e4b7083603e024cf930873fd67830cfb083c9fa33129d9" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index a4f5aa33..af8f8145 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,20 +36,21 @@ debug = false [workspace.dependencies] ansi-to-tui = "8.0.1" -anyhow = "1.0.103" +anyhow = "1.0.104" arc-swap = { version = "1.9.2", features = [ "serde" ] } base64 = "0.22.1" -bitflags = { version = "2.13.0", features = [ "serde" ] } +bitflags = { version = "2.13.1", features = [ "serde" ] } chrono = "0.4.45" -clap = { version = "4.6.1", features = [ "derive" ] } -compact_str = { version = "0.9.1", features = [ "serde" ] } +clap = { version = "4.6.2", features = [ "derive" ] } +compact_str = { version = "0.10.0", features = [ "serde" ] } core-foundation-sys = "0.8.7" +data-encoding = "2.11.0" dirs = "6.0.0" dyn-clone = "1.0.20" either = { version = "1.16.0" } foldhash = "0.2.0" -futures = "0.3.32" -globset = "0.4.18" +futures = "0.3.33" +globset = "0.4.19" hashbrown = { version = "0.17.1", features = [ "serde" ] } image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] } indexmap = { version = "2.14.0", features = [ "serde" ] } @@ -65,21 +66,21 @@ percent-encoding = "2.3.2" rand = { version = "0.10.2", default-features = false, features = [ "std", "sys_rng" ] } ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] } ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] } -regex = "1.13.0" +regex = "1.13.1" russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" -serde = { version = "1.0.228", features = [ "derive" ] } -serde_json = "1.0.150" +serde = { version = "1.0.229", features = [ "derive" ] } +serde_json = "1.0.151" serde_with = "3.21.0" strum = { version = "0.28.0", features = [ "derive" ] } syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } -thiserror = "2.0.18" -tokio = { version = "1.52.3", features = [ "full" ] } +thiserror = "2.0.19" +tokio = { version = "1.53.0", features = [ "full" ] } tokio-stream = "0.1.18" tokio-util = "0.7.18" -toml = { version = "1.1.2" } +toml = { version = "1.1.3" } tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] } -twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } +twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] } typed-path = "0.12.3" unicode-width = { version = "0.2.2", default-features = false } uzers = "0.12.2" diff --git a/yazi-actor/src/mgr/bulk_create.rs b/yazi-actor/src/mgr/bulk_create.rs index ea054392..8179074c 100644 --- a/yazi-actor/src/mgr/bulk_create.rs +++ b/yazi-actor/src/mgr/bulk_create.rs @@ -4,16 +4,16 @@ use anyhow::{Result, anyhow}; use scopeguard::defer; use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRuleArc}; -use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}, file::File}; +use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}}; use yazi_macro::{succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt}; -use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, UrlBuf, UrlLike}}; +use yazi_shared::{data::Data, strand::Strand, url::{UrlBuf, UrlLike}}; use yazi_shim::path::CROSS_SEPARATOR; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::{TTY, sequence::EraseScreen}; -use yazi_vfs::{VfsFile, engine}; +use yazi_vfs::engine; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -31,7 +31,7 @@ impl Actor for BulkCreate { let cwd = cx.cwd().clone(); tokio::spawn(async move { let tmp = YAZI.preview.tmpfile("bulk-create"); - engine::create_new(&tmp).await?; + let file = engine::create_new(&tmp).await?.file().await?; defer! { let tmp = tmp.clone(); @@ -42,7 +42,7 @@ impl Actor for BulkCreate { TasksProxy::process_exec(ShellOpt { cwd: cwd.clone(), - cmd: Splatter::new(&[tmp.as_url()]).splat(&opener.run), + cmd: Splatter::new(&[file]).splat(&opener.run), block: opener.block, orphan: opener.orphan, }) @@ -86,7 +86,7 @@ impl BulkCreate { if let Err(e) = result { failed.push((entry, e.into())); - } else if let Ok(f) = File::new(dist).await { + } else if let Ok(f) = engine::file(dist).await { succeeded.push(f); } else { failed.push((entry, anyhow!("Failed to retrieve file info"))); diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 9e2865f5..19dbeb69 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -7,7 +7,7 @@ use tokio::io::AsyncWriteExt; use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRuleArc}; use yazi_dds::Pubsub; -use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::{Demand, Local}}, file::File, max_common_root, path::skip_url}; +use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::Local}, max_common_root, path::skip_url}; use yazi_macro::{err, succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; @@ -15,7 +15,7 @@ use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt}; use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::{TTY, sequence::EraseScreen}; -use yazi_vfs::{VfsFile, engine, maybe_exists}; +use yazi_vfs::{engine::{self, Demand}, maybe_exists}; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -46,13 +46,8 @@ impl Actor for BulkRename { tokio::spawn(async move { let tmp = YAZI.preview.tmpfile("bulk-rename"); - Demand::default() - .write(true) - .create_new(true) - .open(&tmp) - .await? - .write_all(old.join(Strand::Utf8("\n")).encoded_bytes()) - .await?; + let mut rw = Demand::default().write(true).create_new(true).open(&tmp).await?; + rw.write_all(old.join(Strand::Utf8("\n")).encoded_bytes()).await?; defer! { let tmp = tmp.clone(); @@ -65,7 +60,7 @@ impl Actor for BulkRename { batcher.prime(&tmp); TasksProxy::process_exec(ShellOpt { cwd, - cmd: Splatter::new(&[tmp.as_url()]).splat(&opener.run), + cmd: Splatter::new(&[rw.into_file().await?]).splat(&opener.run), block: opener.block, orphan: opener.orphan, }) @@ -131,7 +126,7 @@ impl BulkRename { failed.push((o, n, anyhow!("Destination already exists"))); } else if let Err(e) = engine::rename(&old, &new).await { failed.push((o, n, e.into())); - } else if let Ok(f) = File::new(new).await { + } else if let Ok(f) = engine::file(new).await { succeeded.insert(old, f); } else { failed.push((o, n, anyhow!("Failed to retrieve file info"))); diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 2e6b9efc..e0c6cae0 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -6,12 +6,12 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::YAZI; use yazi_core::mgr::CdSource; use yazi_dds::Pubsub; -use yazi_fs::{FilesOp, file::File, path::{clean_url, expand_url}}; +use yazi_fs::{FilesOp, path::{clean_url, expand_url}}; use yazi_macro::{act, err, input, render, succ}; use yazi_parser::mgr::CdForm; use yazi_proxy::{CmpProxy, MgrProxy}; use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFile, engine}; +use yazi_vfs::engine; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; @@ -76,7 +76,7 @@ impl Cd { let Ok(url) = engine::absolute(&url).await else { return }; let url = clean_url(url); - let Ok(file) = File::new(&url).await else { return }; + let Ok(file) = engine::file(&url).await else { return }; if file.is_dir() { return MgrProxy::cd(&url, CdSource::Cd); } diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 3bd14da7..9c3bde82 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -80,7 +80,7 @@ impl Create { if let Ok(real) = engine::casefold(&new).await && let Some((parent, key)) = real.pair2() { - let file = File::new(&real).await?; + let file = engine::file(&real).await?; FilesOp::Upserting(parent.into(), [(key.into(), file)].into()).emit(); MgrProxy::reveal(&real); } diff --git a/yazi-actor/src/mgr/download.rs b/yazi-actor/src/mgr/download.rs index 1a6da9ee..0cb88708 100644 --- a/yazi-actor/src/mgr/download.rs +++ b/yazi-actor/src/mgr/download.rs @@ -4,12 +4,12 @@ use anyhow::Result; use futures::{StreamExt, stream::FuturesUnordered}; use hashbrown::HashSet; use yazi_core::mgr::OpenOpt; -use yazi_fs::{FsSpec, engine::{Engine, local::Local}, file::File}; +use yazi_fs::{FsSpec, engine::{Engine, local::Local}}; use yazi_macro::succ; use yazi_parser::mgr::DownloadForm; use yazi_proxy::MgrProxy; use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; use crate::{Actor, Ctx}; @@ -42,7 +42,7 @@ impl Actor for Download { continue; } - let Ok(f) = File::new(&url).await else { continue }; + let Ok(f) = engine::file(&url).await else { continue }; urls.push(url); files.push(f); diff --git a/yazi-actor/src/mgr/follow.rs b/yazi-actor/src/mgr/follow.rs index 50d9e113..a51f53eb 100644 --- a/yazi-actor/src/mgr/follow.rs +++ b/yazi-actor/src/mgr/follow.rs @@ -16,7 +16,7 @@ impl Actor for Follow { fn act(cx: &mut Ctx, _: Self::Form) -> Result { let Some(file) = cx.hovered() else { succ!() }; - let Some(link_to) = &file.link_to else { succ!() }; + let Some(link_to) = file.extra.link_to() else { succ!() }; let Some(parent) = file.url.parent() else { succ!() }; let Ok(joined) = parent.try_join(link_to) else { succ!() }; act!(mgr:reveal, cx, (clean_url(joined), CdSource::Follow)) diff --git a/yazi-actor/src/mgr/hidden.rs b/yazi-actor/src/mgr/hidden.rs index 02cb0ad5..75f7ac63 100644 --- a/yazi-actor/src/mgr/hidden.rs +++ b/yazi-actor/src/mgr/hidden.rs @@ -4,6 +4,7 @@ use yazi_fs::FolderStage; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::{mgr::HiddenForm, spark::SparkKind}; use yazi_shared::{Source, data::Data}; +use yazi_shim::OptionExt; use crate::{Actor, Ctx}; @@ -18,7 +19,7 @@ impl Actor for Hidden { let state = form.state.bool(cx.tab().pref.show_hidden); cx.tab_mut().pref.show_hidden = state; - let hovered = cx.hovered().map(|f| f.entry_key().to_owned()); + let hovered = cx.hovered().map(|f| f.entry_key()).owned(); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); @@ -30,9 +31,7 @@ impl Actor for Hidden { }; // Apply to CWD and parent - if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply)) - && (a | b) - { + if apply(cx.current_mut()) | cx.parent_mut().is_some_and(apply) { act!(mgr:hover, cx)?; act!(mgr:update_paged, cx)?; } diff --git a/yazi-actor/src/mgr/open.rs b/yazi-actor/src/mgr/open.rs index 492abb61..03bef4a5 100644 --- a/yazi-actor/src/mgr/open.rs +++ b/yazi-actor/src/mgr/open.rs @@ -3,12 +3,11 @@ use futures::StreamExt; use hashbrown::HashSet; use yazi_boot::ARGS; use yazi_core::mgr::OpenDoOpt; -use yazi_fs::file::File; use yazi_macro::{act, succ}; use yazi_parser::mgr::OpenForm; use yazi_proxy::MgrProxy; use yazi_shared::data::Data; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; use crate::{Actor, Ctx, mgr::Quit}; @@ -59,7 +58,7 @@ impl Actor for Open { let it = futures::stream::iter(opt.targets) .enumerate() - .map(|(i, url)| async move { File::new(url).await.ok().map(|file| (i, file)) }) + .map(|(i, url)| async move { engine::file(url).await.ok().map(|file| (i, file)) }) .buffered(3) .filter_map(|item| async move { item }); diff --git a/yazi-actor/src/mgr/open_do.rs b/yazi-actor/src/mgr/open_do.rs index db6232bd..86ab8f6d 100644 --- a/yazi-actor/src/mgr/open_do.rs +++ b/yazi-actor/src/mgr/open_do.rs @@ -42,10 +42,10 @@ impl Actor for OpenDo { } let pick = PickProxy::show(YAZI.pick.open(openers.iter().map(|o| o.desc()).collect())); - let urls: Vec<_> = targets.into_iter().map(|(file, _)| file.url).collect(); + let files: Vec<_> = targets.into_iter().map(|(file, _)| file).collect(); tokio::spawn(async move { if let Some(choice) = pick.await { - Self::open_with(&openers[choice], &opt.cwd, &urls); + Self::open_with(&openers[choice], &opt.cwd, &files); } }); succ!(); @@ -59,20 +59,20 @@ impl OpenDo { if let Some(open) = YAZI.open.matches(&file, mime) && let Some(opener) = YAZI.opener.first(&open) { - openers.entry(opener).or_default().push(file.url); + openers.entry(opener).or_default().push(file); } } - for (opener, urls) in openers { - Self::open_with(&opener, &cwd, &urls); + for (opener, files) in openers { + Self::open_with(&opener, &cwd, &files); } } - fn open_with(opener: &OpenerRule, cwd: &UrlBuf, urls: &[UrlBuf]) { - let size = if opener.spread { urls.len().max(1) } else { 1 }; - for urls in urls.chunks(size) { + fn open_with(opener: &OpenerRule, cwd: &UrlBuf, files: &[File]) { + let size = if opener.spread { files.len().max(1) } else { 1 }; + for files in files.chunks(size) { TasksProxy::process_open(ShellOpt { cwd: cwd.clone(), - cmd: Splatter::new(urls).splat(&opener.run), + cmd: Splatter::new(files).splat(&opener.run), block: opener.block, orphan: opener.orphan, }); diff --git a/yazi-actor/src/mgr/peek.rs b/yazi-actor/src/mgr/peek.rs index c1d41d28..e9a00e88 100644 --- a/yazi-actor/src/mgr/peek.rs +++ b/yazi-actor/src/mgr/peek.rs @@ -21,18 +21,14 @@ impl Actor for Peek { } let mime = cx.mgr.mimetype.owned(&hovered.url).unwrap_or_default(); - let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.cha)); + let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.file.clone())); if !cx.tab().preview.same_url(&hovered.url) { - cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_default(); + cx.tab_mut().preview.skip = folder.as_ref().map(|f| f.0).unwrap_or_default(); } if !cx.tab().preview.same_file(&hovered, &mime) { cx.tab_mut().preview.reset(); } - if !cx.tab().preview.same_folder(&hovered.url) { - cx.tab_mut().preview.folder_lock = None; - } - if matches!(form.only_if, Some(u) if u != hovered.url) { succ!(); } @@ -46,11 +42,13 @@ impl Actor for Peek { } } - if hovered.is_dir() { - cx.tab_mut().preview.go_folder(hovered, folder.map(|(_, cha)| cha), mime, form.force); - } else { - cx.tab_mut().preview.go(hovered, mime, form.force); + if let Some((_, file)) = folder { + cx.core.mgr.watcher.refresher.refresh([file]); + } else if hovered.is_dir() { + cx.core.mgr.watcher.refresher.load(&hovered); } + + cx.tab_mut().preview.go(hovered, mime, form.force); succ!(); } } diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index abbf1888..2ea4598d 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -1,10 +1,8 @@ use anyhow::Result; -use yazi_core::tab::Folder; -use yazi_fs::{CWD, Entries, FilesOp, cha::Cha}; +use yazi_fs::CWD; use yazi_macro::{act, succ}; use yazi_parser::VoidForm; -use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; -use yazi_vfs::{VfsEntries, VfsFilesOp}; +use yazi_shared::{data::Data, url::UrlLike}; use yazi_watcher::MgrProxy; use crate::{Actor, Ctx}; @@ -19,11 +17,13 @@ impl Actor for Refresh { fn act(cx: &mut Ctx, _: Self::Form) -> Result { CWD.set(cx.cwd(), Self::cwd_changed); - if let Some(p) = cx.parent() { - Self::trigger_dirs(&[cx.current(), p]); - } else { - Self::trigger_dirs(&[cx.current()]); - } + cx.core.mgr.watcher.refresher.refresh( + [Some(cx.current()), cx.parent()] + .into_iter() + .flatten() + .filter(|f| f.url.is_absolute() && !f.url.is_search()) + .map(|f| &f.file), + ); act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; @@ -40,26 +40,4 @@ impl Refresh { MgrProxy::watch(); } } - - // TODO: performance improvement - fn trigger_dirs(folders: &[&Folder]) { - async fn go(dir: UrlBuf, cha: Cha) { - let Some(cha) = Entries::assert_stale(&dir, cha).await else { return }; - - match Entries::from_dir_bulk(&dir).await { - Ok(files) => FilesOp::Full(dir, files, cha).emit(), - Err(e) => FilesOp::issue_error(&dir, e).await, - } - } - - let futs: Vec<_> = folders - .iter() - .filter(|&f| f.url.is_absolute() && !f.url.is_search()) - .map(|&f| go(f.url.clone(), f.cha)) - .collect(); - - if !futs.is_empty() { - tokio::spawn(futures::future::join_all(futs)); - } - } } diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 031134fe..c3752754 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -84,7 +84,7 @@ impl Rename { let new = engine::casefold(&new).await?; let Some((new_p, new_k)) = new.pair2() else { return Ok(()) }; - let file = File::new(&new).await?; + let file = engine::file(&new).await?; if new_p == old_p { FilesOp::Upserting(old_p.into(), [(old_k.into(), file)].into()).emit(); } else { diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index 70b380a8..87ba3be6 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -5,7 +5,7 @@ use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::YAZI; use yazi_core::mgr::{CdSource, SearchVia}; -use yazi_fs::{FilesOp, cha::Cha}; +use yazi_fs::{FilesOp, cha::ChaType, file::File}; use yazi_macro::{act, input, succ}; use yazi_parser::{VoidForm, mgr::SearchForm}; use yazi_plugin::external; @@ -89,7 +89,7 @@ impl Actor for SearchDo { while let Some(chunk) = rx.next().await { FilesOp::Part(cwd.clone(), chunk, ticket).emit(); } - FilesOp::Done(cwd, Cha::default(), ticket).emit(); + FilesOp::Done(File::from_dummy(cwd, Some(ChaType::Dir)), ticket).emit(); Ok(()) })); diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index dd94b4f8..a3af3785 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -4,6 +4,7 @@ use yazi_fs::{FilesSorter, FolderStage}; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::{mgr::SortForm, spark::SparkKind}; use yazi_shared::{Source, data::Data}; +use yazi_shim::OptionExt; use crate::{Actor, Ctx}; @@ -24,7 +25,7 @@ impl Actor for Sort { pref.sort_fallback = form.fallback.unwrap_or(pref.sort_fallback); let sorter = FilesSorter::from(&*pref); - let hovered = cx.hovered().map(|f| f.entry_key().to_owned()); + let hovered = cx.hovered().map(|f| f.entry_key()).owned(); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); @@ -36,9 +37,7 @@ impl Actor for Sort { }; // Apply to CWD and parent - if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply)) - && (a | b) - { + if apply(cx.current_mut()) | cx.parent_mut().is_some_and(apply) { act!(mgr:hover, cx)?; act!(mgr:update_paged, cx)?; cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.entries); diff --git a/yazi-actor/src/mgr/watch.rs b/yazi-actor/src/mgr/watch.rs index d1dcea18..92e70fde 100644 --- a/yazi-actor/src/mgr/watch.rs +++ b/yazi-actor/src/mgr/watch.rs @@ -15,9 +15,11 @@ impl Actor for Watch { const NAME: &str = "watch"; fn act(cx: &mut Ctx, _: Self::Form) -> Result { - let it = iter::once(cx.core.mgr.tabs.active().cwd()) - .chain(cx.core.mgr.tabs.parent().map(|p| &p.url)) - .chain(cx.core.mgr.tabs.hovered().filter(|h| h.is_dir()).map(|h| &h.url)); + let tab = cx.core.mgr.tabs.active(); + + let it = iter::once(&tab.current.file) + .chain(tab.hovered_folder().map(|h| &h.file).or(tab.hovered().filter(|f| f.is_dir()))) + .chain(tab.parent.as_ref().map(|p| &p.file)); cx.core.mgr.watcher.watch(it); succ!(); diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index b2fd3ac1..f55f6334 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -209,15 +209,14 @@ macro_rules! impl_file_fields { $fields.add_cached_field("cha", |_, me| Ok(me.cha)); $fields.add_cached_field("url", |_, me| Ok(me.url_owned())); - $fields.add_cached_field("link_to", |_, me| Ok(me.link_to.clone())); + $fields.add_cached_field("link_to", |_, me| Ok(me.extra.link_to().cloned())); $fields.add_cached_field("name", |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); $fields.add_cached_field("path", |_, me| { - use yazi_fs::FsUrl; - use yazi_shared::{path::PathBufDyn, url::AsUrl}; - Ok(PathBufDyn::from(me.url.as_url().unified_path())) + use yazi_shared::path::PathBufDyn; + Ok(PathBufDyn::from(me.content_path())) }); $fields.add_cached_field("cache", |_, me| { use yazi_fs::FsUrl; @@ -231,8 +230,8 @@ macro_rules! impl_file_fields { macro_rules! impl_file_methods { ($methods:ident) => { $methods.add_method("hash", |_, me, ()| { - use yazi_fs::FsHash64; - Ok(me.hash_u64()) + use yazi_fs::{FsHash64, file::FileSig}; + Ok(FileSig(me).hash_u64()) }); }; } diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index e4fc91fd..b5b1dff7 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -32,4 +32,4 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } clap = { workspace = true } clap_complete = "4.6.7" clap_complete_fig = "4.5.2" -clap_complete_nushell = "4.6.0" +clap_complete_nushell = "4.6.1" diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 67fb422a..508d92f5 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -21,6 +21,7 @@ yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" } yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } +yazi-shim = { path = "../yazi-shim", version = "26.5.6" } yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.6" } yazi-version = { path = "../yazi-version", version = "26.5.6" } @@ -44,7 +45,7 @@ anyhow = { workspace = true } clap = { workspace = true } clap_complete = "4.6.7" clap_complete_fig = "4.5.2" -clap_complete_nushell = "4.6.0" +clap_complete_nushell = "4.6.1" serde = { workspace = true } serde_json = { workspace = true } diff --git a/yazi-cli/src/env/env.rs b/yazi-cli/src/env/env.rs index 1cc8e981..7e353d2d 100644 --- a/yazi-cli/src/env/env.rs +++ b/yazi-cli/src/env/env.rs @@ -5,6 +5,7 @@ use yazi_config::{THEME, YAZI}; use yazi_emulator::Mux; use yazi_fs::Xdg; use yazi_shared::timestamp_us; +use yazi_shim::OptionExt; use yazi_term::TERM; use crate::env::Env; @@ -156,7 +157,8 @@ impl Env { Regex::new(r"\d+\.\d+(\.\d+-\d+|\.\d+|\b)") .unwrap() .find(&line) - .map(|m| m.as_str().to_owned()) + .map(|m| m.as_str()) + .owned() .unwrap_or(line) } Ok(out) => format!("{:?}, {:?}", out.status, String::from_utf8_lossy(&out.stderr)), diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index 4dfcf0dc..830012a2 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -18,5 +18,5 @@ proc-macro = true [dependencies] # External dependencies proc-macro2 = "1" -quote = "1.0.46" -syn = { version = "2.0.118", features = [ "full" ] } +quote = "1.0.47" +syn = { version = "3.0.2", features = [ "full" ] } diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index db1fa4ff..9e4c4421 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -128,8 +128,9 @@ keymap = [ # Goto { on = [ "g", "h" ], run = "cd ~", desc = "Go home" }, - { on = [ "g", "c" ], run = "cd ~/.config", desc = "Go ~/.config" }, - { on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go ~/Downloads" }, + { on = [ "g", "c" ], run = "cd ~/.config", desc = "Go to ~/.config" }, + { on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go to ~/Downloads" }, + { on = [ "g", "t" ], run = "plugin trash", desc = "Go to trash bin" }, { on = [ "g", "" ], run = "cd --interactive", desc = "Jump interactively" }, { on = [ "g", "f" ], run = "follow", desc = "Follow hovered symlink" }, diff --git a/yazi-config/preset/vfs-default.toml b/yazi-config/preset/vfs-default.toml index e69de29b..b050b697 100644 --- a/yazi-config/preset/vfs-default.toml +++ b/yazi-config/preset/vfs-default.toml @@ -0,0 +1,3 @@ +[trash."*"] +kind = "hub" +run = "trash" diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index 1fc65c7f..0f82e7b0 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -63,9 +63,16 @@ download = [ { run = "ya emit download --open %S", desc = "Download and open" }, { run = "ya emit download %S", desc = "Download" }, ] +trash = [ + { run = "ya pub trash-restore --list %S", desc = "Restore selected files" }, + { run = "ya pub trash-empty --list %S", desc = "Empty trash bin" }, +] [open] rules = [ + # Trash + { url = "trash://*", use = [ "open", "trash" ] }, + { url = "trash://*/", use = [ "edit", "trash" ] }, # Folder { url = "*/", use = [ "edit", "open", "reveal" ] }, # Text @@ -102,6 +109,7 @@ fetchers = [ # MIME-type { url = "*/", run = "mime.dir", prio = "high", group = "mime" }, { url = "local://*", run = "mime.local", prio = "high", group = "mime" }, + { url = "trash://*", run = "mime.local", prio = "high", group = "mime" }, { url = "remote://*", run = "mime.remote", prio = "high", group = "mime" }, ] spotters = [ diff --git a/yazi-core/src/mgr/snap.rs b/yazi-core/src/mgr/snap.rs index fecb4de4..8019aeea 100644 --- a/yazi-core/src/mgr/snap.rs +++ b/yazi-core/src/mgr/snap.rs @@ -1,12 +1,11 @@ -use yazi_fs::Splatable; -use yazi_shared::url::{AsUrl, Url, UrlBuf}; +use yazi_fs::{Splatable, file::File}; use crate::{mgr::Mgr, tab::TabSnap}; pub struct MgrSnap { tab: usize, tabs: Vec, - yanked: Vec, + yanked: Vec, } impl From<&Mgr> for MgrSnap { @@ -14,7 +13,7 @@ impl From<&Mgr> for MgrSnap { Self { tab: value.tabs.cursor, tabs: value.tabs.iter().map(Into::into).collect(), - yanked: value.yanked.urls().cloned().collect(), + yanked: value.yanked.files().cloned().collect(), } } } @@ -22,7 +21,7 @@ impl From<&Mgr> for MgrSnap { impl Splatable for MgrSnap { fn tab(&self) -> usize { self.tab + 1 } - fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { + fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator { idx = idx.and_then(|i| i.checked_sub(1)); tab .checked_sub(1) @@ -31,24 +30,14 @@ impl Splatable for MgrSnap { .iter() .skip(idx.unwrap_or(0)) .take(if idx.is_some() { 1 } else { usize::MAX }) - .map(AsUrl::as_url) } - fn hovered(&self, tab: usize) -> Option> { - tab - .checked_sub(1) - .and_then(|tab| self.tabs.get(tab)) - .and_then(|tab| tab.hovered.as_ref()) - .map(AsUrl::as_url) + fn hovered(&self, tab: usize) -> Option<&File> { + tab.checked_sub(1).and_then(|tab| self.tabs.get(tab)).and_then(|tab| tab.hovered.as_ref()) } - fn yanked(&self, mut idx: Option) -> impl Iterator> { + fn yanked(&self, mut idx: Option) -> impl Iterator { idx = idx.and_then(|i| i.checked_sub(1)); - self - .yanked - .iter() - .skip(idx.unwrap_or(0)) - .take(if idx.is_some() { 1 } else { usize::MAX }) - .map(AsUrl::as_url) + self.yanked.iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }) } } diff --git a/yazi-core/src/mgr/yanked.rs b/yazi-core/src/mgr/yanked.rs index 008189a6..6358b200 100644 --- a/yazi-core/src/mgr/yanked.rs +++ b/yazi-core/src/mgr/yanked.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use hashbrown::HashSet; use indexmap::{IndexSet, set::MutableValues}; use yazi_dds::Pubsub; -use yazi_fs::{FilesOp, file::FileCov}; +use yazi_fs::{FilesOp, file::{File, FileCov}}; use yazi_macro::err; use yazi_shared::url::{Url, UrlBuf, UrlCov, UrlLike}; @@ -27,6 +27,8 @@ impl Yanked { Self { cut, files, ..Default::default() } } + pub fn files(&self) -> impl Iterator { self.files.iter().map(|f| &f.0) } + pub fn urls(&self) -> impl Iterator { self.files.iter().map(|f| &f.url) } pub fn remove_many<'a, I, T>(&mut self, urls: I) diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index e9b75d49..9df747c4 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -1,8 +1,8 @@ -use std::mem; +use std::{mem, ops::Deref}; use yazi_config::{LAYOUT, YAZI}; use yazi_dds::Pubsub; -use yazi_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File}; +use yazi_fs::{Entries, FilesOp, FolderStage, cha::ChaType, file::File}; use yazi_macro::err; use yazi_shared::{id::Id, path::{DynPath, PathBufDyn, PathDyn}, url::UrlBuf}; use yazi_widgets::{Scrollable, Step}; @@ -10,8 +10,7 @@ use yazi_widgets::{Scrollable, Step}; use crate::MgrProxy; pub struct Folder { - pub url: UrlBuf, - pub cha: Cha, + pub file: File, pub entries: Entries, pub stage: FolderStage, @@ -22,11 +21,16 @@ pub struct Folder { pub trace: Option, } +impl Deref for Folder { + type Target = File; + + fn deref(&self) -> &Self::Target { &self.file } +} + impl Default for Folder { fn default() -> Self { Self { - url: Default::default(), - cha: Default::default(), + file: Default::default(), entries: Entries::new(YAZI.mgr.show_hidden.get()), stage: Default::default(), offset: Default::default(), @@ -38,34 +42,36 @@ impl Default for Folder { } impl> From for Folder { - fn from(value: T) -> Self { Self { url: value.into(), ..Default::default() } } + fn from(value: T) -> Self { + Self { file: File::from_dummy(value, Some(ChaType::Dir)), ..Default::default() } + } } impl Folder { pub fn update(&mut self, op: FilesOp) -> bool { let (stage, revision) = (self.stage.clone(), self.entries.revision); match op { - FilesOp::Full(_, _, cha) => { - (self.cha, self.stage) = (cha, FolderStage::Loaded); + FilesOp::Full(ref file, _) => { + (self.file, self.stage) = (file.clone(), FolderStage::Loaded); } FilesOp::Part(_, ref files, _) if files.is_empty() => { - (self.cha, self.stage) = (Cha::default(), FolderStage::Loading); + self.stage = FolderStage::Loading; } FilesOp::Part(_, _, ticket) if ticket == self.entries.ticket() => { self.stage = FolderStage::Loading; } - FilesOp::Done(_, cha, ticket) if ticket == self.entries.ticket() => { - (self.cha, self.stage) = (cha, FolderStage::Loaded); + FilesOp::Done(ref file, ticket) if ticket == self.entries.ticket() => { + (self.file, self.stage) = (file.clone(), FolderStage::Loaded); } FilesOp::IOErr(_, ref err) => { - (self.cha, self.stage) = (Cha::default(), FolderStage::Failed(err.clone())); + self.stage = FolderStage::Failed(err.clone()); } _ => {} } let mut deleted = vec![]; match op { - FilesOp::Full(_, files, _) => self.entries.update_full(files), + FilesOp::Full(_, files) => self.entries.update_full(files), FilesOp::Part(_, files, ticket) => self.entries.update_part(files, ticket), FilesOp::Done(..) => {} FilesOp::Size(_, sizes) => self.entries.update_size(sizes), diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index da4a2f0b..0a1bcec8 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -1,14 +1,10 @@ -use std::time::Duration; - -use tokio::{pin, task::JoinHandle}; -use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; +use tokio::task::JoinHandle; use yazi_adapter::ADAPTOR; use yazi_config::{LAYOUT, YAZI}; -use yazi_fs::{Entries, FilesOp, cha::Cha, file::File}; +use yazi_fs::file::File; use yazi_macro::render; use yazi_runner::{RUNNER, previewer::{PeekError, PeekJob}}; use yazi_shared::{pool::Symbol, url::UrlBuf}; -use yazi_vfs::{VfsEntries, VfsFilesOp}; use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock}; @@ -17,9 +13,7 @@ pub struct Preview { pub lock: Option, pub skip: usize, - handle: Option>, - pub folder_lock: Option, - folder_loader: Option>, + handle: Option>, } impl Preview { @@ -47,36 +41,6 @@ impl Preview { })); } - pub fn go_folder(&mut self, file: File, dir: Option, mime: Symbol, force: bool) { - if self.folder_lock.as_ref() == Some(&file.url) { - return self.go(file, mime, force); - } - - let wd = file.url_owned(); - self.go(file, mime, force); - - self.folder_lock = Some(wd.clone()); - self.folder_loader.take().map(|h| h.abort()); - self.folder_loader = Some(tokio::spawn(async move { - let Some(new) = Entries::assert_stale(&wd, dir.unwrap_or_default()).await else { return }; - - let rx = match Entries::from_dir(&wd).await { - Ok(rx) => rx, - Err(e) => return FilesOp::issue_error(&wd, e).await, - }; - - let stream = - UnboundedReceiverStream::new(rx).chunks_timeout(50000, Duration::from_millis(500)); - pin!(stream); - - let ticket = FilesOp::prepare(&wd); - while let Some(chunk) = stream.next().await { - FilesOp::Part(wd.clone(), chunk, ticket).emit(); - } - FilesOp::Done(wd, new, ticket).emit(); - })); - } - pub fn abort(&mut self) { self.handle.take().map(|ct| ct.abort()); Highlighter::abort(); @@ -103,6 +67,4 @@ impl Preview { pub fn same_lock(&self, file: &File, mime: &str) -> bool { self.same_file(file, mime) && matches!(&self.lock, Some(l) if l.skip == self.skip) } - - pub fn same_folder(&self, url: &UrlBuf) -> bool { self.folder_lock.as_ref() == Some(url) } } diff --git a/yazi-core/src/tab/snap.rs b/yazi-core/src/tab/snap.rs index 5d59bfd2..e21f68bb 100644 --- a/yazi-core/src/tab/snap.rs +++ b/yazi-core/src/tab/snap.rs @@ -1,17 +1,14 @@ -use yazi_shared::url::UrlBuf; +use yazi_fs::file::File; use crate::tab::Tab; pub struct TabSnap { - pub hovered: Option, - pub selected: Vec, + pub hovered: Option, + pub selected: Vec, } impl From<&Tab> for TabSnap { fn from(value: &Tab) -> Self { - Self { - hovered: value.hovered_url().cloned(), - selected: value.selected.urls().cloned().collect(), - } + Self { hovered: value.hovered().cloned(), selected: value.selected.files().cloned().collect() } } } diff --git a/yazi-core/src/tasks/prework.rs b/yazi-core/src/tasks/prework.rs index 23ec2ca5..5f3ff692 100644 --- a/yazi-core/src/tasks/prework.rs +++ b/yazi-core/src/tasks/prework.rs @@ -1,5 +1,5 @@ use yazi_config::{YAZI, plugin::MAX_FETCHERS}; -use yazi_fs::{Entries, FsHash64, SortBy, file::File}; +use yazi_fs::{Entries, FsHash64, SortBy, file::{File, FileSig}}; use super::Tasks; use crate::mgr::Mimetype; @@ -9,7 +9,7 @@ impl Tasks { let mut loaded = self.scheduler.fetch.loaded.lock(); let mut tasks: [Vec<_>; MAX_FETCHERS as usize] = Default::default(); for f in paged { - let hash = f.hash_u64(); + let hash = FileSig(f).hash_u64(); for g in YAZI.plugin.fetchers.matches(f, mimetype.get(&f.url).unwrap_or_default()) { match loaded.get_mut(&hash) { Some(n) if *n & (1 << g.idx) != 0 => continue, @@ -32,7 +32,7 @@ impl Tasks { pub fn preload_paged(&self, paged: &[File], mimetype: &Mimetype) { let mut loaded = self.scheduler.preload.loaded.lock(); for f in paged { - let hash = f.hash_u64(); + let hash = FileSig(f).hash_u64(); for p in YAZI.plugin.preloaders.matches(f, mimetype.get(&f.url).unwrap_or_default()) { match loaded.get_mut(&hash) { Some(n) if *n & (1 << p.idx) != 0 => continue, diff --git a/yazi-ffi/Cargo.toml b/yazi-ffi/Cargo.toml index 9acff995..2ffbd799 100644 --- a/yazi-ffi/Cargo.toml +++ b/yazi-ffi/Cargo.toml @@ -24,3 +24,6 @@ libc = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] core-foundation-sys = { workspace = true } objc2 = { workspace = true } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62.2", features = [ "Win32_System_Com" ] } diff --git a/yazi-ffi/src/com.rs b/yazi-ffi/src/com.rs new file mode 100644 index 00000000..c9aa1cbc --- /dev/null +++ b/yazi-ffi/src/com.rs @@ -0,0 +1,16 @@ +use std::io; + +use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx, CoUninitialize}; + +pub struct Com; + +impl Com { + pub fn new() -> io::Result { + unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok().map_err(io::Error::other)? }; + Ok(Self) + } +} + +impl Drop for Com { + fn drop(&mut self) { unsafe { CoUninitialize() } } +} diff --git a/yazi-ffi/src/lib.rs b/yazi-ffi/src/lib.rs index 182356db..aea5ea5e 100644 --- a/yazi-ffi/src/lib.rs +++ b/yazi-ffi/src/lib.rs @@ -1,2 +1,5 @@ #[cfg(target_os = "macos")] yazi_macro::mod_flat!(cf_dict cf_string disk_arbitration io_kit); + +#[cfg(windows)] +yazi_macro::mod_flat!(com); diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index 973ec3ea..152162f4 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -32,9 +32,9 @@ impl<'a> Input<'a> { THEME.icon.matches( &File { - url: Path::new(path).into(), - cha: Cha { kind: ChaKind::empty(), mode: mode.try_into().ok()?, ..Default::default() }, - link_to: None, + url: Path::new(path).into(), + cha: Cha { kind: ChaKind::empty(), mode: mode.try_into().ok()?, ..Default::default() }, + extra: Default::default(), }, is_hovered, ) diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index 076fdc97..b6d29768 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -23,6 +23,7 @@ yazi-shim = { path = "../yazi-shim", version = "26.5.6" } anyhow = { workspace = true } arc-swap = { workspace = true } bitflags = { workspace = true } +data-encoding = { workspace = true } dirs = { workspace = true } either = { workspace = true } foldhash = { workspace = true } @@ -31,11 +32,11 @@ inventory = { workspace = true } libc = { workspace = true } mlua = { workspace = true } parking_lot = { workspace = true } -percent-encoding = { workspace = true } rand = { workspace = true } regex = { workspace = true } scopeguard = { workspace = true } serde = { workspace = true } +serde_with = { workspace = true } strum = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } @@ -45,10 +46,12 @@ typed-path = { workspace = true } uzers = { workspace = true } [target.'cfg(windows)'.dependencies] +windows = { version = "0.62.2", features = [ "Win32_Foundation", "Win32_Storage_EnhancedStorage", "Win32_System_Com_StructuredStorage", "Win32_System_SystemServices", "Win32_UI_Shell_PropertiesSystem" ] } windows-sys = { version = "0.61.2", features = [ "Win32_Storage_FileSystem" ] } [target.'cfg(target_os = "macos")'.dependencies] core-foundation-sys = { workspace = true } +ds_parser = "0.2.1" objc2 = { workspace = true } [target.'cfg(not(target_os = "android"))'.dependencies] diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 0ee64e82..e5d0d897 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -131,6 +131,16 @@ impl Cha { self.kind |= kind; self } + + #[inline] + pub fn follow(self, followed: Option) -> Self { + if !self.is_link() { + return self; + } + + let retain = self.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM) | ChaKind::FOLLOW; + followed.unwrap_or(self).attach(retain) + } } impl Cha { diff --git a/yazi-fs/src/cha/cha_sig.rs b/yazi-fs/src/cha/cha_sig.rs new file mode 100644 index 00000000..f54ab024 --- /dev/null +++ b/yazi-fs/src/cha/cha_sig.rs @@ -0,0 +1,21 @@ +use std::{hash::{Hash, Hasher}, ops::Deref}; + +use crate::cha::Cha; + +#[derive(Clone, Copy, Debug)] +pub struct ChaSig(pub Cha); + +impl Deref for ChaSig { + type Target = Cha; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl Hash for ChaSig { + fn hash(&self, state: &mut H) { + self.len.hash(state); + self.btime.hash(state); + self.ctime.hash(state); + self.mtime.hash(state); + } +} diff --git a/yazi-fs/src/cha/lua.rs b/yazi-fs/src/cha/lua.rs index 5a761cf9..087087ca 100644 --- a/yazi-fs/src/cha/lua.rs +++ b/yazi-fs/src/cha/lua.rs @@ -67,17 +67,17 @@ impl UserData for Cha { } fn add_methods>(methods: &mut M) { - methods.add_method("hash", |_, me, long: Option| { - Ok(if long.unwrap_or(false) { + methods.add_method("hash", |_, me, long: bool| { + Ok(if long { format!("{:x}", me.hash_u128()) } else { Err("Short hash not supported".into_lua_err())? }) }); - methods.add_method("perm", |lua, _me, ()| { + methods.add_method("perm", |_lua, _me, ()| { Ok( #[cfg(unix)] - lua.create_string(_me.mode.permissions(_me.is_dummy())), + _lua.create_string(_me.mode.permissions(_me.is_dummy())), #[cfg(windows)] Ok::<_, mlua::Error>(mlua::Value::Nil), ) diff --git a/yazi-fs/src/cha/mod.rs b/yazi-fs/src/cha/mod.rs index 75d67400..18819389 100644 --- a/yazi-fs/src/cha/mod.rs +++ b/yazi-fs/src/cha/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(cha kind lua mode r#type); +yazi_macro::mod_flat!(cha cha_sig kind lua mode r#type); diff --git a/yazi-fs/src/cwd.rs b/yazi-fs/src/cwd.rs index 104ea125..843c39a7 100644 --- a/yazi-fs/src/cwd.rs +++ b/yazi-fs/src/cwd.rs @@ -30,7 +30,7 @@ impl Default for Cwd { } impl Cwd { - pub fn path(&self) -> PathBuf { self.0.load().as_url().unified_path().into_owned() } + pub fn path(&self) -> PathBuf { self.0.load().as_url().working_path().into_owned() } pub fn set(&self, url: &UrlBuf, callback: fn()) -> bool { if !url.is_absolute() { @@ -49,7 +49,7 @@ impl Cwd { use std::{io::ErrorKind::{AlreadyExists, NotADirectory, NotFound}, path::Component as C}; let Some(cache) = url.cache() else { - return url.unified_path(); + return url.working_path(); }; if !matches!(std::fs::create_dir_all(&cache), Err(e) if e.kind() == NotADirectory || e.kind() == AlreadyExists) diff --git a/yazi-fs/src/engine/local/dir_entry.rs b/yazi-fs/src/engine/local/dir_entry.rs index fb494640..2b0b6f74 100644 --- a/yazi-fs/src/engine/local/dir_entry.rs +++ b/yazi-fs/src/engine/local/dir_entry.rs @@ -2,7 +2,7 @@ use std::{io, sync::Arc}; use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; -use crate::{cha::{Cha, ChaType}, engine::FileHolder}; +use crate::{cha::{Cha, ChaType}, engine::FileHolder, file::{File, FileExtra}}; pub enum DirEntry { Regular(tokio::fs::DirEntry), @@ -10,6 +10,21 @@ pub enum DirEntry { } impl FileHolder for DirEntry { + async fn file(&self) -> io::Result { + let cha = self.metadata().await?; + let url = self.url(); + + let (mut followed, mut link_to) = (None, None); + if cha.is_link() { + let path = url.as_local().expect("local entry path"); + let name = path.file_name().unwrap_or_default(); + followed = tokio::fs::metadata(path).await.ok().map(|m| Cha::new(name, m)); + link_to = tokio::fs::read_link(path).await.ok().map(Into::into); + } + + Ok(File { url, cha: cha.follow(followed), extra: FileExtra::new(link_to, None) }) + } + async fn file_type(&self) -> io::Result { match self { Self::Regular(entry) | Self::Others { entry, .. } => entry.file_type().await.map(Into::into), diff --git a/yazi-fs/src/engine/traits.rs b/yazi-fs/src/engine/traits.rs index 6f42e63e..c6e3447d 100644 --- a/yazi-fs/src/engine/traits.rs +++ b/yazi-fs/src/engine/traits.rs @@ -4,7 +4,7 @@ use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::{DynPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; -use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}}; +use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}, file::{File, FileExtra}}; pub trait Engine: Sized { type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin; @@ -78,6 +78,24 @@ pub trait Engine: Sized { fn demand(&self) -> Self::Demand { Self::Demand::default() } + fn file(&self) -> impl Future> { + async move { + let cha = self.symlink_metadata().await?; + + let (mut followed, mut link_to) = (None, None); + if cha.is_link() { + followed = self.metadata().await.ok(); + link_to = self.read_link().await.ok(); + } + + Ok(File { + url: self.url().to_owned(), + cha: cha.follow(followed), + extra: FileExtra::new(link_to, None), + }) + } + } + fn hard_link

(&self, to: P) -> impl Future> where P: DynPath; @@ -94,6 +112,25 @@ pub trait Engine: Sized { fn read_link(&self) -> impl Future>; + fn revalidate(&self, mut file: File) -> impl Future>> { + async move { + let cha = if !file.is_link() { + self.symlink_metadata().await? + } else if let Ok(new) = self.metadata().await { + file.cha.follow(Some(new)) + } else { + self.symlink_metadata().await? + }; + + if cha.hits(file.cha) { + Ok(None) + } else { + file.cha = cha; + Ok(Some(file)) + } + } + } + fn remove_dir(&self) -> impl Future>; fn remove_dir_all(&self) -> impl Future> { @@ -204,6 +241,9 @@ pub trait DirReader { // --- FileHolder pub trait FileHolder { + #[must_use] + fn file(&self) -> impl Future>; + #[must_use] fn file_type(&self) -> impl Future>; diff --git a/yazi-fs/src/entries.rs b/yazi-fs/src/entries.rs index 86ac0796..1151da9f 100644 --- a/yazi-fs/src/entries.rs +++ b/yazi-fs/src/entries.rs @@ -292,25 +292,20 @@ impl Entries { // --- Show hidden pub fn set_show_hidden(&mut self, state: bool) { - if self.show_hidden == state { - return; - } - - self.show_hidden = state; - if self.show_hidden && self.hidden.is_empty() { - return; - } else if !self.show_hidden && self.items.is_empty() { + if mem::replace(&mut self.show_hidden, state) == state { return; } + let len = self.items.len(); let take = if self.show_hidden { mem::take(&mut self.hidden) } else { mem::take(&mut self.items) }; - let (hidden, items) = self.split_files(take); - - self.hidden.extend(hidden); - if !items.is_empty() { - self.revision += 1; - self.items.extend(items); + if take.is_empty() { + return; } + + let (hidden, items) = self.split_files(take); + self.hidden.extend(hidden); + self.items.extend(items); + self.revision += (self.items.len() != len) as u64; } } diff --git a/yazi-fs/src/file/file.rs b/yazi-fs/src/file/file.rs index 58a97779..0d36b721 100644 --- a/yazi-fs/src/file/file.rs +++ b/yazi-fs/src/file/file.rs @@ -1,15 +1,16 @@ -use std::{borrow::Cow, hash::{Hash, Hasher}, ops::Deref, path::Path}; +use std::{borrow::Cow, ops::Deref, path::Path}; use serde::{Deserialize, Serialize}; -use yazi_shared::{path::{PathBufDyn, PathDyn}, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}}; +use yazi_shared::{path::PathDyn, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}}; -use crate::cha::{Cha, ChaType}; +use crate::{FsUrl, cha::{Cha, ChaType}, file::FileExtra}; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct File { - pub url: UrlBuf, - pub cha: Cha, - pub link_to: Option, + pub url: UrlBuf, + pub cha: Cha, + #[serde(flatten)] + pub extra: FileExtra, } impl Deref for File { @@ -43,12 +44,23 @@ impl File { pub fn from_dummy(url: impl Into, r#type: Option) -> Self { let url = url.into(); let cha = Cha::from_dummy(&url, r#type); - Self { url, cha, link_to: None } + Self { url, cha, extra: Default::default() } } #[inline] pub fn chdir(&self, wd: &Path) -> Self { - Self { url: self.url.rebase(wd), cha: self.cha, link_to: self.link_to.clone() } + Self { url: self.url.rebase(wd), cha: self.cha, extra: self.extra.clone() } + } + + #[inline] + pub fn content_path(&self) -> Cow<'_, Path> { + if let Some(backing) = self.extra.backing() { + backing.into() + } else if let Some(local) = self.url.as_local() { + local.into() + } else { + self.url.cache().expect("non-local URL should have a cache path").into() + } } } @@ -72,13 +84,3 @@ impl File { #[inline] pub fn stem(&self) -> Option> { self.url.stem() } } - -impl Hash for File { - fn hash(&self, state: &mut H) { - self.url.hash(state); - self.cha.len.hash(state); - self.cha.btime.hash(state); - self.cha.ctime.hash(state); - self.cha.mtime.hash(state); - } -} diff --git a/yazi-fs/src/file/file_extra.rs b/yazi-fs/src/file/file_extra.rs new file mode 100644 index 00000000..2eb8ea9f --- /dev/null +++ b/yazi-fs/src/file/file_extra.rs @@ -0,0 +1,67 @@ +use std::{path::{Path, PathBuf}, sync::Arc}; + +use mlua::{FromLua, Lua, Table, Value}; +use serde::{Deserialize, Serialize}; +use serde_with::{TryFromInto, serde_as}; +use yazi_shared::path::PathBufDyn; + +#[repr(transparent)] +#[derive(Clone, Debug, Default)] +pub struct FileExtra(Option>); + +#[serde_as] +#[derive(Debug, Default, Deserialize, Serialize)] +struct FileExtraInner { + link_to: Option, + #[serde_as(as = "Option>")] + backing: Option, +} + +impl FileExtra { + #[inline] + pub fn new(link_to: Option, backing: Option) -> Self { + Self( + (link_to.is_some() || backing.is_some()) + .then(|| Arc::new(FileExtraInner { link_to, backing })), + ) + } + + #[inline] + pub fn link_to(&self) -> Option<&PathBufDyn> { self.0.as_ref()?.link_to.as_ref() } + + #[inline] + pub fn backing(&self) -> Option<&Path> { self.0.as_ref()?.backing.as_deref() } +} + +impl TryFrom for FileExtra { + type Error = mlua::Error; + + fn try_from(value: Table) -> Result { + Ok(Self::new( + value.raw_get("link_to")?, + value.raw_get::>("backing")?.map(PathBufDyn::into_os).transpose()?, + )) + } +} + +impl Serialize for FileExtra { + fn serialize(&self, serializer: S) -> Result { + match &self.0 { + Some(inner) => inner.serialize(serializer), + None => FileExtraInner::default().serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for FileExtra { + fn deserialize>(deserializer: D) -> Result { + let inner = FileExtraInner::deserialize(deserializer)?; + Ok(Self::new(inner.link_to, inner.backing)) + } +} + +impl FromLua for FileExtra { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + Table::from_lua(value, lua)?.try_into() + } +} diff --git a/yazi-fs/src/file/file_sig.rs b/yazi-fs/src/file/file_sig.rs new file mode 100644 index 00000000..dd3b8db6 --- /dev/null +++ b/yazi-fs/src/file/file_sig.rs @@ -0,0 +1,26 @@ +use std::{hash::{Hash, Hasher}, ops::Deref}; + +use yazi_shared::url::Url; + +use crate::{cha::ChaSig, file::File}; + +#[derive(Clone, Copy, Debug)] +pub struct FileSig<'a>(pub &'a File); + +impl Deref for FileSig<'_> { + type Target = File; + + fn deref(&self) -> &Self::Target { self.0 } +} + +impl Hash for FileSig<'_> { + fn hash(&self, state: &mut H) { + if let Some(backing) = self.extra.backing() { + Url::regular(backing).hash(state); + } else { + self.url.hash(state); + } + + ChaSig(self.cha).hash(state); + } +} diff --git a/yazi-fs/src/file/lua.rs b/yazi-fs/src/file/lua.rs index 32d19bf4..a3003f0f 100644 --- a/yazi-fs/src/file/lua.rs +++ b/yazi-fs/src/file/lua.rs @@ -15,7 +15,11 @@ impl TryFrom
for File { type Error = mlua::Error; fn try_from(value: Table) -> Result { - Ok(Self { url: value.raw_get("url")?, cha: value.raw_get("cha")?, ..Default::default() }) + Ok(Self { + url: value.raw_get("url")?, + cha: value.raw_get("cha")?, + extra: value.try_into()?, + }) } } diff --git a/yazi-fs/src/file/mod.rs b/yazi-fs/src/file/mod.rs index 7e2916c3..4e7de598 100644 --- a/yazi-fs/src/file/mod.rs +++ b/yazi-fs/src/file/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(data file_cov file file_ref files inventory lua); +yazi_macro::mod_flat!(data file file_cov file_extra file_ref file_sig files inventory lua); diff --git a/yazi-fs/src/hash.rs b/yazi-fs/src/hash.rs index 16df2a42..086b86f9 100644 --- a/yazi-fs/src/hash.rs +++ b/yazi-fs/src/hash.rs @@ -1,26 +1,54 @@ use std::hash::{BuildHasher, Hash}; +use data_encoding::BASE32_NOPAD; use mlua::UserDataMethods; -use yazi_shared::url::{AsUrl, UrlBuf, UrlBufInventory}; +use yazi_shared::{auth::Domain, url::{AsUrl, Url, UrlBuf, UrlBufInventory}}; use yazi_shim::Twox128; -use crate::{cha::Cha, file::File}; +use crate::{cha::Cha, file::FileSig}; pub trait FsHash64 { fn hash_u64(&self) -> u64; } -impl FsHash64 for File { - fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } -} - impl FsHash64 for UrlBuf { fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self.as_url()) } } +impl FsHash64 for FileSig<'_> { + fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } +} + // Hash128 pub trait FsHash128 { fn hash_u128(&self) -> u128; + + fn hash_base32<'a>(&self, buf: &'a mut [u8; 26]) -> &'a str { + BASE32_NOPAD.encode_mut_str(&self.hash_u128().to_be_bytes(), buf) + } +} + +impl FsHash128 for Url<'_> { + fn hash_u128(&self) -> u128 { + let mut h = Twox128::default(); + self.auth().hash(&mut h); + for c in self.loc().components() { + c.hash(&mut h); + } + h.finish_128() + } +} + +impl FsHash128 for UrlBuf { + fn hash_u128(&self) -> u128 { self.as_url().hash_u128() } +} + +impl FsHash128 for Domain<'_> { + fn hash_u128(&self) -> u128 { + let mut h = Twox128::default(); + self.hash(&mut h); + h.finish_128() + } } impl FsHash128 for Cha { @@ -35,10 +63,10 @@ impl FsHash128 for Cha { } } -impl FsHash128 for T { +impl FsHash128 for FileSig<'_> { fn hash_u128(&self) -> u128 { let mut h = Twox128::default(); - self.as_url().hash(&mut h); + self.hash(&mut h); h.finish_128() } } diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index 666cd11b..0025458c 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,6 +1,6 @@ extern crate self as yazi_fs; -yazi_macro::mod_pub!(cha file mounts path engine); +yazi_macro::mod_pub!(cha file mounts path engine trash); yazi_macro::mod_flat!(cwd spec entries filter fns hash op sorter sorting splatter stage url xdg); diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index e75733b6..f9321016 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -4,15 +4,15 @@ use hashbrown::{HashMap, HashSet}; use yazi_macro::{impl_data_any, relay}; use yazi_shared::{id::{Id, Ids}, path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlLike, UrlMapExt}}; -use crate::{cha::Cha, file::File}; +use crate::file::File; pub static FILES_TICKET: Ids = Ids::new(); #[derive(Clone, Debug)] pub enum FilesOp { - Full(UrlBuf, Vec, Cha), + Full(File, Vec), Part(UrlBuf, Vec, Id), - Done(UrlBuf, Cha, Id), + Done(File, Id), Size(UrlBuf, HashMap), IOErr(UrlBuf, yazi_shim::fs::Error), @@ -27,9 +27,9 @@ impl_data_any!(FilesOp); impl FilesOp { pub fn cwd(&self) -> &UrlBuf { match self { - Self::Full(u, ..) => u, + Self::Full(f, ..) => &f.url, Self::Part(u, ..) => u, - Self::Done(u, ..) => u, + Self::Done(f, ..) => &f.url, Self::Size(u, _) => u, Self::IOErr(u, _) => u, @@ -42,7 +42,7 @@ impl FilesOp { pub fn files(&self) -> Box + '_> { match self { - Self::Full(_, files, _) | Self::Part(_, files, _) | Self::Creating(_, files) => { + Self::Full(_, files) | Self::Part(_, files, _) | Self::Creating(_, files) => { Box::new(files.iter().filter(|f| !f.entry_key().is_empty())) } Self::Done(..) => Box::new(iter::empty()), @@ -141,9 +141,9 @@ impl FilesOp { let w = UrlBuf::from(wd); match self { - Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha), + Self::Full(file, files) => Self::Full(file.chdir(wd), files!(files)), Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket), - Self::Done(_, cha, ticket) => Self::Done(w, *cha, *ticket), + Self::Done(file, ticket) => Self::Done(file.chdir(wd), *ticket), Self::Size(_, map) => Self::Size(w, map.iter().map(|(urn, &s)| (urn.clone(), s)).collect()), Self::IOErr(_, err) => Self::IOErr(w, err.clone()), diff --git a/yazi-fs/src/path/mod.rs b/yazi-fs/src/path/mod.rs index a3b70bd8..6aee3c6e 100644 --- a/yazi-fs/src/path/mod.rs +++ b/yazi-fs/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(clean expand normalize path percent relative); +yazi_macro::mod_flat!(clean expand normalize path relative); diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs deleted file mode 100644 index 2b5358da..00000000 --- a/yazi-fs/src/path/percent.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::{borrow::Cow, path::{Path, PathBuf}}; - -use anyhow::Result; -use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; -use yazi_shared::path::{PathCow, PathDyn, PathKind}; - -const SET: &AsciiSet = &CONTROLS - .add(b'"') - .add(b'*') - .add(b':') - .add(b'<') - .add(b'>') - .add(b'?') - .add(b'\\') - .add(b'|') - .add(b'%'); - -pub trait PercentEncoding<'a> { - fn percent_encode(self) -> Cow<'a, Path>; - - fn percent_decode(self, kind: K) -> Result> - where - K: Into; -} - -impl<'a> PercentEncoding<'a> for PathDyn<'a> { - fn percent_encode(self) -> Cow<'a, Path> { - match percent_encode(self.encoded_bytes(), SET).into() { - Cow::Borrowed(s) => Path::new(s).into(), - Cow::Owned(s) => PathBuf::from(s).into(), - } - } - - fn percent_decode(self, kind: K) -> Result> - where - K: Into, - { - PathCow::with(kind, percent_decode(self.encoded_bytes())) - } -} diff --git a/yazi-fs/src/spec.rs b/yazi-fs/src/spec.rs index 6768e29a..aa02b4af 100644 --- a/yazi-fs/src/spec.rs +++ b/yazi-fs/src/spec.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; -use yazi_shared::{auth::{Auth, AuthKind, EncodeAuth}, path::PathBufDyn, spec::SpecInventory}; +use yazi_shared::{auth::{Auth, AuthKind}, path::PathBufDyn, spec::SpecInventory}; use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr}; -use crate::Xdg; +use crate::{FsHash128, Xdg}; pub trait FsSpec { fn cache(&self) -> Option; @@ -18,7 +18,7 @@ impl FsSpec for Auth { "{}_{}_{}", self.kind.into_str(), self.scheme, - EncodeAuth::domain(&self.domain) + self.domain.hash_base32(&mut [0; 26]) ))) } } diff --git a/yazi-fs/src/splatter.rs b/yazi-fs/src/splatter.rs index 42cd4af5..c2db5bce 100644 --- a/yazi-fs/src/splatter.rs +++ b/yazi-fs/src/splatter.rs @@ -2,11 +2,11 @@ use std::os::unix::ffi::{OsStrExt, OsStringExt}; #[cfg(windows)] use std::os::windows::ffi::{OsStrExt, OsStringExt}; -use std::{cell::Cell, ffi::{OsStr, OsString}, iter::{self, Peekable}, mem}; +use std::{cell::Cell, ffi::{OsStr, OsString}, iter::{self, Peekable}, mem, path::Path}; -use yazi_shared::url::{AsUrl, Url}; +use yazi_shared::url::UrlLike; -use crate::FsUrl; +use crate::file::File; #[cfg(unix)] type Iter<'a> = Peekable>>; @@ -27,11 +27,11 @@ pub struct Splatter { pub trait Splatable { fn tab(&self) -> usize; - fn selected(&self, tab: usize, idx: Option) -> impl Iterator>; + fn selected(&self, tab: usize, idx: Option) -> impl Iterator; - fn hovered(&self, tab: usize) -> Option>; + fn hovered(&self, tab: usize) -> Option<&File>; - fn yanked(&self, idx: Option) -> impl Iterator>; + fn yanked(&self, idx: Option) -> impl Iterator; } #[cfg(unix)] @@ -91,15 +91,15 @@ where let idx = self.consume_digit(it); let mut first = true; - for url in self.src.selected(self.tab, idx) { + for file in self.src.selected(self.tab, idx) { if !mem::replace(&mut first, false) { buf.push(b' ' as _); } if c == Some('S') { - cue(buf, url.os_str()); + cue(buf, file.url.os_str()); } else { - cue(buf, url.unified_path_str()); + cue(buf, &*file.content_path()); } } if first && idx.is_some() { @@ -110,10 +110,10 @@ where fn visit_hovered(&mut self, it: &mut Iter, buf: &mut Buf) { match it.next().and_then(b2c) { Some('h') => { - cue(buf, self.src.hovered(self.tab).map(|u| u.unified_path_str()).unwrap_or_default()); + cue(buf, &*self.src.hovered(self.tab).map(|f| f.content_path()).unwrap_or_default()); } Some('H') => { - cue(buf, self.src.hovered(self.tab).map(|u| u.os_str()).unwrap_or_default()); + cue(buf, self.src.hovered(self.tab).map(|f| f.url.os_str()).unwrap_or_default()); } _ => unreachable!(), } @@ -124,15 +124,15 @@ where let idx = self.consume_digit(it); let mut first = true; - for url in self.src.selected(self.tab, idx) { + for file in self.src.selected(self.tab, idx) { if !mem::replace(&mut first, false) { buf.push(b' ' as _); } if c == Some('D') { - cue(buf, url.parent().map(|p| p.os_str()).unwrap_or_default()); + cue(buf, file.url.parent().map(|p| p.os_str()).unwrap_or_default()); } else { - cue(buf, url.parent().map(|p| p.unified_path_str()).unwrap_or_default()); + cue(buf, file.content_path().parent().unwrap_or(Path::new(""))); } } if first && idx.is_some() { @@ -157,15 +157,15 @@ where let idx = self.consume_digit(it); let mut first = true; - for url in self.src.yanked(idx) { + for file in self.src.yanked(idx) { if !mem::replace(&mut first, false) { buf.push(b' ' as _); } if c == Some('Y') { - cue(buf, url.os_str()); + cue(buf, file.url.os_str()); } else { - cue(buf, url.unified_path_str()); + cue(buf, &*file.content_path()); } } if first && idx.is_some() { @@ -204,16 +204,16 @@ impl Splatter { impl Splatable for &Source { fn tab(&self) -> usize { 0 } - fn selected(&self, _tab: usize, idx: Option) -> impl Iterator> { + fn selected(&self, _tab: usize, idx: Option) -> impl Iterator { if idx.is_none() { self.0.set(true); } iter::empty() } - fn hovered(&self, _tab: usize) -> Option> { None } + fn hovered(&self, _tab: usize) -> Option<&File> { None } - fn yanked(&self, _idx: Option) -> impl Iterator> { iter::empty() } + fn yanked(&self, _idx: Option) -> impl Iterator { iter::empty() } } let src = Source(Cell::new(false)); @@ -222,68 +222,64 @@ impl Splatter { } } -impl<'a, I, T> Splatable for &'a I +impl Splatable for &I where I: ?Sized, - &'a I: IntoIterator, - T: AsUrl + 'a, + for<'a> &'a I: IntoIterator, { fn tab(&self) -> usize { 1 } - fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { + fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator { idx = idx.and_then(|i| i.checked_sub(1)); - (*self) - .into_iter() - .filter(move |_| tab == 1) - .map(|u| u.as_url()) - .skip(idx.unwrap_or(0)) - .take(if idx.is_some() { 1 } else { usize::MAX }) + (*self).into_iter().filter(move |_| tab == 1).skip(idx.unwrap_or(0)).take(if idx.is_some() { + 1 + } else { + usize::MAX + }) } - fn hovered(&self, _tab: usize) -> Option> { None } + fn hovered(&self, _tab: usize) -> Option<&File> { None } - fn yanked(&self, _idx: Option) -> impl Iterator> { iter::empty() } + fn yanked(&self, _idx: Option) -> impl Iterator { iter::empty() } } #[cfg(test)] mod tests { + use std::sync::LazyLock; + use super::*; struct Source(usize); + static SELECTED: LazyLock<[Vec; 2]> = + LazyLock::new(|| [vec![file("t1/s1"), file("t1/s2")], vec![file("t 2/s 1"), file("t 2/s 2")]]); + static HOVERED: LazyLock<[File; 2]> = LazyLock::new(|| [file("hovered"), file("hover ed")]); + static YANKED: LazyLock<[File; 3]> = LazyLock::new(|| [file("y1"), file("y 2"), file("y3")]); + + fn file(path: &'static str) -> File { File::from_dummy(Path::new(path), None) } + impl Splatable for Source { fn tab(&self) -> usize { self.0 } - fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { - let urls = if tab == 1 { - vec![Url::regular("t1/s1"), Url::regular("t1/s2")] - } else if tab == 2 { - vec![Url::regular("t 2/s 1"), Url::regular("t 2/s 2")] - } else { - vec![] - }; - + fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator { idx = idx.and_then(|i| i.checked_sub(1)); - urls.into_iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }) - } - - fn hovered(&self, tab: usize) -> Option> { - if tab == 1 { - Some(Url::regular("hovered")) - } else if tab == 2 { - Some(Url::regular("hover ed")) - } else { - None - } - } - - fn yanked(&self, mut idx: Option) -> impl Iterator> { - idx = idx.and_then(|i| i.checked_sub(1)); - [Url::regular("y1"), Url::regular("y 2"), Url::regular("y3")] + tab + .checked_sub(1) + .and_then(|tab| SELECTED.get(tab)) .into_iter() + .flatten() .skip(idx.unwrap_or(0)) .take(if idx.is_some() { 1 } else { usize::MAX }) } + + fn hovered(&self, tab: usize) -> Option<&File> { + tab.checked_sub(1).and_then(|tab| HOVERED.get(tab)) + } + + fn yanked(&self, mut idx: Option) -> impl Iterator { + idx = idx.and_then(|i| i.checked_sub(1)); + YANKED.iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }) + } } #[test] @@ -332,4 +328,19 @@ mod tests { assert_eq!(s, OsStr::new(expected), "{cmd}"); } } + + #[test] + #[cfg(unix)] + fn test_content_path() { + use crate::file::FileExtra; + + let file = File { + url: Path::new("/logical/file").into(), + cha: Default::default(), + extra: FileExtra::new(None, Some("/real/file".into())), + }; + + let s = Splatter::new(&[file]).splat(OsStr::new("%s %S %d %D")); + assert_eq!(s, OsStr::new("/real/file /logical/file /real /logical")); + } } diff --git a/yazi-fs/src/trash/entry.rs b/yazi-fs/src/trash/entry.rs new file mode 100644 index 00000000..717f6717 --- /dev/null +++ b/yazi-fs/src/trash/entry.rs @@ -0,0 +1,53 @@ +use std::{ffi::OsString, path::PathBuf}; + +use mlua::{IntoLua, Lua, Value}; +use yazi_shared::path::PathBufDyn; + +use crate::cha::Cha; + +#[derive(Clone, Debug)] +pub struct TrashEntry { + pub name: OsString, + pub key: OsString, + pub cha: Cha, + pub link_to: Option, + pub backing: PathBuf, +} + +impl TrashEntry { + #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] + pub(super) fn new(path: PathBuf, name: OsString, key: OsString) -> std::io::Result { + use super::TrashCha; + + let cha = Cha::from_trash(&path, &name, true)?; + let link_to = if cha.is_link() { std::fs::read_link(&path).ok() } else { None }; + + Ok(Self { name, key, cha, link_to, backing: path }) + } + + #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] + pub(super) fn into_file(self, url: impl Into) -> crate::file::File { + use crate::file::{File, FileExtra}; + + File { + url: url.into(), + cha: self.cha, + extra: FileExtra::new(self.link_to.map(Into::into), Some(self.backing)), + } + } +} + +impl IntoLua for TrashEntry { + fn into_lua(self, lua: &Lua) -> mlua::Result { + 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) + } +} diff --git a/yazi-fs/src/trash/freedesktop.rs b/yazi-fs/src/trash/freedesktop.rs new file mode 100644 index 00000000..ec6a2945 --- /dev/null +++ b/yazi-fs/src/trash/freedesktop.rs @@ -0,0 +1,171 @@ +use std::{ffi::OsStr, fs, hash::Hash, io, path::{Path, PathBuf}}; + +use hashbrown::HashMap; +use trash::{TrashItem, os_limited}; +use yazi_macro::ok_or_not_found; +use yazi_shim::Twox128; + +use super::{TrashCha, TrashEntry, TrashNode, TrashNodes}; +use crate::{cha::{Cha, ChaSig}, file::File}; + +pub struct Trash; + +impl Trash { + pub fn new() -> io::Result { Ok(Self) } + + pub fn list(&self, node: Option<&TrashNode>) -> io::Result> { + 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 { + 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 { + 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> { + let latest = if let Some(node) = node { + self.entry(node)?.into_file(¤t.url) + } else { + let mut roots: Vec<_> = os_limited::trash_folders().unwrap_or_default().into_iter().collect(); + roots.sort_unstable(); + + let mut h = Twox128::default(); + for root in roots { + let meta = ok_or_not_found!(fs::metadata(root.join("info")), continue); + let cha = Cha::new(root.file_name().unwrap_or_default(), meta); + + root.hash(&mut h); + ChaSig(cha).hash(&mut h); + } + + let hash = h.finish_128(); + File { + cha: Cha { len: hash as u64 ^ (hash >> 64) as u64, ..Cha::from_mold(true) }, + ..current.clone() + } + }; + + let changed = !latest.cha.hits(current.cha) + || latest.extra.link_to() != current.extra.link_to() + || latest.extra.backing() != current.extra.backing(); + + Ok(changed.then_some(latest)) + } + + pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> { + if node.rel.as_os_str().is_empty() { + os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other) + } else { + fs::remove_file(self.resolve(node)?) + } + } + + pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> { + if node.rel.as_os_str().is_empty() { + os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other) + } else { + fs::remove_dir(self.resolve(node)?) + } + } + + pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> { + let mut tops = Vec::new(); + let items: HashMap<_, _> = os_limited::list() + .map_err(io::Error::other)? + .into_iter() + .map(|item| (item.id.clone(), item)) + .collect(); + + for node in nodes { + let item = items + .get(&node.top) + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))?; + if node.rel.as_os_str().is_empty() { + tops.push(item.clone()); + continue; + } + + let from = self.resolve(&node)?; + let to = item.original_path().join(node.rel); + + let is_dir = fs::symlink_metadata(&from)?.is_dir(); + if let Some(parent) = to.parent() { + fs::create_dir_all(parent)?; + } + + match if is_dir { fs::create_dir(&to) } else { fs::File::create_new(&to).map(|_| ()) } { + Ok(()) => fs::rename(from, to), + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("restore target already exists: {to:?}"), + )), + Err(e) => Err(e), + }?; + } + + os_limited::restore_all(tops).map_err(io::Error::other) + } + + pub fn empty(&self) -> io::Result<()> { + os_limited::purge_all(os_limited::list().map_err(io::Error::other)?).map_err(io::Error::other) + } + + fn resolve(&self, node: &TrashNode) -> io::Result { + 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 { + 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 { + 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)) + } +} diff --git a/yazi-fs/src/trash/lua.rs b/yazi-fs/src/trash/lua.rs new file mode 100644 index 00000000..36058e63 --- /dev/null +++ b/yazi-fs/src/trash/lua.rs @@ -0,0 +1,73 @@ +use std::io; + +use mlua::{ExternalError, ExternalResult, IntoLuaMulti, LuaString, UserData, UserDataMethods, Value}; +use tokio::task::spawn_blocking; +use yazi_binding::Error; + +use super::{Trash, TrashNode, TrashNodes}; +use crate::file::File; + +impl UserData for Trash { + fn add_methods>(methods: &mut M) { + methods.add_async_function("empty", |lua, ()| async move { + match spawn_blocking(|| Trash::new()?.empty()).await.into_lua_err()? { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function("list", |lua, node: Option| async move { + match spawn_blocking(move || Trash::new()?.list(node.as_ref())).await.into_lua_err()? { + Ok(items) => lua.create_sequence_from(items)?.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function("entry", |lua, node: TrashNode| async move { + match spawn_blocking(move || Trash::new()?.entry(&node)).await.into_lua_err()? { + Ok(entry) => entry.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function("metadata", |lua, (node, follow): (TrashNode, bool)| async move { + match spawn_blocking(move || Trash::new()?.metadata(&node, follow)).await.into_lua_err()? { + Ok(cha) => cha.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function( + "revalidate", + |lua, (node, file): (Option, File)| async move { + match spawn_blocking(move || Trash::new()?.revalidate(node.as_ref(), &file)) + .await + .into_lua_err()? + { + Ok(file) => file.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }, + ); + + methods.add_async_function("remove", |lua, (kind, node): (LuaString, TrashNode)| async move { + let f: fn(&Trash, &TrashNode) -> io::Result<()> = match &*kind.as_bytes() { + b"file" => Trash::remove_file, + b"dir" => Trash::remove_dir, + _ => Err("Removal type must be 'file' or 'dir'".into_lua_err())?, + }; + + match spawn_blocking(move || f(&Trash::new()?, &node)).await.into_lua_err()? { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }); + + methods.add_async_function("restore", |lua, nodes: TrashNodes| async move { + match spawn_blocking(move || Trash::new()?.restore(nodes)).await.into_lua_err()? { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + }); + } +} diff --git a/yazi-fs/src/trash/macos.rs b/yazi-fs/src/trash/macos.rs new file mode 100644 index 00000000..b1a04cf5 --- /dev/null +++ b/yazi-fs/src/trash/macos.rs @@ -0,0 +1,188 @@ +use std::{ffi::{OsStr, OsString}, fs, io, path::{Component, Path, PathBuf}}; + +use ds_parser::Value; +use hashbrown::HashMap; +use yazi_macro::ok_or_not_found; + +use super::{TrashCha, TrashEntry, TrashNode, TrashNodes}; +use crate::{cha::Cha, file::File}; + +pub struct Trash; + +impl Trash { + pub fn new() -> io::Result { Ok(Self) } + + pub fn list(&self, node: Option<&TrashNode>) -> io::Result> { + 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 { + 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 { + 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> { + let latest = if let Some(node) = node { + self.entry(node)?.into_file(¤t.url) + } else { + let path = self.resolve(None)?; + let cha = match fs::symlink_metadata(&path) { + Ok(meta) => Cha::new(path.file_name().unwrap_or_default(), meta), + Err(e) if e.kind() == io::ErrorKind::NotFound => Cha::from_mold(true), + Err(e) => return Err(e), + }; + File { cha, ..current.clone() } + }; + + let changed = !latest.cha.hits(current.cha) + || latest.extra.link_to() != current.extra.link_to() + || latest.extra.backing() != current.extra.backing(); + + Ok(changed.then_some(latest)) + } + + pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> { + fs::remove_file(self.resolve(Some(node))?) + } + + pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> { + fs::remove_dir(self.resolve(Some(node))?) + } + + pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> { + let root = self.resolve(None)?; + let locations = OriginalLocation::parse(&fs::read(root.join(".DS_Store"))?)?; + + for node in nodes { + let name = Path::new(&node.top).file_name().unwrap_or_default(); + let location = locations.get(name).ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + format!("original location is unavailable for trash item: {:?}", node.top), + ) + })?; + + let from = self.resolve(Some(&node))?; + let to = location.join(&node)?; + + let is_dir = fs::symlink_metadata(&from)?.is_dir(); + if let Some(parent) = to.parent() { + fs::create_dir_all(parent)?; + } + + match if is_dir { fs::create_dir(&to) } else { fs::File::create_new(&to).map(|_| ()) } { + Ok(()) => fs::rename(from, to), + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("restore target already exists: {to:?}"), + )), + Err(e) => Err(e), + }?; + } + + Ok(()) + } + + pub fn empty(&self) -> io::Result<()> { + let root = self.resolve(None)?; + for entry in ok_or_not_found!(fs::read_dir(root), return Ok(())) { + let entry = entry?; + if entry.file_type()?.is_dir() { + fs::remove_dir_all(entry.path())?; + } else { + fs::remove_file(entry.path())?; + } + } + Ok(()) + } + + fn resolve(&self, node: Option<&TrashNode>) -> io::Result { + 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, + name: Option, +} + +impl OriginalLocation { + fn parse(bytes: &[u8]) -> io::Result> { + let store = ds_parser::parse(bytes).map_err(io::Error::other)?; + let mut locations = HashMap::::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 { + 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) }) + } +} diff --git a/yazi-fs/src/trash/mod.rs b/yazi-fs/src/trash/mod.rs new file mode 100644 index 00000000..e48f2913 --- /dev/null +++ b/yazi-fs/src/trash/mod.rs @@ -0,0 +1,16 @@ +yazi_macro::mod_flat!(entry lua node nodes); + +#[cfg(target_os = "macos")] +yazi_macro::mod_flat!(macos); + +#[cfg(windows)] +yazi_macro::mod_flat!(windows); + +#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))] +yazi_macro::mod_flat!(freedesktop); + +#[cfg(any(target_os = "android", target_os = "ios"))] +yazi_macro::mod_flat!(unsupported); + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +yazi_macro::mod_flat!(traits); diff --git a/yazi-fs/src/trash/node.rs b/yazi-fs/src/trash/node.rs new file mode 100644 index 00000000..dc97c26f --- /dev/null +++ b/yazi-fs/src/trash/node.rs @@ -0,0 +1,49 @@ +use std::{ffi::OsString, io, path::{Component, PathBuf}}; + +use mlua::{BorrowedBytes, FromLua, IntoLua, Lua, Table, Value}; +use yazi_shared::{path::PathBufDyn, strand::AsStrand}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TrashNode { + pub(super) key: OsString, + pub(super) top: OsString, + pub(super) rel: PathBuf, +} + +impl TrashNode { + pub(super) fn validate(&self) -> io::Result<()> { + if self.key.is_empty() || self.top.is_empty() || !self.rel.is_relative() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node")); + } + if self.rel.components().any(|c| matches!(c, Component::ParentDir)) { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node path")); + } + Ok(()) + } +} + +impl FromLua for TrashNode { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + let t = Table::from_lua(value, lua)?; + let node = Self { + key: t.raw_get::("key")?.as_strand().to_os_string()?, + top: t.raw_get::("top")?.as_strand().to_os_string()?, + rel: t.raw_get::("rel")?.into_os()?, + }; + + node.validate().map_err(mlua::Error::external)?; + Ok(node) + } +} + +impl IntoLua for TrashNode { + fn into_lua(self, lua: &Lua) -> mlua::Result { + 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) + } +} diff --git a/yazi-fs/src/trash/nodes.rs b/yazi-fs/src/trash/nodes.rs new file mode 100644 index 00000000..d6521780 --- /dev/null +++ b/yazi-fs/src/trash/nodes.rs @@ -0,0 +1,36 @@ +use std::vec; + +use mlua::{FromLua, Lua, Value}; + +use super::TrashNode; + +pub struct TrashNodes(Vec); + +impl TrashNodes { + fn new(mut nodes: Vec) -> Self { + nodes.sort_unstable_by_key(|node| node.rel.components().count()); + + let mut seen = Vec::::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 { + let nodes = Vec::::from_lua(value, lua)?; + Ok(Self::new(nodes)) + } +} + +impl IntoIterator for TrashNodes { + type IntoIter = vec::IntoIter; + type Item = TrashNode; + + fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } +} diff --git a/yazi-fs/src/trash/traits.rs b/yazi-fs/src/trash/traits.rs new file mode 100644 index 00000000..2c84c766 --- /dev/null +++ b/yazi-fs/src/trash/traits.rs @@ -0,0 +1,36 @@ +use crate::cha::{Cha, ChaKind, ChaMode}; + +pub(super) trait TrashCha: Sized { + fn from_mold(is_dir: bool) -> Self; + + #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] + fn from_trash( + path: &std::path::Path, + name: &std::ffi::OsStr, + follow: bool, + ) -> std::io::Result; +} + +impl TrashCha for Cha { + fn from_mold(is_dir: bool) -> Self { + let mut cha = Self::default(); + cha.kind.remove(ChaKind::DUMMY); + cha.mode = if is_dir { ChaMode::T_DIR | ChaMode::U_EXEC } else { ChaMode::T_FILE }; + cha.mode |= ChaMode::U_READ | ChaMode::U_WRITE; + cha + } + + #[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))] + fn from_trash( + path: &std::path::Path, + name: &std::ffi::OsStr, + follow: bool, + ) -> std::io::Result { + let cha = Cha::new(name, std::fs::symlink_metadata(path)?); + Ok(if cha.is_link() && follow { + cha.follow(std::fs::metadata(path).ok().map(|meta| Cha::new(name, meta))) + } else { + cha + }) + } +} diff --git a/yazi-fs/src/trash/unsupported.rs b/yazi-fs/src/trash/unsupported.rs new file mode 100644 index 00000000..464e4026 --- /dev/null +++ b/yazi-fs/src/trash/unsupported.rs @@ -0,0 +1,46 @@ +use std::io; + +use super::{TrashEntry, TrashNode, TrashNodes}; +use crate::{cha::Cha, file::File}; + +pub struct Trash; + +impl Trash { + pub fn new() -> io::Result { Ok(Self) } + + pub fn list(&self, _node: Option<&TrashNode>) -> io::Result> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn entry(&self, _node: &TrashNode) -> io::Result { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn metadata(&self, _node: &TrashNode, _: bool) -> io::Result { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub(super) fn revalidate( + &self, + _node: Option<&TrashNode>, + _current: &File, + ) -> io::Result> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn remove_file(&self, _node: &TrashNode) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn remove_dir(&self, _node: &TrashNode) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn restore(&self, _nodes: TrashNodes) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } + + pub fn empty(&self) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform")) + } +} diff --git a/yazi-fs/src/trash/windows.rs b/yazi-fs/src/trash/windows.rs new file mode 100644 index 00000000..167f068d --- /dev/null +++ b/yazi-fs/src/trash/windows.rs @@ -0,0 +1,330 @@ +use std::{ffi::{OsStr, OsString, c_void}, fs, hash::Hash, io, mem, os::windows::ffi::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}}; + +use hashbrown::HashMap; +use trash::{TrashItem, os_limited}; +use windows::{Win32::{Foundation::*, Storage::EnhancedStorage::*, System::{Com::*, SystemServices::*}, UI::Shell::*}, core::{Interface, PCWSTR}}; +use yazi_ffi::Com; +use yazi_shim::Twox128; + +use super::{TrashCha, TrashEntry, TrashNode, TrashNodes}; +use crate::{cha::Cha, file::File}; + +thread_local! { + static COM: io::Result = Com::new(); +} + +pub struct Trash; + +impl Trash { + pub fn new() -> io::Result { + 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> { + 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 { + let item = self.resolve(node)?; + item.entry(item.display_name(SIGDN_PARENTRELATIVE)?, &node.key) + } + + pub fn metadata(&self, node: &TrashNode, _: bool) -> io::Result { self.resolve(node)?.cha() } + + pub(super) fn revalidate( + &self, + node: Option<&TrashNode>, + current: &File, + ) -> io::Result> { + 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 = 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 { + 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 { + 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>, + count_size: Option<(i64, i64)>, +} + +impl TrashSig { + fn root() -> io::Result { + let root = ShellItem::root()?; + let cha = root.cha()?; + + let mut info = + SHQUERYRBINFO { cbSize: mem::size_of::() 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 { + 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> { + let mut names: Vec<_> = item + .children()? + .into_iter() + .map(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING)) + .collect::>()?; + 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 { + let name: Vec = name.encode_wide().chain([0]).collect(); + unsafe { SHCreateItemFromParsingName(PCWSTR(name.as_ptr()), None) }.map(Self).map_err(error) + } + + fn root() -> io::Result { + unsafe { SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, None) } + .map(Self) + .map_err(error) + } + + fn children(&self) -> io::Result> { + 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 { + 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(&self, name: N, key: K) -> io::Result + where + N: Into, + K: Into, + { + 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 { + unsafe { + let name = self.0.GetDisplayName(kind).map_err(error)?; + let result = OsString::from_wide(name.as_wide()); + CoTaskMemFree(Some(name.0.cast::())); + Ok(result) + } + } +} + +fn operate(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 { + 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)) +} diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index effbf782..8310e631 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,54 +1,36 @@ -use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; +use std::{borrow::Cow, path::{Path, PathBuf}}; use mlua::UserDataFields; -use yazi_shared::{path::{DynPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; +use yazi_shared::url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}; use yazi_shim::mlua::UserDataFieldsExt; -use crate::{FsHash128, FsSpec, path::PercentEncoding}; +use crate::{FsHash128, FsSpec}; pub trait FsUrl<'a> { fn cache(&self) -> Option; fn cache_lock(&self) -> Option; - fn unified_path(self) -> Cow<'a, Path>; - - fn unified_path_str(self) -> Cow<'a, OsStr> - where - Self: Sized, - { - match self.unified_path() { - Cow::Borrowed(p) => p.as_os_str().into(), - Cow::Owned(p) => p.into_os_string().into(), - } - } + fn working_path(self) -> Cow<'a, Path>; } impl<'a> FsUrl<'a> for Url<'a> { fn cache(&self) -> Option { - fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { - let mut it = loc.components(); - if it.next() == Some(yazi_shared::path::Component::RootDir) { - root.push(it.dyn_path().percent_encode()); - } else { - root.push(".%2F"); - root.push(loc.percent_encode()); - } + self.auth().cache().map(|mut root| { + root.push(self.hash_base32(&mut [0; 26])); root - } - - self.auth().cache().map(|root| with_loc(self.loc(), root)) + }) } fn cache_lock(&self) -> Option { self.auth().cache().map(|mut root| { root.push("%lock"); - root.push(format!("{:x}", self.hash_u128())); + root.push(self.hash_base32(&mut [0; 26])); root }) } - fn unified_path(self) -> Cow<'a, Path> { + fn working_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(), Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { @@ -63,7 +45,7 @@ impl FsUrl<'_> for UrlBuf { fn cache_lock(&self) -> Option { self.as_url().cache_lock() } - fn unified_path(self) -> Cow<'static, Path> { + fn working_path(self) -> Cow<'static, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(), Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { @@ -78,7 +60,7 @@ impl<'a> FsUrl<'a> for UrlCow<'a> { fn cache_lock(&self) -> Option { self.as_url().cache_lock() } - fn unified_path(self) -> Cow<'a, Path> { + fn working_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(), Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { diff --git a/yazi-macro/src/event.rs b/yazi-macro/src/event.rs index f4f9253a..1e9c8db7 100644 --- a/yazi-macro/src/event.rs +++ b/yazi-macro/src/event.rs @@ -1,13 +1,13 @@ #[macro_export] macro_rules! emit { (Call($action:expr)) => { - yazi_shared::event::Event::Call(yazi_shared::event::ActionCow::from($action)).emit(); + yazi_shared::event::Event::Call(yazi_shared::event::ActionCow::from($action)).emit() }; (Seq($actions:expr)) => { - yazi_shared::event::Event::Seq($actions).emit(); + yazi_shared::event::Event::Seq($actions).emit() }; ($event:ident) => { - yazi_shared::event::Event::$event.emit(); + yazi_shared::event::Event::$event.emit() }; } diff --git a/yazi-macro/src/render.rs b/yazi-macro/src/render.rs index 38c075f2..798bed8e 100644 --- a/yazi-macro/src/render.rs +++ b/yazi-macro/src/render.rs @@ -1,7 +1,7 @@ #[macro_export] macro_rules! render { () => { - yazi_shared::event::NEED_RENDER.store(1, std::sync::atomic::Ordering::Relaxed); + yazi_shared::event::NEED_RENDER.store(1, std::sync::atomic::Ordering::Relaxed) }; ($cond:expr) => { if $cond { diff --git a/yazi-plugin/preset/plugins/archive.lua b/yazi-plugin/preset/plugins/archive.lua index 8788a887..4df54db3 100644 --- a/yazi-plugin/preset/plugins/archive.lua +++ b/yazi-plugin/preset/plugins/archive.lua @@ -317,7 +317,7 @@ function M.treelize(items, tops, parents) end local buf, it = {}, f.path.parent - while it and it ~= tops[#tops] do + while it and #it ~= 0 and it ~= tops[#tops] do buf[#buf + 1], it = it, it.parent end for i = #buf, 1, -1 do diff --git a/yazi-plugin/preset/plugins/image.lua b/yazi-plugin/preset/plugins/image.lua index 7e3315ac..1cfaf494 100644 --- a/yazi-plugin/preset/plugins/image.lua +++ b/yazi-plugin/preset/plugins/image.lua @@ -3,7 +3,7 @@ local M = {} function M:peek(job) local start, url = os.clock(), ya.file_cache(job) if not url or not fs.cha(url) then - url = job.file.url + url = Url(job.file.path) end ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock())) @@ -20,7 +20,7 @@ function M:preload(job) return true end - return ya.image_precache(job.file.url, cache) + return ya.image_precache(Url(job.file.path), cache) end function M:spot(job) @@ -41,7 +41,7 @@ function M:spot(job) end function M:spot_base(job) - local info = ya.image_info(job.file.url) + local info = ya.image_info(Url(job.file.path)) if not info then return {} end diff --git a/yazi-plugin/preset/plugins/mime-local.lua b/yazi-plugin/preset/plugins/mime-local.lua index 9f4bb42b..fc9a26aa 100644 --- a/yazi-plugin/preset/plugins/mime-local.lua +++ b/yazi-plugin/preset/plugins/mime-local.lua @@ -6,11 +6,7 @@ local M = {} function M:fetch(job) local urls, paths = {}, {} for i, file in ipairs(job.files) do - if file.cache then - urls[i], paths[i] = tostring(file.url), tostring(file.cache) - else - paths[i] = tostring(file.path) - end + urls[i], paths[i] = file.url, tostring(file.path) end local child, err = M.spawn_file1(paths) @@ -42,7 +38,7 @@ function M:fetch(job) match, ignore = M.match_mimetype(line) if match then - updates[urls[i] or paths[i]], state[i], i = match, true, i + 1 + updates[urls[i]], state[i], i = match, true, i + 1 flush(false) elseif not ignore then state[i], i = false, i + 1 @@ -103,7 +99,7 @@ function M.placeholder(err, urls, paths) local updates = {} for i = 1, #paths do - updates[urls[i] or paths[i]] = "null/file1-not-found" + updates[urls[i]] = "null/file1-not-found" end ya.emit("update_mimes", { updates = updates }) diff --git a/yazi-plugin/preset/plugins/trash.lua b/yazi-plugin/preset/plugins/trash.lua new file mode 100644 index 00000000..79c3e4a8 --- /dev/null +++ b/yazi-plugin/preset/plugins/trash.lua @@ -0,0 +1,153 @@ +local M = {} + +local function top(url) + while url.parent and url.parent.name do + url = url.parent + end + return url.name and url or nil +end + +local function node(url) + local top = top(url) + if not top then + return + end + return { + key = url.spec.domain, + top = top.spec.domain, + rel = url:strip_prefix(top) or Path.os(""), + } +end + +local function absolute(url) + if url.is_absolute then + return fs.clean_url(url) + end + + local cwd, err = fs.cwd() + if not cwd then + return nil, err + end + + local root = cwd.path + while root.parent do + root = root.parent + end + return fs.clean_url(url:join(root:join(url.path))) +end + +local function file(url, entry) + entry.url = url + return File(entry) +end + +local function files(parent, entries) + for i, entry in ipairs(entries) do + local url = parent:join(Path.os(entry.name)):into_domain(entry.key) + entries[i] = file(url, entry) + end + return entries +end + +local function notify(action, err) + ya.notify { + title = "Trash", + content = string.format("Failed to %s: %s", action, err), + level = "error", + timeout = 10, + } +end + +function M:setup() + ps.sub_remote("trash-restore", function(args) + ya.async(function() + local nodes = {} + for i, arg in ipairs(args) do + nodes[i] = node(Url(arg)) + if not nodes[i] then + return notify("restore", "Cannot restore the trash root") + end + end + + local ok, err = fs.trash.restore(nodes) + if ok then + ya.emit("refresh", {}) + else + notify("restore", err) + end + end) + end) + + ps.sub_remote("trash-empty", function() + ya.async(function() + local confirmed = ya.confirm { + pos = { "center", w = 60, h = 10 }, + title = ui.Line("Empty trash?"):style(th.confirm.title), + body = ui.Text("All items in the trash will be permanently deleted."), + } + if not confirmed then + return + end + + local ok, err = fs.trash.empty() + if ok then + ya.emit("refresh", {}) + else + notify("empty", err) + end + end) + end) +end + +function M:entry() + local url, err = absolute(Url("trash:///@/.")) + if url then + ya.emit("cd", { url }) + else + notify("open", err) + end +end + +function M:provide(job) + local op = job.op + if op == "Absolute" or op == "Canonicalize" then + return absolute(job.url) + elseif op == "Casefold" then + return job.url + elseif op == "Metadata" or op == "SymlinkMetadata" then + local n = node(job.url) + if n then + return fs.trash.metadata(n, op == "Metadata") + else + return Cha { mode = tonumber("40700", 8) } + end + elseif op == "ReadDir" then + local entries, err = fs.trash.list(node(job.url)) + if entries then + return files(job.url, entries) + else + return nil, err + end + elseif op == "Revalidate" then + return fs.trash.revalidate(node(job.file.url), job.file) + elseif op == "File" then + local n = node(job.url) + if not n then + return nil, Error.custom("Cannot construct a file for the trash root") + end + local entry, err = fs.trash.entry(n) + if entry then + return file(job.url, entry) + else + return nil, err + end + elseif op == "RemoveFile" then + return fs.trash.remove("file", node(job.url)) + elseif op == "RemoveDir" then + return fs.trash.remove("dir", node(job.url)) + end + + return false, Error.custom("Unsupported trash operation: " .. op) +end + +return M diff --git a/yazi-plugin/preset/setup.lua b/yazi-plugin/preset/setup.lua index 10a6a343..30213906 100644 --- a/yazi-plugin/preset/setup.lua +++ b/yazi-plugin/preset/setup.lua @@ -2,3 +2,4 @@ os.setlocale("") require("dds"):setup() require("extract"):setup() +require("trash"):setup() diff --git a/yazi-plugin/src/external/fd.rs b/yazi-plugin/src/external/fd.rs index aa245d6f..6a022481 100644 --- a/yazi-plugin/src/external/fd.rs +++ b/yazi-plugin/src/external/fd.rs @@ -4,7 +4,7 @@ use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::{Child, Command}, sync::mpsc::{self, UnboundedReceiver}}; use yazi_fs::{FsUrl, file::File}; use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; pub struct FdOpt { pub cwd: UrlBuf, @@ -27,7 +27,7 @@ pub fn fd(opt: FdOpt) -> Result> { let Ok(url) = opt.cwd.try_join(line) else { continue; }; - if let Ok(file) = File::new(url).await { + if let Ok(file) = engine::file(url).await { tx.send(file).ok(); } } @@ -39,7 +39,7 @@ pub fn fd(opt: FdOpt) -> Result> { fn spawn(program: &str, opt: &FdOpt) -> std::io::Result { Command::new(program) .arg("--base-directory") - .arg(&*opt.cwd.as_url().unified_path()) + .arg(&*opt.cwd.as_url().working_path()) .arg("--regex") .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(&opt.args) diff --git a/yazi-plugin/src/external/rg.rs b/yazi-plugin/src/external/rg.rs index b6abf320..0d917f13 100644 --- a/yazi-plugin/src/external/rg.rs +++ b/yazi-plugin/src/external/rg.rs @@ -4,7 +4,7 @@ use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}}; use yazi_fs::{FsUrl, file::File}; use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; pub struct RgOpt { pub cwd: UrlBuf, @@ -19,7 +19,7 @@ pub fn rg(opt: RgOpt) -> Result> { .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(opt.args) .arg(opt.subject) - .current_dir(&*opt.cwd.as_url().unified_path()) + .current_dir(opt.cwd.as_url().working_path()) .kill_on_drop(true) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -36,7 +36,7 @@ pub fn rg(opt: RgOpt) -> Result> { let Ok(url) = opt.cwd.try_join(line) else { continue; }; - if let Ok(file) = File::new(url).await { + if let Ok(file) = engine::file(url).await { tx.send(file).ok(); } } diff --git a/yazi-plugin/src/external/rga.rs b/yazi-plugin/src/external/rga.rs index bfece528..a31c35e1 100644 --- a/yazi-plugin/src/external/rga.rs +++ b/yazi-plugin/src/external/rga.rs @@ -4,7 +4,7 @@ use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}}; use yazi_fs::{FsUrl, file::File}; use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; pub struct RgaOpt { pub cwd: UrlBuf, @@ -19,7 +19,7 @@ pub fn rga(opt: RgaOpt) -> Result> { .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(opt.args) .arg(opt.subject) - .current_dir(&*opt.cwd.as_url().unified_path()) + .current_dir(opt.cwd.as_url().working_path()) .kill_on_drop(true) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -36,7 +36,7 @@ pub fn rga(opt: RgaOpt) -> Result> { let Ok(url) = opt.cwd.try_join(line) else { continue; }; - if let Ok(file) = File::new(url).await { + if let Ok(file) = engine::file(url).await { tx.send(file).ok(); } } diff --git a/yazi-plugin/src/fs/calculator.rs b/yazi-plugin/src/fs/calculator.rs index 294c01ab..949b34df 100644 --- a/yazi-plugin/src/fs/calculator.rs +++ b/yazi-plugin/src/fs/calculator.rs @@ -3,7 +3,7 @@ use yazi_binding::Error; pub enum SizeCalculator { Local(yazi_fs::engine::local::SizeCalculator), - Remote(yazi_vfs::engine::SizeCalculator), + Virtual(yazi_vfs::engine::SizeCalculator), } impl UserData for SizeCalculator { @@ -11,7 +11,7 @@ impl UserData for SizeCalculator { fields.add_field_method_get("cha", |_, me| { Ok(match me { Self::Local(c) => c.cha(), - Self::Remote(c) => c.cha(), + Self::Virtual(c) => c.cha(), }) }); } @@ -20,7 +20,7 @@ impl UserData for SizeCalculator { methods.add_async_method_mut("recv", |lua, mut me, ()| async move { let next = match &mut *me { Self::Local(c) => c.next().await, - Self::Remote(c) => c.next().await, + Self::Virtual(c) => c.next().await, }; match next { diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 5f6ca642..16da9a2b 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -5,7 +5,7 @@ use yazi_binding::{Composer, ComposerGet, ComposerSet, Error}; use yazi_config::Pattern; use yazi_fs::{engine::{Attrs, DirReader, FileHolder}, file::File, mounts::PARTITIONS}; use yazi_shared::url::{UrlBuf, UrlCow, UrlLike, UrlRef}; -use yazi_vfs::{VfsFile, engine}; +use yazi_vfs::engine; use crate::fs::SizeCalculator; @@ -15,6 +15,7 @@ pub fn compose() -> Composer { b"access" => access(lua)?, b"calc_size" => calc_size(lua)?, b"cha" => cha(lua)?, + b"clean_url" => clean_url(lua)?, b"copy" => copy(lua)?, b"create" => create(lua)?, b"cwd" => cwd(lua)?, @@ -25,6 +26,7 @@ pub fn compose() -> Composer { b"read_dir" => read_dir(lua)?, b"remove" => remove(lua)?, b"rename" => rename(lua)?, + b"trash" => return yazi_fs::trash::Trash.into_lua(lua), b"unique" => unique(lua)?, b"write" => write(lua)?, _ => return Ok(Value::Nil), @@ -46,7 +48,7 @@ fn calc_size(lua: &Lua) -> mlua::Result { let it = if let Some(path) = url.as_local() { yazi_fs::engine::local::SizeCalculator::new(path).await.map(SizeCalculator::Local) } else { - yazi_vfs::engine::SizeCalculator::new(&*url).await.map(SizeCalculator::Remote) + yazi_vfs::engine::SizeCalculator::new(&*url).await.map(SizeCalculator::Virtual) }; match it { @@ -57,12 +59,9 @@ fn calc_size(lua: &Lua) -> mlua::Result { } fn cha(lua: &Lua) -> mlua::Result { - lua.create_async_function(|lua, (url, follow): (UrlRef, Option)| async move { - let cha = if follow.unwrap_or(false) { - engine::metadata(&*url).await - } else { - engine::symlink_metadata(&*url).await - }; + lua.create_async_function(|lua, (url, follow): (UrlRef, bool)| async move { + let cha = + if follow { engine::metadata(&*url).await } else { engine::symlink_metadata(&*url).await }; match cha { Ok(c) => c.into_lua_multi(&lua), @@ -71,6 +70,10 @@ fn cha(lua: &Lua) -> mlua::Result { }) } +fn clean_url(lua: &Lua) -> mlua::Result { + lua.create_function(|_, url: UrlRef| Ok(yazi_fs::path::clean_url(&*url))) +} + fn copy(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (from, to): (UrlRef, UrlRef)| async move { match engine::copy(&*from, &*to, Attrs::default()).await { @@ -124,7 +127,7 @@ fn expand_url(lua: &Lua) -> mlua::Result { fn file(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, url: UrlRef| async move { - match File::new(&*url).await { + match engine::file(&*url).await { Ok(file) => file.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } @@ -185,8 +188,8 @@ fn read_dir(lua: &Lua) -> mlua::Result { let file = if !resolve { File::from_dummy(url, next.file_type().await.ok()) - } else if let Ok(cha) = next.metadata().await { - File::from_follow(url, cha).await + } else if let Ok(file) = next.file().await { + file } else { File::from_dummy(url, next.file_type().await.ok()) }; diff --git a/yazi-plugin/src/fs/op.rs b/yazi-plugin/src/fs/op.rs index b613eb9e..34260b08 100644 --- a/yazi-plugin/src/fs/op.rs +++ b/yazi-plugin/src/fs/op.rs @@ -31,10 +31,9 @@ impl FilesOp { pub(super) fn done(_: &Lua, t: Table) -> mlua::Result { let id = t.raw_get("id")?; - let cha = t.raw_get("cha")?; - let url = t.raw_get("url")?; + let file = t.raw_get("file")?; - Ok(Self(yazi_fs::FilesOp::Done(url, cha, id))) + Ok(Self(yazi_fs::FilesOp::Done(file, id))) } pub(super) fn size(_: &Lua, t: Table) -> mlua::Result { diff --git a/yazi-plugin/src/utils/cache.rs b/yazi-plugin/src/utils/cache.rs index f8f80ba0..03230863 100644 --- a/yazi-plugin/src/utils/cache.rs +++ b/yazi-plugin/src/utils/cache.rs @@ -2,29 +2,34 @@ use std::hash::Hash; use mlua::{Function, Lua, Table}; use yazi_config::YAZI; -use yazi_fs::file::FileRef; -use yazi_shared::url::{UrlBuf, UrlLike}; +use yazi_fs::{FsHash128, file::{FileRef, FileSig}}; +use yazi_shared::url::{Url, UrlBuf, UrlLike}; use yazi_shim::Twox128; use super::Utils; impl Utils { pub(super) fn file_cache(lua: &Lua) -> mlua::Result { + struct Sig<'a>(FileSig<'a>, usize); + + impl FsHash128 for Sig<'_> { + fn hash_u128(&self) -> u128 { + let mut h = Twox128::default(); + self.0.hash(&mut h); + self.1.hash(&mut h); + h.finish_128() + } + } + lua.create_function(|_, t: Table| { let file: FileRef = t.raw_get("file")?; file.borrow(|f| { - if f.url.parent() == Some(yazi_shared::url::Url::regular(&YAZI.preview.cache_dir)) { + if f.url.parent() == Some(Url::regular(&YAZI.preview.cache_dir)) { return Ok(None); } - let hex = { - let mut h = Twox128::default(); - f.hash(&mut h); - t.raw_get("skip").unwrap_or(0usize).hash(&mut h); - format!("{:x}", h.finish_128()) - }; - - Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(hex)))) + let sig = Sig(FileSig(f), t.raw_get("skip").unwrap_or_default()); + Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(sig.hash_base32(&mut [0; 26]))))) }) }) } diff --git a/yazi-plugin/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index c13d9883..f440f908 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -1,15 +1,18 @@ use mlua::{Function, IntoLuaMulti, Lua, Value}; use yazi_adapter::{ADAPTOR, Image}; use yazi_binding::{Error, ImageInfo, elements::Rect}; -use yazi_fs::FsUrl; -use yazi_shared::url::{AsUrl, UrlLike, UrlRef}; +use yazi_shared::url::{UrlLike, UrlRef}; +use yazi_shim::OptionExt; use super::Utils; impl Utils { pub(super) fn image_info(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, url: UrlRef| async move { - let path = url.as_url().unified_path().into_owned(); + let Some(path) = url.as_local().owned() else { + return (Value::Nil, Error::custom("Source must be a local path")).into_lua_multi(&lua); + }; + match ImageInfo::new(path).await { Ok(info) => info.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua), @@ -19,7 +22,10 @@ impl Utils { pub(super) fn image_show(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move { - let path = url.as_url().unified_path(); + let Some(path) = url.as_local() else { + return (Value::Nil, Error::custom("Source must be a local path")).into_lua_multi(&lua); + }; + match ADAPTOR.image_show(path, *rect).await { Ok(area) => Rect::from(area).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua), @@ -29,11 +35,13 @@ impl Utils { pub(super) fn image_precache(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (src, dist): (UrlRef, UrlRef)| async move { - let Some(dist) = dist.as_local() else { - return (Value::Nil, Error::custom("Destination must be a local path")) - .into_lua_multi(&lua); + let Some(src) = src.as_local().owned() else { + return (false, Error::custom("Source must be a local path")).into_lua_multi(&lua); }; - let src = src.as_url().unified_path().into_owned(); + let Some(dist) = dist.as_local() else { + return (false, Error::custom("Destination must be a local path")).into_lua_multi(&lua); + }; + match Image::precache(src, dist).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua), diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index 001bfe04..6fdf4f3d 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -1,9 +1,8 @@ use mlua::{ExternalError, Function, IntoLuaMulti, Lua, Table, Value}; use yazi_binding::{Error, elements::Area}; use yazi_core::{Highlighter, MgrProxy, tab::PreviewLock}; -use yazi_fs::FsUrl; +use yazi_fs::file::FileRef; use yazi_runner::previewer::PeekError; -use yazi_shared::url::AsUrl; use yazi_widgets::Renderable; use super::Utils; @@ -15,9 +14,9 @@ impl Utils { pub(super) fn preview_code(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, t: Table| async move { let area: Area = t.raw_get("area")?; - let mut lock = PreviewLock::try_from(t)?; + let path = t.raw_get::("file")?.borrow(|f| Ok(f.content_path().into_owned()))?; - let path = lock.url.as_url().unified_path(); + let mut lock = PreviewLock::try_from(t)?; let inner = match Highlighter::oneshot(path, lock.skip, area.size()).await { Ok(text) => text, Err(e @ PeekError::Exceeded(max)) => return (e, max).into_lua_multi(&lua), diff --git a/yazi-runner/src/loader/loader.rs b/yazi-runner/src/loader/loader.rs index e4593b46..52b59eab 100644 --- a/yazi-runner/src/loader/loader.rs +++ b/yazi-runner/src/loader/loader.rs @@ -51,6 +51,7 @@ impl Default for Loader { ("pdf".to_owned(), preset!("plugins/pdf").into()), ("session".to_owned(), preset!("plugins/session").into()), ("svg".to_owned(), preset!("plugins/svg").into()), + ("trash".to_owned(), preset!("plugins/trash").into()), ("vfs".to_owned(), preset!("plugins/vfs").into()), ("video".to_owned(), preset!("plugins/video").into()), ("zoxide".to_owned(), preset!("plugins/zoxide").into()), diff --git a/yazi-runner/src/provider/job.rs b/yazi-runner/src/provider/job.rs index 79abdc58..76be4646 100644 --- a/yazi-runner/src/provider/job.rs +++ b/yazi-runner/src/provider/job.rs @@ -3,7 +3,7 @@ use std::io; use mlua::{IntoLua, Lua, Value}; use strum::AsRefStr; use yazi_binding::MpscTx; -use yazi_fs::engine::{Attrs, Demand}; +use yazi_fs::{engine::{Attrs, Demand}, file::File}; use yazi_shared::{path::PathBufDyn, url::UrlBuf}; #[derive(AsRefStr)] @@ -28,6 +28,12 @@ pub enum ProviderJob { ReadDir { url: UrlBuf, }, + Revalidate { + file: File, + }, + File { + url: UrlBuf, + }, Open { url: UrlBuf, attrs: Attrs, @@ -46,6 +52,9 @@ pub enum ProviderJob { RemoveDir { url: UrlBuf, }, + RemoveDirAll { + url: UrlBuf, + }, RemoveFile { url: UrlBuf, }, @@ -99,6 +108,7 @@ impl IntoLua for ProviderJob { match self { Self::Capabilities => {} + Self::Revalidate { file } => t.raw_set("file", file)?, Self::Absolute { url } | Self::Canonicalize { url } @@ -106,9 +116,11 @@ impl IntoLua for ProviderJob { | Self::SymlinkMetadata { url } | Self::Metadata { url } | Self::ReadDir { url } + | Self::File { url } | Self::CreateDir { url } | Self::ReadLink { url } | Self::RemoveDir { url } + | Self::RemoveDirAll { url } | Self::RemoveFile { url } | Self::Trash { url } => t.raw_set("url", url)?, diff --git a/yazi-scheduler/src/fetch/fetch.rs b/yazi-scheduler/src/fetch/fetch.rs index f97ce19c..8dc82089 100644 --- a/yazi-scheduler/src/fetch/fetch.rs +++ b/yazi-scheduler/src/fetch/fetch.rs @@ -6,7 +6,7 @@ use parking_lot::Mutex; use tokio::sync::mpsc; use tracing::error; use yazi_config::Priority; -use yazi_fs::FsHash64; +use yazi_fs::{FsHash64, file::FileSig}; use yazi_runner::RUNNER; use crate::{HIGH, LOW, TaskOp, TaskOps, fetch::{FetchIn, FetchOutFetch}}; @@ -32,7 +32,7 @@ impl Fetch { pub(crate) async fn fetch(&self, task: FetchIn) -> Result<(), FetchOutFetch> { let (id, fetcher) = (task.id, task.fetcher.clone()); - let hashes: Vec<_> = task.targets.iter().map(|f| f.hash_u64()).collect(); + let hashes: Vec<_> = task.targets.iter().map(|f| FileSig(f).hash_u64()).collect(); let (state, err) = RUNNER.fetch(task.into()).await?; let mut loaded = self.loaded.lock(); diff --git a/yazi-scheduler/src/preload/preload.rs b/yazi-scheduler/src/preload/preload.rs index 9aad08b2..a740d282 100644 --- a/yazi-scheduler/src/preload/preload.rs +++ b/yazi-scheduler/src/preload/preload.rs @@ -6,7 +6,7 @@ use parking_lot::Mutex; use tokio::sync::mpsc; use tracing::error; use yazi_config::Priority; -use yazi_fs::FsHash64; +use yazi_fs::{FsHash64, file::FileSig}; use yazi_runner::{RUNNER, preloader::{PreloadError, PreloadJob}}; use yazi_shared::id::Id; @@ -35,7 +35,7 @@ impl Preload { } pub(crate) async fn preload(&self, task: PreloadIn) -> Result<(), PreloadOut> { - let hash = task.target.hash_u64(); + let hash = FileSig(&task.target).hash_u64(); let mut rx = RUNNER.preload(PreloadJob { preloader: task.preloader.clone(), file: task.target }).await; diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 1e2fb594..75beb0aa 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -2,7 +2,7 @@ use std::{ops::Deref, sync::Arc, time::Duration}; use tokio::task::JoinHandle; use yazi_config::{YAZI, plugin::{FetcherArc, PreloaderArc}}; -use yazi_fs::FsHash64; +use yazi_fs::{FsHash64, file::FileSig}; use yazi_shared::{CompletionToken, Throttle, id::Id, url::{UrlBuf, UrlLike}}; use crate::{Behavior, HIGH, LOW, NORMAL, Task, TaskIn, TaskProg, Worker, fetch::FetchIn, file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDownload, FileOutHardlink, FileOutUpload}, hook::{HookIn, HookInDelete, HookInDownload, HookInPreload, HookInTrash, HookInUpload}, plugin::PluginInEntry, preload::PreloadIn, process::{ProcessIn, ProcessInBg, ProcessInBlock, ProcessInOrphan, ShellOpt}, size::SizeIn}; @@ -204,7 +204,7 @@ impl Scheduler { } pub fn preload_paged(&self, preloader: PreloaderArc, target: &yazi_fs::file::File) { - let hook = HookInPreload::new(preloader.idx, target.hash_u64()); + let hook = HookInPreload::new(preloader.idx, FileSig(target).hash_u64()); let mut r#in = PreloadIn { id: Id::ZERO, preloader, target: target.clone() }; self.add_hooked(&mut r#in, hook, |_| ()); diff --git a/yazi-sftp/Cargo.toml b/yazi-sftp/Cargo.toml index aa01cb52..d772f0cd 100644 --- a/yazi-sftp/Cargo.toml +++ b/yazi-sftp/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] bitflags = { workspace = true } +hashbrown = { workspace = true } parking_lot = { workspace = true } russh = { workspace = true } serde = { workspace = true } diff --git a/yazi-sftp/src/fs/attrs.rs b/yazi-sftp/src/fs/attrs.rs index d60acbb2..bfbd057d 100644 --- a/yazi-sftp/src/fs/attrs.rs +++ b/yazi-sftp/src/fs/attrs.rs @@ -1,5 +1,6 @@ -use std::{collections::HashMap, fmt}; +use std::fmt; +use hashbrown::HashMap; use serde::{Deserialize, Deserializer, Serialize, de::Visitor, ser::SerializeStruct}; #[derive(Clone, Debug, Default, Eq, PartialEq)] diff --git a/yazi-sftp/src/requests/init.rs b/yazi-sftp/src/requests/init.rs index 757e2967..1de9000e 100644 --- a/yazi-sftp/src/requests/init.rs +++ b/yazi-sftp/src/requests/init.rs @@ -1,5 +1,4 @@ -use std::collections::HashMap; - +use hashbrown::HashMap; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] diff --git a/yazi-sftp/src/responses/version.rs b/yazi-sftp/src/responses/version.rs index 967ff60a..409f39ca 100644 --- a/yazi-sftp/src/responses/version.rs +++ b/yazi-sftp/src/responses/version.rs @@ -1,5 +1,4 @@ -use std::collections::HashMap; - +use hashbrown::HashMap; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] diff --git a/yazi-sftp/src/session.rs b/yazi-sftp/src/session.rs index 1ad674b5..022783e8 100644 --- a/yazi-sftp/src/session.rs +++ b/yazi-sftp/src/session.rs @@ -1,5 +1,6 @@ -use std::{any::TypeId, collections::HashMap, io::{self, ErrorKind}, sync::Arc, time::Duration}; +use std::{any::TypeId, io::{self, ErrorKind}, sync::Arc, time::Duration}; +use hashbrown::HashMap; use parking_lot::Mutex; use russh::{ChannelStream, client::Msg}; use serde::Serialize; diff --git a/yazi-shared/src/auth/auth.rs b/yazi-shared/src/auth/auth.rs index 16bbebe1..00216569 100644 --- a/yazi-shared/src/auth/auth.rs +++ b/yazi-shared/src/auth/auth.rs @@ -1,7 +1,5 @@ use std::{fmt, hash::{Hash, Hasher}, sync::Arc}; -use anyhow::{Result, anyhow}; -use percent_encoding::percent_decode_str; use serde::Deserialize; use yazi_shim::cell::RoCell; @@ -145,11 +143,4 @@ impl Auth { self.covariant(other) || self.kind == AuthKind::Hub && other.kind == AuthKind::Hub && self.scheme == other.scheme } - - pub fn parse_cache(cache: &str) -> Result> { - let (kind, rest) = cache.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; - let (scheme, domain) = rest.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; - - Ok(Self::new(kind.parse()?, scheme.parse()?, percent_decode_str(domain).decode_utf8()?)) - } } diff --git a/yazi-shared/src/auth/de.rs b/yazi-shared/src/auth/de.rs new file mode 100644 index 00000000..7d86d948 --- /dev/null +++ b/yazi-shared/src/auth/de.rs @@ -0,0 +1,109 @@ +use std::sync::Arc; + +use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}}; + +use super::{Auth, Domain}; +use crate::pool::{InternStr, Symbol}; + +pub(crate) struct AuthDeserializer(pub(crate) Arc); + +impl<'de> Deserializer<'de> for AuthDeserializer { + type Error = de::value::Error; + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes byte_buf + unit unit_struct struct newtype_struct seq tuple tuple_struct enum identifier ignored_any + } + + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + self.deserialize_map(visitor) + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_map(MapDeserializer::new(self.0)) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_some(self) + } +} + +// --- Map +struct MapDeserializer { + kind: Option<&'static str>, + scheme: Option>, + domain: Option>, + parent: Option>, +} + +impl MapDeserializer { + fn new(auth: Arc) -> Self { + Self { + kind: Some(auth.kind.into()), + scheme: Some(auth.scheme.intern()), + domain: Some(auth.domain.clone()), + parent: auth.parent.clone(), + } + } +} + +impl<'de> MapAccess<'de> for MapDeserializer { + type Error = de::value::Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + let key = if self.kind.is_some() { + Some("kind") + } else if self.scheme.is_some() { + Some("scheme") + } else if self.domain.is_some() { + Some("domain") + } else if self.parent.is_some() { + Some("parent") + } else { + None + }; + + key.map(|key| seed.deserialize(key.into_deserializer())).transpose() + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + if let Some(kind) = self.kind.take() { + return seed.deserialize(kind.into_deserializer()); + } + if let Some(scheme) = self.scheme.take() { + return seed.deserialize(scheme.into_deserializer()); + } + if let Some(domain) = self.domain.take() { + return seed.deserialize(domain.into_deserializer()); + } + if let Some(parent) = self.parent.take() { + return seed.deserialize(AuthDeserializer(parent)); + } + + Err(de::Error::custom("value missing for key")) + } + + fn size_hint(&self) -> Option { + Some( + self.kind.is_some() as usize + + self.scheme.is_some() as usize + + self.domain.is_some() as usize + + self.parent.is_some() as usize, + ) + } +} diff --git a/yazi-shared/src/auth/domain.rs b/yazi-shared/src/auth/domain.rs index 98297e98..c0bd3a4c 100644 --- a/yazi-shared/src/auth/domain.rs +++ b/yazi-shared/src/auth/domain.rs @@ -1,69 +1,99 @@ -use std::{borrow::{Borrow, Cow}, fmt::{self, Display, Formatter}, ops::Deref}; +use std::{borrow::{Borrow, Cow}, fmt::{self, Display, Formatter}, ops::Deref, str}; -use serde::{Deserialize, Deserializer, Serialize, de::{self, IntoDeserializer, value::CowStrDeserializer}}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::{self, IntoDeserializer, Visitor}}; -#[derive(Default, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] -#[serde(transparent)] -pub struct Domain<'a>(Cow<'a, str>); +use crate::{BytesExt, data::BytesDeserializer}; + +#[derive(Default, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Domain<'a>(Cow<'a, [u8]>); impl Domain<'static> { - pub const CATCHALL: Self = Self(Cow::Borrowed("*")); - pub const EMPTY: Self = Self(Cow::Borrowed("")); -} - -impl Domain<'_> { - #[inline] - pub fn into_owned(self) -> Domain<'static> { Domain(Cow::Owned(self.0.into_owned())) } - - #[inline] - pub fn is_catchall(&self) -> bool { *self == Domain::CATCHALL } + pub const CATCHALL: Self = Self(Cow::Borrowed(b"*")); + pub const EMPTY: Self = Self(Cow::Borrowed(b"")); } impl Deref for Domain<'_> { - type Target = str; + type Target = [u8]; fn deref(&self) -> &Self::Target { &self.0 } } -impl Borrow for Domain<'_> { - fn borrow(&self) -> &str { &self.0 } +impl Borrow<[u8]> for Domain<'_> { + fn borrow(&self) -> &[u8] { &self.0 } } -impl AsRef for Domain<'_> { - fn as_ref(&self) -> &str { &self.0 } +impl AsRef<[u8]> for Domain<'_> { + fn as_ref(&self) -> &[u8] { &self.0 } +} + +impl<'a> From> for Domain<'a> { + fn from(value: Vec) -> Self { Self(Cow::Owned(value)) } +} + +impl<'a> From> for Domain<'a> { + fn from(value: Cow<'a, [u8]>) -> Self { Self(value) } } impl<'a> From for Domain<'a> { - fn from(value: String) -> Self { Self(Cow::Owned(value)) } + fn from(value: String) -> Self { Self(Cow::Owned(value.into_bytes())) } } -impl<'a> From<&'a str> for Domain<'a> { - fn from(value: &'a str) -> Self { Self(Cow::Borrowed(value)) } -} - -impl<'a> From> for Domain<'a> { - fn from(value: Cow<'a, str>) -> Self { Self(value) } -} - -impl<'a> From<&'a Domain<'_>> for Domain<'a> { - fn from(value: &'a Domain<'_>) -> Self { Self(Cow::Borrowed(&value.0)) } +impl<'a, T> From<&'a T> for Domain<'a> +where + T: AsRef<[u8]> + ?Sized, +{ + fn from(value: &'a T) -> Self { Self(Cow::Borrowed(value.as_ref())) } } impl Display for Domain<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self.0, f) } + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.display().fmt(f) } +} + +impl<'a> Domain<'a> { + pub fn into_owned(self) -> Domain<'static> { Domain(Cow::Owned(self.0.into_owned())) } + + pub fn is_catchall(&self) -> bool { *self == Domain::CATCHALL } + + pub fn to_str(&self) -> Result<&str, str::Utf8Error> { str::from_utf8(&self.0) } +} + +impl<'de, 'a: 'de> IntoDeserializer<'de, de::value::Error> for Domain<'a> { + type Deserializer = BytesDeserializer<'a>; + + fn into_deserializer(self) -> Self::Deserializer { BytesDeserializer(self.0) } +} + +impl Serialize for Domain<'_> { + fn serialize(&self, serializer: S) -> Result { + match self.to_str() { + Ok(s) => serializer.serialize_str(s), + Err(_) => serializer.serialize_bytes(&self.0), + } + } } impl<'de> Deserialize<'de> for Domain<'_> { fn deserialize>(deserializer: D) -> Result { - Ok(Self(Cow::Owned(String::deserialize(deserializer)?))) + struct V; + + impl Visitor<'_> for V { + type Value = Domain<'static>; + + fn expecting(&self, f: &mut Formatter) -> fmt::Result { f.write_str("a string or bytes") } + + fn visit_str(self, v: &str) -> Result { + Ok(v.to_owned().into()) + } + + fn visit_string(self, v: String) -> Result { Ok(v.into()) } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_owned().into()) + } + + fn visit_byte_buf(self, v: Vec) -> Result { Ok(v.into()) } + } + + deserializer.deserialize_any(V) } } - -impl<'de, 'a, E> IntoDeserializer<'de, E> for Domain<'a> -where - E: de::Error, -{ - type Deserializer = CowStrDeserializer<'a, E>; - - fn into_deserializer(self) -> Self::Deserializer { self.0.into_deserializer() } -} diff --git a/yazi-shared/src/auth/encode.rs b/yazi-shared/src/auth/encode.rs index 662a8b0e..53ac74f7 100644 --- a/yazi-shared/src/auth/encode.rs +++ b/yazi-shared/src/auth/encode.rs @@ -1,15 +1,16 @@ -use std::fmt; +use std::fmt::{self, Display}; -use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; +use percent_encoding::{AsciiSet, CONTROLS, percent_encode}; use super::{Auth, AuthKind, Domain}; +// --- EncodeAuth pub struct EncodeAuth<'a>(pub &'a Auth, pub bool); impl EncodeAuth<'_> { - pub fn domain<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> { + pub fn domain<'a>(s: &'a Domain<'_>) -> EncodeDomain<'a> { const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':').add(b'%'); - percent_encode(s.as_bytes(), SET) + EncodeDomain(s, SET) } } @@ -29,9 +30,9 @@ impl fmt::Display for EncodeAuth<'_> { pub struct EncodePrefix<'a>(pub &'a Auth); impl EncodePrefix<'_> { - pub fn parent<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> { + pub fn parent<'a>(s: &'a Domain<'_>) -> EncodeDomain<'a> { const SET: &AsciiSet = &CONTROLS.add(b'/').add(b',').add(b'@').add(b'%'); - percent_encode(s.as_bytes(), SET) + EncodeDomain(s, SET) } } @@ -53,3 +54,22 @@ impl fmt::Display for EncodePrefix<'_> { f.write_str("/") } } + +// --- EncodeDomain +pub struct EncodeDomain<'a>(&'a [u8], &'static AsciiSet); + +impl Display for EncodeDomain<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for chunk in self.0.utf8_chunks() { + for c in chunk.valid().chars() { + if c.is_ascii() { + percent_encode(&[c as u8], self.1).fmt(f)?; + } else { + c.fmt(f)?; + } + } + percent_encode(chunk.invalid(), self.1).fmt(f)?; + } + Ok(()) + } +} diff --git a/yazi-shared/src/auth/mod.rs b/yazi-shared/src/auth/mod.rs index 8ce33bb5..0feefdb2 100644 --- a/yazi-shared/src/auth/mod.rs +++ b/yazi-shared/src/auth/mod.rs @@ -1,3 +1,3 @@ -yazi_macro::mod_flat!(auth domain encode inventory kind scheme); +yazi_macro::mod_flat!(auth de domain encode inventory kind scheme); pub(super) fn init() { DEFAULT_ARC.with(Default::default); } diff --git a/yazi-shared/src/data/de_bytes.rs b/yazi-shared/src/data/de_bytes.rs index 3dcee3c4..5ea9fd63 100644 --- a/yazi-shared/src/data/de_bytes.rs +++ b/yazi-shared/src/data/de_bytes.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserializer, de::{self, value::SeqDeserializer}}; -pub(crate) struct BytesDeserializer<'a>(pub(crate) Cow<'a, [u8]>); +pub struct BytesDeserializer<'a>(pub(crate) Cow<'a, [u8]>); impl<'de, 'a: 'de> Deserializer<'de> for BytesDeserializer<'a> { type Error = de::value::Error; diff --git a/yazi-shared/src/path/error.rs b/yazi-shared/src/path/error.rs index 007885eb..e674c1f5 100644 --- a/yazi-shared/src/path/error.rs +++ b/yazi-shared/src/path/error.rs @@ -53,6 +53,10 @@ impl From for std::io::Error { fn from(err: PathDynError) -> Self { Self::other(err) } } +impl From for mlua::Error { + fn from(err: PathDynError) -> Self { Self::external(err) } +} + // --- SetNameError #[derive(Debug, Error)] #[error("calling set_name on paths with different encodings")] diff --git a/yazi-shared/src/path/lua.rs b/yazi-shared/src/path/lua.rs index c53d9ffe..aedfd9eb 100644 --- a/yazi-shared/src/path/lua.rs +++ b/yazi-shared/src/path/lua.rs @@ -1,5 +1,5 @@ use mlua::{AnyUserData, ExternalError, ExternalResult, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; -use yazi_shim::mlua::UserDataFieldsExt; +use yazi_shim::{OptionExt, mlua::UserDataFieldsExt}; use crate::{LOG_LEVEL, path::{PathBufDyn, PathLike, StripPrefixError}, strand::{AsStrand, StrandCow}}; @@ -74,7 +74,7 @@ impl UserData for PathBufDyn { fields.add_cached_field("name", |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); - fields.add_cached_field("parent", |_, me| Ok(me.parent().map(|p| p.to_owned()))); + fields.add_cached_field("parent", |_, me| Ok(me.parent().owned())); fields.add_cached_field("stem", |lua, me| { me.stem().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); @@ -92,6 +92,7 @@ impl UserData for PathBufDyn { methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: LuaString| { lua.create_external_string([lhs.encoded_bytes(), &rhs.as_bytes()].concat()) }); + methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); methods.add_meta_method(MetaMethod::Eq, |_, me, other: PathRef| Ok(*me == *other)); methods .add_meta_method(MetaMethod::ToString, |lua, me, ()| lua.create_string(me.encoded_bytes())); diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index f8cb1b83..0311fafe 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -2,6 +2,7 @@ use std::{borrow::Cow, ffi::OsStr}; use anyhow::Result; use hashbrown::Equivalent; +use yazi_shim::OptionExt; use super::{RsplitOnceError, StartsWithError}; use crate::{BytesExt, Utf8BytePredictor, path::{Components, Display, DynPath, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand, StrandError}}; @@ -55,6 +56,10 @@ impl Equivalent for PathDyn<'_> { fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.dyn_path() } } +impl<'p> OptionExt for Option> { + fn owned(self) -> Option { self.map(PathDyn::to_owned) } +} + impl<'p> PathDyn<'p> { #[inline] pub fn as_os(self) -> Result<&'p std::path::Path, PathDynError> { diff --git a/yazi-shared/src/spec/spec.rs b/yazi-shared/src/spec/spec.rs index 24b0b5f2..a20aca99 100644 --- a/yazi-shared/src/spec/spec.rs +++ b/yazi-shared/src/spec/spec.rs @@ -65,9 +65,9 @@ impl Spec { *skip += len + slash as usize; let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?; - let domain = percent_decode(&bytes[..len]).decode_utf8()?.into(); + let domain: Cow<[u8]> = percent_decode(&bytes[..len]).into(); - Ok((domain, uri, urn)) + Ok((domain.into(), uri, urn)) } fn decode_ports(bytes: &[u8], skip: &mut usize) -> Result<(Option, Option)> { @@ -117,7 +117,7 @@ impl Spec { parent = Some(Arc::new(Auth { kind: auth.kind, scheme: auth.scheme.clone(), - domain: Domain::from(percent_decode(domain).decode_utf8()?).into_owned(), + domain: Domain::from(Cow::from(percent_decode(domain))).into_owned(), parent, })); } diff --git a/yazi-shared/src/strand/error.rs b/yazi-shared/src/strand/error.rs index d66e86d1..5001275d 100644 --- a/yazi-shared/src/strand/error.rs +++ b/yazi-shared/src/strand/error.rs @@ -12,3 +12,7 @@ pub enum StrandError { impl From for std::io::Error { fn from(err: StrandError) -> Self { Self::other(err) } } + +impl From for mlua::Error { + fn from(err: StrandError) -> Self { Self::external(err) } +} diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs index b66d341f..16fdf6f1 100644 --- a/yazi-shared/src/strand/strand.rs +++ b/yazi-shared/src/strand/strand.rs @@ -1,7 +1,7 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::Display}; +use std::{borrow::Cow, ffi::{OsStr, OsString}, fmt::Display}; use anyhow::Result; -use yazi_shim::wtf8::FromWtf8; +use yazi_shim::{OptionExt, wtf8::FromWtf8}; use crate::{BytesExt, strand::{AsStrand, StrandBuf, StrandError, StrandKind}}; @@ -79,6 +79,10 @@ impl PartialEq<&[u8]> for Strand<'_> { } } +impl<'p> OptionExt for Option> { + fn owned(self) -> Option { self.map(Strand::to_owned) } +} + impl<'a> Strand<'a> { pub fn as_os(self) -> Result<&'a OsStr, StrandError> { match self { @@ -88,6 +92,9 @@ impl<'a> Strand<'a> { } } + #[inline] + pub fn to_os_string(self) -> Result { 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) diff --git a/yazi-shared/src/tests.rs b/yazi-shared/src/tests.rs index e3360402..fc0c0f49 100644 --- a/yazi-shared/src/tests.rs +++ b/yazi-shared/src/tests.rs @@ -11,10 +11,10 @@ pub fn init_tests() { inventory::submit! { AuthInventory { get: |scheme, domain| match (scheme.as_str(), domain.as_ref()) { - ("test-mount", "7z") => Some(Auth::new(AuthKind::Mount, scheme.clone(), "7z")), + ("test-mount", b"7z") => Some(Auth::new(AuthKind::Mount, scheme.clone(), "7z")), ("test-hub", _) => Some(Auth::new(AuthKind::Hub, scheme.clone(), domain.clone())), - ("test-scope", "aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")), - ("sftp", "vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")), + ("test-scope", b"aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")), + ("sftp", b"vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")), _ => None, }, } diff --git a/yazi-shared/src/url/de.rs b/yazi-shared/src/url/de.rs index db494024..8a4147da 100644 --- a/yazi-shared/src/url/de.rs +++ b/yazi-shared/src/url/de.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}}; -use crate::{auth::Domain, data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; +use crate::{auth::{AuthDeserializer, Domain}, data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; pub struct UrlDeserializer<'a>(pub(super) UrlCow<'a>); @@ -36,6 +36,7 @@ struct MapDeserializer<'a> { domain: Option>, uri: Option, urn: Option, + parent: Option, path: Option>, } @@ -50,6 +51,7 @@ impl<'a> MapDeserializer<'a> { domain: Some(spec.domain.clone()), uri: Some(uri), urn: Some(urn), + parent: spec.auth.parent.clone().map(AuthDeserializer), path: Some(path.into_encoded_bytes()), } } @@ -72,6 +74,8 @@ impl<'de, 'a: 'de> MapAccess<'de> for MapDeserializer<'a> { Some("uri") } else if self.urn.is_some() { Some("urn") + } else if self.parent.is_some() { + Some("parent") } else if self.path.is_some() { Some("path") } else { @@ -100,6 +104,9 @@ impl<'de, 'a: 'de> MapAccess<'de> for MapDeserializer<'a> { if let Some(urn) = self.urn.take() { return seed.deserialize(urn.into_deserializer()); } + if let Some(parent) = self.parent.take() { + return seed.deserialize(parent); + } if let Some(path) = self.path.take() { return seed.deserialize(BytesDeserializer(path)); } @@ -114,6 +121,7 @@ impl<'de, 'a: 'de> MapAccess<'de> for MapDeserializer<'a> { + self.domain.is_some() as usize + self.uri.is_some() as usize + self.urn.is_some() as usize + + self.parent.is_some() as usize + self.path.is_some() as usize, ) } diff --git a/yazi-shared/src/url/lua.rs b/yazi-shared/src/url/lua.rs index 2ad5777c..0e623f4d 100644 --- a/yazi-shared/src/url/lua.rs +++ b/yazi-shared/src/url/lua.rs @@ -1,4 +1,4 @@ -use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value}; +use mlua::{AnyUserData, BorrowedBytes, ExternalError, ExternalResult, IntoLua, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value}; use yazi_shim::mlua::UserDataFieldsExt; use crate::{LOG_LEVEL, path::{PathBufDyn, PathLike, StripPrefixError}, spec::Spec, strand::{StrandLike, ToStrand}, url::{UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; @@ -18,7 +18,7 @@ impl UrlBuf { if let Ok(url) = ud.borrow::() { url.clone() } else if let Ok(path) = ud.borrow::() { - path.as_os().into_lua_err()?.into() + path.as_os()?.into() } else { Err(EXPECTED.into_lua_err())? } @@ -121,8 +121,8 @@ impl UserData for UrlBuf { methods.add_method_once("into_search", |_, me, domain: LuaString| { me.into_search(domain.to_str()?).into_lua_err() }); - methods.add_method_once("into_domain", |_, me, domain: LuaString| { - Ok(me.into_domain(domain.to_str()?.to_owned())) + methods.add_method_once("into_domain", |_, me, domain: BorrowedBytes| { + Ok(me.into_domain(domain.to_vec())) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(*me == *other)); diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index c5a30b76..63a3e568 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -104,7 +104,7 @@ pub trait UrlMapExt { fn get_or_insert_with(&mut self, url: U, default: F) -> &mut V where U: AsUrl, - F: FnOnce(Url<'_>) -> V; + F: FnOnce(U) -> V; } impl UrlMapExt for HashMap @@ -122,12 +122,12 @@ where fn get_or_insert_with(&mut self, url: U, default: F) -> &mut V where U: AsUrl, - F: FnOnce(Url<'_>) -> V, + F: FnOnce(U) -> V, { - let url = url.as_url(); - match self.entry_ref(&url) { - EntryRef::Occupied(oe) => oe.into_mut(), - EntryRef::Vacant(ve) => ve.insert_with_key(url.into(), default(url)), + let key = url.as_url(); + match self.raw_entry_mut().from_key(&key) { + hashbrown::hash_map::RawEntryMut::Occupied(oe) => oe.into_mut(), + hashbrown::hash_map::RawEntryMut::Vacant(ve) => ve.insert(key.into(), default(url)).1, } } } @@ -147,10 +147,13 @@ where fn get_or_insert_with(&mut self, url: U, default: F) -> &mut V where U: AsUrl, - F: FnOnce(Url<'_>) -> V, + F: FnOnce(U) -> V, { - let url = url.as_url(); - self.raw_entry_mut_v1().from_key(&url).or_insert_with(|| (url.into(), default(url))).1 + let key = url.as_url(); + match self.raw_entry_mut_v1().from_key(&key) { + indexmap::map::raw_entry_v1::RawEntryMut::Occupied(oe) => oe.into_mut(), + indexmap::map::raw_entry_v1::RawEntryMut::Vacant(ve) => ve.insert(key.into(), default(url)).1, + } } } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index db3d1dbe..44fcc8ba 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -194,7 +194,7 @@ impl<'a> Url<'a> { #[inline] pub fn entry_key(self) -> PathDyn<'a> { match self { - Self::Hub { auth, .. } => PathDyn::Unix(typed_path::UnixPath::new(auth.domain.as_bytes())), + Self::Hub { auth, .. } => PathDyn::Unix(typed_path::UnixPath::new(&auth.domain)), _ => self.urn(), } } diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml index 6c07000c..e36bd485 100644 --- a/yazi-shim/Cargo.toml +++ b/yazi-shim/Cargo.toml @@ -36,3 +36,6 @@ unicode-width = { workspace = true } [dependencies.unicode-segmentation] version = "1.13.3" + +[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))'.dependencies] +trash = "5.2.6" diff --git a/yazi-shim/src/lib.rs b/yazi-shim/src/lib.rs index 6a1c09c3..c5dabb9b 100644 --- a/yazi-shim/src/lib.rs +++ b/yazi-shim/src/lib.rs @@ -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 percent_encoding sstr twox utf8); +yazi_macro::mod_flat!(base64 option percent_encoding sstr twox utf8); #[cfg(windows)] yazi_macro::mod_flat!(win32); diff --git a/yazi-shim/src/option.rs b/yazi-shim/src/option.rs new file mode 100644 index 00000000..ed2f2251 --- /dev/null +++ b/yazi-shim/src/option.rs @@ -0,0 +1,7 @@ +pub trait OptionExt { + fn owned(self) -> Option; +} + +impl OptionExt for Option<&T> { + fn owned(self) -> Option { self.map(ToOwned::to_owned) } +} diff --git a/yazi-vfs/src/cha.rs b/yazi-vfs/src/cha.rs index 639b430d..fb8ae768 100644 --- a/yazi-vfs/src/cha.rs +++ b/yazi-vfs/src/cha.rs @@ -1,6 +1,6 @@ use std::io; -use yazi_fs::cha::{Cha, ChaKind}; +use yazi_fs::cha::Cha; use yazi_shared::url::AsUrl; use crate::engine; @@ -20,18 +20,12 @@ impl VfsCha for Cha { Ok(Self::from_follow(url, engine::symlink_metadata(url).await?).await) } - async fn from_follow(url: U, mut cha: Self) -> Self + async fn from_follow(url: U, cha: Self) -> Self where U: AsUrl, { let url = url.as_url(); - let mut retain = cha.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM); - - if cha.is_link() { - retain |= ChaKind::FOLLOW; - cha = engine::metadata(url).await.unwrap_or(cha); - } - - cha.attach(retain) + let followed = if cha.is_link() { engine::metadata(url).await.ok() } else { None }; + cha.follow(followed) } } diff --git a/yazi-vfs/src/engine/demand.rs b/yazi-vfs/src/engine/demand.rs index 02120a53..372e86dc 100644 --- a/yazi-vfs/src/engine/demand.rs +++ b/yazi-vfs/src/engine/demand.rs @@ -38,12 +38,14 @@ impl FileBuilder for Demand { let url = url.as_url(); Ok(match url.kind() { AuthKind::Regular | AuthKind::Search => { - self.0.build::().open(url).await?.into() + (self.0.build::().open(url).await?, url.to_owned()).into() } AuthKind::Mount | AuthKind::Hub | AuthKind::Scope => { self.0.build::().open(url).await?.into() } - AuthKind::Sftp => self.0.build::().open(url).await?.into(), + AuthKind::Sftp => { + (self.0.build::().open(url).await?, url.to_owned()).into() + } }) } diff --git a/yazi-vfs/src/engine/dir_entry.rs b/yazi-vfs/src/engine/dir_entry.rs index 2a8b8a86..a8e79bda 100644 --- a/yazi-vfs/src/engine/dir_entry.rs +++ b/yazi-vfs/src/engine/dir_entry.rs @@ -1,6 +1,6 @@ use std::io; -use yazi_fs::{cha::{Cha, ChaType}, engine::FileHolder}; +use yazi_fs::{cha::{Cha, ChaType}, engine::FileHolder, file::File}; use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::UrlBuf}; pub enum DirEntry { @@ -10,6 +10,14 @@ pub enum DirEntry { } impl FileHolder for DirEntry { + async fn file(&self) -> io::Result { + match self { + Self::Local(entry) => entry.file().await, + Self::Lua(entry) => entry.file().await, + Self::Sftp(entry) => entry.file().await, + } + } + async fn file_type(&self) -> io::Result { match self { Self::Local(entry) => entry.file_type().await, diff --git a/yazi-vfs/src/engine/engine.rs b/yazi-vfs/src/engine/engine.rs index b727b682..e4c36dba 100644 --- a/yazi-vfs/src/engine/engine.rs +++ b/yazi-vfs/src/engine/engine.rs @@ -1,7 +1,7 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine, local::Local}}; +use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine, local::Local}, file::File}; use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; use super::{Engines, ReadDir, RwFile}; @@ -113,6 +113,13 @@ where Engines::new(url.as_url()).await?.create_new().await } +pub async fn file(url: U) -> io::Result +where + U: AsUrl, +{ + Engines::new(url.as_url()).await?.file().await +} + pub async fn hard_link(original: U, link: V) -> io::Result<()> where U: AsUrl, @@ -174,6 +181,10 @@ where Engines::new(url.as_url()).await?.read_link().await } +pub async fn revalidate(file: &File) -> io::Result> { + Engines::new(file.as_url()).await?.revalidate(file.clone()).await +} + pub async fn remove_dir(url: U) -> io::Result<()> where U: AsUrl, diff --git a/yazi-vfs/src/engine/engines.rs b/yazi-vfs/src/engine/engines.rs index 36a8d722..e948ebcf 100644 --- a/yazi-vfs/src/engine/engines.rs +++ b/yazi-vfs/src/engine/engines.rs @@ -1,7 +1,7 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}}; +use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::File}; use yazi_shared::{path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] @@ -74,10 +74,11 @@ impl<'a> Engine for Engines<'a> { } async fn create(&self) -> io::Result { + let url = self.url(); Ok(match self { - Self::Local(p) => p.create().await?.into(), + Self::Local(p) => (p.create().await?, url.to_owned()).into(), Self::Lua(p) => p.create().await?.into(), - Self::Sftp(p) => p.create().await?.into(), + Self::Sftp(p) => (p.create().await?, url.to_owned()).into(), }) } @@ -98,13 +99,22 @@ impl<'a> Engine for Engines<'a> { } async fn create_new(&self) -> io::Result { + let url = self.url(); Ok(match self { - Self::Local(p) => p.create_new().await?.into(), + Self::Local(p) => (p.create_new().await?, url.to_owned()).into(), Self::Lua(p) => p.create_new().await?.into(), - Self::Sftp(p) => p.create_new().await?.into(), + Self::Sftp(p) => (p.create_new().await?, url.to_owned()).into(), }) } + async fn file(&self) -> io::Result { + match self { + Self::Local(p) => p.file().await, + Self::Lua(p) => p.file().await, + Self::Sftp(p) => p.file().await, + } + } + async fn hard_link

(&self, to: P) -> io::Result<()> where P: DynPath, @@ -135,10 +145,11 @@ impl<'a> Engine for Engines<'a> { } async fn open(&self) -> io::Result { + let url = self.url(); Ok(match self { - Self::Local(p) => p.open().await?.into(), + Self::Local(p) => (p.open().await?, url.to_owned()).into(), Self::Lua(p) => p.open().await?.into(), - Self::Sftp(p) => p.open().await?.into(), + Self::Sftp(p) => (p.open().await?, url.to_owned()).into(), }) } @@ -158,6 +169,14 @@ impl<'a> Engine for Engines<'a> { } } + async fn revalidate(&self, file: File) -> io::Result> { + match self { + Self::Local(p) => p.revalidate(file).await, + Self::Lua(p) => p.revalidate(file).await, + Self::Sftp(p) => p.revalidate(file).await, + } + } + async fn remove_dir(&self) -> io::Result<()> { match self { Self::Local(p) => p.remove_dir().await, diff --git a/yazi-vfs/src/engine/lua/file.rs b/yazi-vfs/src/engine/lua/file.rs index b503ddc7..d2d3b35b 100644 --- a/yazi-vfs/src/engine/lua/file.rs +++ b/yazi-vfs/src/engine/lua/file.rs @@ -51,6 +51,16 @@ impl File { Ok(RUNNER.provide(self.run, ProviderJob::Metadata { url }).await.0?) } + pub(crate) async fn file(&self) -> io::Result { + let url = self.url.clone(); + Ok(RUNNER.provide(self.run, ProviderJob::File { url }).await.0?) + } + + pub(crate) async fn into_file(self) -> io::Result { + let url = self.url; + Ok(RUNNER.provide(self.run, ProviderJob::File { url }).await.0?) + } + fn write_impl( self: Pin<&mut Self>, cx: &mut Context<'_>, diff --git a/yazi-vfs/src/engine/lua/lua.rs b/yazi-vfs/src/engine/lua/lua.rs index 81373ec9..4fd15dc3 100644 --- a/yazi-vfs/src/engine/lua/lua.rs +++ b/yazi-vfs/src/engine/lua/lua.rs @@ -4,7 +4,7 @@ use mlua::FromLua; use tokio::sync::mpsc; use yazi_binding::MpscTx; use yazi_config::vfs::{ServiceLua, Vfs}; -use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::Files}; +use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::{File, Files}}; use yazi_runner::{RUNNER, provider::{ProvideResult, ProviderJob}}; use yazi_shared::{event::Cmd, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; @@ -86,6 +86,12 @@ impl<'a> Engine for Lua<'a> { Ok(self.call(ProviderJob::CreateDir { url }).await?.ok()?) } + async fn file(&self) -> io::Result { + let url = self.url.to_owned(); + + Ok(self.call(ProviderJob::File { url }).await?.0?) + } + async fn hard_link

(&self, to: P) -> io::Result<()> where P: DynPath, @@ -127,6 +133,10 @@ impl<'a> Engine for Lua<'a> { Ok(self.call(ProviderJob::ReadLink { url }).await?.0?) } + async fn revalidate(&self, file: File) -> io::Result> { + Ok(self.call(ProviderJob::Revalidate { file }).await?.0?) + } + async fn remove_dir(&self) -> io::Result<()> { let url = self.url.to_owned(); diff --git a/yazi-vfs/src/engine/lua/read_dir.rs b/yazi-vfs/src/engine/lua/read_dir.rs index 86a99dea..a25a28fe 100644 --- a/yazi-vfs/src/engine/lua/read_dir.rs +++ b/yazi-vfs/src/engine/lua/read_dir.rs @@ -21,6 +21,8 @@ pub struct DirEntry { } impl FileHolder for DirEntry { + async fn file(&self) -> io::Result { Ok(self.file.clone()) } + async fn file_type(&self) -> io::Result { Ok(**self.file.cha) } async fn metadata(&self) -> io::Result { Ok(self.file.cha) } diff --git a/yazi-vfs/src/engine/rw_file.rs b/yazi-vfs/src/engine/rw_file.rs index 20809cf5..45b4923d 100644 --- a/yazi-vfs/src/engine/rw_file.rs +++ b/yazi-vfs/src/engine/rw_file.rs @@ -3,20 +3,23 @@ use std::{io, pin::Pin}; use mlua::{IntoLuaMulti, LuaString, UserData, UserDataMethods, Value}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite, AsyncWriteExt}; use yazi_binding::Error; -use yazi_fs::engine::Attrs; +use yazi_fs::{engine::Attrs, file::File}; +use yazi_shared::url::{UrlBuf, UrlLike}; + +use crate::VfsFile; pub enum RwFile { - Tokio(tokio::fs::File), - Sftp(Box), + Tokio(tokio::fs::File, UrlBuf), + Sftp(Box, UrlBuf), Lua(super::lua::File), } -impl From for RwFile { - fn from(f: tokio::fs::File) -> Self { Self::Tokio(f) } +impl From<(tokio::fs::File, UrlBuf)> for RwFile { + fn from((f, url): (tokio::fs::File, UrlBuf)) -> Self { Self::Tokio(f, url) } } -impl From for RwFile { - fn from(f: yazi_sftp::fs::File) -> Self { Self::Sftp(Box::new(f)) } +impl From<(yazi_sftp::fs::File, UrlBuf)> for RwFile { + fn from((f, url): (yazi_sftp::fs::File, UrlBuf)) -> Self { Self::Sftp(Box::new(f), url) } } impl From for RwFile { @@ -24,18 +27,44 @@ impl From for RwFile { } impl RwFile { - // FIXME: path pub async fn metadata(&self) -> io::Result { Ok(match self { - Self::Tokio(f) => yazi_fs::cha::Cha::new("// FIXME", f.metadata().await?), - Self::Sftp(f) => super::sftp::Cha::try_from(("// FIXME".as_bytes(), &f.fstat().await?))?.0, + Self::Tokio(f, url) => { + yazi_fs::cha::Cha::new(url.name().unwrap_or_default(), f.metadata().await?) + } + Self::Sftp(f, url) => { + let name = url.name().unwrap_or_default().encoded_bytes(); + super::sftp::Cha::try_from((name, &f.fstat().await?))?.0 + } Self::Lua(f) => f.metadata().await?, }) } + pub async fn file(&self) -> io::Result { + Ok(match self { + Self::Tokio(_, url) | Self::Sftp(_, url) => { + let cha = self.metadata().await?; + File::from_follow(url.clone(), cha).await + } + Self::Lua(f) => f.file().await?, + }) + } + + pub async fn into_file(self) -> io::Result { + if let Self::Lua(f) = self { + return f.into_file().await; + } + + let cha = self.metadata().await?; + Ok(match self { + Self::Tokio(_, url) | Self::Sftp(_, url) => File { url, cha, extra: Default::default() }, + Self::Lua(_) => unreachable!(), + }) + } + pub async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> { match self { - Self::Tokio(f) => { + Self::Tokio(f, _) => { let (perm, times) = (attrs.try_into(), attrs.try_into()); if perm.is_err() && times.is_err() { return Ok(()); @@ -48,7 +77,7 @@ impl RwFile { }) .await?; } - Self::Sftp(f) => { + Self::Sftp(f, _) => { if let Ok(attrs) = super::sftp::Attrs(attrs).try_into() { f.fsetstat(&attrs).await?; } @@ -61,8 +90,8 @@ impl RwFile { pub async fn set_len(&self, size: u64) -> io::Result<()> { Ok(match self { - Self::Tokio(f) => f.set_len(size).await?, - Self::Sftp(f) => { + Self::Tokio(f, _) => f.set_len(size).await?, + Self::Sftp(f, _) => { f.fsetstat(&yazi_sftp::fs::Attrs { size: Some(size), ..Default::default() }).await? } Self::Lua(f) => f.set_len(size).await?, @@ -78,9 +107,9 @@ impl AsyncRead for RwFile { buf: &mut tokio::io::ReadBuf<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_read(cx, buf), - Self::Sftp(f) => Pin::new(f).poll_read(cx, buf), - Self::Lua(f) => Pin::new(f).poll_read(cx, buf), + RwFile::Tokio(f, _) => Pin::new(f).poll_read(cx, buf), + RwFile::Sftp(f, _) => Pin::new(f).poll_read(cx, buf), + RwFile::Lua(f) => Pin::new(f).poll_read(cx, buf), } } } @@ -89,9 +118,9 @@ impl AsyncSeek for RwFile { #[inline] fn start_seek(mut self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { match &mut *self { - Self::Tokio(f) => Pin::new(f).start_seek(position), - Self::Sftp(f) => Pin::new(f).start_seek(position), - Self::Lua(f) => Pin::new(f).start_seek(position), + RwFile::Tokio(f, _) => Pin::new(f).start_seek(position), + RwFile::Sftp(f, _) => Pin::new(f).start_seek(position), + RwFile::Lua(f) => Pin::new(f).start_seek(position), } } @@ -101,9 +130,9 @@ impl AsyncSeek for RwFile { cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_complete(cx), - Self::Sftp(f) => Pin::new(f).poll_complete(cx), - Self::Lua(f) => Pin::new(f).poll_complete(cx), + RwFile::Tokio(f, _) => Pin::new(f).poll_complete(cx), + RwFile::Sftp(f, _) => Pin::new(f).poll_complete(cx), + RwFile::Lua(f) => Pin::new(f).poll_complete(cx), } } } @@ -116,9 +145,9 @@ impl AsyncWrite for RwFile { buf: &[u8], ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_write(cx, buf), - Self::Sftp(f) => Pin::new(f).poll_write(cx, buf), - Self::Lua(f) => Pin::new(f).poll_write(cx, buf), + RwFile::Tokio(f, _) => Pin::new(f).poll_write(cx, buf), + RwFile::Sftp(f, _) => Pin::new(f).poll_write(cx, buf), + RwFile::Lua(f) => Pin::new(f).poll_write(cx, buf), } } @@ -128,9 +157,9 @@ impl AsyncWrite for RwFile { cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_flush(cx), - Self::Sftp(f) => Pin::new(f).poll_flush(cx), - Self::Lua(f) => Pin::new(f).poll_flush(cx), + RwFile::Tokio(f, _) => Pin::new(f).poll_flush(cx), + RwFile::Sftp(f, _) => Pin::new(f).poll_flush(cx), + RwFile::Lua(f) => Pin::new(f).poll_flush(cx), } } @@ -140,9 +169,9 @@ impl AsyncWrite for RwFile { cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_shutdown(cx), - Self::Sftp(f) => Pin::new(f).poll_shutdown(cx), - Self::Lua(f) => Pin::new(f).poll_shutdown(cx), + RwFile::Tokio(f, _) => Pin::new(f).poll_shutdown(cx), + RwFile::Sftp(f, _) => Pin::new(f).poll_shutdown(cx), + RwFile::Lua(f) => Pin::new(f).poll_shutdown(cx), } } @@ -153,18 +182,18 @@ impl AsyncWrite for RwFile { bufs: &[io::IoSlice<'_>], ) -> std::task::Poll> { match &mut *self { - Self::Tokio(f) => Pin::new(f).poll_write_vectored(cx, bufs), - Self::Sftp(f) => Pin::new(f).poll_write_vectored(cx, bufs), - Self::Lua(f) => Pin::new(f).poll_write_vectored(cx, bufs), + RwFile::Tokio(f, _) => Pin::new(f).poll_write_vectored(cx, bufs), + RwFile::Sftp(f, _) => Pin::new(f).poll_write_vectored(cx, bufs), + RwFile::Lua(f) => Pin::new(f).poll_write_vectored(cx, bufs), } } #[inline] fn is_write_vectored(&self) -> bool { match self { - Self::Tokio(f) => f.is_write_vectored(), - Self::Sftp(f) => f.is_write_vectored(), - Self::Lua(f) => f.is_write_vectored(), + RwFile::Tokio(f, _) => f.is_write_vectored(), + RwFile::Sftp(f, _) => f.is_write_vectored(), + RwFile::Lua(f) => f.is_write_vectored(), } } } diff --git a/yazi-vfs/src/engine/sftp/read_dir.rs b/yazi-vfs/src/engine/sftp/read_dir.rs index 0d5b0100..e1530e1a 100644 --- a/yazi-vfs/src/engine/sftp/read_dir.rs +++ b/yazi-vfs/src/engine/sftp/read_dir.rs @@ -1,9 +1,10 @@ use std::{io, sync::Arc}; -use yazi_fs::engine::{DirReader, FileHolder}; +use yazi_fs::{engine::{DirReader, FileHolder}, file::File}; use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; use super::{Cha, ChaMode}; +use crate::VfsFile; pub struct ReadDir { pub(super) dir: Arc, @@ -25,6 +26,11 @@ pub struct DirEntry { } impl FileHolder for DirEntry { + async fn file(&self) -> io::Result { + let cha = self.metadata().await?; + Ok(File::from_follow(self.url(), cha).await) + } + async fn file_type(&self) -> io::Result { Ok(ChaMode::try_from(self.entry.attrs())?.0.into()) } diff --git a/yazi-vfs/src/entries.rs b/yazi-vfs/src/entries.rs index 290841a6..cd2247ec 100644 --- a/yazi-vfs/src/entries.rs +++ b/yazi-vfs/src/entries.rs @@ -1,17 +1,17 @@ use std::io; use tokio::{select, sync::mpsc::{self, UnboundedReceiver}}; -use yazi_fs::{Entries, FilesOp, cha::Cha, engine::{DirReader, FileHolder}, file::File, mounts::PARTITIONS}; +use yazi_fs::{Entries, engine::{DirReader, FileHolder}, file::File, mounts::PARTITIONS}; use yazi_shared::url::UrlBuf; -use crate::{VfsCha, VfsFile, VfsFilesOp, engine::{self, DirEntry}}; +use crate::engine::{self, DirEntry}; pub trait VfsEntries { fn from_dir(dir: &UrlBuf) -> impl Future>>; fn from_dir_bulk(dir: &UrlBuf) -> impl Future>>; - fn assert_stale(dir: &UrlBuf, cha: Cha) -> impl Future>; + fn revalidate(file: &File) -> impl Future>>; } impl VfsEntries for Entries { @@ -23,11 +23,10 @@ impl VfsEntries for Entries { while let Ok(Some(ent)) = it.next().await { select! { _ = tx.closed() => break, - result = ent.metadata() => { - let url = ent.url(); + result = ent.file() => { _ = tx.send(match result { - Ok(cha) => File::from_follow(url, cha).await, - Err(_) => File::from_dummy(url, ent.file_type().await.ok()) + Ok(file) => file, + Err(_) => File::from_dummy(ent.url(), ent.file_type().await.ok()), }); } } @@ -48,10 +47,9 @@ impl VfsEntries for Entries { async fn go(entries: &[DirEntry]) -> Vec { let mut files = Vec::with_capacity(entries.len()); for ent in entries { - let url = ent.url(); - files.push(match ent.metadata().await { - Ok(cha) => File::from_follow(url, cha).await, - Err(_) => File::from_dummy(url, ent.file_type().await.ok()), + files.push(match ent.file().await { + Ok(file) => file, + Err(_) => File::from_dummy(ent.url(), ent.file_type().await.ok()), }); } files @@ -66,14 +64,15 @@ impl VfsEntries for Entries { ) } - async fn assert_stale(dir: &UrlBuf, cha: Cha) -> Option { - use std::io::ErrorKind; - match Cha::from_url(dir).await { - Ok(c) if !c.is_dir() => FilesOp::issue_error(dir, ErrorKind::NotADirectory).await, - Ok(c) if c.hits(cha) && !PARTITIONS.read().timeless(cha) => {} - Ok(c) => return Some(c), - Err(e) => FilesOp::issue_error(dir, e).await, + async fn revalidate(old: &File) -> io::Result> { + match engine::revalidate(old).await? { + Some(new) if new.url != old.url => { + Err(io::Error::new(io::ErrorKind::InvalidData, "revalidated file URL changed")) + } + Some(new) if !new.is_dir() => Err(io::ErrorKind::NotADirectory.into()), + Some(new) => Ok(Some(new)), + None if PARTITIONS.read().timeless(old.cha) => Ok(Some(old.clone())), + None => Ok(None), } - None } } diff --git a/yazi-vfs/src/file.rs b/yazi-vfs/src/file.rs index 8b4a7d72..9a45d62e 100644 --- a/yazi-vfs/src/file.rs +++ b/yazi-vfs/src/file.rs @@ -1,40 +1,30 @@ use std::io; -use yazi_fs::{cha::Cha, file::File}; +use yazi_fs::{cha::Cha, file::{File, FileExtra}}; use yazi_shared::url::{UrlBuf, UrlCow}; use crate::{VfsCha, engine}; pub trait VfsFile: Sized { - fn new<'a>(url: impl Into>) -> impl Future>; - fn maybe_new<'a>(url: impl Into>) -> impl Future>>; fn from_follow(url: UrlBuf, cha: Cha) -> impl Future; } impl VfsFile for File { - async fn new<'a>(url: impl Into>) -> io::Result { - let url = url.into(); - let cha = engine::symlink_metadata(&url).await?; - Ok(Self::from_follow(url.into_owned(), cha).await) - } - async fn maybe_new<'a>(url: impl Into>) -> io::Result> { let url = url.into(); - let cha = match engine::symlink_metadata(&url).await { - Ok(cha) => cha, - Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), + Ok(match engine::file(&url).await { + Ok(file) => Some(file), + Err(e) if e.kind() == io::ErrorKind::NotFound => None, Err(e) => return Err(e), - }; - Ok(Some(Self::from_follow(url.into_owned(), cha).await)) + }) } async fn from_follow(url: UrlBuf, cha: Cha) -> Self { let link_to = if cha.is_link() { engine::read_link(&url).await.ok() } else { None }; - let cha = Cha::from_follow(&url, cha).await; - Self { url, cha, link_to } + Self { url, cha, extra: FileExtra::new(link_to, None) } } } diff --git a/yazi-vfs/src/fns.rs b/yazi-vfs/src/fns.rs index 733fc5b5..79e34f4d 100644 --- a/yazi-vfs/src/fns.rs +++ b/yazi-vfs/src/fns.rs @@ -1,6 +1,7 @@ use std::io::{self}; use yazi_shared::{strand::{StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}}; +use yazi_shim::OptionExt; use crate::engine; @@ -23,7 +24,7 @@ pub async fn unique_file(u: UrlBuf, is_dir: bool) -> io::Result { } async fn _unique_file(mut url: UrlBuf, is_dir: bool) -> io::Result { - let Some(stem) = url.stem().map(|s| s.to_owned()) else { + let Some(stem) = url.stem().owned() else { return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem")); }; diff --git a/yazi-vfs/src/lib.rs b/yazi-vfs/src/lib.rs index fb81f98b..5db9e8ef 100644 --- a/yazi-vfs/src/lib.rs +++ b/yazi-vfs/src/lib.rs @@ -1,5 +1,5 @@ yazi_macro::mod_pub!(engine); -yazi_macro::mod_flat!(cha entries file fns op); +yazi_macro::mod_flat!(cha entries file fns); pub fn init() { engine::init(); } diff --git a/yazi-vfs/src/op.rs b/yazi-vfs/src/op.rs deleted file mode 100644 index e317e0a5..00000000 --- a/yazi-vfs/src/op.rs +++ /dev/null @@ -1,21 +0,0 @@ -use yazi_fs::FilesOp; -use yazi_shared::url::{UrlBuf, UrlLike}; - -use crate::maybe_exists; - -pub trait VfsFilesOp { - fn issue_error(cwd: &UrlBuf, kind: impl Into) -> impl Future; -} - -impl VfsFilesOp for FilesOp { - async fn issue_error(cwd: &UrlBuf, err: impl Into) { - let err = err.into(); - if err.kind() != std::io::ErrorKind::NotFound { - Self::IOErr(cwd.clone(), err).emit(); - } else if maybe_exists(cwd).await { - Self::IOErr(cwd.clone(), err).emit(); - } else if let Some((p, n)) = cwd.pair() { - Self::Deleting(p.into(), [n.into()].into()).emit(); - } - } -} diff --git a/yazi-watcher/Cargo.toml b/yazi-watcher/Cargo.toml index df56f6ef..f87d3e57 100644 --- a/yazi-watcher/Cargo.toml +++ b/yazi-watcher/Cargo.toml @@ -24,9 +24,9 @@ yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } # External dependencies futures = { workspace = true } hashbrown = { workspace = true } +indexmap = { workspace = true } notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] } parking_lot = { workspace = true } -percent-encoding = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } diff --git a/yazi-watcher/src/backend.rs b/yazi-watcher/src/backend.rs index ec83fdf7..c8e5b56a 100644 --- a/yazi-watcher/src/backend.rs +++ b/yazi-watcher/src/backend.rs @@ -1,11 +1,11 @@ use notify::Result; use tokio::sync::mpsc; -use crate::{Reporter, WATCHED, Watchee, local::{self, LINKED, Linked}, remote}; +use crate::{Reporter, WATCHED, Watchee, local::{self, LINKED, Linked}, r#virtual}; pub(crate) struct Backend { local: local::Local, - remote: remote::Remote, + r#virtual: r#virtual::Virtual, pub(super) reporter: Reporter, } @@ -19,12 +19,12 @@ impl Backend { }); let (local_tx, local_rx) = mpsc::unbounded_channel(); - let (remote_tx, remote_rx) = mpsc::unbounded_channel(); - let reporter = Reporter { local_tx, remote_tx }; + let (virtual_tx, virtual_rx) = mpsc::unbounded_channel(); + let reporter = Reporter { local_tx, virtual_tx }; Self { local: local::Local::serve(local_rx, reporter.clone()), - remote: remote::Remote::serve(remote_rx, reporter.clone()), + r#virtual: r#virtual::Virtual::serve(virtual_rx), reporter, } } @@ -32,14 +32,14 @@ impl Backend { pub(super) fn watch(&mut self, watchee: &mut Watchee) -> Result<()> { match watchee { Watchee::Local(..) => self.local.watch(watchee), - Watchee::Remote(_) => self.remote.watch(watchee), + Watchee::Virtual(_) => self.r#virtual.watch(watchee), } } pub(super) fn unwatch(&mut self, watchee: &Watchee) -> Result<()> { match watchee { Watchee::Local(..) => self.local.unwatch(watchee), - Watchee::Remote(_) => self.remote.unwatch(watchee), + Watchee::Virtual(_) => self.r#virtual.unwatch(watchee), } } diff --git a/yazi-watcher/src/lib.rs b/yazi-watcher/src/lib.rs index 4ba56039..a28272fe 100644 --- a/yazi-watcher/src/lib.rs +++ b/yazi-watcher/src/lib.rs @@ -1,6 +1,6 @@ -yazi_macro::mod_pub!(local remote); +yazi_macro::mod_pub!(local r#virtual); -yazi_macro::mod_flat!(backend proxy reporter watched watchee watcher); +yazi_macro::mod_flat!(backend proxy refresher reporter watched watchee watcher); pub static WATCHED: yazi_shim::cell::RoCell> = yazi_shim::cell::RoCell::new(); diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index 131e0af7..8f296f23 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -1,13 +1,12 @@ -use std::{path::Path, time::Duration}; +use std::{io, path::Path, time::Duration}; use hashbrown::HashSet; use notify::{PollWatcher, RecommendedWatcher, RecursiveMode, Result, Watcher}; use tokio::{pin, sync::mpsc::{self, UnboundedReceiver}}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tracing::error; -use yazi_fs::{FilesOp, engine::{self, Engine}, file::File, mounts::PARTITIONS}; +use yazi_fs::{FilesOp, engine::{self, Engine}, mounts::PARTITIONS}; use yazi_shared::url::{UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; use crate::{Reporter, WATCHER, Watchee}; @@ -96,16 +95,23 @@ impl Local { let _permit = WATCHER.acquire().await.unwrap(); let mut ops = Vec::with_capacity(urls.len()); - for u in urls { - let Some((parent, key)) = u.pair2() else { continue }; - let Ok(file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); - continue; + for url in urls { + let Some(path) = url.as_local() else { continue }; + let Some((parent, key)) = url.pair2() else { continue }; + + let file = match engine::local::Local::regular(path).file().await { + Ok(file) => file, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); + continue; + } + Err(e) => { + tracing::error!("Failed to update {url:?}: {e:?}"); + continue; + } }; - if let Some(p) = file.url.as_local() - && !engine::local::match_name_case(p).await - { + if !engine::local::match_name_case(path).await { ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); continue; } diff --git a/yazi-watcher/src/refresher.rs b/yazi-watcher/src/refresher.rs new file mode 100644 index 00000000..4d7b4229 --- /dev/null +++ b/yazi-watcher/src/refresher.rs @@ -0,0 +1,174 @@ +use std::{io, ops::Deref, time::{Duration, Instant}}; + +use hashbrown::HashMap; +use indexmap::IndexSet; +use tokio::sync::mpsc; +use yazi_fs::{Entries, FilesOp, file::{File, FileCov}}; +use yazi_shared::url::{UrlBuf, UrlCov, UrlLike, UrlMapExt}; +use yazi_vfs::VfsEntries; + +#[derive(Clone)] +pub struct Refresher { + tx: mpsc::UnboundedSender, +} + +enum Op { + Sync(IndexSet), + Refresh(IndexSet), + Load(File), + Touch(IndexSet), + Done(Entry, io::Result>>), +} + +impl Refresher { + pub(super) fn serve() -> Self { + let (tx, mut rx) = mpsc::unbounded_channel(); + let me = Self { tx }; + + let me_ = me.clone(); + tokio::spawn(async move { + let mut entries = HashMap::new(); + let mut interval = tokio::time::interval(Duration::from_secs(2)); + + loop { + tokio::select! { + Some(op) = rx.recv() => me_.handle(op, &mut entries).await, + _ = interval.tick() => { + for (_, entry) in entries.iter_mut().filter(|(u, _)| u.kind().is_virtual()) { + entry.dirty = true; + me_.spawn(entry); + } + } + } + } + }); + + me + } + + async fn handle(&self, op: Op, entries: &mut HashMap) { + match op { + Op::Sync(files) => { + entries.retain(|url, _| files.contains(&UrlCov::new(url))); + for file in files { + entries.get_or_insert_with(file.0, |file| Entry { file, ..Default::default() }); + } + } + Op::Refresh(files) => { + for file in files { + let entry = + entries.get_or_insert_with(file.0, |file| Entry { file, ..Default::default() }); + (entry.dirty, entry.report) = (true, true); + self.spawn(entry); + } + } + Op::Load(file) => { + let entry = entries.get_or_insert_with(file, |file| Entry { file, ..Default::default() }); + (entry.dirty, entry.report, entry.force) = (true, true, true); + self.spawn(entry); + } + Op::Touch(urls) => { + for url in urls { + let Some(entry) = entries.get_mut(&url) else { continue }; + entry.dirty = true; + self.spawn(entry); + } + } + Op::Done(prev, result) => { + let Some(entry) = entries.get_mut(&prev.url) else { return }; + if entry.busy != prev.busy { + return; + } + + match result { + Ok(Some(files)) => { + entry.file = prev.file.clone(); + FilesOp::Full(prev.file, files).emit(); + } + Ok(None) => {} + Err(e) if e.kind() == io::ErrorKind::NotFound => { + if let Some((p, n)) = prev.url.pair2() { + FilesOp::Deleting(p.into(), [n.into()].into()).emit(); + } + } + Err(e) if prev.report => { + FilesOp::IOErr(prev.file.url, e.into()).emit(); + } + Err(e) => tracing::debug!("Failed to refresh {:?}: {e:?}", prev.url), + } + + entry.busy = None; + } + } + } + + fn spawn(&self, entry: &mut Entry) { + if entry.busy.is_some() || !entry.dirty { + return; + } + + let (tx, mut prev) = (self.tx.clone(), entry.turn()); + tokio::spawn(async move { + let result = async { + Ok(if prev.force { + Some(Entries::from_dir_bulk(&prev.file.url).await?) + } else if let Some(file) = Entries::revalidate(&prev.file).await? { + prev.file = file; + Some(Entries::from_dir_bulk(&prev.url).await?) + } else { + None + }) + } + .await; + tx.send(Op::Done(prev, result)).ok(); + }); + } +} + +impl Refresher { + pub(super) fn sync(&self, files: IndexSet) { self.tx.send(Op::Sync(files)).ok(); } + + pub fn refresh(&self, files: I) + where + I: IntoIterator, + I::Item: Into, + { + let entries = files.into_iter().map(|file| FileCov(file.into())).collect(); + self.tx.send(Op::Refresh(entries)).ok(); + } + + pub fn load(&self, file: impl Into) { self.tx.send(Op::Load(file.into())).ok(); } + + pub fn touch(&self, urls: I) + where + I: IntoIterator, + { + self.tx.send(Op::Touch(urls.into_iter().collect())).ok(); + } +} + +// --- Entry +#[derive(Clone, Default)] +struct Entry { + file: File, + busy: Option, + dirty: bool, + report: bool, + force: bool, +} + +impl Deref for Entry { + type Target = File; + + fn deref(&self) -> &Self::Target { &self.file } +} + +impl Entry { + fn turn(&mut self) -> Self { + self.busy = Some(Instant::now()); + let me = self.clone(); + + (self.dirty, self.report, self.force) = (false, false, false); + me + } +} diff --git a/yazi-watcher/src/remote/mod.rs b/yazi-watcher/src/remote/mod.rs deleted file mode 100644 index 362fa369..00000000 --- a/yazi-watcher/src/remote/mod.rs +++ /dev/null @@ -1 +0,0 @@ -yazi_macro::mod_flat!(remote); diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index 97718f8d..620cf7e8 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -1,6 +1,3 @@ -use std::borrow::Cow; - -use percent_encoding::percent_decode; use tokio::sync::mpsc; use yazi_shared::{auth::AuthKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; @@ -8,8 +5,8 @@ use crate::{WATCHED, local::LINKED}; #[derive(Clone)] pub(crate) struct Reporter { - pub(super) local_tx: mpsc::UnboundedSender, - pub(super) remote_tx: mpsc::UnboundedSender<(UrlBuf, bool)>, + pub(super) local_tx: mpsc::UnboundedSender, + pub(super) virtual_tx: mpsc::UnboundedSender<(UrlBuf, bool)>, } impl Reporter { @@ -21,8 +18,9 @@ impl Reporter { for url in urls.into_iter().map(Into::into) { match url.as_url().kind() { AuthKind::Regular | AuthKind::Search => self.report_local(url), - AuthKind::Mount | AuthKind::Hub => {} // TODO: mounted VFS cache invalidation - AuthKind::Scope | AuthKind::Sftp => self.report_remote(url), + AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { + self.report_virtual(url) + } } } } @@ -48,20 +46,20 @@ impl Reporter { // Virtual caches let Some(dir) = watched.find_by_cache(parent.loc()) else { continue }; let Some(name) = url.name() else { continue }; - if let Ok(u) = dir.try_join(Cow::from(percent_decode(name.encoded_bytes()))) { - self.remote_tx.send((u, true)).ok(); + if let Ok(u) = dir.try_join(name) { + self.virtual_tx.send((u, true)).ok(); } - self.remote_tx.send((dir, false)).ok(); + self.virtual_tx.send((dir, false)).ok(); } } - fn report_remote(&self, url: UrlCow) { + fn report_virtual(&self, url: UrlCow) { let Some(parent) = url.parent() else { return }; if !WATCHED.read().contains_url(parent) { return; } - self.remote_tx.send((parent.to_owned(), false)).ok(); - self.remote_tx.send((url.into_owned(), false)).ok(); + self.virtual_tx.send((parent.to_owned(), false)).ok(); + self.virtual_tx.send((url.into_owned(), false)).ok(); } } diff --git a/yazi-watcher/src/virtual/mod.rs b/yazi-watcher/src/virtual/mod.rs new file mode 100644 index 00000000..813ceb9f --- /dev/null +++ b/yazi-watcher/src/virtual/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(r#virtual); diff --git a/yazi-watcher/src/remote/remote.rs b/yazi-watcher/src/virtual/virtual.rs similarity index 56% rename from yazi-watcher/src/remote/remote.rs rename to yazi-watcher/src/virtual/virtual.rs index 6a94b9d1..2ac4f11c 100644 --- a/yazi-watcher/src/remote/remote.rs +++ b/yazi-watcher/src/virtual/virtual.rs @@ -1,19 +1,19 @@ -use std::time::{Duration, SystemTime}; +use std::{io, time::{Duration, SystemTime}}; use hashbrown::HashMap; use notify::Result; use tokio::{pin, sync::mpsc::UnboundedReceiver}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; -use yazi_fs::{FilesOp, file::File}; +use yazi_fs::FilesOp; use yazi_shared::url::{UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; +use yazi_vfs::engine; -use crate::{MgrProxy, Reporter, WATCHER, Watchee}; +use crate::{MgrProxy, WATCHER, Watchee}; -pub(crate) struct Remote; +pub(crate) struct Virtual; -impl Remote { - pub(crate) fn serve(rx: UnboundedReceiver<(UrlBuf, bool)>, _reporter: Reporter) -> Self { +impl Virtual { + pub(crate) fn serve(rx: UnboundedReceiver<(UrlBuf, bool)>) -> Self { tokio::spawn(Self::changed(rx)); Self @@ -38,20 +38,29 @@ impl Remote { let mut ops = Vec::with_capacity(urls.len()); let mut ups = Vec::with_capacity(urls.len()); - for (u, upload) in urls { - let Some((parent, key)) = u.pair2() else { continue }; - let Ok(mut file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); - continue; + for (url, upload) in urls { + let Some((parent, key)) = url.pair2() else { continue }; + + let mut file = match engine::file(&url).await { + Ok(file) => file, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); + continue; + } + Err(e) => { + tracing::debug!("Failed to update {url:?}: {e:?}"); + continue; + } }; - let is_file = file.is_file(); - file.cha.ctime = Some(SystemTime::now()); + if upload && file.is_file() { + file.cha.ctime = Some(SystemTime::now()); + ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into())); + ups.push(url); + continue; + } ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into())); - if upload && is_file { - ups.push(u); - } } FilesOp::mutate(ops); diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index 2c035378..44ab39a6 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -1,66 +1,69 @@ -use std::{ops::{Deref, DerefMut}, path::Path}; +use std::{ops::Deref, path::{Path, PathBuf}}; -use hashbrown::HashSet; -use yazi_fs::{Xdg, path::PercentEncoding}; -use yazi_shared::{auth::{Auth, AuthKind}, path::{Component, PathBufDyn, PathDyn, PathLike}, url::{AsUrl, UrlBuf}}; +use hashbrown::{HashMap, HashSet}; +use indexmap::IndexSet; +use yazi_fs::FsUrl; +use yazi_shared::{path::PathDyn, url::{AsUrl, UrlBuf}}; use crate::Watchee; #[derive(Debug, Default)] -pub struct Watched(HashSet>); +pub struct Watched { + urls: HashSet>, + caches: HashMap, +} impl Deref for Watched { type Target = HashSet>; - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for Watched { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } + fn deref(&self) -> &Self::Target { &self.urls } } impl Watched { + pub(super) fn insert(&mut self, watchee: Watchee<'static>) { + let url = watchee.as_url(); + if let Some(cache) = url.cache() { + self.caches.insert(cache, url.into()); + } + self.urls.insert(watchee); + } + + pub(super) fn remove(&mut self, watchee: &Watchee<'static>) -> bool { + if !self.urls.remove(watchee) { + return false; + } + if let Some(cache) = watchee.as_url().cache() { + self.caches.remove(&cache); + } + true + } + + pub(super) fn difference<'a>( + &'a self, + other: &'a IndexSet>, + ) -> impl Iterator> { + self.urls.iter().filter(move |&watchee| !other.contains(watchee)) + } + pub(super) fn contains_url(&self, url: impl AsUrl) -> bool { let url = url.as_url(); if url.as_local().is_some() { - self.0.contains(&Watchee::Local(url.into(), false)) - || self.0.contains(&Watchee::Local(url.into(), true)) + self.urls.contains(&Watchee::Local(url.into(), false)) + || self.urls.contains(&Watchee::Local(url.into(), true)) } else { - self.0.contains(&Watchee::Remote(url.into())) + self.urls.contains(&Watchee::Virtual(url.into())) } } pub(super) fn contains_path(&self, path: &Path) -> bool { - self.0.iter().any(|watchee| watchee.as_url().as_local() == Some(path)) + self.urls.iter().any(|watchee| watchee.as_url().as_local() == Some(path)) } pub(super) fn paths(&self) -> impl Iterator { - self.0.iter().filter_map(|watchee| watchee.as_url().as_local()) + self.urls.iter().filter_map(|watchee| watchee.as_url().as_local()) } pub(super) fn find_by_cache(&self, cache: PathDyn) -> Option { - let mut it = cache.try_strip_prefix(Xdg::temp_dir()).ok()?.components(); - - // Parse authority - let cache = it.next()?.as_normal()?.to_str().ok()?; - let auth = Auth::parse_cache(cache).ok()?; - - // Parse path - let (path, abs) = - if let Ok(p) = it.path().try_strip_prefix(".%2F") { (p, false) } else { (it.path(), true) }; - let path = path.percent_decode(auth.kind).ok()?; - let path = PathBufDyn::from_components( - auth.kind, - abs.then_some(Component::RootDir).into_iter().chain(path.components()), - ) - .ok()?; - - let url = match auth.kind { - AuthKind::Mount => UrlBuf::Mount { loc: path.into_os().ok()?.into(), auth }, - AuthKind::Scope => UrlBuf::Scope { loc: path.into_unix().ok()?.into(), auth }, - AuthKind::Sftp => UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), auth }, - AuthKind::Regular | AuthKind::Search | AuthKind::Hub => return None, - }; - if self.contains_url(&url) { Some(url) } else { None } + self.caches.get(cache.as_os().ok()?).cloned() } } diff --git a/yazi-watcher/src/watchee.rs b/yazi-watcher/src/watchee.rs index 47b8c091..2fa88668 100644 --- a/yazi-watcher/src/watchee.rs +++ b/yazi-watcher/src/watchee.rs @@ -7,14 +7,14 @@ use crate::local::Local; #[derive(Debug, Eq, Hash, PartialEq)] pub enum Watchee<'a> { Local(UrlCow<'a>, bool), - Remote(UrlCow<'a>), + Virtual(UrlCow<'a>), } impl AsUrl for Watchee<'_> { fn as_url(&self) -> Url<'_> { match self { Self::Local(url, _) => url.as_url(), - Self::Remote(url) => url.as_url(), + Self::Virtual(url) => url.as_url(), } } } @@ -23,14 +23,14 @@ impl<'a> Watchee<'a> { pub(super) fn as_local(&self) -> Option<(&Path, bool)> { Some(match self { Self::Local(url, alt) => (url.as_local()?, *alt), - Self::Remote(_) => None?, + Self::Virtual(_) => None?, }) } pub(super) fn as_local_mut(&mut self) -> Option<(&Path, &mut bool)> { Some(match self { Self::Local(url, alt) => (url.as_local()?, alt), - Self::Remote(_) => None?, + Self::Virtual(_) => None?, }) } @@ -43,14 +43,14 @@ impl<'a> Watchee<'a> { let b = Local::soundless(path).await; Self::Local(url, b) } else { - Self::Remote(url) + Self::Virtual(url) } } pub(super) fn to_static(&self) -> Watchee<'static> { match self { Self::Local(url, alt) => Watchee::Local(url.to_owned().into(), *alt), - Self::Remote(url) => Watchee::Remote(url.to_owned().into()), + Self::Virtual(url) => Watchee::Virtual(url.to_owned().into()), } } } diff --git a/yazi-watcher/src/watcher.rs b/yazi-watcher/src/watcher.rs index a48d2044..18bfb877 100644 --- a/yazi-watcher/src/watcher.rs +++ b/yazi-watcher/src/watcher.rs @@ -1,45 +1,50 @@ use futures::{StreamExt, stream}; -use hashbrown::HashSet; +use indexmap::IndexSet; use tracing::error; -use yazi_fs::FsUrl; +use yazi_fs::{FsUrl, file::File}; use yazi_shared::{LastValue, url::{UrlBuf, UrlCow, UrlLike}}; -use crate::{Reporter, WATCHED, Watchee, backend::Backend}; +use crate::{Refresher, Reporter, WATCHED, Watchee, backend::Backend}; pub struct Watcher { - last: LastValue>, - reporter: Reporter, + last: LastValue>, + reporter: Reporter, + pub refresher: Refresher, } impl Watcher { pub fn serve() -> Self { let last = LastValue::default(); + let refresher = Refresher::serve(); let backend = Backend::serve(); let reporter = backend.reporter.clone(); - tokio::spawn(Self::watched(last.clone(), backend)); - Self { last, reporter } + tokio::spawn(Self::run(last.clone(), backend)); + Self { last, reporter, refresher } } - pub fn watch<'a, I>(&mut self, urls: I) + pub fn watch(&mut self, files: I) where I: IntoIterator, - I::Item: Into>, + I::Item: Into, { - let it = urls.into_iter(); - let mut urls = HashSet::with_capacity(it.size_hint().0); + let it = files.into_iter(); + let mut urls = IndexSet::with_capacity(it.size_hint().0); + let mut files = IndexSet::with_capacity(it.size_hint().0); - for url in it.map(Into::into) { - if !url.is_absolute() { + for file in it.map(Into::into) { + if !file.url.is_absolute() { continue; - } else if let Some(cache) = url.cache() { + } else if let Some(cache) = file.url.cache() { urls.insert(cache.into()); } - urls.insert(url.into_owned()); + urls.insert(file.url.clone()); + files.insert(file.into()); } self.last.set(urls); + self.refresher.sync(files); } pub fn report<'a, I>(&self, urls: I) @@ -50,7 +55,7 @@ impl Watcher { self.reporter.report(urls); } - async fn watched(last: LastValue>, mut backend: Backend) { + async fn run(last: LastValue>, mut backend: Backend) { loop { let (to_unwatch, to_watch) = Self::diff(last.get().await).await; @@ -61,8 +66,8 @@ impl Watcher { } } - async fn diff(urls: HashSet) -> (Vec>, HashSet>) { - let mut new: HashSet<_> = stream::iter(urls).then(Watchee::new).collect().await; + async fn diff(urls: IndexSet) -> (Vec>, IndexSet>) { + let mut new: IndexSet<_> = stream::iter(urls).then(Watchee::new).collect().await; let old = WATCHED.read(); let to_unwatch = old.difference(&new).map(Watchee::to_static).collect(); @@ -74,7 +79,7 @@ impl Watcher { async fn sync( mut backend: Backend, to_unwatch: Vec>, - to_watch: HashSet>, + to_watch: IndexSet>, ) -> Backend { tokio::task::spawn_blocking(move || { for watchee in to_unwatch { @@ -85,7 +90,7 @@ impl Watcher { } for mut watchee in to_watch { match backend.watch(&mut watchee) { - Ok(()) => _ = WATCHED.write().insert(watchee), + Ok(()) => WATCHED.write().insert(watchee), Err(e) if matches!(e.kind, notify::ErrorKind::PathNotFound) => {} Err(e) => error!("Watch failed: {e:?}"), }