diff --git a/.github/DISCUSSION_TEMPLATE/1-q-a.yml b/.github/DISCUSSION_TEMPLATE/1-q-a.yml new file mode 100644 index 00000000..6d818615 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/1-q-a.yml @@ -0,0 +1,54 @@ +body: + - type: dropdown + id: os + attributes: + label: What system are you running Yazi on? + options: + - Linux X11 + - Linux Wayland + - macOS + - Windows + - Windows WSL + - FreeBSD X11 + - FreeBSD Wayland + - Android + validations: + required: true + - type: input + id: terminal + attributes: + label: What terminal are you running Yazi in? + placeholder: "ex: kitty v0.32.2" + validations: + required: true + - type: textarea + id: debug + attributes: + label: "`yazi --debug` output" + description: Please run `yazi --debug` and paste the debug information here. + render: Shell + validations: + required: true + - type: textarea + id: description + attributes: + label: Describe the question + description: A clear and concise description of what the question is + placeholder: Tell us what you want to know + validations: + required: true + - type: textarea + id: other + attributes: + label: Anything else? + description: | + Add any other context about the problem here. You can attach screenshots by clicking + this area to highlight it and then drag the files in. + - type: checkboxes + id: validations + attributes: + label: Validations + description: Before submitting the post, please make sure you have completed the following + options: + - label: I have searched the existing discussions/issues + required: true diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index a4a16578..6aeaede8 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -15,6 +15,7 @@ body: - Windows WSL - FreeBSD X11 - FreeBSD Wayland + - Android validations: required: true - type: input @@ -32,15 +33,6 @@ body: render: Shell validations: required: true - - type: dropdown - id: tried_main - attributes: - label: Did you try the latest nightly build to see if the problem got fixed? - options: - - Yes, and I updated the debug information above (`yazi --debug`) to the nightly that I tried - - No, and I'll explain why below - validations: - required: true - type: textarea id: description attributes: @@ -52,7 +44,7 @@ body: - type: textarea id: reproducer attributes: - label: Minimal reproducer + label: Minimal reproducer description: A [minimal reproducer](https://stackoverflow.com/help/minimal-reproducible-example) is required, otherwise the issue might be closed without further notice. placeholder: | Please include as much information as possible that can help to reproduce and understand the issue. @@ -65,3 +57,14 @@ body: description: | Add any other context about the problem here. You can attach screenshots by clicking this area to highlight it and then drag the files in. + - type: checkboxes + id: validations + attributes: + label: Validations + description: Before submitting the issue, please make sure you have completed the following + options: + - label: I tried the [latest nightly build](https://yazi-rs.github.io/docs/installation#official-binaries), and the issue is still reproducible + required: true + - label: I updated the debug information (`yazi --debug`) input box to the nightly that I tried + required: true + - label: I can reproduce it after disabling all custom configs/plugins (`mv ~/.config/yazi ~/.config/yazi-backup`) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9976ab8a..84145ce7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,9 +3,9 @@ contact_links: - name: 📝 Documentation Improvement url: https://github.com/yazi-rs/yazi-rs.github.io about: If you'd like to help improve the documentation + - name: 💬 GitHub Discussions + url: https://github.com/sxyazi/yazi/discussions/new?category=1-q-a + about: When you have questions that are not bug reports or feature requests - name: 🌐 Discord Server / Telegram Group url: https://github.com/sxyazi/yazi#discussion about: If you'd prefer more realtime conversation with the community - - name: 💬 GitHub Discussions - url: https://github.com/sxyazi/yazi/discussions - about: When you have questions that are not bug reports or feature requests diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 1ac3a36e..478f0667 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -45,5 +45,5 @@ body: options: - label: I have searched the existing issues/discussions required: true - - label: The latest nightly build of Yazi doesn't already have this feature + - label: The [latest nightly build](https://yazi-rs.github.io/docs/installation/#official-binaries) doesn't already have this feature required: true diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0e7b1d54..f9b4bc73 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,6 +19,9 @@ jobs: - name: Setup Rust cache uses: Swatinem/rust-cache@v2 + with: + prefix-key: rust + shared-key: ubuntu-latest@debug - name: Clippy run: cargo clippy --all @@ -36,6 +39,9 @@ jobs: - name: Setup Rust cache uses: Swatinem/rust-cache@v2 + with: + prefix-key: rust + shared-key: ubuntu-latest@debug - name: Rustfmt run: cargo +nightly fmt --all -- --check diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml index f8a3d8b4..977c77cb 100644 --- a/.github/workflows/draft.yml +++ b/.github/workflows/draft.yml @@ -32,6 +32,15 @@ jobs: echo "JEMALLOC_SYS_WITH_LG_PAGE=16" >> $GITHUB_ENV echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc" >> $GITHUB_ENV + - name: Setup Rust toolchain + run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }} + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + with: + prefix-key: rust + shared-key: ${{ matrix.target }}@release + - name: Build run: ./scripts/build.sh ${{ matrix.target }} @@ -54,10 +63,13 @@ jobs: - uses: actions/checkout@v4 - name: Setup Rust toolchain - run: rustup toolchain install stable --profile minimal + run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }} - - name: Add target - run: rustup target add ${{ matrix.target }} + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + with: + prefix-key: rust + shared-key: ${{ matrix.target }}@release - name: Build env: @@ -98,6 +110,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Add musl target + run: rustup target add ${{ matrix.target }} + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + with: + prefix-key: rust + shared-key: ${{ matrix.target }}@release + - name: Build run: ./scripts/build.sh ${{ matrix.target }} @@ -117,10 +138,16 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Build snap - uses: snapcore/action-build@v1 + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + with: + prefix-key: rust + shared-key: ${{ matrix.target }}@release - name: Build + uses: snapcore/action-build@v1 + + - name: Rename snap run: mv yazi_*.snap yazi-${{ matrix.target }}.snap - name: Upload artifact diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 261c8988..9d0819fe 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -23,7 +23,7 @@ jobs: I'm going to lock this issue because it has been closed for _30 days_. ⏳ This helps our maintainers find and focus on the active issues. - If you have found a problem that seems similar to this, please open a new + If you have found a problem that seems similar to this, please file a new issue and complete the issue template so we can capture all the details necessary to investigate further. pr-inactive-days: "30" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c7296246..d4a15e8a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,9 @@ jobs: - name: Setup Rust cache uses: Swatinem/rust-cache@v2 + with: + prefix-key: rust + shared-key: ${{ matrix.os }}@debug - name: Build run: cargo build --verbose diff --git a/Cargo.lock b/Cargo.lock index e2931bf5..43d14dbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "ansi-to-tui" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c4af0bef1b514c9b6a32a773caf604c1390fa7913f4eaa23bfe76f251d6a42" +checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c" dependencies = [ "nom", "ratatui", @@ -74,9 +74,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -89,43 +89,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "arbitrary" @@ -133,12 +133,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -147,7 +141,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -187,9 +181,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" dependencies = [ "arrayvec", ] @@ -288,9 +282,9 @@ dependencies = [ [[package]] name = "built" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" [[package]] name = "bumpalo" @@ -300,9 +294,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" [[package]] name = "byteorder" @@ -318,9 +312,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cassowary" @@ -339,9 +333,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.30" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", @@ -400,9 +394,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.33" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9646e2e245bf62f45d39a0f3f36f1171ad1ea0d6967fd114bca72cb02a8fcdfb" +checksum = "07a13ab5b8cb13dbe35e68b83f6c12f9293b2f601797b71bc9f23befdb329feb" dependencies = [ "clap", ] @@ -436,7 +430,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -462,9 +456,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_str" @@ -561,7 +555,7 @@ dependencies = [ "filedescriptor", "futures-core", "libc", - "mio 1.0.2", + "mio", "parking_lot", "rustix", "signal-hook", @@ -615,7 +609,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -626,7 +620,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -656,7 +650,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -666,7 +660,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -773,9 +767,9 @@ dependencies = [ [[package]] name = "fdeflate" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" dependencies = [ "simd-adler32", ] @@ -825,9 +819,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "spin", ] @@ -918,7 +912,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1084,9 +1078,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.2" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" dependencies = [ "bytemuck", "byteorder-lite", @@ -1106,9 +1100,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" dependencies = [ "byteorder-lite", "quick-error", @@ -1122,9 +1116,9 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imgref" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" @@ -1137,6 +1131,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "inotify" version = "0.10.2" @@ -1164,7 +1164,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1184,7 +1184,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1235,15 +1235,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kamadak-exif" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" -dependencies = [ - "mutate_once", -] - [[package]] name = "kqueue" version = "1.0.8" @@ -1278,9 +1269,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libfuzzer-sys" @@ -1413,18 +1404,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.0.2" @@ -1458,9 +1437,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe026d6bd1583a9cf9080e189030ddaea7e6f5f0deb366a8e26f8a26c4135b8" +checksum = "e9eebac25c35a13285456c88ee2fde93d9aee8bcfdaf03f9d6d12be3391351ec" dependencies = [ "cc", "cfg-if", @@ -1481,15 +1460,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.79", + "syn 2.0.85", ] -[[package]] -name = "mutate_once" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" - [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -1513,10 +1486,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] -name = "notify-fork" -version = "6.1.1" +name = "notify" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f610737468f7a95610d2b320b5d5e246b3cd1c3603782680c0f454ac5825b64b" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" dependencies = [ "bitflags 2.6.0", "filetime", @@ -1525,17 +1498,17 @@ dependencies = [ "kqueue", "libc", "log", - "mio 0.8.11", - "notify-types-fork", + "mio", + "notify-types", "walkdir", "windows-sys 0.52.0", ] [[package]] -name = "notify-types-fork" +name = "notify-types" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcc01866a421a4ce1eddc764da8a95b30b82aa0d9d7f33508265c196d4863b18" +checksum = "7393c226621f817964ffb3dc5704f9509e107a8b024b489cc2c1b217378785df" dependencies = [ "instant", ] @@ -1574,7 +1547,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1750,9 +1723,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -1833,30 +1806,30 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1924,23 +1897,23 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags 2.6.0", "cassowary", "compact_str", "crossterm", + "indoc", "instability", "itertools 0.13.0", "lru", "paste", "strum", - "strum_macros", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -1980,9 +1953,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" dependencies = [ "avif-serialize", "imgref", @@ -2034,9 +2007,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2066,9 +2039,6 @@ name = "rgb" version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" -dependencies = [ - "bytemuck", -] [[package]] name = "rustc-demangle" @@ -2084,9 +2054,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags 2.6.0", "errno", @@ -2097,9 +2067,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -2124,9 +2094,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] @@ -2143,20 +2113,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -2211,7 +2181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio 1.0.2", + "mio", "signal-hook", ] @@ -2322,7 +2292,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2337,9 +2307,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -2388,22 +2358,22 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2486,14 +2456,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.2", + "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -2510,7 +2480,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2603,7 +2573,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2643,9 +2613,9 @@ dependencies = [ [[package]] name = "trash" -version = "5.1.1" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33caf2a9be1812a263a4bfce74d2de225fcde12ee7b77001361abd2b34ffdcc4" +checksum = "defe1fdd4232e407b312377885a2c5396764972bddad87baf304753374a1bfc8" dependencies = [ "chrono", "libc", @@ -2705,7 +2675,7 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools 0.13.0", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -2714,6 +2684,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "url" version = "2.5.2" @@ -2785,7 +2761,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2882,7 +2858,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "wasm-bindgen-shared", ] @@ -2904,7 +2880,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3003,7 +2979,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3014,7 +2990,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3195,14 +3171,12 @@ version = "0.3.3" dependencies = [ "ansi-to-tui", "anyhow", - "arc-swap", "base64", "color_quant", "crossterm", "futures", "image", "imagesize", - "kamadak-exif", "ratatui", "scopeguard", "tokio", @@ -3255,7 +3229,7 @@ name = "yazi-codegen" version = "0.3.3" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3263,7 +3237,6 @@ name = "yazi-config" version = "0.3.3" dependencies = [ "anyhow", - "arc-swap", "bitflags 2.6.0", "crossterm", "globset", @@ -3287,7 +3260,7 @@ dependencies = [ "dirs", "futures", "libc", - "notify-fork", + "notify", "parking_lot", "ratatui", "scopeguard", @@ -3296,7 +3269,7 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", - "unicode-width", + "unicode-width 0.2.0", "yazi-adapter", "yazi-boot", "yazi-codegen", @@ -3403,7 +3376,7 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", - "unicode-width", + "unicode-width 0.2.0", "uzers", "yazi-adapter", "yazi-boot", @@ -3495,7 +3468,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b56b104c..e5125f71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,8 @@ panic = "abort" strip = true [workspace.dependencies] -ansi-to-tui = "6.0.0" -anyhow = "1.0.89" -arc-swap = "1.7.1" +ansi-to-tui = "7.0.0" +anyhow = "1.0.91" base64 = "0.22.1" bitflags = "2.6.0" clap = { version = "4.5.20", features = [ "derive" ] } @@ -20,19 +19,19 @@ crossterm = { version = "0.28.1", features = [ "event-stream" ] } dirs = "5.0.1" futures = "0.3.31" globset = "0.4.15" -libc = "0.2.159" +libc = "0.2.161" md-5 = "0.10.6" mlua = { version = "0.9.9", features = [ "lua54", "serialize", "macros", "async" ] } parking_lot = "0.12.3" -ratatui = { version = "0.28.1", features = [ "unstable-rendered-line-info" ] } -regex = "1.11.0" +ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info" ] } +regex = "1.11.1" scopeguard = "1.2.0" -serde = { version = "1.0.210", features = [ "derive" ] } -serde_json = "1.0.128" +serde = { version = "1.0.213", features = [ "derive" ] } +serde_json = "1.0.132" shell-words = "1.1.0" -tokio = { version = "1.40.0", features = [ "full" ] } +tokio = { version = "1.41.0", features = [ "full" ] } tokio-stream = "0.1.16" tokio-util = "0.7.12" tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } -unicode-width = "0.1.14" +unicode-width = "0.2.0" uzers = "0.12.1" diff --git a/README.md b/README.md index 5ae75e2e..01478be2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ For breaking changes, see [Migrating to Yazi v0.4.0](https://github.com/sxyazi/yazi/issues/1772). +

+
Yazi logo
@@ -42,26 +44,40 @@ https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265 ## Image Preview -| Platform | Protocol | Support | -| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [kitty](https://github.com/kovidgoyal/kitty) | [Kitty unicode placeholders](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) | ✅ Built-in | -| [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol](https://github.com/sxyazi/yazi/blob/main/yazi-adapter/src/kgp_old.rs) | ✅ Built-in | -| [iTerm2](https://iterm2.com) | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| [Mintty](https://github.com/mintty/mintty) (Git Bash) | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in | -| [Ghostty](https://mitchellh.com/ghostty) | [Kitty unicode placeholders](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) | ✅ Built-in | -| [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.2702.0) | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in | -| [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in | -| [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| [Hyper](https://github.com/vercel/hyper) | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| [Rio](https://github.com/raphamorim/rio) | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| X11 / Wayland | Window system protocol | ☑️ [Überzug++](https://github.com/jstkdng/ueberzugpp) required | -| Fallback | [ASCII art (Unicode block)](https://en.wikipedia.org/wiki/ASCII_art) | ☑️ [Chafa](https://hpjansson.org/chafa/) required | +| Platform | Protocol | Support | +| --------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------- | +| [kitty](https://github.com/kovidgoyal/kitty) | [Kitty unicode placeholders][kgp] | ✅ Built-in | +| [iTerm2](https://iterm2.com) | [Inline images protocol][iip] | ✅ Built-in | +| [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol][iip] | ✅ Built-in | +| [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol][kgp-old] | ✅ Built-in | +| [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format][sixel] | ✅ Built-in | +| [Ghostty](https://mitchellh.com/ghostty) | [Kitty unicode placeholders][kgp] | ✅ Built-in | +| [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.2702.0) | [Sixel graphics format][sixel] | ✅ Built-in | +| [st with Sixel patch](https://github.com/bakkeby/st-flexipatch) | [Sixel graphics format][sixel] | ✅ Built-in | +| [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol][iip] | ✅ Built-in | +| [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol][iip] | ✅ Built-in | +| [Rio](https://github.com/raphamorim/rio) | [Inline images protocol][iip] | ❌ Rio doesn't correctly clear images (#1786) | +| [Mintty](https://github.com/mintty/mintty) (Git Bash) | [Inline images protocol][iip] | ✅ Built-in | +| [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format][sixel] | ✅ Built-in | +| [Hyper](https://github.com/vercel/hyper) | [Inline images protocol][iip] | ✅ Built-in | +| X11 / Wayland | Window system protocol | ☑️ [Überzug++][ueberzug] required | +| Fallback | [ASCII art (Unicode block)][ascii-art] | ☑️ [Chafa][chafa] required | See https://yazi-rs.github.io/docs/image-preview for details. + + +[kgp]: https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders +[kgp-old]: https://github.com/sxyazi/yazi/blob/main/yazi-adapter/src/kgp_old.rs +[iip]: https://iterm2.com/documentation-images.html +[sixel]: https://www.vt100.net/docs/vt3xx-gp/chapter14.html +[ascii-art]: https://en.wikipedia.org/wiki/ASCII_art + + + +[ueberzug]: https://github.com/jstkdng/ueberzugpp +[chafa]: https://hpjansson.org/chafa/ + ## License Yazi is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/cspell.json b/cspell.json index 8863c4db..c9ca6db9 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname"],"flagWords":[],"version":"0.2","language":"en"} \ No newline at end of file +{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod"],"version":"0.2","language":"en"} \ No newline at end of file diff --git a/flake.lock b/flake.lock index 633586e6..0ce60016 100644 --- a/flake.lock +++ b/flake.lock @@ -1,83 +1,80 @@ { - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1722415718, - "narHash": "sha256-5US0/pgxbMksF92k1+eOa8arJTJiPvsdZj9Dl+vJkM4=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "c3392ad349a5227f4a3464dce87bcc5046692fce", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1721441897, - "narHash": "sha256-gYGX9/22tPNeF7dR6bWN5rsrpU4d06GnQNNgZ6ZiXz0=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "b7996075da11a2d441cfbf4e77c2939ce51506fd", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "b7996075da11a2d441cfbf4e77c2939ce51506fd", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1729265718, + "narHash": "sha256-4HQI+6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ccc0c2126893dd20963580b6478d1a10a4512185", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": ["nixpkgs"] + }, + "locked": { + "lastModified": 1729391507, + "narHash": "sha256-as0I9xieJUHf7kiK2a9znDsVZQTFWhM1pLivII43Gi0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "784981a9feeba406de38c1c9a3decf966d853cca", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 } diff --git a/flake.nix b/flake.nix index 63132be4..c6a11b0a 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-utils.url = "github:numtide/flake-utils"; rust-overlay = { - url = "github:oxalica/rust-overlay/b7996075da11a2d441cfbf4e77c2939ce51506fd"; # FIX: pin to a specific commit until cargo-c is updated + url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; }; diff --git a/scripts/build.sh b/scripts/build.sh index a1e70be6..5c473c96 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -5,13 +5,6 @@ export ARTIFACT_NAME="yazi-$1" export YAZI_GEN_COMPLETIONS=1 export MACOSX_DEPLOYMENT_TARGET="10.11" -# Setup Rust toolchain -if [[ "$1" == *-musl ]]; then - rustup target add "$1" -else - rustup toolchain install stable --profile minimal --target "$1" -fi - # Build for the target cargo build --release --locked --target "$1" diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index b32ec22a..a6fcd1b9 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -14,20 +14,18 @@ yazi-macro = { path = "../yazi-macro", version = "0.3.3" } yazi-shared = { path = "../yazi-shared", version = "0.3.3" } # External dependencies -ansi-to-tui = { workspace = true } -anyhow = { workspace = true } -arc-swap = { workspace = true } -base64 = { workspace = true } -color_quant = "1.1.0" -crossterm = { workspace = true } -futures = { workspace = true } -image = { version = "0.25.2", default-features = false, features = [ "rayon", "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] } -imagesize = "0.13.0" -kamadak-exif = "0.5.5" -ratatui = { workspace = true } -scopeguard = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } +ansi-to-tui = { workspace = true } +anyhow = { workspace = true } +base64 = { workspace = true } +color_quant = "1.1.0" +crossterm = { workspace = true } +futures = { workspace = true } +image = { version = "0.25.4", default-features = false, features = [ "rayon", "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] } +imagesize = "0.13.0" +ratatui = { workspace = true } +scopeguard = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-adapter/src/adapter.rs b/yazi-adapter/src/adapter.rs index 841effe8..7f281234 100644 --- a/yazi-adapter/src/adapter.rs +++ b/yazi-adapter/src/adapter.rs @@ -1,4 +1,4 @@ -use std::{env, fmt::Display, path::Path, sync::Arc}; +use std::{env, fmt::Display, path::Path}; use anyhow::Result; use ratatui::layout::Rect; @@ -52,7 +52,7 @@ impl Adapter { } pub fn image_hide(self) -> Result<()> { - if let Some(area) = SHOWN.swap(None) { self.image_erase(*area) } else { Ok(()) } + if let Some(area) = SHOWN.replace(None) { self.image_erase(area) } else { Ok(()) } } pub fn image_erase(self, area: Rect) -> Result<()> { @@ -67,10 +67,10 @@ impl Adapter { } #[inline] - pub fn shown_load(self) -> Option { SHOWN.load_full().map(|r| *r) } + pub fn shown_load(self) -> Option { SHOWN.get() } #[inline] - pub(super) fn shown_store(area: Rect) { SHOWN.store(Some(Arc::new(area))); } + pub(super) fn shown_store(area: Rect) { SHOWN.set(Some(area)); } pub(super) fn start(self) { Ueberzug::start(self); } diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index a858ac82..a2ba06e4 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -1,8 +1,7 @@ -use std::{fs::File, io::BufReader, path::{Path, PathBuf}}; +use std::path::{Path, PathBuf}; use anyhow::Result; -use exif::{In, Tag}; -use image::{DynamicImage, ExtendedColorType, ImageEncoder, ImageError, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::{self, FilterType}}; +use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; use ratatui::layout::Rect; use yazi_config::{PREVIEW, TASKS}; @@ -12,38 +11,27 @@ pub struct Image; impl Image { pub async fn precache(path: &Path, cache: PathBuf) -> Result<()> { - let orientation = Self::orientation(path).await?; - - let path = path.to_owned(); - let mut img = tokio::task::spawn_blocking(move || { - Self::set_limits(image::ImageReader::open(path)?.with_guessed_format()?).decode() - }) - .await??; - - let (mut w, mut h) = (PREVIEW.max_width, PREVIEW.max_height); - if (5..=8).contains(&orientation) { - (w, h) = (h, w); - } + let (mut img, orientation, icc) = Self::decode_from(path).await?; + let (w, h) = Self::flip_size(orientation, (PREVIEW.max_width, PREVIEW.max_height)); let buf = tokio::task::spawn_blocking(move || { if img.width() > w || img.height() > h { img = img.resize(w, h, Self::filter()); } + if orientation != Orientation::NoTransforms { + img.apply_orientation(orientation); + } let mut buf = Vec::new(); - img = Self::rotate(img, orientation); - if img.color().has_alpha() { let rgba = img.into_rgba8(); - PngEncoder::new(&mut buf).write_image( - &rgba, - rgba.width(), - rgba.height(), - ExtendedColorType::Rgba8, - )?; + let mut encoder = PngEncoder::new(&mut buf); + icc.map(|b| encoder.set_icc_profile(b)); + encoder.write_image(&rgba, rgba.width(), rgba.height(), ExtendedColorType::Rgba8)?; } else { - JpegEncoder::new_with_quality(&mut buf, PREVIEW.image_quality) - .encode_image(&img.into_rgb8())?; + let mut encoder = JpegEncoder::new_with_quality(&mut buf, PREVIEW.image_quality); + icc.map(|b| encoder.set_icc_profile(b)); + encoder.encode_image(&img.into_rgb8())?; } Ok::<_, ImageError>(buf) @@ -54,31 +42,26 @@ impl Image { } pub(super) async fn downscale(path: &Path, rect: Rect) -> Result { - let orientation = Self::orientation(path).await?; - - let path = path.to_owned(); - let mut img = tokio::task::spawn_blocking(move || { - Self::set_limits(image::ImageReader::open(path)?.with_guessed_format()?).decode() - }) - .await??; - - let (mut w, mut h) = Self::max_pixel(rect); - if (5..=8).contains(&orientation) { - (w, h) = (h, w); - } + let (mut img, orientation, _) = Self::decode_from(path).await?; + let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect)); // Fast path. - if img.width() <= w && img.height() <= h && orientation <= 1 { + if img.width() <= w && img.height() <= h && orientation == Orientation::NoTransforms { return Ok(img); } - tokio::task::spawn_blocking(move || { + let img = tokio::task::spawn_blocking(move || { if img.width() > w || img.height() > h { img = img.resize(w, h, Self::filter()) } - Ok(Self::rotate(img, orientation)) + if orientation != Orientation::NoTransforms { + img.apply_orientation(orientation); + } + img }) - .await? + .await?; + + Ok(img) } pub(super) fn max_pixel(rect: Rect) -> (u32, u32) { @@ -113,53 +96,7 @@ impl Image { } } - async fn orientation(path: &Path) -> Result { - // We don't want to read the orientation of the cached image that has been - // rotated in the `Self::precache()` step. - if path.parent() == Some(&PREVIEW.cache_dir) { - return Ok(0); - } - - let path = path.to_owned(); - tokio::task::spawn_blocking(move || { - let file = std::fs::File::open(path)?; - - let mut reader = std::io::BufReader::new(&file); - let Ok(exif) = exif::Reader::new().read_from_container(&mut reader) else { - return Ok(0); - }; - - Ok(match exif.get_field(Tag::Orientation, In::PRIMARY) { - Some(orientation) => match orientation.value.get_uint(0) { - Some(v @ 1..=8) => v as u8, - _ => 1, - }, - None => 1, - }) - }) - .await? - } - - // https://magnushoff.com/articles/jpeg-orientation/ - fn rotate(mut img: DynamicImage, orientation: u8) -> DynamicImage { - let alpha = img.color().has_alpha(); - img = match orientation { - 2 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)), - 3 => DynamicImage::ImageRgba8(imageops::rotate180(&img)), - 4 => DynamicImage::ImageRgba8(imageops::flip_vertical(&img)), - 5 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&imageops::rotate90(&img))), - 6 => DynamicImage::ImageRgba8(imageops::rotate90(&img)), - 7 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&imageops::rotate270(&img))), - 8 => DynamicImage::ImageRgba8(imageops::rotate270(&img)), - _ => img, - }; - if !alpha { - img = DynamicImage::ImageRgb8(img.into_rgb8()); - } - img - } - - fn set_limits(mut r: image::ImageReader>) -> image::ImageReader> { + async fn decode_from(path: &Path) -> ImageResult<(DynamicImage, Orientation, Option>)> { let mut limits = Limits::no_limits(); if TASKS.image_alloc > 0 { limits.max_alloc = Some(TASKS.image_alloc as u64); @@ -170,7 +107,27 @@ impl Image { if TASKS.image_bound[1] > 0 { limits.max_image_height = Some(TASKS.image_bound[1] as u32); } - r.limits(limits); - r + + let path = path.to_owned(); + tokio::task::spawn_blocking(move || { + let mut reader = ImageReader::open(path)?; + reader.limits(limits); + + let mut decoder = reader.with_guessed_format()?.into_decoder()?; + let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms); + let icc = decoder.icc_profile().unwrap_or_default(); + + Ok((DynamicImage::from_decoder(decoder)?, orientation, icc)) + }) + .await + .map_err(|e| ImageError::IoError(e.into()))? + } + + fn flip_size(orientation: Orientation, (w, h): (u32, u32)) -> (u32, u32) { + use image::metadata::Orientation::{Rotate90, Rotate90FlipH, Rotate270, Rotate270FlipH}; + match orientation { + Rotate90 | Rotate270 | Rotate90FlipH | Rotate270FlipH => (h, w), + _ => (w, h), + } } } diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index 8999cec5..a9879185 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -4,7 +4,7 @@ yazi_macro::mod_flat!( adapter chafa dimension emulator iip image kgp kgp_old mux sixel ueberzug ); -use yazi_shared::{RoCell, env_exists, in_wsl}; +use yazi_shared::{RoCell, SyncCell, env_exists, in_wsl}; pub static ADAPTOR: RoCell = RoCell::new(); // Tmux support @@ -17,7 +17,7 @@ static CLOSE: RoCell<&'static str> = RoCell::new(); pub static WSL: RoCell = RoCell::new(); // Image state -static SHOWN: RoCell> = RoCell::new(); +static SHOWN: SyncCell> = SyncCell::new(None); pub fn init() { // Tmux support @@ -38,9 +38,6 @@ pub fn init() { // WSL support WSL.init(in_wsl()); - // Image state - SHOWN.with(<_>::default); - ADAPTOR.init(Adapter::matches()); ADAPTOR.start(); } diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index ff3ee1e3..837b2ef4 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -21,7 +21,7 @@ serde = { workspace = true } [build-dependencies] clap = { workspace = true } -clap_complete = "4.5.33" +clap_complete = "4.5.35" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.5.4" vergen-gitcl = { version = "1.0.1", features = [ "build" ] } diff --git a/yazi-boot/build.rs b/yazi-boot/build.rs index a4ab04fd..c84a976c 100644 --- a/yazi-boot/build.rs +++ b/yazi-boot/build.rs @@ -22,11 +22,10 @@ fn main() -> Result<(), Box> { let out = "completions"; std::fs::create_dir_all(out)?; - generate_to(Shell::Bash, cmd, bin, out)?; - generate_to(Shell::Fish, cmd, bin, out)?; - generate_to(Shell::Zsh, cmd, bin, out)?; - generate_to(Shell::Elvish, cmd, bin, out)?; - generate_to(Shell::PowerShell, cmd, bin, out)?; + for sh in [Shell::Bash, Shell::Fish, Shell::Zsh, Shell::Elvish, Shell::PowerShell] { + generate_to(sh, cmd, bin, out)?; + } + generate_to(clap_complete_nushell::Nushell, cmd, bin, out)?; generate_to(clap_complete_fig::Fig, cmd, bin, out)?; diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 1258094f..302d567b 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -27,7 +27,7 @@ toml_edit = "0.22.22" # External build dependencies anyhow = { workspace = true } clap = { workspace = true } -clap_complete = "4.5.33" +clap_complete = "4.5.35" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.5.4" serde_json = { workspace = true } diff --git a/yazi-cli/build.rs b/yazi-cli/build.rs index f9f33a67..0ae2f659 100644 --- a/yazi-cli/build.rs +++ b/yazi-cli/build.rs @@ -22,11 +22,10 @@ fn main() -> Result<(), Box> { let out = "completions"; std::fs::create_dir_all(out)?; - generate_to(Shell::Bash, cmd, bin, out)?; - generate_to(Shell::Fish, cmd, bin, out)?; - generate_to(Shell::Zsh, cmd, bin, out)?; - generate_to(Shell::Elvish, cmd, bin, out)?; - generate_to(Shell::PowerShell, cmd, bin, out)?; + for sh in [Shell::Bash, Shell::Fish, Shell::Zsh, Shell::Elvish, Shell::PowerShell] { + generate_to(sh, cmd, bin, out)?; + } + generate_to(clap_complete_nushell::Nushell, cmd, bin, out)?; generate_to(clap_complete_fig::Fig, cmd, bin, out)?; diff --git a/yazi-cli/src/main.rs b/yazi-cli/src/main.rs index 4adade6f..dd8807df 100644 --- a/yazi-cli/src/main.rs +++ b/yazi-cli/src/main.rs @@ -47,7 +47,6 @@ async fn main() -> anyhow::Result<()> { Command::Pack(cmd) => { package::init()?; - package::Package::migrate().await?; if cmd.install { package::Package::install_from_config("plugin", false).await?; package::Package::install_from_config("flavor", false).await?; @@ -57,15 +56,7 @@ async fn main() -> anyhow::Result<()> { } else if cmd.upgrade { package::Package::install_from_config("plugin", true).await?; package::Package::install_from_config("flavor", true).await?; - } else if let Some(mut repo) = cmd.add { - // TODO: remove this in the future - if repo.contains("#") { - repo = repo.replace("#", ":"); - println!( - "WARNING: `#` has been deprecated in Yazi 0.3.1, please use `:` instead. See https://github.com/sxyazi/yazi/issues/1471 for more information." - ); - } - + } else if let Some(repo) = cmd.add { package::Package::add_to_config(&repo).await?; } } diff --git a/yazi-cli/src/package/parser.rs b/yazi-cli/src/package/parser.rs index c58020e8..49a22a40 100644 --- a/yazi-cli/src/package/parser.rs +++ b/yazi-cli/src/package/parser.rs @@ -6,33 +6,6 @@ use yazi_shared::Xdg; use super::Package; impl Package { - // TODO: remove this in the future - pub(crate) async fn migrate() -> Result<()> { - let path = Xdg::config_dir().join("package.toml"); - let mut doc = Self::ensure_config(&fs::read_to_string(&path).await.unwrap_or_default())?; - - fn impl_(deps: &mut Array) -> Result<()> { - for dep in deps.iter_mut() { - let dep = dep.as_inline_table_mut().context("Dependency must be an inline table")?; - let use_ = dep.get("use").and_then(|d| d.as_str()).context("Missing `use` field")?; - if use_.contains("#") { - dep["use"] = use_.replace("#", ":").into(); - } - if let Some(commit) = dep.get("commit").map(ToOwned::to_owned) { - dep.remove("commit"); - dep.insert("rev", commit); - } - } - Ok(()) - } - - impl_(doc["plugin"]["deps"].as_array_mut().unwrap())?; - impl_(doc["flavor"]["deps"].as_array_mut().unwrap())?; - - fs::write(path, doc.to_string()).await?; - Ok(()) - } - pub(crate) async fn add_to_config(use_: &str) -> Result<()> { let mut package = Self::new(use_, None); let Some(name) = package.name() else { bail!("Invalid package `use`") }; diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index 537b5e9c..c22fef5f 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -13,5 +13,5 @@ proc-macro = true [dependencies] # External dependencies -syn = "2.0.79" +syn = "2.0.85" quote = "1.0.37" diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index 32fc3a9d..ed6efed7 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -14,7 +14,6 @@ yazi-shared = { path = "../yazi-shared", version = "0.3.3" } # External dependencies anyhow = { workspace = true } -arc-swap = { workspace = true } bitflags = { workspace = true } crossterm = { workspace = true } globset = { workspace = true } diff --git a/yazi-config/preset/theme.toml b/yazi-config/preset/theme.toml index a5001a2d..f6c1f347 100644 --- a/yazi-config/preset/theme.toml +++ b/yazi-config/preset/theme.toml @@ -98,6 +98,20 @@ selected = { reversed = true } # : }}} +# : Confirm {{{ + +[confirm] +border = { fg = "blue" } +title = { fg = "blue" } +content = {} +list = {} +btn_yes = { reversed = true } +btn_no = {} +btn_labels = [ " [Y]es ", " (N)o " ] + +# : }}} + + # : Completion {{{ [completion] diff --git a/yazi-config/src/keymap/keymap.rs b/yazi-config/src/keymap/keymap.rs index 2cf291ad..7948e968 100644 --- a/yazi-config/src/keymap/keymap.rs +++ b/yazi-config/src/keymap/keymap.rs @@ -1,5 +1,6 @@ use std::{collections::HashSet, str::FromStr}; +use anyhow::Context; use indexmap::IndexSet; use serde::{Deserialize, Deserializer}; use yazi_shared::Layer; @@ -20,7 +21,7 @@ pub struct Keymap { impl Keymap { #[inline] - pub fn get(&self, layer: Layer) -> &Vec { + pub fn get(&self, layer: Layer) -> &[Chord] { match layer { Layer::App => unreachable!(), Layer::Manager => &self.manager, @@ -36,9 +37,11 @@ impl Keymap { } impl FromStr for Keymap { - type Err = toml::de::Error; + type Err = anyhow::Error; - fn from_str(s: &str) -> Result { toml::from_str(s) } + fn from_str(s: &str) -> Result { + toml::from_str(s).context("Failed to parse your keymap.toml") + } } impl<'de> Deserialize<'de> for Keymap { diff --git a/yazi-config/src/layout.rs b/yazi-config/src/layout.rs index 80b3d986..071c25b5 100644 --- a/yazi-config/src/layout.rs +++ b/yazi-config/src/layout.rs @@ -1,7 +1,14 @@ use ratatui::layout::Rect; -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Default, PartialEq, Eq)] pub struct Layout { - pub current: Rect, - pub preview: Rect, + pub current: Rect, + pub preview: Rect, + pub progress: Rect, +} + +impl Layout { + pub const fn default() -> Self { + Self { current: Rect::ZERO, preview: Rect::ZERO, progress: Rect::ZERO } + } } diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 7c09c86d..bd8a2d1b 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -6,9 +6,7 @@ yazi_macro::mod_flat!(layout pattern preset priority); use std::str::FromStr; -use yazi_shared::{RoCell, Xdg}; - -pub static LAYOUT: RoCell> = RoCell::new(); +use yazi_shared::{RoCell, SyncCell, Xdg}; pub static KEYMAP: RoCell = RoCell::new(); pub static LOG: RoCell = RoCell::new(); @@ -23,26 +21,28 @@ pub static CONFIRM: RoCell = RoCell::new(); pub static PICK: RoCell = RoCell::new(); pub static WHICH: RoCell = RoCell::new(); +pub static LAYOUT: SyncCell = SyncCell::new(Layout::default()); + pub fn init() -> anyhow::Result<()> { - let config_dir = Xdg::config_dir(); - let yazi_toml = &Preset::yazi(&config_dir)?; - let keymap_toml = &Preset::keymap(&config_dir)?; - let theme_toml = &Preset::theme(&config_dir)?; + if let Err(e) = try_init(true) { + eprintln!("{e}"); + if let Some(src) = e.source() { + eprintln!("\nCaused by:\n{src}"); + } - LAYOUT.with(<_>::default); + use crossterm::style::{Attribute, Print, SetAttributes}; + crossterm::execute!( + std::io::stderr(), + SetAttributes(Attribute::Reverse.into()), + SetAttributes(Attribute::Bold.into()), + Print("Press to continue with preset settings..."), + SetAttributes(Attribute::Reset.into()), + Print("\n"), + )?; - KEYMAP.init(<_>::from_str(keymap_toml)?); - LOG.init(<_>::from_str(yazi_toml)?); - MANAGER.init(<_>::from_str(yazi_toml)?); - OPEN.init(<_>::from_str(yazi_toml)?); - PLUGIN.init(<_>::from_str(yazi_toml)?); - PREVIEW.init(<_>::from_str(yazi_toml)?); - TASKS.init(<_>::from_str(yazi_toml)?); - THEME.init(<_>::from_str(theme_toml)?); - INPUT.init(<_>::from_str(yazi_toml)?); - CONFIRM.init(<_>::from_str(yazi_toml)?); - PICK.init(<_>::from_str(yazi_toml)?); - WHICH.init(<_>::from_str(yazi_toml)?); + std::io::stdin().read_line(&mut String::new())?; + try_init(false)?; + } // TODO: Remove in v0.3.2 for c in &KEYMAP.manager { @@ -78,3 +78,41 @@ Please change `create_title = "Create:"` to `create_title = ["Create:", "Create Ok(()) } + +fn try_init(merge: bool) -> anyhow::Result<()> { + let (yazi_toml, keymap_toml, theme_toml) = if merge { + let p = Xdg::config_dir(); + (Preset::yazi(&p)?, Preset::keymap(&p)?, Preset::theme(&p)?) + } else { + use yazi_macro::config_preset as preset; + (preset!("yazi"), preset!("keymap"), preset!("theme")) + }; + + let keymap = <_>::from_str(&keymap_toml)?; + let log = <_>::from_str(&yazi_toml)?; + let manager = <_>::from_str(&yazi_toml)?; + let open = <_>::from_str(&yazi_toml)?; + let plugin = <_>::from_str(&yazi_toml)?; + let preview = <_>::from_str(&yazi_toml)?; + let tasks = <_>::from_str(&yazi_toml)?; + let theme = <_>::from_str(&theme_toml)?; + let input = <_>::from_str(&yazi_toml)?; + let confirm = <_>::from_str(&yazi_toml)?; + let pick = <_>::from_str(&yazi_toml)?; + let which = <_>::from_str(&yazi_toml)?; + + KEYMAP.init(keymap); + LOG.init(log); + MANAGER.init(manager); + OPEN.init(open); + PLUGIN.init(plugin); + PREVIEW.init(preview); + TASKS.init(tasks); + THEME.init(theme); + INPUT.init(input); + CONFIRM.init(confirm); + PICK.init(pick); + WHICH.init(which); + + Ok(()) +} diff --git a/yazi-config/src/log/log.rs b/yazi-config/src/log/log.rs index c92cf604..d5defe0c 100644 --- a/yazi-config/src/log/log.rs +++ b/yazi-config/src/log/log.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use anyhow::Context; use serde::{Deserialize, Deserializer}; #[derive(Debug)] @@ -8,9 +9,11 @@ pub struct Log { } impl FromStr for Log { - type Err = toml::de::Error; + type Err = anyhow::Error; - fn from_str(s: &str) -> Result { toml::from_str(s) } + fn from_str(s: &str) -> Result { + toml::from_str(s).context("Failed to parse the [log] section in your yazi.toml") + } } impl<'de> Deserialize<'de> for Log { diff --git a/yazi-config/src/manager/manager.rs b/yazi-config/src/manager/manager.rs index 8e48bbde..78efa5c3 100644 --- a/yazi-config/src/manager/manager.rs +++ b/yazi-config/src/manager/manager.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use anyhow::Context; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -35,9 +36,10 @@ impl FromStr for Manager { manager: Manager, } - let manager = toml::from_str::(s)?.manager; - manager.validate()?; + let outer = toml::from_str::(s) + .context("Failed to parse the [manager] section in your yazi.toml")?; + outer.manager.validate()?; - Ok(manager) + Ok(outer.manager) } } diff --git a/yazi-config/src/open/open.rs b/yazi-config/src/open/open.rs index 60361818..a9e2d0e6 100644 --- a/yazi-config/src/open/open.rs +++ b/yazi-config/src/open/open.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, path::Path, str::FromStr}; +use anyhow::Context; use indexmap::IndexSet; use serde::{Deserialize, Deserializer}; use yazi_shared::MIME_DIR; @@ -55,9 +56,11 @@ impl Open { } impl FromStr for Open { - type Err = toml::de::Error; + type Err = anyhow::Error; - fn from_str(s: &str) -> Result { toml::from_str(s) } + fn from_str(s: &str) -> Result { + toml::from_str(s).context("Failed to parse the [open] or [opener] section in your yazi.toml") + } } impl<'de> Deserialize<'de> for Open { diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 3ca5823a..37be5c6a 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{path::Path, str::FromStr}; use globset::GlobBuilder; use serde::Deserialize; @@ -9,6 +9,8 @@ pub struct Pattern { inner: globset::GlobMatcher, is_dir: bool, is_star: bool, + #[cfg(windows)] + sep_lit: bool, } impl Pattern { @@ -19,7 +21,20 @@ impl Pattern { #[inline] pub fn match_path(&self, path: impl AsRef, is_dir: bool) -> bool { - is_dir == self.is_dir && (self.is_star || self.inner.is_match(path)) + if is_dir != self.is_dir { + return false; + } else if self.is_star { + return true; + } + + #[cfg(windows)] + let path = if self.sep_lit { + yazi_shared::fs::backslash_to_slash(path.as_ref()) + } else { + std::borrow::Cow::Borrowed(path.as_ref()) + }; + + self.inner.is_match(path) } #[inline] @@ -29,27 +44,115 @@ impl Pattern { pub fn any_dir(&self) -> bool { self.is_star && self.is_dir } } -impl TryFrom<&str> for Pattern { - type Error = anyhow::Error; +impl FromStr for Pattern { + type Err = globset::Error; - fn try_from(s: &str) -> Result { + fn from_str(s: &str) -> Result { let a = s.trim_start_matches("\\s"); let b = a.trim_end_matches('/'); + let sep_lit = b.contains('/'); let inner = GlobBuilder::new(b) .case_insensitive(a.len() == s.len()) - .literal_separator(false) + .literal_separator(sep_lit) .backslash_escape(false) .empty_alternates(true) .build()? .compile_matcher(); - Ok(Self { inner, is_dir: b.len() < a.len(), is_star: b == "*" }) + Ok(Self { + inner, + is_dir: b.len() < a.len(), + is_star: b == "*", + #[cfg(windows)] + sep_lit, + }) } } impl TryFrom for Pattern { - type Error = anyhow::Error; + type Error = globset::Error; - fn try_from(s: String) -> Result { Self::try_from(s.as_str()) } + fn try_from(s: String) -> Result { Self::from_str(s.as_str()) } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn matches(glob: &str, path: &str) -> bool { + Pattern::from_str(glob).unwrap().match_path(path, false) + } + + #[cfg(unix)] + #[test] + fn test_unix() { + // Wildcard + assert!(matches("*", "/foo")); + assert!(matches("*", "/foo/bar")); + assert!(matches("**", "foo")); + assert!(matches("**", "/foo")); + assert!(matches("**", "/foo/bar")); + + // Filename + assert!(matches("*.md", "foo.md")); + assert!(matches("*.md", "/foo.md")); + assert!(matches("*.md", "/foo/bar.md")); + + // 1-star + assert!(matches("/*", "/foo")); + assert!(matches("/*/*.md", "/foo/bar.md")); + + // 2-star + assert!(matches("/**", "/foo")); + assert!(matches("/**", "/foo/bar")); + assert!(matches("**/**", "/foo")); + assert!(matches("**/**", "/foo/bar")); + assert!(matches("/**/*", "/foo")); + assert!(matches("/**/*", "/foo/bar")); + + // Failures + assert!(!matches("/*/*", "/foo")); + assert!(!matches("/*/*.md", "/foo.md")); + assert!(!matches("/*", "/foo/bar")); + assert!(!matches("/*.md", "/foo/bar.md")); + } + + #[cfg(windows)] + #[test] + fn test_windows() { + // Wildcard + assert!(matches("*", r#"C:\foo"#)); + assert!(matches("*", r#"C:\foo\bar"#)); + assert!(matches("**", r#"foo"#)); + assert!(matches("**", r#"C:\foo"#)); + assert!(matches("**", r#"C:\foo\bar"#)); + + // Filename + assert!(matches("*.md", r#"foo.md"#)); + assert!(matches("*.md", r#"C:\foo.md"#)); + assert!(matches("*.md", r#"C:\foo\bar.md"#)); + + // 1-star + assert!(matches(r#"C:/*"#, r#"C:\foo"#)); + assert!(matches(r#"C:/*/*.md"#, r#"C:\foo\bar.md"#)); + + // 2-star + assert!(matches(r#"C:/**"#, r#"C:\foo"#)); + assert!(matches(r#"C:/**"#, r#"C:\foo\bar"#)); + assert!(matches(r#"**/**"#, r#"C:\foo"#)); + assert!(matches(r#"**/**"#, r#"C:\foo\bar"#)); + assert!(matches(r#"C:/**/*"#, r#"C:\foo"#)); + assert!(matches(r#"C:/**/*"#, r#"C:\foo\bar"#)); + + // Drive letter + assert!(matches(r#"*:/*"#, r#"C:\foo"#)); + assert!(matches(r#"*:/**/*.md"#, r#"C:\foo\bar.md"#)); + + // Failures + assert!(!matches(r#"C:/*/*"#, r#"C:\foo"#)); + assert!(!matches(r#"C:/*/*.md"#, r#"C:\foo.md"#)); + assert!(!matches(r#"C:/*"#, r#"C:\foo\bar"#)); + assert!(!matches(r#"C:/*.md"#, r#"C:\foo\bar.md"#)); + } } diff --git a/yazi-config/src/plugin/plugin.rs b/yazi-config/src/plugin/plugin.rs index ba274011..4cd84eee 100644 --- a/yazi-config/src/plugin/plugin.rs +++ b/yazi-config/src/plugin/plugin.rs @@ -1,11 +1,11 @@ use std::{collections::HashSet, path::Path, str::FromStr}; -use serde::Deserialize; +use anyhow::Context; +use serde::{Deserialize, Deserializer}; use super::{Fetcher, Preloader, Previewer}; use crate::{Preset, plugin::MAX_PREWORKERS}; -#[derive(Deserialize)] pub struct Plugin { pub fetchers: Vec, pub preloaders: Vec, @@ -55,9 +55,18 @@ impl Plugin { } impl FromStr for Plugin { - type Err = toml::de::Error; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { + toml::from_str(s).context("Failed to parse the [plugin] section in your yazi.toml") + } +} + +impl<'de> Deserialize<'de> for Plugin { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { #[derive(Deserialize)] struct Outer { plugin: Shadow, @@ -84,7 +93,7 @@ impl FromStr for Plugin { append_previewers: Vec, } - let mut shadow = toml::from_str::(s)?.plugin; + let mut shadow = Outer::deserialize(deserializer)?.plugin; if shadow.append_previewers.iter().any(|r| r.any_file()) { shadow.previewers.retain(|r| !r.any_file()); } diff --git a/yazi-config/src/popup/confirm.rs b/yazi-config/src/popup/confirm.rs index b55686f7..e4cca76d 100644 --- a/yazi-config/src/popup/confirm.rs +++ b/yazi-config/src/popup/confirm.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use anyhow::Context; use serde::Deserialize; use super::{Offset, Origin}; @@ -30,7 +31,7 @@ pub struct Confirm { } impl FromStr for Confirm { - type Err = toml::de::Error; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { #[derive(Deserialize)] @@ -38,7 +39,10 @@ impl FromStr for Confirm { confirm: Confirm, } - Ok(toml::from_str::(s)?.confirm) + let outer = toml::from_str::(s) + .context("Failed to parse the [confirm] section in your yazi.toml")?; + + Ok(outer.confirm) } } diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index f0f35166..1cba01f0 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use anyhow::Context; use serde::Deserialize; use super::{Offset, Origin}; @@ -49,7 +50,7 @@ impl Input { } impl FromStr for Input { - type Err = toml::de::Error; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { #[derive(Deserialize)] @@ -57,7 +58,10 @@ impl FromStr for Input { input: Input, } - Ok(toml::from_str::(s)?.input) + let outer = toml::from_str::(s) + .context("Failed to parse the [input] section in your yazi.toml")?; + + Ok(outer.input) } } diff --git a/yazi-config/src/popup/pick.rs b/yazi-config/src/popup/pick.rs index ea2218c3..786fb8a8 100644 --- a/yazi-config/src/popup/pick.rs +++ b/yazi-config/src/popup/pick.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use anyhow::Context; use serde::Deserialize; use super::{Offset, Origin}; @@ -17,7 +18,7 @@ impl Pick { } impl FromStr for Pick { - type Err = toml::de::Error; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { #[derive(Deserialize)] @@ -25,6 +26,9 @@ impl FromStr for Pick { pick: Pick, } - Ok(toml::from_str::(s)?.pick) + let outer = + toml::from_str::(s).context("Failed to parse the [pick] section in your yazi.toml")?; + + Ok(outer.pick) } } diff --git a/yazi-config/src/preset.rs b/yazi-config/src/preset.rs index 02cf8046..d52a3bac 100644 --- a/yazi-config/src/preset.rs +++ b/yazi-config/src/preset.rs @@ -9,15 +9,15 @@ use crate::theme::Flavor; pub(crate) struct Preset; impl Preset { - pub(crate) fn yazi(p: &Path) -> Result> { + pub(crate) fn yazi(p: &Path) -> Result> { Self::merge_path(p.join("yazi.toml"), preset!("yazi")) } - pub(crate) fn keymap(p: &Path) -> Result> { + pub(crate) fn keymap(p: &Path) -> Result> { Self::merge_path(p.join("keymap.toml"), preset!("keymap")) } - pub(crate) fn theme(p: &Path) -> Result> { + pub(crate) fn theme(p: &Path) -> Result> { let Ok(user) = std::fs::read_to_string(p.join("theme.toml")) else { return Ok(preset!("theme")); }; diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index 8e9f3f8a..f6a48264 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, path::PathBuf, str::FromStr, time::{SystemTime, UNIX_EPOCH}}; use anyhow::Context; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use validator::Validate; use yazi_shared::fs::expand_path; @@ -49,6 +49,20 @@ impl FromStr for Preview { type Err = anyhow::Error; fn from_str(s: &str) -> Result { + let preview: Self = + toml::from_str(s).context("Failed to parse the [preview] section in your yazi.toml")?; + + std::fs::create_dir_all(&preview.cache_dir).context("Failed to create cache directory")?; + + Ok(preview) + } +} + +impl<'de> Deserialize<'de> for Preview { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { #[derive(Deserialize)] struct Outer { preview: Shadow, @@ -74,27 +88,26 @@ impl FromStr for Preview { ueberzug_offset: (f32, f32, f32, f32), } - let preview = toml::from_str::(s)?.preview; - preview.validate()?; - - let cache_dir = - preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path); - std::fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?; + let preview = Outer::deserialize(deserializer)?.preview; + preview.validate().map_err(serde::de::Error::custom)?; Ok(Preview { - wrap: preview.wrap, - tab_size: preview.tab_size, - max_width: preview.max_width, + wrap: preview.wrap, + tab_size: preview.tab_size, + max_width: preview.max_width, max_height: preview.max_height, - cache_dir, + cache_dir: preview + .cache_dir + .filter(|p| !p.is_empty()) + .map_or_else(Xdg::cache_dir, expand_path), - image_delay: preview.image_delay, - image_filter: preview.image_filter, - image_quality: preview.image_quality, + image_delay: preview.image_delay, + image_filter: preview.image_filter, + image_quality: preview.image_quality, sixel_fraction: preview.sixel_fraction, - ueberzug_scale: preview.ueberzug_scale, + ueberzug_scale: preview.ueberzug_scale, ueberzug_offset: preview.ueberzug_offset, }) } diff --git a/yazi-config/src/tasks/tasks.rs b/yazi-config/src/tasks/tasks.rs index e922d359..b401e075 100644 --- a/yazi-config/src/tasks/tasks.rs +++ b/yazi-config/src/tasks/tasks.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use anyhow::Context; use serde::Deserialize; use validator::Validate; @@ -27,9 +28,10 @@ impl FromStr for Tasks { tasks: Tasks, } - let tasks = toml::from_str::(s)?.tasks; - tasks.validate()?; + let outer = toml::from_str::(s) + .context("Failed to parse the [tasks] section in your yazi.toml")?; + outer.tasks.validate()?; - Ok(tasks) + Ok(outer.tasks) } } diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index 6f872bf1..7712b102 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -1,5 +1,6 @@ use std::{path::PathBuf, str::FromStr}; +use anyhow::Context; use serde::{Deserialize, Serialize}; use validator::Validate; use yazi_shared::{Xdg, fs::expand_path, theme::Style}; @@ -12,6 +13,7 @@ pub struct Theme { pub manager: Manager, status: Status, pub input: Input, + pub confirm: Confirm, pub pick: Pick, pub completion: Completion, pub tasks: Tasks, @@ -30,7 +32,7 @@ impl FromStr for Theme { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let mut theme: Self = toml::from_str(s)?; + let mut theme: Self = toml::from_str(s).context("Failed to parse your yazi.toml")?; theme.manager.validate()?; theme.which.validate()?; @@ -114,6 +116,17 @@ pub struct Input { pub selected: Style, } +#[derive(Deserialize, Serialize)] +pub struct Confirm { + pub border: Style, + pub title: Style, + pub content: Style, + pub list: Style, + pub btn_yes: Style, + pub btn_no: Style, + pub btn_labels: [String; 2], +} + #[derive(Deserialize, Serialize)] pub struct Pick { pub border: Style, diff --git a/yazi-config/src/which/which.rs b/yazi-config/src/which/which.rs index 205a2bc4..8a488d3f 100644 --- a/yazi-config/src/which/which.rs +++ b/yazi-config/src/which/which.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use anyhow::Context; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -23,6 +24,9 @@ impl FromStr for Which { which: Which, } - Ok(toml::from_str::(s)?.which) + let outer = toml::from_str::(s) + .context("Failed to parse the [which] section in your yazi.toml")?; + + Ok(outer.which) } } diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index f4744c63..fc7bb804 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -27,7 +27,7 @@ bitflags = { workspace = true } crossterm = { workspace = true } dirs = { workspace = true } futures = { workspace = true } -notify = { package = "notify-fork", version = "6.1.1", default-features = false, features = [ "macos_fsevent" ] } +notify = { version = "7.0.0", default-features = false, features = [ "macos_fsevent" ] } parking_lot = { workspace = true } ratatui = { workspace = true } scopeguard = { workspace = true } diff --git a/yazi-core/src/manager/commands/bulk_rename.rs b/yazi-core/src/manager/commands/bulk_rename.rs index 88bf79d2..e8e4adf1 100644 --- a/yazi-core/src/manager/commands/bulk_rename.rs +++ b/yazi-core/src/manager/commands/bulk_rename.rs @@ -53,7 +53,8 @@ impl Manager { return Ok(()); } - let todo: Vec<_> = old.into_iter().zip(new).filter(|(o, n)| o != n).collect(); + let (old, new) = old.into_iter().zip(new).filter(|(o, n)| o != n).unzip(); + let todo = Self::prioritized_paths(old, new); if todo.is_empty() { return Ok(()); } @@ -117,4 +118,90 @@ impl Manager { stdin().read_exact(&mut [0]).await?; Ok(()) } + + fn prioritized_paths(old: Vec, new: Vec) -> Vec<(PathBuf, PathBuf)> { + let orders: HashMap<_, _> = old.iter().enumerate().map(|(i, p)| (p, i)).collect(); + let mut incomes: HashMap<_, _> = old.iter().map(|p| (p, false)).collect(); + let mut todos: HashMap<_, _> = old + .iter() + .zip(new) + .map(|(o, n)| { + incomes.get_mut(&n).map(|b| *b = true); + (o, n) + }) + .collect(); + + let mut sorted = Vec::with_capacity(old.len()); + while !todos.is_empty() { + // Paths that are non-incomes and don't need to be prioritized in this round + let mut outcomes: Vec<_> = incomes.iter().filter(|(_, &b)| !b).map(|(&p, _)| p).collect(); + outcomes.sort_unstable_by(|a, b| orders[b].cmp(&orders[a])); + + // If there're no outcomes, it means there are cycles in the renaming + if outcomes.is_empty() { + let mut remain: Vec<_> = todos.into_iter().map(|(o, n)| (o.clone(), n)).collect(); + remain.sort_unstable_by(|(a, _), (b, _)| orders[a].cmp(&orders[b])); + sorted.reverse(); + sorted.extend(remain); + return sorted; + } + + for old in outcomes { + let Some(new) = todos.remove(old) else { unreachable!() }; + incomes.remove(&old); + incomes.get_mut(&new).map(|b| *b = false); + sorted.push((old.clone(), new)); + } + } + sorted.reverse(); + sorted + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sort() { + fn cmp(input: &[(&str, &str)], expected: &[(&str, &str)]) { + let sorted = Manager::prioritized_paths( + input.iter().map(|&(o, _)| o.into()).collect(), + input.iter().map(|&(_, n)| n.into()).collect(), + ); + let sorted: Vec<_> = + sorted.iter().map(|(o, n)| (o.to_str().unwrap(), n.to_str().unwrap())).collect(); + assert_eq!(sorted, expected); + } + + #[rustfmt::skip] + cmp( + &[("2", "3"), ("1", "2"), ("3", "4")], + &[("3", "4"), ("2", "3"), ("1", "2")] + ); + + #[rustfmt::skip] + cmp( + &[("1", "3"), ("2", "3"), ("3", "4")], + &[("3", "4"), ("1", "3"), ("2", "3")] + ); + + #[rustfmt::skip] + cmp( + &[("2", "1"), ("1", "2")], + &[("2", "1"), ("1", "2")] + ); + + #[rustfmt::skip] + cmp( + &[("3", "2"), ("2", "1"), ("1", "3"), ("a", "b"), ("b", "c")], + &[("b", "c"), ("a", "b"), ("3", "2"), ("2", "1"), ("1", "3")] + ); + + #[rustfmt::skip] + cmp( + &[("b", "b_"), ("a", "a_"), ("c", "c_")], + &[("b", "b_"), ("a", "a_"), ("c", "c_")], + ); + } } diff --git a/yazi-core/src/manager/commands/hover.rs b/yazi-core/src/manager/commands/hover.rs index 632e41f2..45caad8e 100644 --- a/yazi-core/src/manager/commands/hover.rs +++ b/yazi-core/src/manager/commands/hover.rs @@ -2,21 +2,18 @@ use std::{collections::HashSet, path::PathBuf}; use yazi_dds::Pubsub; use yazi_macro::render; -use yazi_shared::{event::{Cmd, Data}, fs::{Url, Urn}}; +use yazi_shared::{Id, event::{Cmd, Data}, fs::{Url, Urn}}; use crate::manager::Manager; struct Opt { url: Option, - tab: Option, + tab: Option, } impl From for Opt { fn from(mut c: Cmd) -> Self { - Self { - url: c.take_first().and_then(Data::into_url), - tab: c.get("tab").and_then(Data::as_usize), - } + Self { url: c.take_first().and_then(Data::into_url), tab: c.get("tab").and_then(Data::as_id) } } } impl From> for Opt { @@ -49,10 +46,10 @@ impl Manager { self.watcher.watch(to_watch); // Publish through DDS - Pubsub::pub_from_hover(self.active().idx, self.hovered().map(|h| &h.url)); + Pubsub::pub_from_hover(self.active().id, self.hovered().map(|h| &h.url)); } - fn hover_do(&mut self, url: Url, tab: Option) { + fn hover_do(&mut self, url: Url, tab: Option) { // Hover on the file if let Ok(p) = url.strip_prefix(&self.current_or(tab).url).map(PathBuf::from) { render!(self.current_or_mut(tab).repos(Some(Urn::new(&p)))); diff --git a/yazi-core/src/manager/commands/peek.rs b/yazi-core/src/manager/commands/peek.rs index 62e49738..842cea0e 100644 --- a/yazi-core/src/manager/commands/peek.rs +++ b/yazi-core/src/manager/commands/peek.rs @@ -1,3 +1,4 @@ +use yazi_proxy::HIDER; use yazi_shared::{event::{Cmd, Data}, fs::Url}; use crate::manager::Manager; @@ -30,6 +31,9 @@ impl Manager { let Some(hovered) = self.hovered().cloned() else { return self.active_mut().preview.reset(); }; + if HIDER.try_acquire().is_err() { + return self.active_mut().preview.reset_image(); + } let mime = self.mimetype.get_owned(&hovered.url).unwrap_or_default(); let folder = self.active().hovered_folder().map(|f| (f.offset, f.cha)); diff --git a/yazi-core/src/manager/commands/tab_close.rs b/yazi-core/src/manager/commands/tab_close.rs index a66115ed..3f475ac6 100644 --- a/yazi-core/src/manager/commands/tab_close.rs +++ b/yazi-core/src/manager/commands/tab_close.rs @@ -24,11 +24,12 @@ impl Tabs { } self.items.remove(opt.idx).shutdown(); - if opt.idx <= self.cursor { + if opt.idx > self.cursor { + self.set_idx(self.cursor); + } else { self.set_idx(self.absolute(1)); } - self.reorder(); render!(); } } diff --git a/yazi-core/src/manager/commands/tab_create.rs b/yazi-core/src/manager/commands/tab_create.rs index b104aba0..39993e73 100644 --- a/yazi-core/src/manager/commands/tab_create.rs +++ b/yazi-core/src/manager/commands/tab_create.rs @@ -36,8 +36,7 @@ impl Tabs { return; } - let mut tab = Tab { idx: self.cursor + 1, ..Default::default() }; - + let mut tab = Tab::default(); if !opt.current { tab.cd(opt.url); } else if let Some(h) = self.active().hovered() { @@ -52,7 +51,6 @@ impl Tabs { self.items.insert(self.cursor + 1, tab); self.set_idx(self.cursor + 1); - self.reorder(); render!(); } } diff --git a/yazi-core/src/manager/commands/tab_swap.rs b/yazi-core/src/manager/commands/tab_swap.rs index 183f005e..14439744 100644 --- a/yazi-core/src/manager/commands/tab_swap.rs +++ b/yazi-core/src/manager/commands/tab_swap.rs @@ -21,7 +21,6 @@ impl Tabs { self.items.swap(self.cursor, idx); self.set_idx(idx); - self.reorder(); render!(); } } diff --git a/yazi-core/src/manager/commands/update_files.rs b/yazi-core/src/manager/commands/update_files.rs index 2be38e4d..b88abb05 100644 --- a/yazi-core/src/manager/commands/update_files.rs +++ b/yazi-core/src/manager/commands/update_files.rs @@ -87,7 +87,7 @@ impl Manager { return; } - ManagerProxy::hover(None, tab.idx); // Re-hover in next loop + ManagerProxy::hover(None, tab.id); // Re-hover in next loop ManagerProxy::update_paged(); // Update for paged files in next loop if calc { tasks.prework_sorted(&tab.current.files); diff --git a/yazi-core/src/manager/manager.rs b/yazi-core/src/manager/manager.rs index b262fc11..4efd9b27 100644 --- a/yazi-core/src/manager/manager.rs +++ b/yazi-core/src/manager/manager.rs @@ -2,7 +2,7 @@ use ratatui::layout::Rect; use yazi_adapter::Dimension; use yazi_config::popup::{Origin, Position}; use yazi_fs::Folder; -use yazi_shared::fs::{File, Url}; +use yazi_shared::{Id, fs::{File, Url}}; use super::{Mimetype, Tabs, Watcher, Yanked}; use crate::tab::Tab; @@ -48,10 +48,10 @@ impl Manager { pub fn active_mut(&mut self) -> &mut Tab { self.tabs.active_mut() } #[inline] - pub fn active_or(&self, idx: Option) -> &Tab { self.tabs.active_or(idx) } + pub fn active_or(&self, id: Option) -> &Tab { self.tabs.active_or(id) } #[inline] - pub fn active_or_mut(&mut self, idx: Option) -> &mut Tab { self.tabs.active_or_mut(idx) } + pub fn active_or_mut(&mut self, id: Option) -> &mut Tab { self.tabs.active_or_mut(id) } #[inline] pub fn current(&self) -> &Folder { &self.active().current } @@ -60,10 +60,10 @@ impl Manager { pub fn current_mut(&mut self) -> &mut Folder { &mut self.active_mut().current } #[inline] - pub fn current_or(&self, idx: Option) -> &Folder { &self.active_or(idx).current } + pub fn current_or(&self, idx: Option) -> &Folder { &self.active_or(idx).current } #[inline] - pub fn current_or_mut(&mut self, idx: Option) -> &mut Folder { + pub fn current_or_mut(&mut self, idx: Option) -> &mut Folder { &mut self.active_or_mut(idx).current } diff --git a/yazi-core/src/manager/tabs.rs b/yazi-core/src/manager/tabs.rs index c3ad191f..1dfce8f1 100644 --- a/yazi-core/src/manager/tabs.rs +++ b/yazi-core/src/manager/tabs.rs @@ -3,7 +3,7 @@ use std::ops::{Deref, DerefMut}; use yazi_boot::BOOT; use yazi_dds::Pubsub; use yazi_proxy::ManagerProxy; -use yazi_shared::fs::Url; +use yazi_shared::{Id, fs::Url}; use crate::tab::Tab; @@ -16,7 +16,6 @@ impl Tabs { pub fn make() -> Self { let mut tabs = Self { cursor: 0, items: (0..BOOT.cwds.len()).map(|_| Tab::default()).collect() }; - tabs.reorder(); for (i, tab) in tabs.iter_mut().enumerate() { let file = &BOOT.files[i]; @@ -29,7 +28,6 @@ impl Tabs { tabs } - #[inline] pub(super) fn absolute(&self, rel: isize) -> usize { if rel > 0 { (self.cursor + rel as usize).min(self.items.len() - 1) @@ -38,16 +36,7 @@ impl Tabs { } } - #[inline] - pub(super) fn reorder(&mut self) { - self.items.iter_mut().enumerate().for_each(|(i, tab)| tab.idx = i); - } - pub(super) fn set_idx(&mut self, idx: usize) { - if self.cursor == idx { - return; - } - // Reset the preview of the last active tab if let Some(active) = self.items.get_mut(self.cursor) { active.preview.reset_image(); @@ -68,16 +57,16 @@ impl Tabs { pub(super) fn active_mut(&mut self) -> &mut Tab { &mut self.items[self.cursor] } #[inline] - pub fn active_or(&self, idx: Option) -> &Tab { - idx.and_then(|i| self.items.get(i)).unwrap_or(&self.items[self.cursor]) + pub fn active_or(&self, id: Option) -> &Tab { + id.and_then(|id| self.iter().find(|&t| t.id == id)).unwrap_or(self.active()) } #[inline] - pub(super) fn active_or_mut(&mut self, idx: Option) -> &mut Tab { - if let Some(i) = idx.filter(|&i| i < self.items.len()) { + pub(super) fn active_or_mut(&mut self, id: Option) -> &mut Tab { + if let Some(i) = id.and_then(|id| self.iter().position(|t| t.id == id)) { &mut self.items[i] } else { - &mut self.items[self.cursor] + self.active_mut() } } } diff --git a/yazi-core/src/tab/commands/arrow.rs b/yazi-core/src/tab/commands/arrow.rs index 6e29cd55..c635d449 100644 --- a/yazi-core/src/tab/commands/arrow.rs +++ b/yazi-core/src/tab/commands/arrow.rs @@ -42,7 +42,7 @@ impl Tab { } } - ManagerProxy::hover(None, self.idx); + ManagerProxy::hover(None, self.id); render!(); } } diff --git a/yazi-core/src/tab/commands/cd.rs b/yazi-core/src/tab/commands/cd.rs index ae693168..69799cc2 100644 --- a/yazi-core/src/tab/commands/cd.rs +++ b/yazi-core/src/tab/commands/cd.rs @@ -66,7 +66,7 @@ impl Tab { self.backstack.push(opt.target.clone()); } - Pubsub::pub_from_cd(self.idx, self.cwd()); + Pubsub::pub_from_cd(self.id, self.cwd()); ManagerProxy::refresh(); render!(); } diff --git a/yazi-core/src/tab/commands/copy.rs b/yazi-core/src/tab/commands/copy.rs index 777c6fed..af797ce5 100644 --- a/yazi-core/src/tab/commands/copy.rs +++ b/yazi-core/src/tab/commands/copy.rs @@ -35,6 +35,11 @@ impl Tab { } } + // Copy the CWD path regardless even if the directory is empty + if s.is_empty() && opt.type_ == "dirname" { + s.push(self.cwd()); + } + futures::executor::block_on(CLIPBOARD.set(s)); } } diff --git a/yazi-core/src/tab/commands/filter_do.rs b/yazi-core/src/tab/commands/filter_do.rs index d7e1ded5..44606628 100644 --- a/yazi-core/src/tab/commands/filter_do.rs +++ b/yazi-core/src/tab/commands/filter_do.rs @@ -27,7 +27,7 @@ impl Tab { self.current.repos(hovered.as_ref().map(|u| u.as_urn())); if self.hovered().map(|f| f.urn()) != hovered.as_ref().map(|u| u.as_urn()) { - ManagerProxy::hover(None, self.idx); + ManagerProxy::hover(None, self.id); } render!(); diff --git a/yazi-core/src/tab/commands/hidden.rs b/yazi-core/src/tab/commands/hidden.rs index 527f6886..266b119d 100644 --- a/yazi-core/src/tab/commands/hidden.rs +++ b/yazi-core/src/tab/commands/hidden.rs @@ -15,7 +15,7 @@ impl Tab { self.apply_files_attrs(); if hovered.as_ref() != self.hovered().map(|f| &f.url) { - ManagerProxy::hover(hovered, self.idx); + ManagerProxy::hover(hovered, self.id); } else if self.hovered().is_some_and(|f| f.is_dir()) { ManagerProxy::peek(true); } diff --git a/yazi-core/src/tab/commands/reveal.rs b/yazi-core/src/tab/commands/reveal.rs index 18b7e2d7..00edc64f 100644 --- a/yazi-core/src/tab/commands/reveal.rs +++ b/yazi-core/src/tab/commands/reveal.rs @@ -30,6 +30,6 @@ impl Tab { self.cd(parent.clone()); FilesOp::Creating(parent, vec![File::from_dummy(opt.target.clone(), None)]).emit(); - ManagerProxy::hover(Some(opt.target), self.idx); + ManagerProxy::hover(Some(opt.target), self.id); } } diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 345425de..3fe64ad7 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -7,14 +7,13 @@ use yazi_adapter::Dimension; use yazi_config::{LAYOUT, popup::{Origin, Position}}; use yazi_fs::{Folder, FolderStage}; use yazi_macro::render; -use yazi_shared::fs::{File, Url}; +use yazi_shared::{Id, Ids, fs::{File, Url}}; use super::{Backstack, Config, Finder, History, Mode, Preview}; use crate::tab::Selected; -#[derive(Default)] pub struct Tab { - pub idx: usize, + pub id: Id, pub mode: Mode, pub conf: Config, pub current: Folder, @@ -29,6 +28,28 @@ pub struct Tab { pub search: Option>>, } +impl Default for Tab { + fn default() -> Self { + static IDS: Ids = Ids::new(); + + Self { + id: IDS.next(), + mode: Default::default(), + conf: Default::default(), + current: Default::default(), + parent: Default::default(), + + backstack: Default::default(), + history: Default::default(), + selected: Default::default(), + + preview: Default::default(), + finder: Default::default(), + search: Default::default(), + } + } +} + impl Tab { pub fn shutdown(&mut self) { if let Some(handle) = self.search.take() { @@ -48,7 +69,7 @@ impl Tab { pub fn hovered_rect(&self) -> Option { let y = self.current.files.position(self.hovered()?.urn())? - self.current.offset; - let mut rect = LAYOUT.load().current; + let mut rect = LAYOUT.get().current; rect.y = rect.y.saturating_sub(1) + y as u16; rect.height = 1; Some(rect) diff --git a/yazi-core/src/tasks/progress.rs b/yazi-core/src/tasks/progress.rs index e62899a2..faf8a1e9 100644 --- a/yazi-core/src/tasks/progress.rs +++ b/yazi-core/src/tasks/progress.rs @@ -1,7 +1,7 @@ use serde::Serialize; use yazi_scheduler::Ongoing; -#[derive(Clone, Copy, Default, Eq, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)] pub struct TasksProgress { pub total: u32, pub succ: u32, diff --git a/yazi-dds/src/body/cd.rs b/yazi-dds/src/body/cd.rs index 587a6f1b..c6711c75 100644 --- a/yazi-dds/src/body/cd.rs +++ b/yazi-dds/src/body/cd.rs @@ -2,13 +2,13 @@ use std::borrow::Cow; use mlua::{IntoLua, Lua, Value}; use serde::{Deserialize, Serialize}; -use yazi_shared::fs::Url; +use yazi_shared::{Id, fs::Url}; use super::Body; #[derive(Debug, Serialize, Deserialize)] pub struct BodyCd<'a> { - pub tab: usize, + pub tab: Id, pub url: Cow<'a, Url>, #[serde(skip)] dummy: bool, @@ -16,14 +16,14 @@ pub struct BodyCd<'a> { impl<'a> BodyCd<'a> { #[inline] - pub fn borrowed(tab: usize, url: &'a Url) -> Body<'a> { + pub fn borrowed(tab: Id, url: &'a Url) -> Body<'a> { Self { tab, url: Cow::Borrowed(url), dummy: false }.into() } } impl BodyCd<'static> { #[inline] - pub fn dummy(tab: usize) -> Body<'static> { + pub fn dummy(tab: Id) -> Body<'static> { Self { tab, url: Default::default(), dummy: true }.into() } } @@ -36,11 +36,11 @@ impl IntoLua<'_> for BodyCd<'static> { fn into_lua(self, lua: &Lua) -> mlua::Result { if let Some(Cow::Owned(url)) = Some(self.url).filter(|_| !self.dummy) { lua.create_table_from([ - ("tab", self.tab.into_lua(lua)?), + ("tab", self.tab.get().into_lua(lua)?), ("url", lua.create_any_userdata(url)?.into_lua(lua)?), ])? } else { - lua.create_table_from([("tab", self.tab)])? + lua.create_table_from([("tab", self.tab.get())])? } .into_lua(lua) } diff --git a/yazi-dds/src/body/hover.rs b/yazi-dds/src/body/hover.rs index e3916fa4..893ca699 100644 --- a/yazi-dds/src/body/hover.rs +++ b/yazi-dds/src/body/hover.rs @@ -2,26 +2,26 @@ use std::borrow::Cow; use mlua::{IntoLua, Lua, Value}; use serde::{Deserialize, Serialize}; -use yazi_shared::fs::Url; +use yazi_shared::{Id, fs::Url}; use super::Body; #[derive(Debug, Serialize, Deserialize)] pub struct BodyHover<'a> { - pub tab: usize, + pub tab: Id, pub url: Option>, } impl<'a> BodyHover<'a> { #[inline] - pub fn borrowed(tab: usize, url: Option<&'a Url>) -> Body<'a> { + pub fn borrowed(tab: Id, url: Option<&'a Url>) -> Body<'a> { Self { tab, url: url.map(Cow::Borrowed) }.into() } } impl BodyHover<'static> { #[inline] - pub fn dummy(tab: usize) -> Body<'static> { Self { tab, url: None }.into() } + pub fn dummy(tab: Id) -> Body<'static> { Self { tab, url: None }.into() } } impl<'a> From> for Body<'a> { @@ -32,11 +32,11 @@ impl IntoLua<'_> for BodyHover<'static> { fn into_lua(self, lua: &Lua) -> mlua::Result { if let Some(Cow::Owned(url)) = self.url { lua.create_table_from([ - ("tab", self.tab.into_lua(lua)?), + ("tab", self.tab.get().into_lua(lua)?), ("url", lua.create_any_userdata(url)?.into_lua(lua)?), ])? } else { - lua.create_table_from([("tab", self.tab)])? + lua.create_table_from([("tab", self.tab.get())])? } .into_lua(lua) } diff --git a/yazi-dds/src/pubsub.rs b/yazi-dds/src/pubsub.rs index 59035c6e..0be6b36d 100644 --- a/yazi-dds/src/pubsub.rs +++ b/yazi-dds/src/pubsub.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use mlua::Function; use parking_lot::RwLock; use yazi_boot::BOOT; -use yazi_shared::{RoCell, fs::Url}; +use yazi_shared::{Id, RoCell, fs::Url}; use crate::{Client, ID, PEERS, body::{Body, BodyBulk, BodyCd, BodyDelete, BodyHi, BodyHover, BodyMove, BodyMoveItem, BodyRename, BodyTab, BodyTrash, BodyYank}}; @@ -88,7 +88,7 @@ impl Pubsub { true } - pub fn pub_from_cd(tab: usize, url: &Url) { + pub fn pub_from_cd(tab: Id, url: &Url) { if LOCAL.read().contains_key("cd") { Self::pub_(BodyCd::dummy(tab)); } @@ -100,7 +100,7 @@ impl Pubsub { } } - pub fn pub_from_hover(tab: usize, url: Option<&Url>) { + pub fn pub_from_hover(tab: Id, url: Option<&Url>) { if LOCAL.read().contains_key("hover") { Self::pub_(BodyHover::dummy(tab)); } diff --git a/yazi-fm/src/app/commands/mod.rs b/yazi-fm/src/app/commands/mod.rs index 8c920955..2e1fc09a 100644 --- a/yazi-fm/src/app/commands/mod.rs +++ b/yazi-fm/src/app/commands/mod.rs @@ -4,6 +4,7 @@ yazi_macro::mod_flat!( notify plugin quit + reflow render resize resume diff --git a/yazi-fm/src/app/commands/reflow.rs b/yazi-fm/src/app/commands/reflow.rs new file mode 100644 index 00000000..20451fd6 --- /dev/null +++ b/yazi-fm/src/app/commands/reflow.rs @@ -0,0 +1,55 @@ +use mlua::Value; +use ratatui::layout::Position; +use tracing::error; +use yazi_config::LAYOUT; +use yazi_macro::render; +use yazi_shared::event::Cmd; + +use crate::{Root, app::App, lives::Lives}; + +struct Opt; + +impl From for Opt { + fn from(_: Cmd) -> Self { Self } +} + +impl From<()> for Opt { + fn from(_: ()) -> Self { Self } +} + +impl App { + #[yazi_codegen::command] + pub fn reflow(&mut self, _: Opt) { + let Some(size) = self.term.as_ref().and_then(|t| t.size().ok()) else { return }; + let mut layout = LAYOUT.get(); + + let result = Lives::scope(&self.cx, |_| { + let comps = Root::reflow((Position::ORIGIN, size).into())?; + + for v in comps.sequence_values::() { + let Value::Table(t) = v? else { + error!("`reflow()` must return a table of components"); + continue; + }; + + let id: mlua::String = t.get("_id")?; + match id.to_str()? { + "current" => layout.current = *t.raw_get::<_, yazi_plugin::elements::Rect>("_area")?, + "preview" => layout.preview = *t.raw_get::<_, yazi_plugin::elements::Rect>("_area")?, + "progress" => layout.progress = *t.raw_get::<_, yazi_plugin::elements::Rect>("_area")?, + _ => {} + } + } + Ok(()) + }); + + if layout != LAYOUT.get() { + LAYOUT.set(layout); + render!(); + } + + if let Err(e) = result { + error!("Failed to `reflow()` the `Root` component:\n{e}"); + } + } +} diff --git a/yazi-fm/src/app/commands/render.rs b/yazi-fm/src/app/commands/render.rs index 6fd73573..123e146a 100644 --- a/yazi-fm/src/app/commands/render.rs +++ b/yazi-fm/src/app/commands/render.rs @@ -31,7 +31,7 @@ impl App { Self::patch(frame, self.cx.cursor()); } if !self.cx.notify.messages.is_empty() { - self.render_notify(); + self.render_partially(); } // Reload preview if collision is resolved @@ -40,18 +40,19 @@ impl App { } } - pub(crate) fn render_notify(&mut self) { - let Some(term) = &mut self.term else { - return; - }; - + pub(crate) fn render_partially(&mut self) { + let Some(term) = &mut self.term else { return }; if !term.can_partial() { return self.render(); } let frame = term .draw_partial(|f| { - f.render_widget(crate::notify::Layout::new(&self.cx), f.area()); + _ = Lives::scope(&self.cx, |_| { + f.render_widget(crate::tasks::Progress, f.area()); + f.render_widget(crate::notify::Notify::new(&self.cx), f.area()); + Ok(()) + }); if let Some(pos) = self.cx.cursor() { f.set_cursor_position(pos); diff --git a/yazi-fm/src/app/commands/resize.rs b/yazi-fm/src/app/commands/resize.rs index a8fbe0ad..250226c7 100644 --- a/yazi-fm/src/app/commands/resize.rs +++ b/yazi-fm/src/app/commands/resize.rs @@ -16,7 +16,7 @@ impl App { #[yazi_codegen::command] pub fn resize(&mut self, _: Opt) { self.cx.manager.active_mut().preview.reset(); - self.render(); + self.reflow(()); self.cx.manager.current_mut().sync_page(true); self.cx.manager.hover(None); diff --git a/yazi-fm/src/app/commands/update_notify.rs b/yazi-fm/src/app/commands/update_notify.rs index 4729ead2..08a5cc51 100644 --- a/yazi-fm/src/app/commands/update_notify.rs +++ b/yazi-fm/src/app/commands/update_notify.rs @@ -9,14 +9,14 @@ impl App { pub(crate) fn update_notify(&mut self, cmd: Cmd) { let WindowSize { rows, columns, .. } = Dimension::available(); let area = - notify::Layout::available(Rect { x: 0, y: 0, width: columns, height: rows }); + notify::Notify::available(Rect { x: 0, y: 0, width: columns, height: rows }); self.cx.notify.tick(cmd, area); if self.cx.notify.messages.is_empty() { self.render(); } else { - self.render_notify(); + self.render_partially(); } } } diff --git a/yazi-fm/src/app/commands/update_progress.rs b/yazi-fm/src/app/commands/update_progress.rs index 98d645b2..15d9633c 100644 --- a/yazi-fm/src/app/commands/update_progress.rs +++ b/yazi-fm/src/app/commands/update_progress.rs @@ -1,9 +1,8 @@ -use ratatui::backend::Backend; use yazi_core::tasks::TasksProgress; use yazi_macro::render; use yazi_shared::event::Cmd; -use crate::{app::App, components::Progress, lives::Lives}; +use crate::app::App; pub struct Opt { progress: TasksProgress, @@ -19,41 +18,28 @@ impl TryFrom for Opt { impl App { pub(crate) fn update_progress(&mut self, opt: impl TryInto) { - let Ok(opt) = opt.try_into() else { - return; - }; + let Ok(opt) = opt.try_into() else { return }; // Update the progress of all tasks. let tasks = &mut self.cx.tasks; + let progressed = tasks.progress != opt.progress; tasks.progress = opt.progress; // If the task manager is visible, update the summaries with a complete render. if tasks.visible { let new = tasks.paginate(); - if new.len() != tasks.summaries.len() - || new.iter().zip(&tasks.summaries).any(|(a, b)| a.name != b.name) - { + if tasks.summaries != new { tasks.summaries = new; tasks.arrow(0); return render!(); } } - // Otherwise, only partially update the progress. - let Some(term) = &mut self.term else { - return; - }; - - _ = Lives::scope(&self.cx, |_| { - for patch in Progress::partial_render(term.current_buffer_mut()) { - term.backend_mut().draw(patch.iter().map(|(x, y, cell)| (*x, *y, cell)))?; - if let Some(pos) = self.cx.cursor() { - term.show_cursor()?; - term.set_cursor_position(pos)?; - } - term.backend_mut().flush()?; - } - Ok(()) - }); + if !progressed { + } else if tasks.progress.total == 0 { + render!(); + } else { + self.render_partially(); + } } } diff --git a/yazi-fm/src/components/mod.rs b/yazi-fm/src/components/mod.rs deleted file mode 100644 index 2b7b0da6..00000000 --- a/yazi-fm/src/components/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -#![allow(clippy::module_inception)] - -yazi_macro::mod_flat!(preview progress); diff --git a/yazi-fm/src/components/progress.rs b/yazi-fm/src/components/progress.rs deleted file mode 100644 index a9423ff3..00000000 --- a/yazi-fm/src/components/progress.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::mem; - -use mlua::{AnyUserData, Table, TableExt}; -use tracing::error; -use yazi_plugin::{LUA, cast_to_renderable}; - -pub(crate) struct Progress; - -impl Progress { - pub(crate) fn partial_render( - buf: &mut ratatui::buffer::Buffer, - ) -> Vec> { - let mut patches = vec![]; - let mut f = || { - let comp: Table = LUA.globals().raw_get("Progress")?; - for widget in comp.call_method::<_, Vec>("partial_render", ())? { - let Some(w) = cast_to_renderable(&widget) else { continue }; - - let area = w.area(); - w.render(buf); - - let mut patch = Vec::with_capacity(area.width as usize * area.height as usize); - for y in area.top()..area.bottom() { - for x in area.left()..area.right() { - patch.push((x, y, mem::take(&mut buf[(x, y)]))); - } - } - - buf.reset(); - patches.push(patch); - } - - Ok::<_, anyhow::Error>(()) - }; - - if let Err(e) = f() { - error!("{e}"); - } - patches - } -} diff --git a/yazi-fm/src/confirm/buttons.rs b/yazi-fm/src/confirm/buttons.rs index 13997eaf..727bc913 100644 --- a/yazi-fm/src/confirm/buttons.rs +++ b/yazi-fm/src/confirm/buttons.rs @@ -1,4 +1,5 @@ -use ratatui::{buffer::Buffer, layout::{Constraint, Rect}, style::Stylize, text::Span, widgets::{Paragraph, Widget}}; +use ratatui::{buffer::Buffer, layout::{Constraint, Rect}, text::Span, widgets::{Paragraph, Widget}}; +use yazi_config::THEME; pub(crate) struct Buttons; @@ -7,7 +8,11 @@ impl Widget for Buttons { let chunks = ratatui::layout::Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).split(area); - Paragraph::new(Span::raw(" [Y]es ").reversed()).centered().render(chunks[0], buf); - Paragraph::new(Span::raw(" (N)o ")).centered().render(chunks[1], buf); + Paragraph::new(Span::raw(&THEME.confirm.btn_labels[0]).style(THEME.confirm.btn_yes)) + .centered() + .render(chunks[0], buf); + Paragraph::new(Span::raw(&THEME.confirm.btn_labels[1]).style(THEME.confirm.btn_no)) + .centered() + .render(chunks[1], buf); } } diff --git a/yazi-fm/src/confirm/confirm.rs b/yazi-fm/src/confirm/confirm.rs index 450a3f20..5730a86a 100644 --- a/yazi-fm/src/confirm/confirm.rs +++ b/yazi-fm/src/confirm/confirm.rs @@ -1,4 +1,5 @@ -use ratatui::{buffer::Buffer, layout::{Alignment, Constraint, Layout, Margin, Rect}, style::{Style, Stylize}, text::Line, widgets::{Block, BorderType, Widget}}; +use ratatui::{buffer::Buffer, layout::{Alignment, Constraint, Layout, Margin, Rect}, text::Line, widgets::{Block, BorderType, Widget}}; +use yazi_config::THEME; use crate::Ctx; @@ -19,8 +20,8 @@ impl<'a> Widget for Confirm<'a> { Block::bordered() .border_type(BorderType::Rounded) - .border_style(Style::new().blue()) - .title(Line::styled(&confirm.title, Style::new().blue())) + .border_style(THEME.confirm.border) + .title(Line::styled(&confirm.title, THEME.confirm.title)) .title_alignment(Alignment::Center) .render(area, buf); diff --git a/yazi-fm/src/confirm/content.rs b/yazi-fm/src/confirm/content.rs index c1a553d4..e98b1c25 100644 --- a/yazi-fm/src/confirm/content.rs +++ b/yazi-fm/src/confirm/content.rs @@ -1,4 +1,5 @@ -use ratatui::{buffer::Buffer, layout::{Margin, Rect}, style::{Style, Stylize}, widgets::{Block, Borders, Paragraph, Widget}}; +use ratatui::{buffer::Buffer, layout::{Margin, Rect}, widgets::{Block, Borders, Paragraph, Widget}}; +use yazi_config::THEME; pub(crate) struct Content<'a> { p: Paragraph<'a>, @@ -14,9 +15,14 @@ impl<'a> Widget for Content<'a> { let inner = area.inner(Margin::new(1, 0)); // Bottom border - let block = Block::new().borders(Borders::BOTTOM).border_style(Style::new().blue()); + let block = Block::new().borders(Borders::BOTTOM).border_style(THEME.confirm.border); block.clone().render(area.inner(Margin::new(1, 0)), buf); - self.p.alignment(ratatui::layout::Alignment::Center).block(block).render(inner, buf); + self + .p + .alignment(ratatui::layout::Alignment::Center) + .block(block) + .style(THEME.confirm.content) + .render(inner, buf); } } diff --git a/yazi-fm/src/confirm/list.rs b/yazi-fm/src/confirm/list.rs index 22709c05..120202ef 100644 --- a/yazi-fm/src/confirm/list.rs +++ b/yazi-fm/src/confirm/list.rs @@ -1,4 +1,5 @@ -use ratatui::{buffer::Buffer, layout::{Margin, Rect}, style::{Style, Stylize}, widgets::{Block, Borders, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget, Wrap}}; +use ratatui::{buffer::Buffer, layout::{Margin, Rect}, widgets::{Block, Borders, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget, Wrap}}; +use yazi_config::THEME; use crate::Ctx; @@ -16,7 +17,7 @@ impl<'a> Widget for List<'a> { let inner = area.inner(Margin::new(2, 0)); // Bottom border - let block = Block::new().borders(Borders::BOTTOM).border_style(Style::new().blue()); + let block = Block::new().borders(Borders::BOTTOM).border_style(THEME.confirm.border); block.clone().render(area.inner(Margin::new(1, 0)), buf); let list = self @@ -26,6 +27,7 @@ impl<'a> Widget for List<'a> { .clone() .scroll((self.cx.confirm.offset as u16, 0)) .block(block) + .style(THEME.confirm.list) .wrap(Wrap { trim: false }); // Vertical scrollbar diff --git a/yazi-fm/src/help/layout.rs b/yazi-fm/src/help/help.rs similarity index 91% rename from yazi-fm/src/help/layout.rs rename to yazi-fm/src/help/help.rs index f0ba75da..6aec0937 100644 --- a/yazi-fm/src/help/layout.rs +++ b/yazi-fm/src/help/help.rs @@ -4,11 +4,11 @@ use yazi_config::{KEYMAP, THEME}; use super::Bindings; use crate::Ctx; -pub(crate) struct Layout<'a> { +pub(crate) struct Help<'a> { cx: &'a Ctx, } -impl<'a> Layout<'a> { +impl<'a> Help<'a> { pub fn new(cx: &'a Ctx) -> Self { Self { cx } } fn tips() -> String { @@ -19,7 +19,7 @@ impl<'a> Layout<'a> { } } -impl<'a> Widget for Layout<'a> { +impl<'a> Widget for Help<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let help = &self.cx.help; yazi_plugin::elements::Clear::default().render(area, buf); diff --git a/yazi-fm/src/help/mod.rs b/yazi-fm/src/help/mod.rs index 53f65c8a..3b22a7c9 100644 --- a/yazi-fm/src/help/mod.rs +++ b/yazi-fm/src/help/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(bindings layout); +yazi_macro::mod_flat!(bindings help); diff --git a/yazi-fm/src/lives/finder.rs b/yazi-fm/src/lives/finder.rs index 1907064d..a2e14a1a 100644 --- a/yazi-fm/src/lives/finder.rs +++ b/yazi-fm/src/lives/finder.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use mlua::{AnyUserData, Lua}; +use mlua::{AnyUserData, Lua, MetaMethod, UserDataMethods}; use super::SCOPE; @@ -21,6 +21,8 @@ impl Finder { } pub(super) fn register(lua: &Lua) -> mlua::Result<()> { - lua.register_userdata_type::(|_| {}) + lua.register_userdata_type::(|reg| { + reg.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.filter.to_string())); + }) } } diff --git a/yazi-fm/src/lives/folder.rs b/yazi-fm/src/lives/folder.rs index e4fd4874..3a855ba5 100644 --- a/yazi-fm/src/lives/folder.rs +++ b/yazi-fm/src/lives/folder.rs @@ -28,7 +28,7 @@ impl Folder { let window = match window { Some(w) => w, None => { - let limit = LAYOUT.load().preview.height as usize; + let limit = LAYOUT.get().preview.height as usize; inner.offset..inner.files.len().min(inner.offset + limit) } }; diff --git a/yazi-fm/src/lives/preview.rs b/yazi-fm/src/lives/preview.rs index 83018ed7..402ec170 100644 --- a/yazi-fm/src/lives/preview.rs +++ b/yazi-fm/src/lives/preview.rs @@ -28,7 +28,7 @@ impl Preview { me.tab() .hovered_folder() .map(|f| { - let limit = LAYOUT.load().preview.height as usize; + let limit = LAYOUT.get().preview.height as usize; Folder::make(Some(me.skip..f.files.len().min(me.skip + limit)), f, me.tab()) }) .transpose() diff --git a/yazi-fm/src/lives/tab.rs b/yazi-fm/src/lives/tab.rs index aba84b33..eaff877b 100644 --- a/yazi-fm/src/lives/tab.rs +++ b/yazi-fm/src/lives/tab.rs @@ -23,6 +23,7 @@ impl Tab { pub(super) fn register(lua: &Lua) -> mlua::Result<()> { lua.register_userdata_type::(|reg| { + reg.add_field_method_get("id", |_, me| Ok(me.id.get())); reg.add_method("name", |lua, me, ()| { lua.create_string(me.current.url.name().as_encoded_bytes()) }); diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index ac33b1c9..026f66a9 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -5,9 +5,7 @@ #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -yazi_macro::mod_pub!( - app completion components confirm help input lives notify pick tasks which -); +yazi_macro::mod_pub!(app completion confirm help input lives manager notify pick tasks which); yazi_macro::mod_flat!(context executor logs panic root router signals term); diff --git a/yazi-fm/src/manager/mod.rs b/yazi-fm/src/manager/mod.rs new file mode 100644 index 00000000..b2788a42 --- /dev/null +++ b/yazi-fm/src/manager/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(preview); diff --git a/yazi-fm/src/components/preview.rs b/yazi-fm/src/manager/preview.rs similarity index 100% rename from yazi-fm/src/components/preview.rs rename to yazi-fm/src/manager/preview.rs diff --git a/yazi-fm/src/notify/mod.rs b/yazi-fm/src/notify/mod.rs index b6274db1..247b6e3b 100644 --- a/yazi-fm/src/notify/mod.rs +++ b/yazi-fm/src/notify/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(layout); +yazi_macro::mod_flat!(notify); diff --git a/yazi-fm/src/notify/layout.rs b/yazi-fm/src/notify/notify.rs similarity index 95% rename from yazi-fm/src/notify/layout.rs rename to yazi-fm/src/notify/notify.rs index ee4cbfad..7d3b061c 100644 --- a/yazi-fm/src/notify/layout.rs +++ b/yazi-fm/src/notify/notify.rs @@ -3,11 +3,11 @@ use yazi_core::notify::Message; use crate::Ctx; -pub(crate) struct Layout<'a> { +pub(crate) struct Notify<'a> { cx: &'a Ctx, } -impl<'a> Layout<'a> { +impl<'a> Notify<'a> { pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } pub(crate) fn available(area: Rect) -> Rect { @@ -38,7 +38,7 @@ impl<'a> Layout<'a> { } } -impl<'a> Widget for Layout<'a> { +impl<'a> Widget for Notify<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let notify = &self.cx.notify; diff --git a/yazi-fm/src/root.rs b/yazi-fm/src/root.rs index 60d60746..2f847f4f 100644 --- a/yazi-fm/src/root.rs +++ b/yazi-fm/src/root.rs @@ -3,8 +3,8 @@ use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; use tracing::error; use yazi_plugin::{LUA, elements::render_widgets}; -use super::{completion, confirm, input, pick, tasks, which}; -use crate::{Ctx, components, help}; +use super::{completion, confirm, help, input, manager, pick, tasks, which}; +use crate::Ctx; pub(super) struct Root<'a> { cx: &'a Ctx, @@ -12,6 +12,12 @@ pub(super) struct Root<'a> { impl<'a> Root<'a> { pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } } + + pub(super) fn reflow<'lua>(area: Rect) -> mlua::Result> { + let area = yazi_plugin::elements::Rect::from(area); + let root = LUA.globals().raw_get::<_, Table>("Root")?.call_method::<_, Table>("new", area)?; + root.call_method("reflow", ()) + } } impl<'a> Widget for Root<'a> { @@ -20,17 +26,17 @@ impl<'a> Widget for Root<'a> { let area = yazi_plugin::elements::Rect::from(area); let root = LUA.globals().raw_get::<_, Table>("Root")?.call_method::<_, Table>("new", area)?; - render_widgets(root.call_method("render", ())?, buf); + render_widgets(root.call_method("redraw", ())?, buf); Ok::<_, mlua::Error>(()) }; if let Err(e) = f() { - error!("Failed to render the `Root` component:\n{e}"); + error!("Failed to redraw the `Root` component:\n{e}"); } - components::Preview::new(self.cx).render(area, buf); + manager::Preview::new(self.cx).render(area, buf); if self.cx.tasks.visible { - tasks::Layout::new(self.cx).render(area, buf); + tasks::Tasks::new(self.cx).render(area, buf); } if self.cx.pick.visible { @@ -46,7 +52,7 @@ impl<'a> Widget for Root<'a> { } if self.cx.help.visible { - help::Layout::new(self.cx).render(area, buf); + help::Help::new(self.cx).render(area, buf); } if self.cx.completion.visible { diff --git a/yazi-fm/src/tasks/mod.rs b/yazi-fm/src/tasks/mod.rs index b6274db1..52d57d37 100644 --- a/yazi-fm/src/tasks/mod.rs +++ b/yazi-fm/src/tasks/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(layout); +yazi_macro::mod_flat!(progress tasks); diff --git a/yazi-fm/src/tasks/progress.rs b/yazi-fm/src/tasks/progress.rs new file mode 100644 index 00000000..f9795dcc --- /dev/null +++ b/yazi-fm/src/tasks/progress.rs @@ -0,0 +1,23 @@ +use mlua::{Table, TableExt}; +use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; +use tracing::error; +use yazi_config::LAYOUT; +use yazi_plugin::{LUA, elements::render_widgets}; + +pub(crate) struct Progress; + +impl Widget for Progress { + fn render(self, _: Rect, buf: &mut Buffer) { + let mut f = || { + let area = yazi_plugin::elements::Rect::from(LAYOUT.get().progress); + let progress = + LUA.globals().raw_get::<_, Table>("Progress")?.call_method::<_, Table>("use", area)?; + + render_widgets(progress.call_method("redraw", ())?, buf); + Ok::<_, mlua::Error>(()) + }; + if let Err(e) = f() { + error!("Failed to redraw the `Progress` component:\n{e}"); + } + } +} diff --git a/yazi-fm/src/tasks/layout.rs b/yazi-fm/src/tasks/tasks.rs similarity index 94% rename from yazi-fm/src/tasks/layout.rs rename to yazi-fm/src/tasks/tasks.rs index d8785f54..88ffcb00 100644 --- a/yazi-fm/src/tasks/layout.rs +++ b/yazi-fm/src/tasks/tasks.rs @@ -4,11 +4,11 @@ use yazi_core::tasks::TASKS_PERCENT; use crate::Ctx; -pub(crate) struct Layout<'a> { +pub(crate) struct Tasks<'a> { cx: &'a Ctx, } -impl<'a> Layout<'a> { +impl<'a> Tasks<'a> { pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } pub(super) fn area(area: Rect) -> Rect { @@ -28,7 +28,7 @@ impl<'a> Layout<'a> { } } -impl<'a> Widget for Layout<'a> { +impl<'a> Widget for Tasks<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let area = Self::area(area); diff --git a/yazi-fm/src/which/mod.rs b/yazi-fm/src/which/mod.rs index 2d0635bd..d54bac3b 100644 --- a/yazi-fm/src/which/mod.rs +++ b/yazi-fm/src/which/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(cand layout); +yazi_macro::mod_flat!(cand which); diff --git a/yazi-fm/src/which/layout.rs b/yazi-fm/src/which/which.rs similarity index 100% rename from yazi-fm/src/which/layout.rs rename to yazi-fm/src/which/which.rs diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index fc38f0b6..482e5750 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -1,15 +1,15 @@ -use std::{collections::{HashMap, HashSet}, mem, ops::Deref, sync::atomic::Ordering}; +use std::{collections::{HashMap, HashSet}, mem, ops::Deref}; use tokio::{fs::{self, DirEntry}, select, sync::mpsc::{self, UnboundedReceiver}}; use yazi_config::{MANAGER, manager::SortBy}; -use yazi_shared::fs::{Cha, FILES_TICKET, File, FilesOp, Url, Urn, UrnBuf, maybe_exists}; +use yazi_shared::{Id, fs::{Cha, FILES_TICKET, File, FilesOp, Url, Urn, UrnBuf, maybe_exists}}; use super::{FilesSorter, Filter}; pub struct Files { hidden: Vec, items: Vec, - ticket: u64, + ticket: Id, version: u64, pub revision: u64, @@ -118,7 +118,7 @@ impl Files { impl Files { pub fn update_full(&mut self, files: Vec) { - self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); + self.ticket = FILES_TICKET.next(); (self.hidden, self.items) = self.split_files(files); if !self.items.is_empty() { @@ -126,7 +126,7 @@ impl Files { } } - pub fn update_part(&mut self, files: Vec, ticket: u64) { + pub fn update_part(&mut self, files: Vec, ticket: Id) { if !files.is_empty() { if ticket != self.ticket { return; @@ -162,7 +162,7 @@ impl Files { } pub fn update_ioerr(&mut self) { - self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); + self.ticket = FILES_TICKET.next(); self.hidden.clear(); self.items.clear(); } @@ -349,7 +349,7 @@ impl Files { // --- Ticket #[inline] - pub fn ticket(&self) -> u64 { self.ticket } + pub fn ticket(&self) -> Id { self.ticket } // --- Sorter #[inline] diff --git a/yazi-fs/src/folder.rs b/yazi-fs/src/folder.rs index ac1d4aa0..11e7205e 100644 --- a/yazi-fs/src/folder.rs +++ b/yazi-fs/src/folder.rs @@ -94,7 +94,7 @@ impl Folder { } pub fn sync_page(&mut self, force: bool) { - let limit = LAYOUT.load().current.height as usize; + let limit = LAYOUT.get().current.height as usize; if limit == 0 { return; } @@ -109,7 +109,7 @@ impl Folder { let old = (self.cursor, self.offset); let len = self.files.len(); - let limit = LAYOUT.load().current.height as usize; + let limit = LAYOUT.get().current.height as usize; let scrolloff = (limit / 2).min(MANAGER.scrolloff as usize); self.cursor = step.add(self.cursor, limit).min(len.saturating_sub(1)); @@ -126,7 +126,7 @@ impl Folder { let old = (self.cursor, self.offset); let max = self.files.len().saturating_sub(1); - let limit = LAYOUT.load().current.height as usize; + let limit = LAYOUT.get().current.height as usize; let scrolloff = (limit / 2).min(MANAGER.scrolloff as usize); self.cursor = step.add(self.cursor, limit).min(max); @@ -143,13 +143,13 @@ impl Folder { let old = self.offset; let len = self.files.len(); - let limit = LAYOUT.load().current.height as usize; + let limit = LAYOUT.get().current.height as usize; let scrolloff = (limit / 2).min(MANAGER.scrolloff as usize); self.offset = if self.cursor < (self.offset + limit).min(len).saturating_sub(scrolloff) { len.saturating_sub(limit).min(self.offset) } else { - len.saturating_sub(limit).min(self.cursor.saturating_sub(limit) + scrolloff) + len.saturating_sub(limit).min(self.cursor.saturating_sub(limit) + 1 + scrolloff) }; old != self.offset @@ -162,7 +162,7 @@ impl Folder { pub fn paginate(&self, page: usize) -> &[File] { let len = self.files.len(); - let limit = LAYOUT.load().current.height as usize; + let limit = LAYOUT.get().current.height as usize; let start = (page.saturating_sub(1) * limit).min(len.saturating_sub(1)); let end = ((page + 2) * limit).min(len); diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index fee270bc..86315bcd 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -22,7 +22,11 @@ impl FilesSorter { if self.sensitive { self.cmp(a.name(), b.name(), self.promote(a, b)) } else { - self.cmp(a.name().to_ascii_uppercase(), b.name().to_ascii_uppercase(), self.promote(a, b)) + self.cmp_insensitive( + a.name().as_encoded_bytes(), + b.name().as_encoded_bytes(), + self.promote(a, b), + ) } }; @@ -40,9 +44,9 @@ impl FilesSorter { let ord = if self.sensitive { self.cmp(a.url.extension(), b.url.extension(), self.promote(a, b)) } else { - self.cmp( - a.url.extension().map(|s| s.to_ascii_lowercase()), - b.url.extension().map(|s| s.to_ascii_lowercase()), + self.cmp_insensitive( + a.url.extension().map_or(&[], |s| s.as_encoded_bytes()), + b.url.extension().map_or(&[], |s| s.as_encoded_bytes()), self.promote(a, b), ) }; @@ -99,6 +103,25 @@ impl FilesSorter { } } + #[inline(always)] + fn cmp_insensitive(&self, a: &[u8], b: &[u8], promote: Ordering) -> Ordering { + if promote != Ordering::Equal { + return promote; + } + + let l = a.len().min(b.len()); + let (lhs, rhs) = if self.reverse { (&b[..l], &a[..l]) } else { (&a[..l], &b[..l]) }; + + for i in 0..l { + match lhs[i].to_ascii_lowercase().cmp(&rhs[i].to_ascii_lowercase()) { + Ordering::Equal => (), + not_eq => return not_eq, + } + } + + if self.reverse { b.len().cmp(&a.len()) } else { a.len().cmp(&b.len()) } + } + #[inline(always)] fn promote(&self, a: &File, b: &File) -> Ordering { if self.dir_first { b.is_dir().cmp(&a.is_dir()) } else { Ordering::Equal } diff --git a/yazi-plugin/preset/components/current.lua b/yazi-plugin/preset/components/current.lua index f472783d..2a8bdebb 100644 --- a/yazi-plugin/preset/components/current.lua +++ b/yazi-plugin/preset/components/current.lua @@ -11,19 +11,21 @@ function Current:new(area, tab) end function Current:empty() - local line + local text if self._folder.files.filter then - line = ui.Line("No filter results") + text = ui.Text("No filter results") else - line = ui.Line(self._folder.stage.is_loading and "Loading..." or "No items") + text = ui.Text(self._folder.stage.is_loading and "Loading..." or "No items") end return { - ui.Text(line):area(self._area):align(ui.Text.CENTER), + text:area(self._area):align(ui.Text.CENTER), } end -function Current:render() +function Current:reflow() return { self } end + +function Current:redraw() local files = self._folder.window if #files == 0 then return self:empty() @@ -31,8 +33,8 @@ function Current:render() local entities, linemodes = {}, {} for _, f in ipairs(files) do - linemodes[#linemodes + 1] = Linemode:new(f):render() - entities[#entities + 1] = Entity:new(f):render() + entities[#entities + 1] = Entity:new(f):redraw() + linemodes[#linemodes + 1] = Linemode:new(f):redraw() end return { diff --git a/yazi-plugin/preset/components/entity.lua b/yazi-plugin/preset/components/entity.lua index 5da34f02..852fbb78 100644 --- a/yazi-plugin/preset/components/entity.lua +++ b/yazi-plugin/preset/components/entity.lua @@ -76,7 +76,7 @@ function Entity:symlink() return to and ui.Line(" -> " .. tostring(to)):italic() or ui.Line {} end -function Entity:render() +function Entity:redraw() local lines = {} for _, c in ipairs(self._children) do lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) diff --git a/yazi-plugin/preset/components/header.lua b/yazi-plugin/preset/components/header.lua index d8340808..1a59a09d 100644 --- a/yazi-plugin/preset/components/header.lua +++ b/yazi-plugin/preset/components/header.lua @@ -17,6 +17,7 @@ function Header:new(area, tab) return setmetatable({ _area = area, _tab = tab, + _current = tab.current, }, { __index = self }) end @@ -26,22 +27,26 @@ function Header:cwd() return ui.Span("") end - local s = ya.readable_path(tostring(self._tab.current.cwd)) .. self:flags() + local s = ya.readable_path(tostring(self._current.cwd)) .. self:flags() return ui.Span(ya.truncate(s, { max = max, rtl = true })):style(THEME.manager.cwd) end function Header:flags() - local cwd = self._tab.current.cwd - local filter = self._tab.current.files.filter + local cwd = self._current.cwd + local filter = self._current.files.filter + local finder = self._tab.finder - local s = cwd.is_search and string.format(" (search: %s", cwd:frag()) or "" - if not filter then - return s == "" and s or s .. ")" - elseif s == "" then - return string.format(" (filter: %s)", tostring(filter)) - else - return string.format("%s, filter: %s)", s, tostring(filter)) + local t = {} + if cwd.is_search then + t[#t + 1] = string.format("search: %s", cwd:frag()) end + if filter then + t[#t + 1] = string.format("filter: %s", filter) + end + if finder then + t[#t + 1] = string.format("find: %s", finder) + end + return #t == 0 and "" or " (" .. table.concat(t, ", ") .. ")" end function Header:count() @@ -65,7 +70,7 @@ function Header:count() return ui.Line { ui.Span(string.format(" %d ", count)):style(style), - ui.Span(" "), + " ", } end @@ -90,11 +95,14 @@ function Header:tabs() return ui.Line(spans) end -function Header:render() - local right = self:children_render(self.RIGHT) +function Header:reflow() return { self } end + +function Header:redraw() + local right = self:children_redraw(self.RIGHT) self._right_width = right:width() - local left = self:children_render(self.LEFT) + local left = self:children_redraw(self.LEFT) + return { ui.Text(left):area(self._area), ui.Text(right):area(self._area):align(ui.Text.RIGHT), @@ -129,7 +137,7 @@ function Header:children_remove(id, side) end end -function Header:children_render(side) +function Header:children_redraw(side) local lines = {} for _, c in ipairs(side == self.RIGHT and self._right or self._left) do lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) diff --git a/yazi-plugin/preset/components/linemode.lua b/yazi-plugin/preset/components/linemode.lua index 42c26008..6d59c423 100644 --- a/yazi-plugin/preset/components/linemode.lua +++ b/yazi-plugin/preset/components/linemode.lua @@ -62,7 +62,7 @@ function Linemode:owner() return ui.Line(string.format("%s:%s", user or "-", group or "-")) end -function Linemode:render() +function Linemode:redraw() local lines = {} for _, c in ipairs(self._children) do lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) diff --git a/yazi-plugin/preset/components/marker.lua b/yazi-plugin/preset/components/marker.lua index f75b370b..dea96bab 100644 --- a/yazi-plugin/preset/components/marker.lua +++ b/yazi-plugin/preset/components/marker.lua @@ -9,7 +9,7 @@ function Marker:new(area, folder) }, { __index = self }) end -function Marker:render() +function Marker:redraw() if self._area.w * self._area.h == 0 then return {} elseif not self._folder or #self._folder.window == 0 then @@ -29,7 +29,7 @@ function Marker:render() w = 1, h = math.min(1 + last[2] - last[1], self._area.y + self._area.h - y), } - elements[#elements + 1] = ui.Bar(rect, ui.Bar.LEFT):style(last[3]) + elements[#elements + 1] = ui.Bar(ui.Bar.LEFT):area(rect):style(last[3]) end local last = { 0, 0, nil } -- start, end, style diff --git a/yazi-plugin/preset/components/parent.lua b/yazi-plugin/preset/components/parent.lua index c9ecdd60..b257f82c 100644 --- a/yazi-plugin/preset/components/parent.lua +++ b/yazi-plugin/preset/components/parent.lua @@ -10,18 +10,20 @@ function Parent:new(area, tab) }, { __index = self }) end -function Parent:render() +function Parent:reflow() return { self } end + +function Parent:redraw() if not self._folder then return {} end - local items = {} + local entities = {} for _, f in ipairs(self._folder.window) do - items[#items + 1] = Entity:new(f):render() + entities[#entities + 1] = Entity:new(f):redraw() end return { - ui.List(items):area(self._area), + ui.List(entities):area(self._area), } end diff --git a/yazi-plugin/preset/components/preview.lua b/yazi-plugin/preset/components/preview.lua index 63b5c6d6..87eaf031 100644 --- a/yazi-plugin/preset/components/preview.lua +++ b/yazi-plugin/preset/components/preview.lua @@ -10,7 +10,9 @@ function Preview:new(area, tab) }, { __index = self }) end -function Preview:render() return {} end +function Preview:reflow() return { self } end + +function Preview:redraw() return {} end -- Mouse events function Preview:click(event, up) diff --git a/yazi-plugin/preset/components/progress.lua b/yazi-plugin/preset/components/progress.lua index 463f8935..c9cf53dd 100644 --- a/yazi-plugin/preset/components/progress.lua +++ b/yazi-plugin/preset/components/progress.lua @@ -1,27 +1,33 @@ Progress = { - _area = ui.Rect.default, -- TODO: remove this + _id = "progress", } -function Progress:render(area, offset) - self._area = ui.Rect { - x = math.max(0, area.w - offset - 21), - y = area.y, - w = ya.clamp(0, area.w - offset - 1, 20), - h = math.min(1, area.h), - } - return self:partial_render() +function Progress:new(area, offset) + local me = setmetatable({ _area = area, _offset = offset }, { __index = self }) + me:layout() + return me end --- Progress bars usually need frequent updates to report the latest task progress. --- We use `partial_render()` to partially render it when there is progress change, --- which has almost no cost compared to a full render by `render()`. -function Progress:partial_render() +function Progress:use(area) return setmetatable({ _area = area }, { __index = self }) end + +function Progress:layout() + self._area = ui.Rect { + x = math.max(0, self._area.w - self._offset - 21), + y = self._area.y, + w = ya.clamp(0, self._area.w - self._offset - 1, 20), + h = math.min(1, self._area.h), + } +end + +function Progress:reflow() return { self } end + +function Progress:redraw() local progress = cx.tasks.progress if progress.total == 0 then - return { ui.Text {} } + return {} end - local gauge = ui.Gauge(self._area) + local gauge = ui.Gauge():area(self._area) if progress.fail == 0 then gauge = gauge:gauge_style(THEME.status.progress_normal) else diff --git a/yazi-plugin/preset/components/rail.lua b/yazi-plugin/preset/components/rail.lua index 822ec942..8f6011ef 100644 --- a/yazi-plugin/preset/components/rail.lua +++ b/yazi-plugin/preset/components/rail.lua @@ -11,8 +11,8 @@ end function Rail:build() self._base = { - ui.Bar(self._chunks[1], ui.Bar.RIGHT):symbol(THEME.manager.border_symbol):style(THEME.manager.border_style), - ui.Bar(self._chunks[3], ui.Bar.LEFT):symbol(THEME.manager.border_symbol):style(THEME.manager.border_style), + ui.Bar(ui.Bar.RIGHT):area(self._chunks[1]):symbol(THEME.manager.border_symbol):style(THEME.manager.border_style), + ui.Bar(ui.Bar.LEFT):area(self._chunks[3]):symbol(THEME.manager.border_symbol):style(THEME.manager.border_style), } self._children = { Marker:new(self._chunks[1], self._tab.parent), @@ -20,12 +20,14 @@ function Rail:build() } end -function Rail:render() - local children = self._base or {} +function Rail:reflow() return { self } end + +function Rail:redraw() + local elements = self._base or {} for _, child in ipairs(self._children) do - children = ya.list_merge(children, ya.render_with(child)) + elements = ya.list_merge(elements, ya.redraw_with(child)) end - return children + return elements end -- Mouse events diff --git a/yazi-plugin/preset/components/root.lua b/yazi-plugin/preset/components/root.lua index 61fc6cd0..d373bf42 100644 --- a/yazi-plugin/preset/components/root.lua +++ b/yazi-plugin/preset/components/root.lua @@ -29,12 +29,20 @@ function Root:build() } end -function Root:render() - local children = self._base or {} +function Root:reflow() + local components = { self } for _, child in ipairs(self._children) do - children = ya.list_merge(children, ya.render_with(child)) + components = ya.list_merge(components, child:reflow()) end - return children + return components +end + +function Root:redraw() + local elements = self._base or {} + for _, child in ipairs(self._children) do + elements = ya.list_merge(elements, ya.redraw_with(child)) + end + return elements end -- Mouse events diff --git a/yazi-plugin/preset/components/status.lua b/yazi-plugin/preset/components/status.lua index 1c64e40c..ddc6cbaf 100644 --- a/yazi-plugin/preset/components/status.lua +++ b/yazi-plugin/preset/components/status.lua @@ -10,8 +10,8 @@ Status = { { "name", id = 3, order = 3000 }, }, _right = { - { "permissions", id = 4, order = 1000 }, - { "percentage", id = 5, order = 2000 }, + { "perm", id = 4, order = 1000 }, + { "percent", id = 5, order = 2000 }, { "position", id = 6, order = 3000 }, }, } @@ -20,6 +20,7 @@ function Status:new(area, tab) return setmetatable({ _area = area, _tab = tab, + _current = tab.current, }, { __index = self }) end @@ -45,7 +46,7 @@ function Status:mode() end function Status:size() - local h = self._tab.current.hovered + local h = self._current.hovered if not h then return ui.Line {} end @@ -58,7 +59,7 @@ function Status:size() end function Status:name() - local h = self._tab.current.hovered + local h = self._current.hovered if not h then return ui.Line {} end @@ -66,8 +67,8 @@ function Status:name() return ui.Line(" " .. h.name) end -function Status:permissions() - local h = self._tab.current.hovered +function Status:perm() + local h = self._current.hovered if not h then return ui.Line {} end @@ -95,10 +96,10 @@ function Status:permissions() return ui.Line(spans) end -function Status:percentage() +function Status:percent() local percent = 0 - local cursor = self._tab.current.cursor - local length = #self._tab.current.files + local cursor = self._current.cursor + local length = #self._current.files if cursor ~= 0 and length ~= 0 then percent = math.floor((cursor + 1) * 100 / length) end @@ -119,8 +120,8 @@ function Status:percentage() end function Status:position() - local cursor = self._tab.current.cursor - local length = #self._tab.current.files + local cursor = self._current.cursor + local length = #self._current.files local style = self:style() return ui.Line { @@ -130,14 +131,18 @@ function Status:position() } end -function Status:render() - local left = self:children_render(self.LEFT) - local right = self:children_render(self.RIGHT) +function Status:reflow() return { self } end + +function Status:redraw() + local left = self:children_redraw(self.LEFT) + + local right = self:children_redraw(self.RIGHT) local right_width = right:width() + return { ui.Text(left):area(self._area), ui.Text(right):area(self._area):align(ui.Text.RIGHT), - table.unpack(Progress:render(self._area, right_width)), + table.unpack(ya.redraw_with(Progress:new(self._area, right_width))), } end @@ -169,7 +174,7 @@ function Status:children_remove(id, side) end end -function Status:children_render(side) +function Status:children_redraw(side) local lines = {} for _, c in ipairs(side == self.RIGHT and self._right or self._left) do lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) diff --git a/yazi-plugin/preset/components/tab.lua b/yazi-plugin/preset/components/tab.lua index 7d7abdc5..1246f110 100644 --- a/yazi-plugin/preset/components/tab.lua +++ b/yazi-plugin/preset/components/tab.lua @@ -29,12 +29,20 @@ function Tab:build() } end -function Tab:render() - local children = self._base or {} +function Tab:reflow() + local components = { self } for _, child in ipairs(self._children) do - children = ya.list_merge(children, ya.render_with(child)) + components = ya.list_merge(components, child:reflow()) end - return children + return components +end + +function Tab:redraw() + local elements = self._base or {} + for _, child in ipairs(self._children) do + elements = ya.list_merge(elements, ya.redraw_with(child)) + end + return elements end -- Mouse events diff --git a/yazi-plugin/preset/plugins/archive.lua b/yazi-plugin/preset/plugins/archive.lua index e992d630..0d139d05 100644 --- a/yazi-plugin/preset/plugins/archive.lua +++ b/yazi-plugin/preset/plugins/archive.lua @@ -7,9 +7,7 @@ function M:peek() local files, bound, code = self.list_files({ "-p", tostring(self.file.url) }, self.skip, limit) if code ~= 0 then return ya.preview_widgets(self, { - ui.Text( - ui.Line(code == 2 and "File list in this archive is encrypted" or "Spawn `7z` and `7zz` both commands failed") - ) + ui.Text(code == 2 and "File list in this archive is encrypted" or "Spawn `7z` and `7zz` both commands failed") :area(self.area), }) end @@ -27,9 +25,9 @@ function M:peek() end if f.size > 0 then - sizes[#sizes + 1] = ui.Line(string.format(" %s ", ya.readable_size(f.size))) + sizes[#sizes + 1] = string.format(" %s ", ya.readable_size(f.size)) else - sizes[#sizes + 1] = ui.Line("") + sizes[#sizes + 1] = "" end end @@ -81,7 +79,7 @@ end --- 2: wrong password --- 3: partial success function M.list_files(args, skip, limit) - local child = M.spawn_7z { "l", "-ba", "-slt", table.unpack(args) } + local child = M.spawn_7z { "l", "-ba", "-slt", "-sccUTF-8", table.unpack(args) } if not child then return {}, 0, 1 end @@ -138,7 +136,7 @@ end --- 2: wrong password --- 3: partial success function M.list_meta(args) - local child = M.spawn_7z { "l", "-slt", table.unpack(args) } + local child = M.spawn_7z { "l", "-slt", "-sccUTF-8", table.unpack(args) } if not child then return nil, 1 end diff --git a/yazi-plugin/preset/plugins/code.lua b/yazi-plugin/preset/plugins/code.lua index 8396dc34..5efdd8a1 100644 --- a/yazi-plugin/preset/plugins/code.lua +++ b/yazi-plugin/preset/plugins/code.lua @@ -6,7 +6,7 @@ function M:peek() ya.manager_emit("peek", { bound, only_if = self.file.url, upper_bound = true }) elseif err and not err:find("cancelled", 1, true) then ya.preview_widgets(self, { - ui.Text(ui.Line(err):reverse()):area(self.area), + ui.Text(err):area(self.area):reverse(), }) end end diff --git a/yazi-plugin/preset/plugins/empty.lua b/yazi-plugin/preset/plugins/empty.lua index bc664a8b..8c7bd21e 100644 --- a/yazi-plugin/preset/plugins/empty.lua +++ b/yazi-plugin/preset/plugins/empty.lua @@ -1,6 +1,6 @@ local M = {} -function M:msg(s) ya.preview_widgets(self, { ui.Text(ui.Line(s):reverse()):area(self.area):wrap(ui.Text.WRAP) }) end +function M:msg(s) ya.preview_widgets(self, { ui.Text(s):area(self.area):reverse():wrap(ui.Text.WRAP) }) end function M:peek() local path = tostring(self.file.url) diff --git a/yazi-plugin/preset/plugins/extract.lua b/yazi-plugin/preset/plugins/extract.lua index 3ed701d3..96770870 100644 --- a/yazi-plugin/preset/plugins/extract.lua +++ b/yazi-plugin/preset/plugins/extract.lua @@ -48,7 +48,7 @@ function M:try_with(from, pwd, to) end local archive = require("archive") - local child, code = archive.spawn_7z { "x", "-aou", "-p" .. pwd, "-o" .. tostring(tmp), tostring(from) } + local child, code = archive.spawn_7z { "x", "-aou", "-sccUTF-8", "-p" .. pwd, "-o" .. tostring(tmp), tostring(from) } if not child then fail("Spawn `7z` and `7zz` both commands failed, error code %s", code) end @@ -105,7 +105,7 @@ function M:tidy(from, to, tmp) fs.remove("dir", tmp) end -function M.tmp_name(url) return ".tmp_" .. ya.md5(string.format("extract//%s//%.10f", url, ya.time())) end +function M.tmp_name(url) return ".tmp_" .. ya.hash(string.format("extract//%s//%.10f", url, ya.time())) end function M.trim_ext(name) -- stylua: ignore diff --git a/yazi-plugin/preset/plugins/file.lua b/yazi-plugin/preset/plugins/file.lua index cc8b8255..f271f9aa 100644 --- a/yazi-plugin/preset/plugins/file.lua +++ b/yazi-plugin/preset/plugins/file.lua @@ -2,16 +2,16 @@ local M = {} function M:peek() local cmd = os.getenv("YAZI_FILE_ONE") or "file" - local output, code = Command(cmd):args({ "-bL", tostring(self.file.url) }):stdout(Command.PIPED):output() + local output, code = Command(cmd):args({ "-bL", "--", tostring(self.file.url) }):stdout(Command.PIPED):output() - local p + local text if output then - p = ui.Text.parse("----- File Type Classification -----\n\n" .. output.stdout):area(self.area) + text = ui.Text.parse("----- File Type Classification -----\n\n" .. output.stdout) else - p = ui.Text(string.format("Spawn `%s` command returns %s", cmd, code)):area(self.area) + text = ui.Text(string.format("Spawn `%s` command returns %s", cmd, code)) end - ya.preview_widgets(self, { p:wrap(ui.Text.WRAP) }) + ya.preview_widgets(self, { text:area(self.area):wrap(ui.Text.WRAP) }) end function M:seek() end diff --git a/yazi-plugin/preset/plugins/folder.lua b/yazi-plugin/preset/plugins/folder.lua index 61fcc8e8..ab8f96ea 100644 --- a/yazi-plugin/preset/plugins/folder.lua +++ b/yazi-plugin/preset/plugins/folder.lua @@ -13,18 +13,18 @@ function M:peek() if #folder.files == 0 then return ya.preview_widgets(self, { - ui.Text(ui.Line(folder.stage.is_loading and "Loading..." or "No items")):area(self.area):align(ui.Text.CENTER), + ui.Text(folder.stage.is_loading and "Loading..." or "No items"):area(self.area):align(ui.Text.CENTER), }) end - local items = {} + local entities = {} for _, f in ipairs(folder.window) do - items[#items + 1] = Entity:new(f):render() + entities[#entities + 1] = Entity:new(f):redraw() end ya.preview_widgets(self, { - ui.List(items):area(self.area), - table.unpack(Marker:new(self.area, folder):render()), + ui.List(entities):area(self.area), + table.unpack(Marker:new(self.area, folder):redraw()), }) end diff --git a/yazi-plugin/preset/plugins/mime.lua b/yazi-plugin/preset/plugins/mime.lua index b0613c06..9d6772e6 100644 --- a/yazi-plugin/preset/plugins/mime.lua +++ b/yazi-plugin/preset/plugins/mime.lua @@ -16,7 +16,7 @@ function M:fetch() end local cmd = os.getenv("YAZI_FILE_ONE") or "file" - local child, code = Command(cmd):args({ "-bL", "--mime-type" }):args(urls):stdout(Command.PIPED):spawn() + local child, code = Command(cmd):args({ "-bL", "--mime-type", "--" }):args(urls):stdout(Command.PIPED):spawn() if not child then ya.err(string.format("Spawn `%s` command returns %s", cmd, code)) return 0 diff --git a/yazi-plugin/preset/plugins/video.lua b/yazi-plugin/preset/plugins/video.lua index c4846ed5..74e462ff 100644 --- a/yazi-plugin/preset/plugins/video.lua +++ b/yazi-plugin/preset/plugins/video.lua @@ -22,8 +22,8 @@ function M:seek(units) end function M:preload() - local percentage = 5 + self.skip - if percentage > 95 then + local percent = 5 + self.skip + if percent > 95 then ya.manager_emit("peek", { 90, only_if = self.file.url, upper_bound = true }) return 2 end @@ -48,7 +48,7 @@ function M:preload() "-o", tostring(cache), "-t", - tostring(percentage), + tostring(percent), "-s", tostring(PREVIEW.max_width), }):spawn() diff --git a/yazi-plugin/preset/plugins/zoxide.lua b/yazi-plugin/preset/plugins/zoxide.lua index 0e2c21d5..b3687361 100644 --- a/yazi-plugin/preset/plugins/zoxide.lua +++ b/yazi-plugin/preset/plugins/zoxide.lua @@ -9,24 +9,54 @@ local set_state = ya.sync(function(st, empty) st.empty = empty end) local function fail(s, ...) ya.notify { title = "Zoxide", content = s:format(...), timeout = 5, level = "error" } end -local function head(cwd) - local child = Command("zoxide"):args({ "query", "-l" }):stdout(Command.PIPED):spawn() - if not child then - return 0 +local function opts() + -- https://github.com/ajeetdsouza/zoxide/blob/main/src/cmd/query.rs#L92 + local default = { + -- Search mode + "--exact", + -- Search result + "--no-sort", + -- Interface + "--bind=ctrl-z:ignore,btab:up,tab:down", + "--cycle", + "--keep-right", + -- Layout + "--layout=reverse", + "--height=50%", + "--border", + "--scrollbar=▌", + "--info=inline", + -- Display + "--tabstop=1", + -- Scripting + "--exit-0", + } + + if ya.target_family() == "unix" then + default[#default + 1] = "--preview-window=down,30%,sharp" + if ya.target_os() == "linux" then + default[#default + 1] = [[--preview='\command -p ls -Cp --color=always --group-directories-first {2..}']] + else + default[#default + 1] = [[--preview='\command -p ls -Cp {2..}']] + end end - local n = 0 - repeat - local next, event = child:read_line() - if event ~= 0 then - break - elseif cwd ~= next:gsub("\n$", "") then - n = n + 1 - end - until n >= 2 + return (os.getenv("FZF_DEFAULT_OPTS") or "") + .. " " + .. table.concat(default, " ") + .. " " + .. (os.getenv("YAZI_ZOXIDE_OPTS") or "") +end +local function empty(cwd) + local child = Command("zoxide"):args({ "query", "-l", "--exclude" }):arg(cwd):stdout(Command.PIPED):spawn() + if not child then + return true + end + + local first = child:read_line() child:start_kill() - return n + return not first end local function setup(_, opts) @@ -49,18 +79,22 @@ end local function entry() local st = state() if st.empty == nil then - st.empty = head(st.cwd) < 2 + st.empty = empty(st.cwd) set_state(st.empty) end if st.empty then - return fail("No directory history in the database, check out the `zoxide` docs to set it up.") + return fail("No directory history found, check Zoxide's doc to set it up and restart Yazi.") end local _permit = ya.hide() local child, err = Command("zoxide") :args({ "query", "-i", "--exclude" }) :arg(st.cwd) + :env("SHELL", "sh") + :env("CLICOLOR", "1") + :env("CLICOLOR_FORCE", "1") + :env("_ZO_FZF_OPTS", opts()) :stdin(Command.INHERIT) :stdout(Command.PIPED) :stderr(Command.INHERIT) diff --git a/yazi-plugin/src/cha/cha.rs b/yazi-plugin/src/cha/cha.rs index beebfc4d..95e0ed13 100644 --- a/yazi-plugin/src/cha/cha.rs +++ b/yazi-plugin/src/cha/cha.rs @@ -67,7 +67,7 @@ impl Cha { reg.add_method("perm", |_, _me, ()| { Ok( #[cfg(unix)] - Some(yazi_shared::fs::permissions(_me.perm, _me.is_dummy())), + Some(yazi_shared::fs::permissions(_me.mode, _me.is_dummy())), #[cfg(windows)] None::, ) @@ -95,7 +95,7 @@ impl Cha { warn_deprecated(lua.named_registry_value::("rt")?.current()); Ok( #[cfg(unix)] - Some(yazi_shared::fs::permissions(_me.perm, _me.is_dummy())), + Some(yazi_shared::fs::permissions(_me.mode, _me.is_dummy())), #[cfg(windows)] None::, ) @@ -131,7 +131,7 @@ impl Cha { ctime: parse_time(t.raw_get("ctime").ok())?, mtime: parse_time(t.raw_get("mtime").ok())?, #[cfg(unix)] - perm: t.raw_get("permissions").unwrap_or_default(), + mode: t.raw_get("mode").unwrap_or_default(), #[cfg(unix)] uid: t.raw_get("uid").unwrap_or_default(), #[cfg(unix)] diff --git a/yazi-plugin/src/elements/bar.rs b/yazi-plugin/src/elements/bar.rs index 769a3b24..bb0e4953 100644 --- a/yazi-plugin/src/elements/bar.rs +++ b/yazi-plugin/src/elements/bar.rs @@ -3,7 +3,7 @@ use ratatui::widgets::Borders; use super::{Rect, Renderable}; -#[derive(Clone)] +#[derive(Clone, Default)] pub struct Bar { area: Rect, @@ -14,14 +14,8 @@ pub struct Bar { impl Bar { pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { - let new = lua.create_function(|_, (_, area, direction): (Table, Rect, u8)| { - Ok(Self { - area, - - direction: Borders::from_bits_truncate(direction), - symbol: Default::default(), - style: Default::default(), - }) + let new = lua.create_function(|_, (_, direction): (Table, u8)| { + Ok(Self { direction: Borders::from_bits_truncate(direction), ..Default::default() }) })?; let bar = lua.create_table_from([ diff --git a/yazi-plugin/src/elements/border.rs b/yazi-plugin/src/elements/border.rs index d25c25a0..de86e1e2 100644 --- a/yazi-plugin/src/elements/border.rs +++ b/yazi-plugin/src/elements/border.rs @@ -22,9 +22,8 @@ pub struct Border { impl Border { pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { - let new = lua.create_function(|_, (_, area, position): (Table, Rect, u8)| { + let new = lua.create_function(|_, (_, position): (Table, u8)| { Ok(Border { - area, position: ratatui::widgets::Borders::from_bits_truncate(position), ..Default::default() }) diff --git a/yazi-plugin/src/elements/constraint.rs b/yazi-plugin/src/elements/constraint.rs index 777455b7..34ffecc3 100644 --- a/yazi-plugin/src/elements/constraint.rs +++ b/yazi-plugin/src/elements/constraint.rs @@ -1,42 +1,11 @@ use mlua::{FromLua, Lua, Table, UserData}; -#[derive(Clone, Copy, FromLua)] +#[derive(Clone, Copy, Default, FromLua)] pub struct Constraint(pub(super) ratatui::layout::Constraint); impl Constraint { - pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { - let constraint = lua.create_table_from([ - ( - "Min", - lua.create_function(|_, n: u16| Ok(Constraint(ratatui::layout::Constraint::Min(n))))?, - ), - ( - "Max", - lua.create_function(|_, n: u16| Ok(Constraint(ratatui::layout::Constraint::Max(n))))?, - ), - ( - "Length", - lua.create_function(|_, n: u16| Ok(Constraint(ratatui::layout::Constraint::Length(n))))?, - ), - ( - "Percentage", - lua.create_function(|_, n: u16| { - Ok(Constraint(ratatui::layout::Constraint::Percentage(n))) - })?, - ), - ( - "Ratio", - lua.create_function(|_, (a, b): (u32, u32)| { - Ok(Constraint(ratatui::layout::Constraint::Ratio(a, b))) - })?, - ), - ( - "Fill", - lua.create_function(|_, n: u16| Ok(Constraint(ratatui::layout::Constraint::Fill(n))))?, - ), - ])?; - - ui.raw_set("Constraint", constraint) + pub fn install(_: &Lua, ui: &Table) -> mlua::Result<()> { + ui.raw_set("Constraint", Constraint::default()) } } @@ -44,4 +13,15 @@ impl From for ratatui::layout::Constraint { fn from(value: Constraint) -> Self { value.0 } } -impl UserData for Constraint {} +impl UserData for Constraint { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + use ratatui::layout::Constraint as C; + + methods.add_function("Min", |_, n: u16| Ok(Self(C::Min(n)))); + methods.add_function("Max", |_, n: u16| Ok(Self(C::Max(n)))); + methods.add_function("Length", |_, n: u16| Ok(Self(C::Length(n)))); + methods.add_function("Percentage", |_, n: u16| Ok(Self(C::Percentage(n)))); + methods.add_function("Ratio", |_, (a, b): (u32, u32)| Ok(Self(C::Ratio(a, b)))); + methods.add_function("Fill", |_, n: u16| Ok(Self(C::Fill(n)))); + } +} diff --git a/yazi-plugin/src/elements/elements.rs b/yazi-plugin/src/elements/elements.rs index aa8c6be7..537cfed9 100644 --- a/yazi-plugin/src/elements/elements.rs +++ b/yazi-plugin/src/elements/elements.rs @@ -16,6 +16,7 @@ pub fn pour(lua: &Lua) -> mlua::Result<()> { super::Line::install(lua, &ui)?; super::List::install(lua, &ui)?; super::Padding::install(lua, &ui)?; + super::Paragraph::install(lua, &ui)?; super::Position::install(lua, &ui)?; super::Rect::install(lua, &ui)?; super::Span::install(lua, &ui)?; diff --git a/yazi-plugin/src/elements/gauge.rs b/yazi-plugin/src/elements/gauge.rs index 9561db68..fbfd00be 100644 --- a/yazi-plugin/src/elements/gauge.rs +++ b/yazi-plugin/src/elements/gauge.rs @@ -16,10 +16,7 @@ pub struct Gauge { impl Gauge { pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { - ui.raw_set( - "Gauge", - lua.create_function(|_, area: Rect| Ok(Gauge { area, ..Default::default() }))?, - ) + ui.raw_set("Gauge", lua.create_function(|_, ()| Ok(Gauge::default()))?) } } diff --git a/yazi-plugin/src/elements/line.rs b/yazi-plugin/src/elements/line.rs index d025e005..0c91c404 100644 --- a/yazi-plugin/src/elements/line.rs +++ b/yazi-plugin/src/elements/line.rs @@ -10,49 +10,14 @@ const LEFT: u8 = 0; const CENTER: u8 = 1; const RIGHT: u8 = 2; +const EXPECTED: &str = "expected a string, ui.Span, ui.Line, or a table of them"; + #[derive(Clone, FromLua)] pub struct Line(pub(super) ratatui::text::Line<'static>); -impl TryFrom> for Line { - type Error = mlua::Error; - - fn try_from(tb: Table) -> Result { - let seq: Vec<_> = tb.sequence_values().filter_map(|v| v.ok()).collect(); - let mut spans = Vec::with_capacity(seq.len()); - for value in seq { - if let Value::UserData(ud) = value { - if let Ok(span) = ud.take::() { - spans.push(span.0); - } else if let Ok(line) = ud.take::() { - let style = line.0.style; - spans.extend(line.0.spans.into_iter().map(|mut span| { - span.style = style.patch(span.style); - span - })); - } else { - return Err("expected a table of Spans or Lines".into_lua_err()); - } - } - } - Ok(Self(ratatui::text::Line::from(spans))) - } -} - -impl TryFrom> for Line { - type Error = mlua::Error; - - fn try_from(s: mlua::String) -> Result { - Ok(Self(ratatui::text::Line::from(s.to_string_lossy().into_owned()))) - } -} - impl Line { pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { - let new = lua.create_function(|_, (_, value): (Table, Value)| match value { - Value::Table(tb) => Line::try_from(tb), - Value::String(s) => Line::try_from(s), - _ => Err("expected a String, or a table of Spans and Lines".into_lua_err()), - })?; + let new = lua.create_function(|_, (_, value): (Table, Value)| Line::try_from(value))?; let parse = lua.create_function(|_, code: mlua::String| { let Some(line) = code.as_bytes().split_inclusive(|&b| b == b'\n').next() else { @@ -81,6 +46,53 @@ impl Line { } } +impl TryFrom> for Line { + type Error = mlua::Error; + + fn try_from(value: Value) -> Result { + Ok(Self(match value { + Value::Table(tb) => return Self::try_from(tb), + Value::String(s) => s.to_string_lossy().into_owned().into(), + Value::UserData(ud) => { + if let Ok(span) = ud.take::() { + span.0.into() + } else if let Ok(mut line) = ud.take::() { + line.0.spans.iter_mut().for_each(|s| s.style = line.0.style.patch(s.style)); + line.0 + } else { + Err(EXPECTED.into_lua_err())? + } + } + _ => Err(EXPECTED.into_lua_err())?, + })) + } +} + +impl TryFrom> for Line { + type Error = mlua::Error; + + fn try_from(tb: Table) -> Result { + let mut spans = Vec::with_capacity(tb.raw_len()); + for v in tb.sequence_values() { + match v? { + Value::String(s) => spans.push(s.to_string_lossy().into_owned().into()), + Value::UserData(ud) => { + if let Ok(span) = ud.take::() { + spans.push(span.0); + } else if let Ok(mut line) = ud.take::() { + line.0.spans.iter_mut().for_each(|s| s.style = line.0.style.patch(s.style)); + spans.extend(line.0.spans); + } else { + return Err(EXPECTED.into_lua_err()); + } + } + _ => Err(EXPECTED.into_lua_err())?, + } + } + Ok(Self(spans.into())) + } +} + impl UserData for Line { fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { crate::impl_style_method!(methods, 0.style); diff --git a/yazi-plugin/src/elements/list.rs b/yazi-plugin/src/elements/list.rs index 3e7c31b5..a2c5b362 100644 --- a/yazi-plugin/src/elements/list.rs +++ b/yazi-plugin/src/elements/list.rs @@ -1,4 +1,4 @@ -use mlua::{Lua, Table, UserData, Value}; +use mlua::{ExternalError, Lua, Table, UserData, Value}; use ratatui::widgets::Widget; use super::{Rect, Renderable, Text}; @@ -15,10 +15,13 @@ impl List { pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { ui.raw_set( "List", - lua.create_function(|_, values: Vec| { - let mut items = Vec::with_capacity(values.len()); - for value in values { - items.push(ratatui::widgets::ListItem::new(Text::try_from(value)?)); + lua.create_function(|_, tb: Table| { + let mut items = Vec::with_capacity(tb.raw_len()); + for v in tb.sequence_values::() { + match v? { + Value::Table(_) => Err("Nested table not supported".into_lua_err())?, + v => items.push(Text::try_from(v)?), + } } Ok(Self { inner: ratatui::widgets::List::new(items), ..Default::default() }) diff --git a/yazi-plugin/src/elements/mod.rs b/yazi-plugin/src/elements/mod.rs index 57ccc3bc..59439aa7 100644 --- a/yazi-plugin/src/elements/mod.rs +++ b/yazi-plugin/src/elements/mod.rs @@ -1,3 +1,3 @@ #![allow(clippy::module_inception)] -yazi_macro::mod_flat!(bar border clear constraint elements gauge layout line list padding position rect span style table text); +yazi_macro::mod_flat!(bar border clear constraint elements gauge layout line list padding paragraph position rect span style table text); diff --git a/yazi-plugin/src/elements/paragraph.rs b/yazi-plugin/src/elements/paragraph.rs new file mode 100644 index 00000000..df5b6f7c --- /dev/null +++ b/yazi-plugin/src/elements/paragraph.rs @@ -0,0 +1,65 @@ +use std::time::Duration; + +use mlua::{FromLua, Lua, MultiValue, Table, Value}; + +use super::Rect; +use crate::RtRef; + +static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +#[inline] +fn warn_deprecated(id: Option<&str>) { + if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) { + let id = match id { + Some(id) => format!("`{id}.yazi` plugin"), + None => "`init.lua` config".to_owned(), + }; + let s = "The `ui.Paragraph` and `ui.ListItem` elements have been deprecated in Yazi v0.4. + +Please use the new `ui.Text` instead, in your {id}. See https://github.com/sxyazi/yazi/pull/1776 for details."; + yazi_proxy::AppProxy::notify(yazi_proxy::options::NotifyOpt { + title: "Deprecated API".to_owned(), + content: s.replace("{id}", &id), + level: yazi_proxy::options::NotifyLevel::Warn, + timeout: Duration::from_secs(20), + }); + } +} + +// TODO: remove this after v0.4 release +#[derive(Clone, Default, FromLua)] +pub struct Paragraph; + +impl Paragraph { + pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { + let mt = lua.create_table_from([ + ( + "__call", + lua.create_function(|lua, (_, area, lines): (Table, Rect, Value)| { + warn_deprecated(lua.named_registry_value::("rt")?.current()); + lua + .load(mlua::chunk! { + return ui.Text($lines):area($area) + }) + .call::<_, MultiValue>(()) + })?, + ), + ( + "__index", + lua.create_function(|lua, (_, key): (Table, mlua::String)| { + warn_deprecated(lua.named_registry_value::("rt")?.current()); + lua + .load(mlua::chunk! { + return ui.Text[$key] + }) + .call::<_, MultiValue>(()) + })?, + ), + ])?; + + let paragraph = lua.create_table()?; + paragraph.set_metatable(Some(mt)); + + ui.raw_set("Paragraph", paragraph) + } +} diff --git a/yazi-plugin/src/elements/span.rs b/yazi-plugin/src/elements/span.rs index f44ced7c..ecdf0e1a 100644 --- a/yazi-plugin/src/elements/span.rs +++ b/yazi-plugin/src/elements/span.rs @@ -1,17 +1,32 @@ -use mlua::{FromLua, Lua, Table, UserData, UserDataMethods}; +use mlua::{ExternalError, FromLua, Lua, Table, UserData, UserDataMethods, Value}; use unicode_width::UnicodeWidthChar; +const EXPECTED: &str = "expected a string or ui.Span"; + #[derive(Clone, FromLua)] pub struct Span(pub(super) ratatui::text::Span<'static>); impl Span { pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { - ui.raw_set( - "Span", - lua.create_function(|_, content: mlua::String| { - Ok(Self(ratatui::text::Span::raw(content.to_string_lossy().into_owned()))) - })?, - ) + ui.raw_set("Span", lua.create_function(|_, value: Value| Span::try_from(value))?) + } +} + +impl TryFrom> for Span { + type Error = mlua::Error; + + fn try_from(value: Value<'_>) -> Result { + Ok(Self(match value { + Value::String(s) => s.to_string_lossy().into_owned().into(), + Value::UserData(ud) => { + if let Ok(span) = ud.take::() { + span.0 + } else { + Err(EXPECTED.into_lua_err())? + } + } + _ => Err(EXPECTED.into_lua_err())?, + })) } } diff --git a/yazi-plugin/src/elements/table.rs b/yazi-plugin/src/elements/table.rs index ab3016ed..5c62bc8e 100644 --- a/yazi-plugin/src/elements/table.rs +++ b/yazi-plugin/src/elements/table.rs @@ -9,17 +9,17 @@ use crate::elements::Constraint; pub struct Table { area: Rect, - rows: Vec>, - header: Option>, - footer: Option>, - widths: Vec, - column_spacing: u16, - block: Option>, - style: ratatui::style::Style, - highlight_style: ratatui::style::Style, - highlight_symbol: ratatui::text::Text<'static>, - highlight_spacing: ratatui::widgets::HighlightSpacing, - flex: ratatui::layout::Flex, + rows: Vec>, + header: Option>, + footer: Option>, + widths: Vec, + column_spacing: u16, + block: Option>, + style: ratatui::style::Style, + row_highlight_style: ratatui::style::Style, + highlight_symbol: ratatui::text::Text<'static>, + highlight_spacing: ratatui::widgets::HighlightSpacing, + flex: ratatui::layout::Flex, state: ratatui::widgets::TableState, } @@ -61,7 +61,7 @@ impl Renderable for Table { let mut table = ratatui::widgets::Table::new(self.rows, self.widths) .column_spacing(self.column_spacing) .style(self.style) - .highlight_style(self.highlight_style) + .row_highlight_style(self.row_highlight_style) .highlight_symbol(self.highlight_symbol) .highlight_spacing(self.highlight_spacing) .flex(self.flex); diff --git a/yazi-plugin/src/elements/text.rs b/yazi-plugin/src/elements/text.rs index 25a7d777..f51bd9b9 100644 --- a/yazi-plugin/src/elements/text.rs +++ b/yazi-plugin/src/elements/text.rs @@ -14,6 +14,8 @@ pub const WRAP_NO: u8 = 0; pub const WRAP: u8 = 1; pub const WRAP_TRIM: u8 = 2; +const EXPECTED: &str = "expected a string, ui.Line, ui.Span or a table of them"; + #[derive(Clone, Default, FromLua)] pub struct Text { pub area: Rect, @@ -54,29 +56,45 @@ impl TryFrom> for Text { type Error = mlua::Error; fn try_from(value: Value) -> mlua::Result { - match value { - Value::String(s) => { - Ok(Self { inner: s.to_string_lossy().into_owned().into(), ..Default::default() }) - } + let inner = match value { + Value::Table(tb) => return Self::try_from(tb), + Value::String(s) => s.to_string_lossy().into_owned().into(), Value::UserData(ud) => { - let inner: ratatui::text::Text = if let Ok(line) = ud.take::() { + if let Ok(line) = ud.take::() { line.0.into() } else if let Ok(span) = ud.take::() { span.0.into() } else { - return Err("expected a String, Line or Span".into_lua_err()); - }; - Ok(Self { inner, ..Default::default() }) - } - Value::Table(tb) => { - let mut lines = Vec::with_capacity(tb.raw_len()); - for v in tb.sequence_values::() { - lines.extend(Self::try_from(v?)?.inner.lines); + Err(EXPECTED.into_lua_err())? } - Ok(Self { inner: lines.into(), ..Default::default() }) } - _ => Err("expected a String, Line, Span or a Table of them".into_lua_err()), + _ => Err(EXPECTED.into_lua_err())?, + }; + Ok(Self { inner, ..Default::default() }) + } +} + +impl TryFrom> for Text { + type Error = mlua::Error; + + fn try_from(tb: Table<'_>) -> Result { + let mut lines = Vec::with_capacity(tb.raw_len()); + for v in tb.sequence_values() { + match v? { + Value::String(s) => lines.push(s.to_string_lossy().into_owned().into()), + Value::UserData(ud) => { + if let Ok(span) = ud.take::() { + lines.push(span.0.into()); + } else if let Ok(line) = ud.take::() { + lines.push(line.0); + } else { + return Err(EXPECTED.into_lua_err()); + } + } + _ => Err(EXPECTED.into_lua_err())?, + } } + Ok(Self { inner: lines.into(), ..Default::default() }) } } @@ -86,7 +104,7 @@ impl From for ratatui::text::Text<'static> { impl From for ratatui::widgets::Paragraph<'static> { fn from(value: Text) -> Self { - let align = value.inner.alignment.unwrap_or(ratatui::layout::Alignment::Left); + let align = value.inner.alignment.unwrap_or_default(); let mut p = ratatui::widgets::Paragraph::new(value.inner); if value.wrap != WRAP_NO { diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index dfc506ec..ef21db55 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -1,13 +1,13 @@ -use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}, sync::atomic::{AtomicUsize, Ordering}}; +use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}}; use anyhow::{Result, anyhow}; use ratatui::{layout::Rect, text::{Line, Span, Text}}; use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}}; use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}, sync::OnceCell}; use yazi_config::{PREVIEW, THEME, preview::PreviewWrap}; -use yazi_shared::{errors::PeekError, replace_to_printable}; +use yazi_shared::{Ids, errors::PeekError, replace_to_printable}; -static INCR: AtomicUsize = AtomicUsize::new(0); +static INCR: Ids = Ids::new(); static SYNTECT: OnceCell<(Theme, SyntaxSet)> = OnceCell::const_new(); pub struct Highlighter { @@ -39,7 +39,7 @@ impl Highlighter { } #[inline] - pub fn abort() { INCR.fetch_add(1, Ordering::Relaxed); } + pub fn abort() { INCR.next(); } pub async fn highlight(&self, skip: usize, area: Rect) -> Result, PeekError> { let mut reader = BufReader::new(File::open(&self.path).await?); @@ -104,13 +104,13 @@ impl Highlighter { after: Vec, syntax: &'static SyntaxReference, ) -> Result, PeekError> { - let ticket = INCR.load(Ordering::Relaxed); + let ticket = INCR.current(); let (theme, syntaxes) = Self::init().await; tokio::task::spawn_blocking(move || { let mut h = HighlightLines::new(syntax, theme); for line in before { - if ticket != INCR.load(Ordering::Relaxed) { + if ticket != INCR.current() { return Err("Highlighting cancelled".into()); } h.highlight_line(&line, syntaxes).map_err(|e| anyhow!(e))?; @@ -119,7 +119,7 @@ impl Highlighter { let indent = PREVIEW.indent(); let mut lines = Vec::with_capacity(after.len()); for line in after { - if ticket != INCR.load(Ordering::Relaxed) { + if ticket != INCR.current() { return Err("Highlighting cancelled".into()); } diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 61ba5464..b9b7cd47 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -110,7 +110,7 @@ pub fn install(lua: &Lua) -> mlua::Result<()> { Some( GlobBuilder::new(s.to_str()?) .case_insensitive(true) - .literal_separator(false) + .literal_separator(true) .backslash_escape(false) .empty_alternates(true) .build() @@ -162,7 +162,7 @@ pub fn install(lua: &Lua) -> mlua::Result<()> { ( "unique_name", lua.create_async_function(|lua, url: UrlRef| async move { - match yazi_shared::fs::unique_name(url.clone()).await { + match yazi_shared::fs::unique_name(url.clone(), async { false }).await { Ok(u) => (Url::cast(lua, u)?, Value::Nil).into_lua_multi(lua), Err(e) => (Value::Nil, e.raw_os_error()).into_lua_multi(lua), } diff --git a/yazi-plugin/src/isolate/fetch.rs b/yazi-plugin/src/isolate/fetch.rs index 1e92b36e..36863601 100644 --- a/yazi-plugin/src/isolate/fetch.rs +++ b/yazi-plugin/src/isolate/fetch.rs @@ -26,7 +26,7 @@ pub async fn fetch(name: &str, files: Vec) -> mlua::Resul } plugin.raw_set("skip", 0)?; - plugin.raw_set("area", Rect::from(LAYOUT.load().preview))?; + plugin.raw_set("area", Rect::from(LAYOUT.get().preview))?; plugin.raw_set("files", files)?; Handle::current().block_on(plugin.call_async_method("fetch", ())) diff --git a/yazi-plugin/src/isolate/isolate.rs b/yazi-plugin/src/isolate/isolate.rs index 7152de27..9bb9d24b 100644 --- a/yazi-plugin/src/isolate/isolate.rs +++ b/yazi-plugin/src/isolate/isolate.rs @@ -23,6 +23,7 @@ pub fn slim_lua(name: &str) -> mlua::Result { // Elements let ui = lua.create_table()?; elements::Line::install(&lua, &ui)?; + elements::Paragraph::install(&lua, &ui)?; elements::Rect::install(&lua, &ui)?; elements::Span::install(&lua, &ui)?; elements::Text::install(&lua, &ui)?; diff --git a/yazi-plugin/src/isolate/peek.rs b/yazi-plugin/src/isolate/peek.rs index 4c1ca3da..5d0866d2 100644 --- a/yazi-plugin/src/isolate/peek.rs +++ b/yazi-plugin/src/isolate/peek.rs @@ -41,7 +41,7 @@ pub fn peek( plugin.raw_set("file", File::cast(&lua, file)?)?; plugin.raw_set("_mime", mime)?; plugin.raw_set("skip", skip)?; - plugin.raw_set("area", Rect::from(LAYOUT.load().preview))?; + plugin.raw_set("area", Rect::from(LAYOUT.get().preview))?; plugin.raw_set("window", Window::default())?; if ct2.is_cancelled() { Ok(()) } else { plugin.call_async_method("peek", ()).await } @@ -69,7 +69,7 @@ pub fn peek_sync(cmd: &Cmd, file: yazi_shared::fs::File, mime: Cow<'static, str> plugin.raw_set("file", File::cast(&LUA, file)?)?; plugin.raw_set("_mime", mime)?; plugin.raw_set("skip", skip)?; - plugin.raw_set("area", Rect::from(LAYOUT.load().preview))?; + plugin.raw_set("area", Rect::from(LAYOUT.get().preview))?; plugin.raw_set("window", Window::default())?; plugin.call_method("peek", ()) }); diff --git a/yazi-plugin/src/isolate/preload.rs b/yazi-plugin/src/isolate/preload.rs index 863a2b53..cf837367 100644 --- a/yazi-plugin/src/isolate/preload.rs +++ b/yazi-plugin/src/isolate/preload.rs @@ -18,7 +18,7 @@ pub async fn preload(name: &str, file: yazi_shared::fs::File) -> mlua::Result>(methods: &mut M) { #[inline] - // TODO: return mlua::String instead of String - async fn read_line(me: &mut Child) -> (String, u8) { - async fn read(r: Option) -> Option { - let mut r = r?; - let mut buf = String::new(); - match r.read_line(&mut buf).await { + async fn read_line(me: &mut Child) -> (Option>, u8) { + async fn read(r: Option) -> Option> { + let mut buf = Vec::new(); + match r?.read_until(b'\n', &mut buf).await { Ok(0) | Err(_) => None, Ok(_) => Some(buf), } } select! { - Some(r) = read(me.stdout.as_mut()) => (r, 0u8), - Some(r) = read(me.stderr.as_mut()) => (r, 1u8), - else => (String::new(), 2u8), + r @ Some(_) = read(me.stdout.as_mut()) => (r, 0u8), + r @ Some(_) = read(me.stderr.as_mut()) => (r, 1u8), + else => (None, 2u8), } } @@ -61,12 +59,20 @@ impl UserData for Child { else => (vec![], 2u8) }) }); - methods.add_async_method_mut("read_line", |_, me, ()| async move { Ok(read_line(me).await) }); - methods.add_async_method_mut("read_line_with", |_, me, options: Table| async move { - let timeout: u64 = options.raw_get("timeout")?; - match tokio::time::timeout(Duration::from_millis(timeout), read_line(me)).await { - Ok(value) => Ok(value), - Err(_) => Ok((String::new(), 3u8)), + methods.add_async_method_mut("read_line", |lua, me, ()| async move { + match read_line(me).await { + (Some(b), event) => (lua.create_string(b)?, event).into_lua_multi(lua), + (None, event) => (Value::Nil, event).into_lua_multi(lua), + } + }); + methods.add_async_method_mut("read_line_with", |lua, me, options: Table| async move { + let timeout = Duration::from_millis(options.raw_get("timeout")?); + let Ok(result) = tokio::time::timeout(timeout, read_line(me)).await else { + return (Value::Nil, 3u8).into_lua_multi(lua); + }; + match result { + (Some(b), event) => (lua.create_string(b)?, event).into_lua_multi(lua), + (None, event) => (Value::Nil, event).into_lua_multi(lua), } }); diff --git a/yazi-plugin/src/utils/call.rs b/yazi-plugin/src/utils/call.rs index 968e328a..20faeb51 100644 --- a/yazi-plugin/src/utils/call.rs +++ b/yazi-plugin/src/utils/call.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; use mlua::{ExternalError, Lua, Table, TableExt, Value}; use tracing::error; @@ -37,30 +37,23 @@ impl Utils { )?; ya.raw_set( - "render_with", + "redraw_with", lua.create_function(|lua, c: Table| { let id: mlua::String = c.get("_id")?; let id = id.to_str()?; + let mut layout = LAYOUT.get(); match id { - "current" => { - LAYOUT.store(Arc::new(yazi_config::Layout { - current: *c.raw_get::<_, crate::elements::Rect>("_area")?, - ..*LAYOUT.load_full() - })); - } - "preview" => { - LAYOUT.store(Arc::new(yazi_config::Layout { - preview: *c.raw_get::<_, crate::elements::Rect>("_area")?, - ..*LAYOUT.load_full() - })); - } + "current" => layout.current = *c.raw_get::<_, crate::elements::Rect>("_area")?, + "preview" => layout.preview = *c.raw_get::<_, crate::elements::Rect>("_area")?, + "progress" => layout.progress = *c.raw_get::<_, crate::elements::Rect>("_area")?, _ => {} } - match c.call_method::<_, Table>("render", ()) { + LAYOUT.set(layout); + match c.call_method::<_, Table>("redraw", ()) { Err(e) => { - error!("Failed to `render()` the `{id}` component:\n{e}"); + error!("Failed to `redraw()` the `{id}` component:\n{e}"); lua.create_table() } ok => ok, diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index 1c407894..ee08fbb6 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -53,7 +53,6 @@ impl Utils { area, inner, wrap: if PREVIEW.wrap == PreviewWrap::Yes { WRAP } else { WRAP_NO }, - ..Default::default() })]; emit!(Call(Cmd::new("update_peeked").with_any("lock", lock), Layer::Manager)); diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index 9ac04f74..83ce73f2 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -9,6 +9,7 @@ use crate::CLIPBOARD; impl Utils { pub(super) fn text(lua: &Lua, ya: &Table) -> mlua::Result<()> { + // TODO: deprecate this in the future ya.raw_set( "md5", lua.create_async_function(|_, s: mlua::String| async move { @@ -16,6 +17,13 @@ impl Utils { })?, )?; + ya.raw_set( + "hash", + lua.create_async_function(|_, s: mlua::String| async move { + Ok(format!("{:x}", Md5::new_with_prefix(s.as_bytes()).finalize())) + })?, + )?; + ya.raw_set( "quote", lua.create_function(|_, (s, unix): (mlua::String, Option)| { diff --git a/yazi-proxy/src/manager.rs b/yazi-proxy/src/manager.rs index e3a1b171..1a4337f2 100644 --- a/yazi-proxy/src/manager.rs +++ b/yazi-proxy/src/manager.rs @@ -1,5 +1,5 @@ use yazi_macro::emit; -use yazi_shared::{Layer, event::Cmd, fs::Url}; +use yazi_shared::{Id, Layer, event::Cmd, fs::Url}; use crate::options::OpenDoOpt; @@ -12,7 +12,7 @@ impl ManagerProxy { } #[inline] - pub fn hover(url: Option, tab: usize) { + pub fn hover(url: Option, tab: Id) { emit!(Call( Cmd::args("hover", &url.map_or_else(Vec::new, |u| vec![u])).with("tab", tab), Layer::Manager diff --git a/yazi-scheduler/Cargo.toml b/yazi-scheduler/Cargo.toml index 288add37..330b89bd 100644 --- a/yazi-scheduler/Cargo.toml +++ b/yazi-scheduler/Cargo.toml @@ -29,4 +29,4 @@ tracing = { workspace = true } libc = { workspace = true } [target.'cfg(not(target_os = "android"))'.dependencies] -trash = "5.1.1" +trash = "5.2.0" diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index cdb2f6a3..e46cdae5 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,10 +1,10 @@ -use std::{borrow::Cow, collections::VecDeque, fs::Metadata, path::{Path, PathBuf}}; +use std::{borrow::Cow, collections::VecDeque, path::{Path, PathBuf}}; use anyhow::{Result, anyhow}; -use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; +use tokio::{fs::{self, DirEntry}, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::TASKS; -use yazi_shared::fs::{Url, calculate_size, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to}; +use yazi_shared::fs::{Cha, Url, calculate_size, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to}; use super::{FileOp, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash}; use crate::{LOW, NORMAL, TaskOp, TaskProg}; @@ -26,7 +26,7 @@ impl File { match op { FileOp::Paste(mut task) => { ok_or_not_found(fs::remove_file(&task.to).await)?; - let mut it = copy_with_progress(&task.from, &task.to, task.meta.as_ref().unwrap()); + let mut it = copy_with_progress(&task.from, &task.to, task.cha.unwrap()); while let Some(res) = it.recv().await { match res { @@ -58,14 +58,14 @@ impl File { self.prog.send(TaskProg::Adv(task.id, 1, 0))?; } FileOp::Link(task) => { - let meta = task.meta.as_ref().unwrap(); + let cha = task.cha.unwrap(); let src = if task.resolve { match fs::read_link(&task.from).await { Ok(p) => Cow::Owned(p), Err(e) if e.kind() == NotFound => { warn!("Link task partially done: {task:?}"); - return Ok(self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?); + return Ok(self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?); } Err(e) => Err(e)?, } @@ -86,7 +86,7 @@ impl File { } #[cfg(windows)] { - if meta.is_dir() { + if cha.is_dir() { fs::symlink_dir(src, &task.to).await?; } else { fs::symlink_file(src, &task.to).await?; @@ -96,10 +96,10 @@ impl File { if task.delete { fs::remove_file(&task.from).await.ok(); } - self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?; + self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?; } FileOp::Hardlink(task) => { - let meta = task.meta.as_ref().unwrap(); + let cha = task.cha.unwrap(); let src = if !task.follow { Cow::Borrowed(task.from.as_path()) } else if let Ok(p) = fs::canonicalize(&task.from).await { @@ -116,7 +116,7 @@ impl File { v => v?, } - self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?; + self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?; } FileOp::Delete(task) => { if let Err(e) = fs::remove_file(&task.target).await { @@ -154,19 +154,19 @@ impl File { return self.succ(task.id); } - if task.meta.is_none() { - task.meta = Some(Self::metadata(&task.from, task.follow).await?); + if task.cha.is_none() { + task.cha = Some(Self::cha(&task.from, task.follow).await?); } - let meta = task.meta.as_ref().unwrap(); - if !meta.is_dir() { + let cha = task.cha.unwrap(); + if !cha.is_dir() { let id = task.id; - self.prog.send(TaskProg::New(id, meta.len()))?; + self.prog.send(TaskProg::New(id, cha.len))?; - if meta.is_file() { - self.queue(FileOp::Paste(task), LOW).await?; - } else if meta.is_symlink() { + if cha.is_orphan() || (cha.is_link() && !task.follow) { self.queue(FileOp::Link(task.into()), NORMAL).await?; + } else { + self.queue(FileOp::Paste(task), LOW).await?; } return self.succ(id); } @@ -198,20 +198,20 @@ impl File { let mut it = continue_unless_ok!(fs::read_dir(&src).await); while let Ok(Some(entry)) = it.next_entry().await { let from = Url::from(entry.path()); - let meta = continue_unless_ok!(Self::metadata(&from, task.follow).await); + let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await); - if meta.is_dir() { + if cha.is_dir() { dirs.push_back(from); continue; } let to = dest.join(from.file_name().unwrap()); - self.prog.send(TaskProg::New(task.id, meta.len()))?; + self.prog.send(TaskProg::New(task.id, cha.len))?; - if meta.is_file() { - self.queue(FileOp::Paste(task.spawn(from, to, meta)), LOW).await?; - } else if meta.is_symlink() { - self.queue(FileOp::Link(task.spawn(from, to, meta).into()), NORMAL).await?; + if cha.is_orphan() || (cha.is_link() && !task.follow) { + self.queue(FileOp::Link(task.spawn(from, to, cha).into()), NORMAL).await?; + } else { + self.queue(FileOp::Paste(task.spawn(from, to, cha)), LOW).await?; } } } @@ -220,24 +220,24 @@ impl File { pub async fn link(&self, mut task: FileOpLink) -> Result<()> { let id = task.id; - if task.meta.is_none() { - task.meta = Some(fs::symlink_metadata(&task.from).await?); + if task.cha.is_none() { + task.cha = Some(Self::cha(&task.from, false).await?); } - self.prog.send(TaskProg::New(id, task.meta.as_ref().unwrap().len()))?; + self.prog.send(TaskProg::New(id, task.cha.unwrap().len))?; self.queue(FileOp::Link(task), NORMAL).await?; self.succ(id) } pub async fn hardlink(&self, mut task: FileOpHardlink) -> Result<()> { - if task.meta.is_none() { - task.meta = Some(Self::metadata(&task.from, task.follow).await?); + if task.cha.is_none() { + task.cha = Some(Self::cha(&task.from, task.follow).await?); } - let meta = task.meta.as_ref().unwrap(); - if !meta.is_dir() { + let cha = task.cha.unwrap(); + if !cha.is_dir() { let id = task.id; - self.prog.send(TaskProg::New(id, meta.len()))?; + self.prog.send(TaskProg::New(id, cha.len))?; self.queue(FileOp::Hardlink(task), NORMAL).await?; return self.succ(id); } @@ -269,16 +269,16 @@ impl File { let mut it = continue_unless_ok!(fs::read_dir(&src).await); while let Ok(Some(entry)) = it.next_entry().await { let from = Url::from(entry.path()); - let meta = continue_unless_ok!(Self::metadata(&from, task.follow).await); + let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await); - if meta.is_dir() { + if cha.is_dir() { dirs.push_back(from); continue; } let to = dest.join(from.file_name().unwrap()); - self.prog.send(TaskProg::New(task.id, meta.len()))?; - self.queue(FileOp::Hardlink(task.spawn(from, to, meta)), NORMAL).await?; + self.prog.send(TaskProg::New(task.id, cha.len))?; + self.queue(FileOp::Hardlink(task.spawn(from, to, cha)), NORMAL).await?; } } self.succ(task.id) @@ -325,13 +325,18 @@ impl File { } #[inline] - async fn metadata(path: &Path, follow: bool) -> io::Result { - if !follow { - return fs::symlink_metadata(path).await; - } + async fn cha(path: &Path, follow: bool) -> io::Result { + let meta = fs::symlink_metadata(path).await?; + Ok(if follow { Cha::new(path, meta).await } else { Cha::new_nofollow(path, meta) }) + } - let meta = fs::metadata(path).await; - if meta.is_ok() { meta } else { fs::symlink_metadata(path).await } + #[inline] + async fn cha_from(entry: DirEntry, path: &Path, follow: bool) -> io::Result { + Ok(if follow { + Cha::new(path, entry.metadata().await?).await + } else { + Cha::new_nofollow(path, entry.metadata().await?) + }) } } diff --git a/yazi-scheduler/src/file/op.rs b/yazi-scheduler/src/file/op.rs index 7f898fce..b88019d7 100644 --- a/yazi-scheduler/src/file/op.rs +++ b/yazi-scheduler/src/file/op.rs @@ -1,6 +1,4 @@ -use std::fs::Metadata; - -use yazi_shared::fs::Url; +use yazi_shared::fs::{Cha, Url}; #[derive(Debug)] pub enum FileOp { @@ -29,19 +27,19 @@ pub struct FileOpPaste { pub id: usize, pub from: Url, pub to: Url, - pub meta: Option, + pub cha: Option, pub cut: bool, pub follow: bool, pub retry: u8, } impl FileOpPaste { - pub(super) fn spawn(&self, from: Url, to: Url, meta: Metadata) -> Self { + pub(super) fn spawn(&self, from: Url, to: Url, cha: Cha) -> Self { Self { id: self.id, from, to, - meta: Some(meta), + cha: Some(cha), cut: self.cut, follow: self.follow, retry: self.retry, @@ -55,7 +53,7 @@ pub struct FileOpLink { pub id: usize, pub from: Url, pub to: Url, - pub meta: Option, + pub cha: Option, pub resolve: bool, pub relative: bool, pub delete: bool, @@ -67,7 +65,7 @@ impl From for FileOpLink { id: value.id, from: value.from, to: value.to, - meta: value.meta, + cha: value.cha, resolve: true, relative: false, delete: value.cut, @@ -81,13 +79,13 @@ pub struct FileOpHardlink { pub id: usize, pub from: Url, pub to: Url, - pub meta: Option, + pub cha: Option, pub follow: bool, } impl FileOpHardlink { - pub(super) fn spawn(&self, from: Url, to: Url, meta: Metadata) -> Self { - Self { id: self.id, from, to, meta: Some(meta), follow: self.follow } + pub(super) fn spawn(&self, from: Url, to: Url, cha: Cha) -> Self { + Self { id: self.id, from, to, cha: Some(cha), follow: self.follow } } } diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 027a3e17..075e517b 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -7,7 +7,7 @@ use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, task:: use yazi_config::{TASKS, open::Opener, plugin::{Fetcher, Preloader}}; use yazi_dds::Pump; use yazi_proxy::ManagerProxy; -use yazi_shared::{Throttle, event::Data, fs::{Url, remove_dir_clean, unique_name}}; +use yazi_shared::{Throttle, event::Data, fs::{Url, must_be_dir, remove_dir_clean, unique_name}}; use super::{Ongoing, TaskProg, TaskStage}; use crate::{HIGH, LOW, NORMAL, TaskKind, TaskOp, file::{File, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, prework::{Prework, PreworkOpFetch, PreworkOpLoad, PreworkOpSize}, process::{Process, ProcessOpBg, ProcessOpBlock, ProcessOpOrphan}}; @@ -71,7 +71,7 @@ impl Scheduler { pub fn file_cut(&self, from: Url, mut to: Url, force: bool) { let mut ongoing = self.ongoing.lock(); - let id = ongoing.add(TaskKind::User, format!("Cut {:?} to {:?}", from, to)); + let id = ongoing.add(TaskKind::User, format!("Cut {from} to {to}")); if to.starts_with(&from) && to != from { self.new_and_fail(id, "Cannot cut directory into itself").ok(); @@ -97,9 +97,9 @@ impl Scheduler { let file = self.file.clone(); self.send_micro(id, LOW, async move { if !force { - to = unique_name(to).await?; + to = unique_name(to, must_be_dir(&from)).await?; } - file.paste(FileOpPaste { id, from, to, meta: None, cut: true, follow: false, retry: 0 }).await + file.paste(FileOpPaste { id, from, to, cha: None, cut: true, follow: false, retry: 0 }).await }); } @@ -114,9 +114,9 @@ impl Scheduler { let file = self.file.clone(); self.send_micro(id, LOW, async move { if !force { - to = unique_name(to).await?; + to = unique_name(to, must_be_dir(&from)).await?; } - file.paste(FileOpPaste { id, from, to, meta: None, cut: false, follow, retry: 0 }).await + file.paste(FileOpPaste { id, from, to, cha: None, cut: false, follow, retry: 0 }).await }); } @@ -126,10 +126,10 @@ impl Scheduler { let file = self.file.clone(); self.send_micro(id, LOW, async move { if !force { - to = unique_name(to).await?; + to = unique_name(to, must_be_dir(&from)).await?; } file - .link(FileOpLink { id, from, to, meta: None, resolve: false, relative, delete: false }) + .link(FileOpLink { id, from, to, cha: None, resolve: false, relative, delete: false }) .await }); } @@ -145,9 +145,9 @@ impl Scheduler { let file = self.file.clone(); self.send_micro(id, LOW, async move { if !force { - to = unique_name(to).await?; + to = unique_name(to, must_be_dir(&from)).await?; } - file.hardlink(FileOpHardlink { id, from, to, meta: None, follow }).await + file.hardlink(FileOpHardlink { id, from, to, cha: None, follow }).await }); } diff --git a/yazi-scheduler/src/task.rs b/yazi-scheduler/src/task.rs index 27baceea..8041479b 100644 --- a/yazi-scheduler/src/task.rs +++ b/yazi-scheduler/src/task.rs @@ -31,7 +31,7 @@ pub enum TaskKind { Preload, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct TaskSummary { pub name: String, diff --git a/yazi-shared/src/event/data.rs b/yazi-shared/src/event/data.rs index 558d34ff..e5675a98 100644 --- a/yazi-shared/src/event/data.rs +++ b/yazi-shared/src/event/data.rs @@ -143,3 +143,5 @@ impl_integer_as!(isize, as_isize); impl_integer_as!(i16, as_i16); impl_number_as!(f64, as_f64); + +impl_integer_as!(crate::Id, as_id); diff --git a/yazi-shared/src/fs/cha.rs b/yazi-shared/src/fs/cha.rs index 2e26ba2e..8f9597fb 100644 --- a/yazi-shared/src/fs/cha.rs +++ b/yazi-shared/src/fs/cha.rs @@ -1,4 +1,4 @@ -use std::{fs::{FileType, Metadata}, time::SystemTime}; +use std::{fs::{FileType, Metadata}, path::Path, time::SystemTime}; use bitflags::bitflags; use yazi_macro::unix_either; @@ -26,7 +26,7 @@ pub struct Cha { pub ctime: Option, pub mtime: Option, #[cfg(unix)] - pub perm: libc::mode_t, + pub mode: libc::mode_t, #[cfg(unix)] pub uid: libc::uid_t, #[cfg(unix)] @@ -37,40 +37,42 @@ pub struct Cha { impl From for Cha { fn from(m: Metadata) -> Self { - let mut ck = ChaKind::empty(); + let mut kind = ChaKind::empty(); if m.is_dir() { - ck |= ChaKind::DIR; + kind |= ChaKind::DIR; + } else if m.is_symlink() { + kind |= ChaKind::LINK; } Self { - kind: ck, - len: m.len(), - atime: m.accessed().ok(), - btime: m.created().ok(), + kind, + len: m.len(), + atime: m.accessed().ok(), + btime: m.created().ok(), #[cfg(unix)] - ctime: { + ctime: { use std::{os::unix::fs::MetadataExt, time::{Duration, UNIX_EPOCH}}; UNIX_EPOCH.checked_add(Duration::new(m.ctime() as u64, m.ctime_nsec() as u32)) }, - mtime: m.modified().ok(), + mtime: m.modified().ok(), #[cfg(unix)] - perm: { + mode: { use std::os::unix::prelude::PermissionsExt; m.permissions().mode() as _ }, #[cfg(unix)] - uid: { + uid: { use std::os::unix::fs::MetadataExt; m.uid() as _ }, #[cfg(unix)] - gid: { + gid: { use std::os::unix::fs::MetadataExt; m.gid() as _ }, #[cfg(unix)] - nlink: { + nlink: { use std::os::unix::fs::MetadataExt; m.nlink() as _ }, @@ -83,7 +85,7 @@ impl From for Cha { let mut kind = ChaKind::DUMMY; #[cfg(unix)] - let perm = { + let mode = { use std::os::unix::fs::FileTypeExt; if t.is_dir() { kind |= ChaKind::DIR; @@ -116,7 +118,7 @@ impl From for Cha { Self { kind, #[cfg(unix)] - perm, + mode, ..Default::default() } } @@ -124,14 +126,46 @@ impl From for Cha { impl Cha { #[inline] - pub fn dummy() -> Self { Self { kind: ChaKind::DUMMY, ..Default::default() } } + pub async fn new(path: &Path, mut meta: Metadata) -> Self { + let mut attached = ChaKind::empty(); + + if meta.is_symlink() { + attached |= ChaKind::LINK; + meta = tokio::fs::metadata(path).await.unwrap_or(meta); + } + if meta.is_symlink() { + attached |= ChaKind::ORPHAN; + } + + let mut cha = Self::new_nofollow(path, meta); + cha.kind |= attached; + cha + } #[inline] - pub fn with_kind(mut self, kind: ChaKind) -> Self { - self.kind |= kind; - self + pub fn new_nofollow(path: &Path, meta: Metadata) -> Self { + let mut attached = ChaKind::empty(); + + #[cfg(unix)] + if super::Urn::new(path).is_hidden() { + attached |= ChaKind::HIDDEN; + } + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + if meta.file_attributes() & 2 != 0 { + attached |= ChaKind::HIDDEN; + } + } + + let mut cha = Self::from(meta); + cha.kind |= attached; + cha } + #[inline] + pub fn dummy() -> Self { Self { kind: ChaKind::DUMMY, ..Default::default() } } + #[inline] pub fn hits(self, c: Self) -> bool { self.len == c.len @@ -139,7 +173,7 @@ impl Cha { && unix_either!(self.ctime == c.ctime, true) && self.btime == c.btime && self.kind == c.kind - && unix_either!(self.perm == c.perm, true) + && unix_either!(self.mode == c.mode, true) } } @@ -161,27 +195,27 @@ impl Cha { #[inline] pub const fn is_block(&self) -> bool { - unix_either!(self.perm & libc::S_IFMT == libc::S_IFBLK, false) + unix_either!(self.mode & libc::S_IFMT == libc::S_IFBLK, false) } #[inline] pub const fn is_char(&self) -> bool { - unix_either!(self.perm & libc::S_IFMT == libc::S_IFCHR, false) + unix_either!(self.mode & libc::S_IFMT == libc::S_IFCHR, false) } #[inline] pub const fn is_fifo(&self) -> bool { - unix_either!(self.perm & libc::S_IFMT == libc::S_IFIFO, false) + unix_either!(self.mode & libc::S_IFMT == libc::S_IFIFO, false) } #[inline] pub const fn is_sock(&self) -> bool { - unix_either!(self.perm & libc::S_IFMT == libc::S_IFSOCK, false) + unix_either!(self.mode & libc::S_IFMT == libc::S_IFSOCK, false) } #[inline] - pub const fn is_exec(&self) -> bool { unix_either!(self.perm & libc::S_IXUSR != 0, false) } + pub const fn is_exec(&self) -> bool { unix_either!(self.mode & libc::S_IXUSR != 0, false) } #[inline] - pub const fn is_sticky(&self) -> bool { unix_either!(self.perm & libc::S_ISVTX != 0, false) } + pub const fn is_sticky(&self) -> bool { unix_either!(self.mode & libc::S_ISVTX != 0, false) } } diff --git a/yazi-shared/src/fs/file.rs b/yazi-shared/src/fs/file.rs index 4726b856..41e1bf0a 100644 --- a/yazi-shared/src/fs/file.rs +++ b/yazi-shared/src/fs/file.rs @@ -1,17 +1,17 @@ -use std::{cell::Cell, ffi::OsStr, fs::{FileType, Metadata}, ops::Deref}; +use std::{ffi::OsStr, fs::{FileType, Metadata}, ops::Deref}; use anyhow::Result; use tokio::fs; use super::{Urn, UrnBuf}; -use crate::{fs::{Cha, ChaKind, Url}, theme::IconCache}; +use crate::{SyncCell, fs::{Cha, Url}, theme::IconCache}; #[derive(Clone, Debug, Default)] pub struct File { pub url: Url, pub cha: Cha, pub link_to: Option, - pub icon: Cell, + pub icon: SyncCell, } impl Deref for File { @@ -34,34 +34,13 @@ impl File { } #[inline] - pub async fn from_meta(url: Url, mut meta: Metadata) -> Self { - let mut ck = ChaKind::empty(); - let (is_link, mut link_to) = (meta.is_symlink(), None); + pub async fn from_meta(url: Url, meta: Metadata) -> Self { + let link_to = + if meta.is_symlink() { fs::read_link(&url).await.map(Url::from).ok() } else { None }; - if is_link { - meta = fs::metadata(&url).await.unwrap_or(meta); - link_to = fs::read_link(&url).await.map(Url::from).ok(); - } + let cha = Cha::new(&url, meta).await; - if is_link && meta.is_symlink() { - ck |= ChaKind::ORPHAN; - } else if is_link { - ck |= ChaKind::LINK; - } - - #[cfg(unix)] - if url.is_hidden() { - ck |= ChaKind::HIDDEN; - } - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - if meta.file_attributes() & 2 != 0 { - ck |= ChaKind::HIDDEN; - } - } - - Self { url, cha: Cha::from(meta).with_kind(ck), link_to, icon: Default::default() } + Self { url, cha, link_to, icon: Default::default() } } #[inline] diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index 82b2fe77..0424332f 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -1,8 +1,10 @@ -use std::{borrow::Cow, collections::{HashMap, HashSet, VecDeque}, ffi::{OsStr, OsString}, fs::Metadata, path::{Path, PathBuf}}; +use std::{borrow::Cow, collections::{HashMap, HashSet, VecDeque}, ffi::{OsStr, OsString}, path::{Path, PathBuf}}; use anyhow::{Result, bail}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; +use super::Cha; + #[inline] pub async fn must_exists(p: impl AsRef) -> bool { fs::symlink_metadata(p).await.is_ok() } @@ -14,6 +16,11 @@ pub async fn maybe_exists(p: impl AsRef) -> bool { } } +#[inline] +pub async fn must_be_dir(p: impl AsRef) -> bool { + fs::metadata(p).await.map_or(false, |m| m.is_dir()) +} + #[inline] pub fn ok_or_not_found(result: io::Result<()>) -> io::Result<()> { match result { @@ -47,7 +54,7 @@ async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result { use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}}; async fn final_name(p: &Path) -> std::io::Result { - let file = tokio::fs::OpenOptions::new() + let file = fs::OpenOptions::new() .access_mode(0) .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) .open(p) @@ -175,7 +182,7 @@ pub async fn calculate_size(path: &Path) -> u64 { pub fn copy_with_progress( from: &Path, to: &Path, - meta: &Metadata, + cha: Cha, ) -> mpsc::Receiver> { let (tx, rx) = mpsc::channel(1); let (tick_tx, mut tick_rx) = oneshot::channel(); @@ -183,31 +190,8 @@ pub fn copy_with_progress( tokio::spawn({ let (from, to) = (from.to_owned(), to.to_owned()); - let mut ft = std::fs::FileTimes::new(); - meta.accessed().map(|t| ft = ft.set_accessed(t)).ok(); - meta.modified().map(|t| ft = ft.set_modified(t)).ok(); - #[cfg(target_os = "macos")] - { - use std::os::macos::fs::FileTimesExt; - meta.created().map(|t| ft = ft.set_created(t)).ok(); - } - #[cfg(windows)] - { - use std::os::windows::fs::FileTimesExt; - meta.created().map(|t| ft = ft.set_created(t)).ok(); - } - async move { - _ = match fs::copy(&from, &to).await { - Ok(len) => { - _ = tokio::task::spawn_blocking(move || { - std::fs::File::options().write(true).open(to).and_then(|f| f.set_times(ft)).ok(); - }) - .await; - tick_tx.send(Ok(len)) - } - Err(e) => tick_tx.send(Err(e)), - }; + tick_tx.send(_copy_with_progress(from, to, cha).await).ok(); } }); @@ -252,6 +236,54 @@ pub fn copy_with_progress( rx } +async fn _copy_with_progress(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result { + let mut ft = std::fs::FileTimes::new(); + cha.atime.map(|t| ft = ft.set_accessed(t)); + cha.mtime.map(|t| ft = ft.set_modified(t)); + #[cfg(target_os = "macos")] + { + use std::os::macos::fs::FileTimesExt; + cha.btime.map(|t| ft = ft.set_created(t)); + } + #[cfg(windows)] + { + use std::os::windows::fs::FileTimesExt; + cha.btime.map(|t| ft = ft.set_created(t)); + } + + #[cfg(any(target_os = "linux", target_os = "android"))] + { + use std::os::{fd::AsRawFd, unix::fs::OpenOptionsExt}; + + tokio::task::spawn_blocking(move || { + let mut reader = std::fs::File::open(from)?; + let mut writer = std::fs::OpenOptions::new() + .mode(cha.mode as u32) + .write(true) + .create(true) + .truncate(true) + .open(to)?; + + let written = std::io::copy(&mut reader, &mut writer)?; + unsafe { libc::fchmod(writer.as_raw_fd(), cha.mode) }; + writer.set_times(ft).ok(); + + Ok(written) + }) + .await? + } + + #[cfg(not(any(target_os = "linux", target_os = "android")))] + { + tokio::task::spawn_blocking(move || { + let written = std::fs::copy(from, &to)?; + std::fs::File::options().write(true).open(to).and_then(|f| f.set_times(ft)).ok(); + Ok(written) + }) + .await? + } +} + pub async fn remove_dir_clean(dir: &Path) { let Ok(mut it) = fs::read_dir(dir).await else { return }; diff --git a/yazi-shared/src/fs/loc.rs b/yazi-shared/src/fs/loc.rs index 190e6be7..332c48d5 100644 --- a/yazi-shared/src/fs/loc.rs +++ b/yazi-shared/src/fs/loc.rs @@ -1,32 +1,18 @@ -use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; +use std::{cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; use super::{Urn, UrnBuf}; +#[derive(Clone, Default)] pub struct Loc { path: PathBuf, - urn: *const OsStr, - name: *const OsStr, + urn: usize, + name: usize, } unsafe impl Send for Loc {} unsafe impl Sync for Loc {} -impl Default for Loc { - fn default() -> Self { - Self { path: PathBuf::default(), urn: OsStr::new(""), name: OsStr::new("") } - } -} - -impl Clone for Loc { - fn clone(&self) -> Self { - let path = self.path.clone(); - let name = path.file_name().unwrap_or(OsStr::new("")) as *const OsStr; - let urn = if self.urn() == self.name() { name } else { self.twin_urn(&path) }; - Self { path, urn, name } - } -} - impl Deref for Loc { type Target = PathBuf; @@ -63,56 +49,114 @@ impl Debug for Loc { impl Loc { pub fn new(path: PathBuf) -> Self { - let urn = path.file_name().unwrap_or(OsStr::new("")) as *const OsStr; - Self { path, urn, name: urn } + let Some(name) = path.file_name() else { + let urn = path.as_os_str().len(); + return Self { path, urn, name: 0 }; + }; + + let name_len = name.len(); + let prefix_len = unsafe { + name.as_encoded_bytes().as_ptr().offset_from(path.as_os_str().as_encoded_bytes().as_ptr()) + }; + + let mut bytes = path.into_os_string().into_encoded_bytes(); + bytes.truncate(name_len + prefix_len as usize); + Self { + path: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), + urn: name_len, + name: name_len, + } } pub fn from(base: &Path, path: PathBuf) -> Self { - let urn = path.strip_prefix(base).unwrap_or(&path).as_os_str() as *const OsStr; - let name = path.file_name().unwrap_or(OsStr::new("")) as *const OsStr; - Self { path, urn, name } - } - - pub fn base(&self) -> &Path { - let mut it = self.path.components(); - for _ in 0..self.urn().components().count() { - it.next_back().unwrap(); - } - it.as_path() - } - - pub fn rebase(&self, parent: &Path) -> Self { - debug_assert!(self.urn() == self.name()); - - let path = parent.join(self.name()); - let name = path.file_name().unwrap_or(OsStr::new("")) as *const OsStr; - Self { path, urn: name, name } + let mut loc = Self::new(path); + loc.urn = loc.path.strip_prefix(base).unwrap_or(&loc.path).as_os_str().len(); + loc } #[inline] - fn twin_urn<'a>(&self, new: &'a Path) -> &'a OsStr { - let total = new.components().count(); - let take = self.urn().components().count(); - - let mut it = new.components(); - for _ in 0..total - take { - it.next().unwrap(); - } - - it.as_path().as_os_str() + pub fn urn(&self) -> &Urn { + Urn::new(unsafe { + OsStr::from_encoded_bytes_unchecked( + self.bytes().get_unchecked(self.bytes().len() - self.urn..), + ) + }) } -} - -impl Loc { - #[inline] - pub fn urn(&self) -> &Urn { Urn::new(unsafe { &*self.urn }) } #[inline] pub fn urn_owned(&self) -> UrnBuf { self.urn().to_owned() } #[inline] - pub fn name(&self) -> &OsStr { unsafe { &*self.name } } + pub fn name(&self) -> &OsStr { + unsafe { + OsStr::from_encoded_bytes_unchecked( + self.bytes().get_unchecked(self.bytes().len() - self.name..), + ) + } + } + + #[inline] + pub fn base(&self) -> &Path { + Path::new(unsafe { + OsStr::from_encoded_bytes_unchecked( + self.bytes().get_unchecked(..self.bytes().len() - self.urn), + ) + }) + } + + #[inline] + pub fn rebase(&self, parent: &Path) -> Self { + debug_assert!(self.urn == self.name); + let path = parent.join(self.name()); + + debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name)); + Self { path, urn: self.name, name: self.name } + } #[inline] pub fn into_path(self) -> PathBuf { self.path } + + #[inline] + fn bytes(&self) -> &[u8] { self.path.as_os_str().as_encoded_bytes() } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new() { + let loc = Loc::new("/".into()); + assert_eq!(loc.urn(), Urn::new("/")); + assert_eq!(loc.name(), OsStr::new("")); + assert_eq!(loc.base(), Path::new("")); + + let loc = Loc::new("/root".into()); + assert_eq!(loc.urn(), Urn::new("root")); + assert_eq!(loc.name(), OsStr::new("root")); + assert_eq!(loc.base(), Path::new("/")); + + let loc = Loc::new("/root/code/foo/".into()); + assert_eq!(loc.urn(), Urn::new("foo")); + assert_eq!(loc.name(), OsStr::new("foo")); + assert_eq!(loc.base(), Path::new("/root/code/")); + } + + #[test] + fn test_from() { + let loc = Loc::from(Path::new("/"), "/".into()); + assert_eq!(loc.urn(), Urn::new("")); + assert_eq!(loc.name(), OsStr::new("")); + assert_eq!(loc.base(), Path::new("/")); + + let loc = Loc::from(Path::new("/root/"), "/root/code/".into()); + assert_eq!(loc.urn(), Urn::new("code")); + assert_eq!(loc.name(), OsStr::new("code")); + assert_eq!(loc.base(), Path::new("/root")); + + let loc = Loc::from(Path::new("/root//"), "/root/code/foo//".into()); + assert_eq!(loc.urn(), Urn::new("code/foo")); + assert_eq!(loc.name(), OsStr::new("foo")); + assert_eq!(loc.base(), Path::new("/root")); + } } diff --git a/yazi-shared/src/fs/op.rs b/yazi-shared/src/fs/op.rs index d7c7721f..2f0a7605 100644 --- a/yazi-shared/src/fs/op.rs +++ b/yazi-shared/src/fs/op.rs @@ -1,15 +1,15 @@ -use std::{collections::{HashMap, HashSet}, sync::atomic::{AtomicU64, Ordering}}; +use std::collections::{HashMap, HashSet}; use super::{Cha, File, UrnBuf}; -use crate::{Layer, event::Cmd, fs::Url}; +use crate::{Id, Ids, Layer, event::Cmd, fs::Url}; -pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0); +pub static FILES_TICKET: Ids = Ids::new(); #[derive(Clone, Debug)] pub enum FilesOp { Full(Url, Vec, Cha), - Part(Url, Vec, u64), - Done(Url, Cha, u64), + Part(Url, Vec, Id), + Done(Url, Cha, Id), Size(Url, HashMap), IOErr(Url, std::io::ErrorKind), @@ -41,8 +41,8 @@ impl FilesOp { crate::event::Event::Call(Cmd::new("update_files").with_any("op", self), Layer::Manager).emit(); } - pub fn prepare(cwd: &Url) -> u64 { - let ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); + pub fn prepare(cwd: &Url) -> Id { + let ticket = FILES_TICKET.next(); Self::Part(cwd.clone(), vec![], ticket).emit(); ticket } diff --git a/yazi-shared/src/fs/path.rs b/yazi-shared/src/fs/path.rs index 592e4f17..a9681b5b 100644 --- a/yazi-shared/src/fs/path.rs +++ b/yazi-shared/src/fs/path.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, env, ffi::OsString, io, path::{Component, Path, PathBuf}}; +use std::{borrow::Cow, env, ffi::OsString, future::Future, io, path::{Component, Path, PathBuf}}; use tokio::fs; @@ -74,38 +74,51 @@ fn _expand_path(p: &Path) -> PathBuf { } } -pub async fn unique_name(mut u: Url) -> io::Result { +pub async fn unique_name(u: Url, append: F) -> io::Result +where + F: Future, +{ + match fs::symlink_metadata(&u).await { + Ok(_) => _unique_name(u, append.await).await, + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(u), + Err(e) => Err(e), + } +} + +async fn _unique_name(mut u: Url, append: bool) -> io::Result { let Some(stem) = u.file_stem().map(|s| s.to_owned()) else { return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem")); }; - let ext = u - .extension() - .map(|s| { - let mut n = OsString::with_capacity(s.len() + 1); - n.push("."); - n.push(s); - n - }) - .unwrap_or_default(); + let dot_ext = u.extension().map_or_else(OsString::new, |e| { + let mut s = OsString::with_capacity(e.len() + 1); + s.push("."); + s.push(e); + s + }); let mut i = 1u64; let mut p = u.to_path(); loop { + let mut name = OsString::with_capacity(stem.len() + dot_ext.len() + 5); + name.push(&stem); + + if append { + name.push(&dot_ext); + name.push("_"); + name.push(i.to_string()); + } else { + name.push("_"); + name.push(i.to_string()); + name.push(&dot_ext); + } + + p.set_file_name(name); match fs::symlink_metadata(&p).await { - Ok(_) => {} + Ok(_) => i += 1, Err(e) if e.kind() == io::ErrorKind::NotFound => break, Err(e) => return Err(e), } - - let mut name = OsString::with_capacity(stem.len() + ext.len() + 5); - name.push(&stem); - name.push("_"); - name.push(i.to_string()); - name.push(&ext); - - p.set_file_name(name); - i += 1; } u.set_loc(Loc::from(u.base(), p)); @@ -157,6 +170,27 @@ pub fn path_relative_to<'a>(path: &'a Path, root: &Path) -> Cow<'a, Path> { Cow::from(buf) } +#[cfg(windows)] +pub fn backslash_to_slash(p: &Path) -> Cow { + let bytes = p.as_os_str().as_encoded_bytes(); + + // Fast path to skip if there are no backslashes + let skip_len = bytes.iter().take_while(|&&b| b != b'\\').count(); + if skip_len >= bytes.len() { + return Cow::Borrowed(p); + } + + let (skip, rest) = bytes.split_at(skip_len); + let mut out = Vec::new(); + out.try_reserve_exact(bytes.len()).unwrap_or_else(|_| panic!()); + out.extend(skip); + + for &b in rest { + out.push(if b == b'\\' { b'/' } else { b }); + } + Cow::Owned(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(out) })) +} + #[cfg(test)] mod tests { use std::{borrow::Cow, path::Path}; diff --git a/yazi-shared/src/fs/url.rs b/yazi-shared/src/fs/url.rs index 6d8264ca..28114467 100644 --- a/yazi-shared/src/fs/url.rs +++ b/yazi-shared/src/fs/url.rs @@ -170,10 +170,6 @@ impl Url { #[inline] pub fn pair(&self) -> Option<(Self, UrnBuf)> { Some((self.parent_url()?, self.loc.urn_owned())) } - #[cfg(unix)] - #[inline] - pub fn is_hidden(&self) -> bool { self.loc.urn().is_hidden() } - #[inline] pub fn rebase(&self, parent: &Path) -> Self { debug_assert!(self.is_regular()); diff --git a/yazi-shared/src/id.rs b/yazi-shared/src/id.rs new file mode 100644 index 00000000..ab5f1523 --- /dev/null +++ b/yazi-shared/src/id.rs @@ -0,0 +1,54 @@ +use std::{fmt::Display, str::FromStr, sync::atomic::{AtomicUsize, Ordering}}; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct Id(usize); + +impl Id { + #[inline] + pub fn get(&self) -> usize { self.0 } +} + +impl Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } +} + +impl FromStr for Id { + type Err = ::Err; + + fn from_str(s: &str) -> Result { s.parse().map(Self) } +} + +impl TryFrom for Id { + type Error = >::Error; + + fn try_from(value: i64) -> Result { usize::try_from(value).map(Self) } +} + +// --- Ids +pub struct Ids { + next: AtomicUsize, +} + +impl Ids { + #[inline] + pub const fn new() -> Self { Self { next: AtomicUsize::new(1) } } + + #[inline] + pub fn next(&self) -> Id { + loop { + let old = self.next.fetch_add(1, Ordering::Relaxed); + if old != 0 { + return Id(old); + } + } + } + + #[inline] + pub fn current(&self) -> Id { Id(self.next.load(Ordering::Relaxed)) } +} + +impl Default for Ids { + fn default() -> Self { Self::new() } +} diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index fd5b37be..1dd01b9d 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -2,10 +2,7 @@ yazi_macro::mod_pub!(errors event fs shell theme translit); -yazi_macro::mod_flat!( - chars condition debounce env layer natsort number os rand ro_cell terminal throttle - time xdg -); +yazi_macro::mod_flat!(chars condition debounce env id layer natsort number os rand ro_cell sync_cell terminal throttle time xdg); pub fn init() { #[cfg(unix)] diff --git a/yazi-shared/src/ro_cell.rs b/yazi-shared/src/ro_cell.rs index 5d7c95c8..e5c830fd 100644 --- a/yazi-shared/src/ro_cell.rs +++ b/yazi-shared/src/ro_cell.rs @@ -27,12 +27,6 @@ impl RoCell { self.init(f()); } - #[inline] - pub fn replace(&self, value: T) -> T { - debug_assert!(self.initialized()); - unsafe { mem::replace(&mut *self.0.get(), Some(value)).unwrap_unchecked() } - } - #[inline] pub fn drop(&self) -> T { debug_assert!(self.initialized()); diff --git a/yazi-shared/src/sync_cell.rs b/yazi-shared/src/sync_cell.rs new file mode 100644 index 00000000..7e15d095 --- /dev/null +++ b/yazi-shared/src/sync_cell.rs @@ -0,0 +1,36 @@ +use std::{cell::Cell, fmt::{Debug, Formatter}, ops::Deref}; + +/// [`SyncCell`], but [`Sync`]. +/// +/// This is just an `Cell`, except it implements `Sync` +/// if `T` implements `Sync`. +pub struct SyncCell(Cell); + +unsafe impl Sync for SyncCell {} + +impl SyncCell { + #[inline] + pub const fn new(value: T) -> Self { Self(Cell::new(value)) } +} + +impl Default for SyncCell { + fn default() -> Self { Self::new(T::default()) } +} + +impl Deref for SyncCell { + type Target = Cell; + + #[inline] + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl Clone for SyncCell { + #[inline] + fn clone(&self) -> SyncCell { SyncCell::new(self.get()) } +} + +impl Debug for SyncCell { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SyncCell").field("value", &self.get()).finish() + } +}