diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 686a1971..9a082530 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -15,7 +15,7 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install Nix uses: cachix/install-nix-action@v31 diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b9adca93..f9412396 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,7 +17,7 @@ jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Rust toolchain run: | @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup Rust toolchain run: | @@ -45,12 +45,12 @@ jobs: uses: mozilla-actions/sccache-action@v0.0.10 - name: Rustfmt - run: rustfmt +nightly --check **/*.rs + run: find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} + stylua: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: JohnnyMorganz/stylua-action@v5 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml index 976dd90f..410d1878 100644 --- a/.github/workflows/draft.yml +++ b/.github/workflows/draft.yml @@ -42,7 +42,7 @@ jobs: CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install gcc if: matrix.gcc != '' @@ -81,7 +81,7 @@ jobs: CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: @@ -97,10 +97,11 @@ jobs: env: TARGET_NAME: yazi-${{ matrix.target }} run: | - New-Item -ItemType Directory -Path ${env:TARGET_NAME} + New-Item -ItemType Directory -Path "${env:TARGET_NAME}\completions" Copy-Item -Path "target\${{ matrix.target }}\release-windows\ya.exe" -Destination ${env:TARGET_NAME} Copy-Item -Path "target\${{ matrix.target }}\release-windows\yazi.exe" -Destination ${env:TARGET_NAME} - Copy-Item -Path "yazi-boot\completions" -Destination ${env:TARGET_NAME} -Recurse + Copy-Item -Path "yazi-cli\completions\*" -Destination "${env:TARGET_NAME}\completions" + Copy-Item -Path "yazi-boot\completions\*" -Destination "${env:TARGET_NAME}\completions" Copy-Item -Path "README.md", "LICENSE" -Destination ${env:TARGET_NAME} Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip" @@ -120,7 +121,7 @@ jobs: container: image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: @@ -150,7 +151,7 @@ jobs: arch: arm64 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -233,7 +234,7 @@ jobs: echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/download-artifact@v8 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c00d7bca..7ea29bc1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Rust toolchain run: rustup toolchain install stable --profile minimal diff --git a/.github/workflows/validate-form.yml b/.github/workflows/validate-form.yml index bd8d9764..81ccc28a 100644 --- a/.github/workflows/validate-form.yml +++ b/.github/workflows/validate-form.yml @@ -12,7 +12,7 @@ jobs: permissions: issues: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0240dec6..eec968e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Drag and drop ([#4005]) - Bulk create ([#3793]) +- Make help menu a command palette ([#4074]) +- Make visual mode support wraparound scrolling ([#4101]) +- H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) +- Context-aware icons for inputs ([#4080]) +- Show file icons in trash/delete/overwrite confirmations ([#4096]) - Dynamic keymap Lua API ([#4031]) - New `ui.Input` element ([#4040]) - Image preview with Überzug++ on Niri ([#3990]) @@ -24,11 +29,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Changed - Rename `` to `` ([#3989]) +- Make `mgr::Yanked`, `tab::Selected`, and the `@yank` DDS event return `File` instead of `Url` from `__pairs()` ([#4096]) +- Remove `help:filter` action since the filter input is now always available ([#4074]) +- `[help]` of `theme.toml`: supersede `on` with `chord`, supersede `run` and `desc` with `action`, remove `footer` ([#4074]) - Remove Legacy Console Mode on Windows ([#3989]) ### Deprecated - Deprecate `backward --far` and `forward --far` in favor of `backward wide` and `forward wide`, respectively ([#4012]) +- Deprecate `tab::Mode.is_visual` in favor of the new `tab::Mode.is_normal` ([#4101]) ### Fixed @@ -1746,6 +1755,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3906]: https://github.com/sxyazi/yazi/pull/3906 [#3934]: https://github.com/sxyazi/yazi/pull/3934 [#3943]: https://github.com/sxyazi/yazi/pull/3943 +[#3970]: https://github.com/sxyazi/yazi/pull/3970 [#3989]: https://github.com/sxyazi/yazi/pull/3989 [#3990]: https://github.com/sxyazi/yazi/pull/3990 [#4005]: https://github.com/sxyazi/yazi/pull/4005 @@ -1757,3 +1767,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4065]: https://github.com/sxyazi/yazi/pull/4065 [#4067]: https://github.com/sxyazi/yazi/pull/4067 [#4068]: https://github.com/sxyazi/yazi/pull/4068 +[#4074]: https://github.com/sxyazi/yazi/pull/4074 +[#4080]: https://github.com/sxyazi/yazi/pull/4080 +[#4096]: https://github.com/sxyazi/yazi/pull/4096 +[#4101]: https://github.com/sxyazi/yazi/pull/4101 diff --git a/Cargo.lock b/Cargo.lock index b0376978..cec37e11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.11.0-rc.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" dependencies = [ "aead", "aes", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "approx" @@ -182,9 +182,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", "serde", @@ -215,9 +215,9 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "as-slice" @@ -459,12 +459,12 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] @@ -567,9 +567,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cipher", @@ -628,9 +628,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.5" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" +checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b" dependencies = [ "clap", ] @@ -820,9 +820,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97bb4a855e3b10f84c4e7e895a7de01db7f9a7b7eb7f73ed9773fd52ac686451" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" dependencies = [ "cpubits", "ctutils", @@ -877,9 +877,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "5.0.0-rc.0" +version = "5.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f359e08ca85e7bd759e1fd933ff2bccd81864c60a8fba0e259c7f822b0924bf" +checksum = "c906a87e53a36ff795d72e06e8162a83c5436e3ea89e942a9cb9fc083f0a384f" dependencies = [ "cfg-if", "cpufeatures", @@ -1043,9 +1043,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" -version = "0.17.0-rc.18" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54fb064faabbee66e1fc8e5c5a9458d4269dc2d8b638fe86a425adb2510d1a96" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ "der", "digest", @@ -1068,9 +1068,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "3.0.0-rc.0" +version = "3.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60" +checksum = "1685663e23882cd8517dcbcb1c23a6ebff4433c22dfb681d760219b62cd1b849" dependencies = [ "curve25519-dalek", "ed25519", @@ -1090,9 +1090,9 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" -version = "0.14.0-rc.33" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "102d3643d30dd8b559613c5cced68317199597fffb278cdc88daa2ef7fafc935" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", @@ -1102,7 +1102,6 @@ dependencies = [ "group", "hkdf", "hybrid-array", - "once_cell", "pem-rfc7468", "pkcs8", "rand_core 0.10.1", @@ -1583,9 +1582,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "ctutils", "subtle", @@ -1707,9 +1706,9 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +checksum = "9ea94e891b3606826e9c998be69ddca42247dad8ad50b1649a5cb7e1c9ae06fd" dependencies = [ "libc", ] @@ -1745,7 +1744,7 @@ checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8" dependencies = [ "num-integer", "num-traits", - "rand 0.10.1", + "rand 0.10.2", "rand_core 0.10.1", ] @@ -1802,9 +1801,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -1898,9 +1897,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -1931,9 +1930,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "loop9" @@ -2199,9 +2198,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2369,9 +2368,9 @@ dependencies = [ [[package]] name = "p256" -version = "0.14.0-rc.10" +version = "0.14.0-rc.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41adc63effe99d48837a8cc0e6d7a77e32ae6a07f6000df466178dbc2193093e" +checksum = "b6bb40a5099e2c38a09dd29321a7a7f045f165a54317679c7cdfb0cbaf8f6b1e" dependencies = [ "ecdsa", "elliptic-curve", @@ -2382,9 +2381,9 @@ dependencies = [ [[package]] name = "p384" -version = "0.14.0-rc.10" +version = "0.14.0-rc.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd5333afa5ae0347f39e6a0f2c9c155da431583fd71fe5555bd0521b4ccaf02" +checksum = "492f329d7eb11d22dadc988626b9ea1f503b4ab043a8b1e4e2cc4ae45dabdd70" dependencies = [ "ecdsa", "elliptic-curve", @@ -2396,9 +2395,9 @@ dependencies = [ [[package]] name = "p521" -version = "0.14.0-rc.10" +version = "0.14.0-rc.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a5297f53dc16d35909060ba3032cff7867e8809f01e273ff325579d5f0ceae" +checksum = "ff42e4ace5424e3b6d7cb82514be89866b85af87015f80341e37dcc21a66ce6e" dependencies = [ "base16ct", "ecdsa", @@ -2420,7 +2419,7 @@ dependencies = [ "delegate", "futures", "log", - "rand 0.10.1", + "rand 0.10.2", "sha2", "thiserror 2.0.18", "tokio", @@ -2556,11 +2555,12 @@ dependencies = [ [[package]] name = "pkcs5" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453" +checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" dependencies = [ "aes", + "aes-gcm", "cbc", "der", "pbkdf2", @@ -2590,9 +2590,9 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64", "indexmap 2.14.0", @@ -2669,9 +2669,9 @@ dependencies = [ [[package]] name = "primefield" -version = "0.14.0-rc.12" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8675564771a62f69a0af716b03e89b917b963c7b173b5855575e84fd4f605ca0" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" dependencies = [ "crypto-bigint", "crypto-common", @@ -2683,11 +2683,15 @@ dependencies = [ [[package]] name = "primeorder" -version = "0.14.0-rc.10" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2793f22b9b6fd11ef3ac1d59bf003c2573593e4968702341605c2748fd90bf" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", ] [[package]] @@ -2779,18 +2783,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -2835,9 +2839,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -3060,12 +3064,12 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rfc6979" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5236ce872cac07e0fb3969b0cbf468c7d2f37d432f1b627dcb7b8d34563fb0c3" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ + "crypto-bigint", "hmac", - "subtle", ] [[package]] @@ -3109,9 +3113,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.61.2" +version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0" +checksum = "20d2039c3e50abb1ec3bdadea27098679da12a33befa9ab4e62adad9a13ebcb2" dependencies = [ "aes", "bitflags", @@ -3154,7 +3158,7 @@ dependencies = [ "pkcs5", "pkcs8", "polyval", - "rand 0.10.1", + "rand 0.10.2", "rand_core 0.10.1", "ring", "rsa", @@ -3180,9 +3184,9 @@ dependencies = [ [[package]] name = "russh-cryptovec" -version = "0.61.0" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443f6bbcfacb34a1aab2b12b99bf08e0c63abdc5a0db261901365df9d57fff51" +checksum = "3aec6cb630dbe85d72ffd7bcd95f07e1bd69f9f270ee8adfa1afe443a6331438" dependencies = [ "log", "nix", @@ -3210,9 +3214,9 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -3586,17 +3590,15 @@ dependencies = [ [[package]] name = "ssh-cipher" -version = "0.3.0-rc.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36" +checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" dependencies = [ "aead", "aes", "aes-gcm", - "cbc", "chacha20", "cipher", - "ctr", "ctutils", "des", "poly1305", @@ -3606,9 +3608,9 @@ dependencies = [ [[package]] name = "ssh-encoding" -version = "0.3.0-rc.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abf34aa716da5d5b4c496936d042ea282ab392092cd68a72ef6a8863ff8c96a" +checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" dependencies = [ "base64ct", "bytes", @@ -3621,9 +3623,9 @@ dependencies = [ [[package]] name = "ssh-key" -version = "0.7.0-rc.10" +version = "0.7.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45735ce3dea95690e4a9e414c4cfde7f79835063c3dcd35881df85a84118e74b" +checksum = "f9a32fae177b74a22aa9c5b01bf7e68b33545be32d9e381e248058d2adc15ce3" dependencies = [ "argon2", "bcrypt-pbkdf", @@ -3831,9 +3833,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.49" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "libc", @@ -3853,9 +3855,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.29" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -4183,9 +4185,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vergen" -version = "10.0.0" +version = "10.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bdf18a54cf91b4d98a8e8b67f6321606539fbcdcac02536286ad1de37b53fd2" +checksum = "b5574dd2f922b1a46a06a4b1dc11193a4012108fd54cf725e1816cb8183d8778" dependencies = [ "anyhow", "bon", @@ -4197,9 +4199,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "10.0.0" +version = "10.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4961429ed12888cb3c6dd20f7dc9508c821091a3ba5fec0156ed5a654c1c4572" +checksum = "23d9fead6d7c515ac3a658e1a65d36925525d5cfdea414e27eb99b99ffd92bfa" dependencies = [ "anyhow", "bon", @@ -4211,9 +4213,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "10.0.0" +version = "10.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910e8471e27130bbc019e9bfa6bda16dfc4c6dd7c5d0793da70a9256caeae984" +checksum = "8cd42fd155c2c2971f6d00face12ec245fbb604fce011ccaf2306d014c2e97ca" dependencies = [ "anyhow", "bon", @@ -4253,9 +4255,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4266,9 +4268,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.75" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4276,9 +4278,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4286,9 +4288,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -4299,9 +4301,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -4680,6 +4682,17 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + [[package]] name = "wyz" version = "0.5.1" @@ -4704,6 +4717,7 @@ dependencies = [ "futures", "hashbrown 0.17.1", "indexmap 2.14.0", + "inventory", "libc", "mlua", "paste", @@ -5033,7 +5047,7 @@ dependencies = [ "objc2", "parking_lot", "percent-encoding", - "rand 0.10.1", + "rand 0.10.2", "regex", "scopeguard", "serde", @@ -5222,6 +5236,7 @@ dependencies = [ "foldhash", "futures", "hashbrown 0.17.1", + "indexmap 2.14.0", "inventory", "libc", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 6985c10a..2778deef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,8 @@ debug = false [workspace.dependencies] ansi-to-tui = "8.0.1" -anyhow = "1.0.102" -arc-swap = { version = "1.9.1", features = [ "serde" ] } +anyhow = "1.0.103" +arc-swap = { version = "1.9.2", features = [ "serde" ] } base64 = "0.22.1" bitflags = { version = "2.13.0", features = [ "serde" ] } chrono = "0.4.45" @@ -61,11 +61,11 @@ ordered-float = { version = "5.3.0", features = [ "serde" ] } parking_lot = "0.12.5" paste = "1.0.15" percent-encoding = "2.3.2" -rand = { version = "0.10.1", default-features = false, features = [ "std", "sys_rng" ] } +rand = { version = "0.10.2", default-features = false, features = [ "std", "sys_rng" ] } ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] } ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] } regex = "1.12.4" -russh = { version = "0.61.2", default-features = false, features = [ "ring", "rsa" ] } +russh = { version = "0.62.1", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.150" diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index 1015e74a..8c05e59e 100644 --- a/yazi-actor/Cargo.toml +++ b/yazi-actor/Cargo.toml @@ -45,6 +45,7 @@ either = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } indexmap = { workspace = true } +inventory = { workspace = true } mlua = { workspace = true } paste = { workspace = true } ratatui-core = { workspace = true } diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index 964d715e..bb4a631e 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -66,14 +66,8 @@ impl Actor for Trigger { impl Trigger { fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> { - let sep = if cfg!(windows) { - AnyAsciiChar::new(b"/\\").unwrap() - } else { - AnyAsciiChar::new(b"/").unwrap() - }; - let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?; - if path.is_empty() && !sep.predicate(s.bytes().last()?) { + if path.is_empty() && !AnyAsciiChar::SEP.predicate(s.bytes().last()?) { return None; // We don't complete a `sftp://test`, but `sftp://test/` } @@ -84,7 +78,7 @@ impl Trigger { } // Child - let child = path.rsplit_pred(sep).map_or(path.as_path(), |(_, c)| c).to_owned(); + let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.as_path(), |(_, c)| c).to_owned(); // Parent let url = UrlCow::try_from((scheme.clone().zeroed(), path)).ok()?; diff --git a/yazi-actor/src/confirm/arrow.rs b/yazi-actor/src/confirm/arrow.rs index 3318b3fc..bc75ccbb 100644 --- a/yazi-actor/src/confirm/arrow.rs +++ b/yazi-actor/src/confirm/arrow.rs @@ -19,7 +19,7 @@ impl Actor for Arrow { let len = confirm.list.line_count(area.width); let old = confirm.offset; - confirm.offset = form.step.add(confirm.offset, len, area.height as _); + confirm.offset = form.step.add(confirm.offset, len, area.height as _, 0, 0); succ!(render!(old != confirm.offset)); } diff --git a/yazi-actor/src/help/close.rs b/yazi-actor/src/help/close.rs new file mode 100644 index 00000000..2fa17034 --- /dev/null +++ b/yazi-actor/src/help/close.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use yazi_macro::{emit, render, succ}; +use yazi_parser::help::CloseForm; +use yazi_shared::data::Data; + +use crate::{Actor, Ctx}; + +pub struct Close; + +impl Actor for Close { + type Form = CloseForm; + + const NAME: &str = "close"; + + fn act(cx: &mut Ctx, opt: Self::Form) -> Result { + let help = &mut cx.help; + + if let Some(chord) = help.bindings.get(help.cursor).filter(|_| opt.submit) { + emit!(Seq(chord.to_seq(help.layer))); + } + + help.visible = false; + succ!(render!()); + } +} diff --git a/yazi-actor/src/help/escape.rs b/yazi-actor/src/help/escape.rs index 3e26003c..16e8147c 100644 --- a/yazi-actor/src/help/escape.rs +++ b/yazi-actor/src/help/escape.rs @@ -2,6 +2,7 @@ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::VoidForm; use yazi_shared::data::Data; +use yazi_widgets::input::InputMode; use crate::{Actor, Ctx}; @@ -13,15 +14,11 @@ impl Actor for Escape { const NAME: &str = "escape"; fn act(cx: &mut Ctx, _: Self::Form) -> Result { - if cx.help.keyword().is_none() { - return act!(help:toggle, cx, cx.help.layer); + if cx.help.input.mode() == InputMode::Normal { + return act!(help:close, cx); } - let help = &mut cx.help; - help.keyword = String::new(); - help.in_filter = None; - help.filter_apply(); - + act!(escape, cx.help.input)?; succ!(render!()); } } diff --git a/yazi-actor/src/help/filter.rs b/yazi-actor/src/help/filter.rs deleted file mode 100644 index 94cab689..00000000 --- a/yazi-actor/src/help/filter.rs +++ /dev/null @@ -1,22 +0,0 @@ -use anyhow::Result; -use yazi_macro::{render, succ}; -use yazi_parser::VoidForm; -use yazi_shared::data::Data; - -use crate::{Actor, Ctx}; - -pub struct Filter; - -impl Actor for Filter { - type Form = VoidForm; - - const NAME: &str = "filter"; - - fn act(cx: &mut Ctx, _: Self::Form) -> Result { - let help = &mut cx.help; - - help.in_filter = Some(Default::default()); - help.filter_apply(); - succ!(render!()); - } -} diff --git a/yazi-actor/src/help/mod.rs b/yazi-actor/src/help/mod.rs index 37276cc1..dae98bf5 100644 --- a/yazi-actor/src/help/mod.rs +++ b/yazi-actor/src/help/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(arrow escape filter toggle); +yazi_macro::mod_flat!(arrow close escape toggle); diff --git a/yazi-actor/src/help/toggle.rs b/yazi-actor/src/help/toggle.rs index 2a55080d..3311c863 100644 --- a/yazi-actor/src/help/toggle.rs +++ b/yazi-actor/src/help/toggle.rs @@ -1,7 +1,10 @@ use anyhow::Result; +use ratatui_core::layout::Margin; +use yazi_config::popup::Help; use yazi_macro::{render, succ}; use yazi_parser::help::ToggleForm; use yazi_shared::data::Data; +use yazi_widgets::input::Input; use crate::{Actor, Ctx}; @@ -13,17 +16,24 @@ impl Actor for Toggle { const NAME: &str = "toggle"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { + let position = Help::position(); + let area = cx.mgr.area(position); + let input_area = area.inner(Margin::new(1, 1)); + let help = &mut cx.help; - - help.visible = !help.visible; + help.visible = true; help.layer = form.layer; + help.position = position; + help.height = area.height; - help.keyword = String::new(); - help.in_filter = None; - help.filter_apply(); + help.input = Input::default(); + help.input.repos(input_area); + help.keyword.clear(); help.offset = 0; help.cursor = 0; + help.filter_apply(); + succ!(render!()); } } diff --git a/yazi-actor/src/input/show.rs b/yazi-actor/src/input/show.rs index 3c3272c3..cdd9d397 100644 --- a/yazi-actor/src/input/show.rs +++ b/yazi-actor/src/input/show.rs @@ -21,9 +21,10 @@ impl Actor for Show { let area = cx.mgr.area(opt.position).padding(cx.input.padding()); let input = &mut cx.input; - input.main.visible = true; + input.main.name = opt.name.clone(); input.main.title = opt.title.clone(); input.main.position = opt.position; + input.main.visible = true; opt.styles = (&THEME.input).into(); opt.blinking = YAZI.input.cursor_blink; diff --git a/yazi-actor/src/lives/files.rs b/yazi-actor/src/lives/entries.rs similarity index 85% rename from yazi-actor/src/lives/files.rs rename to yazi-actor/src/lives/entries.rs index e67998ae..4737bfeb 100644 --- a/yazi-actor/src/lives/files.rs +++ b/yazi-actor/src/lives/entries.rs @@ -5,19 +5,19 @@ use yazi_shim::mlua::UserDataFieldsExt; use super::{File, Filter, Lives, PtrCell}; -pub(super) struct Files { +pub(super) struct Entries { window: Range, folder: PtrCell, tab: PtrCell, } -impl Deref for Files { - type Target = yazi_fs::Files; +impl Deref for Entries { + type Target = yazi_fs::Entries; - fn deref(&self) -> &Self::Target { &self.folder.files } + fn deref(&self) -> &Self::Target { &self.folder.entries } } -impl Files { +impl Entries { pub(super) fn make( window: Range, folder: &yazi_core::tab::Folder, @@ -27,7 +27,7 @@ impl Files { } } -impl UserData for Files { +impl UserData for Entries { fn add_fields>(fields: &mut F) { fields.add_static_field("filter", |_, me| me.filter().map(Filter::make).transpose()); } diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index adca9c5f..8a283bed 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -3,6 +3,7 @@ use std::{ops::Deref, ptr}; use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods}; use yazi_binding::{Range, style::Style}; use yazi_config::THEME; +use yazi_fs::file::FileInventory; use yazi_shared::{path::AsPath, url::UrlLike}; use super::{FILE_CACHE, Lives}; @@ -17,7 +18,7 @@ pub(super) struct File { impl Deref for File { type Target = yazi_fs::file::File; - fn deref(&self) -> &Self::Target { &self.folder.files[self.idx] } + fn deref(&self) -> &Self::Target { &self.folder.entries[self.idx] } } impl AsRef for File { @@ -32,14 +33,16 @@ impl File { ) -> mlua::Result { use hashbrown::hash_map::Entry; - Ok(match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.files[idx])) { - Entry::Occupied(oe) => oe.into_mut().clone(), - Entry::Vacant(ve) => { - let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into() })?; - ve.insert(ud.clone()); - ud - } - }) + Ok( + match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.entries[idx])) { + Entry::Occupied(oe) => oe.into_mut().clone(), + Entry::Vacant(ve) => { + let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into() })?; + ve.insert(ud.clone()); + ud + } + }, + ) } #[inline] @@ -49,7 +52,6 @@ impl File { impl UserData for File { fn add_fields>(fields: &mut F) { yazi_binding::impl_file_fields!(fields); - fields.add_cached_field("bare", |_, me| Ok(yazi_fs::file::File::from(&**me))); fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); fields.add_field_method_get("is_hovered", |_, me| Ok(me.is_hovered())); @@ -62,12 +64,16 @@ impl UserData for File { fn add_methods>(methods: &mut M) { yazi_binding::impl_file_methods!(methods); - methods.add_method("icon", |_, me, ()| { + methods.add_method("icon", |lua, me, ()| { + yazi_binding::deprecate!( + lua, + "{}: `File:icon()` is deprecated, use `th.icon:match(file)` instead" + ); // TODO: use a cache Ok(yazi_config::THEME.icon.matches(me, me.is_hovered())) }); methods.add_method("size", |_, me, ()| { - Ok(if me.is_dir() { me.folder.files.sizes.get(&me.urn()).copied() } else { Some(me.len) }) + Ok(if me.is_dir() { me.folder.entries.sizes.get(&me.urn()).copied() } else { Some(me.len) }) }); methods.add_method("mime", |lua, me, ()| { let core: CoreRef = lua.named_registry_value("cx")?; @@ -98,16 +104,16 @@ impl UserData for File { }) }); methods.add_method("is_marked", |_, me, ()| { - use yazi_core::tab::Mode::*; - if !me.tab.mode.is_visual() || me.folder.url != me.tab.current.url { + let Some(visual) = me.tab.mode.visual() else { + return Ok(0u8); + }; + if !visual.contains(me.idx, me.tab.current.cursor, me.folder.entries.len()) { return Ok(0u8); } - - Ok(match &me.tab.mode { - Select(_, indices) if indices.contains(&me.idx) => 1u8, - Unset(_, indices) if indices.contains(&me.idx) => 2u8, - _ => 0u8, - }) + if me.folder.url != me.tab.current.url { + return Ok(0u8); + } + Ok(if me.tab.mode.is_select() { 1u8 } else { 2u8 }) }); methods.add_method("is_selected", |_, me, ()| Ok(me.tab.selected.contains(&me.url))); methods.add_method("found", |lua, me, ()| { @@ -138,3 +144,10 @@ impl UserData for File { }); } } + +inventory::submit! { + FileInventory { + register: |_| {}, + borrow: |ud, f| f(&*ud.borrow::()?), + } +} diff --git a/yazi-actor/src/lives/folder.rs b/yazi-actor/src/lives/folder.rs index e7fa3e38..c090d7c6 100644 --- a/yazi-actor/src/lives/folder.rs +++ b/yazi-actor/src/lives/folder.rs @@ -4,7 +4,7 @@ use mlua::{AnyUserData, UserData, UserDataFields}; use yazi_config::LAYOUT; use yazi_shim::mlua::UserDataFieldsExt; -use super::{File, Files, Lives, PtrCell}; +use super::{Entries, File, Lives, PtrCell}; pub(super) struct Folder { window: Range, @@ -28,7 +28,7 @@ impl Folder { Some(w) => w, None => { let limit = LAYOUT.get().preview.height as usize; - inner.offset..inner.files.len().min(inner.offset + limit) + inner.offset..inner.entries.len().min(inner.offset + limit) } }; @@ -39,9 +39,9 @@ impl Folder { impl UserData for Folder { fn add_fields>(fields: &mut F) { fields.add_cached_field("cwd", |_, me| Ok(me.url.clone())); - fields.add_static_field("files", |_, me| Files::make(0..me.files.len(), me, &me.tab)); + fields.add_static_field("files", |_, me| Entries::make(0..me.entries.len(), me, &me.tab)); fields.add_cached_field("stage", |_, me| Ok(me.stage.clone())); - fields.add_static_field("window", |_, me| Files::make(me.window.clone(), me, &me.tab)); + fields.add_static_field("window", |_, me| Entries::make(me.window.clone(), me, &me.tab)); fields.add_field_method_get("offset", |_, me| Ok(me.offset)); fields.add_field_method_get("cursor", |_, me| Ok(me.cursor)); diff --git a/yazi-actor/src/lives/mod.rs b/yazi-actor/src/lives/mod.rs index ea093ddb..05b6599e 100644 --- a/yazi-actor/src/lives/mod.rs +++ b/yazi-actor/src/lives/mod.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_flat!(behavior core file files filter finder folder input input_alt lives mode mut_cell preference preview ptr selected tab tabs task tasks which yanked); +yazi_macro::mod_flat!(behavior core entries file filter finder folder input input_alt lives mode mut_cell preference preview ptr selected tab tabs task tasks which yanked); pub(super) fn init() { unsafe { FILE_CACHE.get().write(std::mem::MaybeUninit::new(<_>::default())) }; diff --git a/yazi-actor/src/lives/mode.rs b/yazi-actor/src/lives/mode.rs index 71515ca2..6c37a60b 100644 --- a/yazi-actor/src/lives/mode.rs +++ b/yazi-actor/src/lives/mode.rs @@ -1,6 +1,7 @@ use std::ops::Deref; use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods}; +use yazi_binding::deprecate; use super::{Lives, PtrCell}; @@ -22,9 +23,13 @@ impl Mode { impl UserData for Mode { fn add_fields>(fields: &mut F) { + fields.add_field_method_get("is_normal", |_, me| Ok(me.is_normal())); fields.add_field_method_get("is_select", |_, me| Ok(me.is_select())); fields.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); - fields.add_field_method_get("is_visual", |_, me| Ok(me.is_visual())); + fields.add_field_method_get("is_visual", |lua, me| { + deprecate!(lua, "{}: `mode.is_visual` is deprecated, use `not mode.is_normal` instead. \nSee #4101 for more details: https://github.com/sxyazi/yazi/pull/4101."); + Ok(!me.is_normal()) + }); } fn add_methods>(methods: &mut M) { diff --git a/yazi-actor/src/lives/preview.rs b/yazi-actor/src/lives/preview.rs index 3cb58345..61db852a 100644 --- a/yazi-actor/src/lives/preview.rs +++ b/yazi-actor/src/lives/preview.rs @@ -34,7 +34,7 @@ impl UserData for Preview { .hovered_folder() .map(|f| { let limit = LAYOUT.get().preview.height as usize; - Folder::make(Some(me.skip..f.files.len().min(me.skip + limit)), f, &me.tab) + Folder::make(Some(me.skip..f.entries.len().min(me.skip + limit)), f, &me.tab) }) .transpose() }); diff --git a/yazi-actor/src/lives/selected.rs b/yazi-actor/src/lives/selected.rs index 68694257..a9119a83 100644 --- a/yazi-actor/src/lives/selected.rs +++ b/yazi-actor/src/lives/selected.rs @@ -1,8 +1,6 @@ use mlua::AnyUserData; -use yazi_shared::url::UrlBuf; -use super::Lives; -use crate::lives::PtrCell; +use super::{Lives, PtrCell}; #[derive(Clone, Copy)] pub(super) struct Selected; @@ -12,7 +10,7 @@ impl Selected { let inner = PtrCell::from(inner); Lives::scoped_userdata(yazi_binding::Iter::new( - inner.as_static().values().map(UrlBuf::from), + inner.as_static().files().cloned(), Some(inner.len()), )) } diff --git a/yazi-actor/src/lives/which.rs b/yazi-actor/src/lives/which.rs index 3e80cb7f..3b2aa936 100644 --- a/yazi-actor/src/lives/which.rs +++ b/yazi-actor/src/lives/which.rs @@ -24,10 +24,10 @@ impl Which { impl UserData for Which { fn add_fields>(fields: &mut F) { fields.add_cached_field("tx", |_, me| Ok(me.tx.clone().map(yazi_binding::MpscUnboundedTx))); - fields.add_field_method_get("times", |_, me| Ok(me.inner.times)); fields.add_cached_field("cands", |lua, me| { lua.create_sequence_from(me.inner.cands.iter().cloned()) }); + fields.add_field_method_get("times", |_, me| Ok(me.inner.times)); fields.add_field_method_get("active", |_, me| Ok(me.inner.active)); fields.add_field_method_get("silent", |_, me| Ok(me.inner.silent)); diff --git a/yazi-actor/src/lives/yanked.rs b/yazi-actor/src/lives/yanked.rs index 1f30826c..38581bae 100644 --- a/yazi-actor/src/lives/yanked.rs +++ b/yazi-actor/src/lives/yanked.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use mlua::{AnyUserData, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods}; use yazi_binding::Iter; -use yazi_shared::url::UrlBuf; +use yazi_fs::file::File; use yazi_shim::mlua::get_metatable; use super::{Lives, PtrCell}; @@ -25,7 +25,7 @@ impl Yanked { Lives::scoped_userdata(Self { inner, iter: Lives::scoped_userdata(Iter::new( - inner.as_static().iter().map(UrlBuf::from), + inner.as_static().iter().map(File::from), Some(inner.len()), ))?, }) diff --git a/yazi-actor/src/mgr/arrow.rs b/yazi-actor/src/mgr/arrow.rs index cfcdd272..6734d316 100644 --- a/yazi-actor/src/mgr/arrow.rs +++ b/yazi-actor/src/mgr/arrow.rs @@ -14,6 +14,7 @@ impl Actor for Arrow { fn act(cx: &mut Ctx, form: Self::Form) -> Result { let tab = cx.tab_mut(); + let old = tab.current.cursor; if !tab.current.arrow(form.step) { succ!(); } @@ -22,9 +23,8 @@ impl Actor for Arrow { tab.current.retrace(); // Visual selection - if let Some((start, items)) = tab.mode.visual_mut() { - let end = tab.current.cursor; - *items = (start.min(end)..=end.max(start)).collect(); + if let Some(visual) = tab.mode.visual_mut() { + visual.arrow(form.step, old, tab.current.cursor); } act!(mgr:hover, cx)?; diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 0bd71e9b..ba001880 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -32,7 +32,7 @@ impl Actor for BulkRename { succ!(NotifyProxy::push_warn("Bulk rename", "No text opener found")); }; - let selected: Vec<_> = cx.tab().selected_or_hovered().cloned().collect(); + let selected: Vec<_> = cx.tab().selected_or_hovered_urls().cloned().collect(); if selected.is_empty() { succ!(NotifyProxy::push_warn("Bulk rename", "No files selected")); } diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index f2b06391..8361da5a 100644 --- a/yazi-actor/src/mgr/copy.rs +++ b/yazi-actor/src/mgr/copy.rs @@ -20,7 +20,7 @@ impl Actor for Copy { let mut it = if form.hovered { Box::new(cx.hovered().map(|h| &h.url).into_iter()) } else { - cx.tab().selected_or_hovered() + cx.tab().selected_or_hovered_urls() } .peekable(); diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 08c6b027..9128720b 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -1,13 +1,16 @@ +use std::pin::Pin; + use anyhow::{Result, bail}; +use futures::{Stream, StreamExt}; +use tokio_stream::wrappers::UnboundedReceiverStream; use yazi_config::{YAZI, popup::ConfirmCfg}; use yazi_fs::{FilesOp, file::File}; use yazi_macro::{input, ok_or_not_found, succ}; use yazi_parser::mgr::CreateForm; use yazi_proxy::{ConfirmProxy, MgrProxy}; -use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFile, maybe_exists, provider}; +use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, strand::{StrandBuf, StrandLike}, url::{UrlBuf, UrlLike}}; +use yazi_vfs::{VfsFile, provider}; use yazi_watcher::WATCHER; -use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; @@ -18,28 +21,37 @@ impl Actor for Create { const NAME: &str = "create"; - fn act(cx: &mut Ctx, form: Self::Form) -> Result { + fn act(cx: &mut Ctx, CreateForm { target, dir, force }: Self::Form) -> Result { let cwd = cx.cwd().to_owned(); - let mut input = input!(cx, YAZI.input.create(form.dir))?; + + let mut target: Pin + Send>> = if target.is_empty() { + let input = input!(cx, YAZI.input.create(dir))?; + Box::pin( + UnboundedReceiverStream::new(input).filter_map(|event| async { event.map(Into::into) }), + ) + } else { + Box::pin(tokio_stream::iter(vec![target])) + }; tokio::spawn(async move { - let Some(InputEvent::Submit(name)) = input.recv().await else { return }; + let Some(name) = target.next().await else { return Ok(()) }; if name.is_empty() { - return; + return Ok(()); } let Ok(new) = cwd.try_join(&name) else { - return; + bail!("Failed to join new name with CWD"); }; - if !form.force - && maybe_exists(&new).await - && !ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await + if !force + && let Some(file) = File::maybe_new(&new).await? + && !ConfirmProxy::show(ConfirmCfg::overwrite(&file)).await { - return; + return Ok(()); } - _ = Self::r#do(new, form.dir || name.ends_with('/') || name.ends_with('\\')).await; + let end_sep = AnyAsciiChar::SEP.predicate(*name.encoded_bytes().last().unwrap()); + Self::r#do(new, dir || end_sep).await }); succ!(); } diff --git a/yazi-actor/src/mgr/escape.rs b/yazi-actor/src/mgr/escape.rs index 85ce7b85..afb472b8 100644 --- a/yazi-actor/src/mgr/escape.rs +++ b/yazi-actor/src/mgr/escape.rs @@ -67,15 +67,16 @@ impl Actor for EscapeVisual { let tab = cx.tab_mut(); let select = tab.mode.is_select(); - let Some((_, indices)) = tab.mode.take_visual() else { succ!(false) }; + let Some(indices) = tab.mode.take_visual(tab.current.cursor, tab.current.entries.len()) else { + succ!(false) + }; render!(); - let urls: Vec<_> = - indices.into_iter().filter_map(|i| tab.current.files.get(i)).map(|f| &f.url).collect(); + let files: Vec<_> = indices.into_iter().filter_map(|i| tab.current.entries.get(i)).collect(); if !select { - tab.selected.remove_many(urls); - } else if urls.len() != tab.selected.add_many(urls) { + tab.selected.remove_many(files); + } else if files.len() != tab.selected.add_many(files) { NotifyProxy::push_warn( "Escape visual mode", "Some files cannot be selected, due to path nesting conflict.", @@ -96,7 +97,7 @@ impl Actor for EscapeFilter { const NAME: &str = "escape_filter"; fn act(cx: &mut Ctx, _: Self::Form) -> Result { - if cx.current_mut().files.filter().is_none() { + if cx.current_mut().entries.filter().is_none() { succ!(false); } diff --git a/yazi-actor/src/mgr/filter_do.rs b/yazi-actor/src/mgr/filter_do.rs index b1758234..0d76cf11 100644 --- a/yazi-actor/src/mgr/filter_do.rs +++ b/yazi-actor/src/mgr/filter_do.rs @@ -17,7 +17,7 @@ impl Actor for FilterDo { let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) }; let hovered = cx.hovered().map(|f| f.urn().into()); - cx.current_mut().files.set_filter(filter); + cx.current_mut().entries.set_filter(filter); if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { act!(mgr:hover, cx, hovered)?; diff --git a/yazi-actor/src/mgr/find_arrow.rs b/yazi-actor/src/mgr/find_arrow.rs index 815567ba..ad4be900 100644 --- a/yazi-actor/src/mgr/find_arrow.rs +++ b/yazi-actor/src/mgr/find_arrow.rs @@ -18,9 +18,9 @@ impl Actor for FindArrow { render!(finder.catchup(&tab.current)); let offset = if form.prev { - finder.prev(&tab.current.files, tab.current.cursor, false) + finder.prev(&tab.current.entries, tab.current.cursor, false) } else { - finder.next(&tab.current.files, tab.current.cursor, false) + finder.next(&tab.current.entries, tab.current.cursor, false) }; if let Some(offset) = offset { diff --git a/yazi-actor/src/mgr/find_do.rs b/yazi-actor/src/mgr/find_do.rs index abc2cf1e..abe73439 100644 --- a/yazi-actor/src/mgr/find_do.rs +++ b/yazi-actor/src/mgr/find_do.rs @@ -24,9 +24,9 @@ impl Actor for FindDo { } let step = if opt.prev { - finder.prev(&cx.current().files, cx.current().cursor, true) + finder.prev(&cx.current().entries, cx.current().cursor, true) } else { - finder.next(&cx.current().files, cx.current().cursor, true) + finder.next(&cx.current().entries, cx.current().cursor, true) }; if let Some(step) = step { diff --git a/yazi-actor/src/mgr/hidden.rs b/yazi-actor/src/mgr/hidden.rs index 66edccd3..6113805a 100644 --- a/yazi-actor/src/mgr/hidden.rs +++ b/yazi-actor/src/mgr/hidden.rs @@ -24,8 +24,8 @@ impl Actor for Hidden { render!(); false } else { - f.files.set_show_hidden(state); - render_and!(f.files.catchup_revision()) + f.entries.set_show_hidden(state); + render_and!(f.entries.catchup_revision()) } }; diff --git a/yazi-actor/src/mgr/mod.rs b/yazi-actor/src/mgr/mod.rs index dfbc3aae..0bee758f 100644 --- a/yazi-actor/src/mgr/mod.rs +++ b/yazi-actor/src/mgr/mod.rs @@ -33,6 +33,7 @@ yazi_macro::mod_flat!( quit refresh remove + remove_do rename reveal search diff --git a/yazi-actor/src/mgr/open.rs b/yazi-actor/src/mgr/open.rs index 2d789ed0..03daa337 100644 --- a/yazi-actor/src/mgr/open.rs +++ b/yazi-actor/src/mgr/open.rs @@ -27,7 +27,7 @@ impl Actor for Open { Quit::with_selected(cx.hovered().map(|h| &h.url)) } else { act!(mgr:escape_visual, cx)?; - Quit::with_selected(cx.tab().selected_or_hovered()) + Quit::with_selected(cx.tab().selected_or_hovered_urls()) }); } @@ -36,7 +36,7 @@ impl Actor for Open { cx.hovered().map(|h| vec![h.url.clone()]).unwrap_or_default() } else { act!(mgr:escape_visual, cx)?; - cx.tab().selected_or_hovered().cloned().collect() + cx.tab().selected_or_hovered_urls().cloned().collect() }; } if opt.targets.is_empty() { diff --git a/yazi-actor/src/mgr/paste.rs b/yazi-actor/src/mgr/paste.rs index b030474a..63037efe 100644 --- a/yazi-actor/src/mgr/paste.rs +++ b/yazi-actor/src/mgr/paste.rs @@ -20,7 +20,7 @@ impl Actor for Paste { if mgr.yanked.cut { cx.core.tasks.file_cut(&mgr.yanked, dest, form.force); - mgr.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(&*mgr.yanked)); + mgr.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(mgr.yanked.urls())); act!(mgr:unyank, cx) } else { succ!(cx.core.tasks.file_copy(&mgr.yanked, dest, form.force, form.follow)); diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index 00b05022..5d4ae16f 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -1,10 +1,10 @@ use anyhow::Result; use yazi_core::tab::Folder; -use yazi_fs::{CWD, Files, FilesOp, cha::Cha}; +use yazi_fs::{CWD, Entries, FilesOp, cha::Cha}; use yazi_macro::{act, succ}; use yazi_parser::VoidForm; use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFiles, VfsFilesOp}; +use yazi_vfs::{VfsEntries, VfsFilesOp}; use yazi_watcher::MgrProxy; use crate::{Actor, Ctx}; @@ -29,7 +29,7 @@ impl Actor for Refresh { act!(mgr:watch, cx)?; act!(mgr:update_paged, cx)?; - cx.tasks.prework_sorted(&cx.current().files); + cx.tasks.prework_sorted(&cx.current().entries); succ!(); } } @@ -44,9 +44,9 @@ impl Refresh { // TODO: performance improvement fn trigger_dirs(folders: &[&Folder]) { async fn go(dir: UrlBuf, cha: Cha) { - let Some(cha) = Files::assert_stale(&dir, cha).await else { return }; + let Some(cha) = Entries::assert_stale(&dir, cha).await else { return }; - match Files::from_dir_bulk(&dir).await { + match Entries::from_dir_bulk(&dir).await { Ok(files) => FilesOp::Full(dir, files, cha).emit(), Err(e) => FilesOp::issue_error(&dir, e).await, } diff --git a/yazi-actor/src/mgr/remove.rs b/yazi-actor/src/mgr/remove.rs index 077f072f..75d409a4 100644 --- a/yazi-actor/src/mgr/remove.rs +++ b/yazi-actor/src/mgr/remove.rs @@ -1,7 +1,8 @@ use anyhow::Result; use yazi_config::popup::ConfirmCfg; +use yazi_fs::file::Files; use yazi_macro::{act, confirm, succ}; -use yazi_parser::mgr::RemoveForm; +use yazi_parser::mgr::{RemoveDoForm, RemoveForm}; use yazi_proxy::MgrProxy; use yazi_shared::data::Data; @@ -14,58 +15,31 @@ impl Actor for Remove { const NAME: &str = "remove"; - fn act(cx: &mut Ctx, mut form: Self::Form) -> Result { + fn act(cx: &mut Ctx, form: Self::Form) -> Result { act!(mgr:escape_visual, cx)?; - form.targets = if form.hovered { - cx.hovered().map_or(vec![], |h| vec![h.url.clone()]) + let targets = Files(if form.hovered { + cx.hovered().map_or(vec![], |h| vec![h.clone()]) } else { - cx.tab().selected_or_hovered().cloned().collect() - }; + cx.tab().selected_or_hovered_files().cloned().collect() + }); - if form.targets.is_empty() { + if targets.is_empty() { succ!(); } else if form.force { - return act!(mgr:remove_do, cx, form); + return act!(mgr:remove_do, cx, RemoveDoForm { permanently: form.permanently, targets: targets.into() }); } let confirm = confirm!( cx, - if form.permanently { - ConfirmCfg::delete(&form.targets) - } else { - ConfirmCfg::trash(&form.targets) - } + if form.permanently { ConfirmCfg::delete(&targets) } else { ConfirmCfg::trash(&targets) } )?; tokio::spawn(async move { if confirm.future().await { - MgrProxy::remove_do(form.targets, form.permanently); + MgrProxy::remove_do(form.permanently, targets); } }); succ!(); } } - -// --- Do -pub struct RemoveDo; - -impl Actor for RemoveDo { - type Form = RemoveForm; - - const NAME: &str = "remove_do"; - - fn act(cx: &mut Ctx, form: Self::Form) -> Result { - let mgr = &mut cx.mgr; - - mgr.tabs.iter_mut().for_each(|t| { - t.selected.remove_many(&form.targets); - }); - - mgr.yanked.remove_many(&form.targets); - mgr.yanked.catchup_revision(false); - - cx.tasks.file_remove(form.targets, form.permanently); - succ!(); - } -} diff --git a/yazi-actor/src/mgr/remove_do.rs b/yazi-actor/src/mgr/remove_do.rs new file mode 100644 index 00000000..c1672130 --- /dev/null +++ b/yazi-actor/src/mgr/remove_do.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use yazi_macro::succ; +use yazi_parser::mgr::RemoveDoForm; +use yazi_shared::data::Data; + +use crate::{Actor, Ctx}; + +pub struct RemoveDo; + +impl Actor for RemoveDo { + type Form = RemoveDoForm; + + const NAME: &str = "remove_do"; + + fn act(cx: &mut Ctx, form: Self::Form) -> Result { + let mgr = &mut cx.mgr; + + mgr.tabs.iter_mut().for_each(|t| { + t.selected.remove_many(&form.targets); + }); + + mgr.yanked.remove_many(&form.targets); + mgr.yanked.catchup_revision(false); + + cx.tasks.file_remove(form.targets, form.permanently); + succ!(); + } +} diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 22d54467..b654e33c 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Result, bail}; use yazi_config::{YAZI, popup::ConfirmCfg}; use yazi_dds::Pubsub; use yazi_fs::{FilesOp, file::File}; @@ -6,7 +6,7 @@ use yazi_macro::{act, err, input, ok_or_not_found, succ}; use yazi_parser::mgr::RenameForm; use yazi_proxy::{ConfirmProxy, MgrProxy}; use yazi_shared::{data::Data, id::Id, url::{UrlBuf, UrlLike}}; -use yazi_vfs::{VfsFile, maybe_exists, provider}; +use yazi_vfs::{VfsFile, provider}; use yazi_watcher::WATCHER; use yazi_widgets::input::InputEvent; @@ -42,23 +42,23 @@ impl Actor for Rename { }; let (tab, old) = (cx.tab().id, hovered.url_owned()); - let mut input = input!(cx, YAZI.input.rename().with_value(name).with_cursor(cursor))?; + let mut input = + input!(cx, YAZI.input.rename(hovered.is_dir()).with_value(name).with_cursor(cursor))?; tokio::spawn(async move { - let Some(InputEvent::Submit(name)) = input.recv().await else { return }; + let Some(InputEvent::Submit(name)) = input.recv().await else { return Ok(()) }; if name.is_empty() { - return; + return Ok(()); } let Some(Ok(new)) = old.parent().map(|u| u.try_join(name)) else { - return; + bail!("Failed to join new name with parent directory"); }; - if form.force || !maybe_exists(&new).await || provider::must_identical(&old, &new).await { - Self::r#do(tab, old, new).await.ok(); - } else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await { - Self::r#do(tab, old, new).await.ok(); + if form.force || Self::try_ask(&old, &new).await? { + Self::r#do(tab, old, new).await?; } + Ok::<(), anyhow::Error>(()) }); succ!(); } @@ -97,6 +97,17 @@ impl Rename { Ok(()) } + async fn try_ask(old: &UrlBuf, new: &UrlBuf) -> Result { + let Some(file) = File::maybe_new(&new).await? else { + return Ok(true); + }; + + Ok( + provider::must_identical(old, new).await + || ConfirmProxy::show(ConfirmCfg::overwrite(&file)).await, + ) + } + fn empty_url_part(url: &UrlBuf, by: &str) -> String { if by == "all" { return String::new(); diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index 3e99eef7..270e2067 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -30,8 +30,8 @@ impl Actor for Sort { render!(); false } else { - f.files.set_sorter(sorter); - render_and!(f.files.catchup_revision()) + f.entries.set_sorter(sorter); + render_and!(f.entries.catchup_revision()) } }; @@ -41,7 +41,7 @@ impl Actor for Sort { { act!(mgr:hover, cx)?; act!(mgr:update_paged, cx)?; - cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.files); + cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.entries); } // Apply to hovered diff --git a/yazi-actor/src/mgr/tab_swap.rs b/yazi-actor/src/mgr/tab_swap.rs index f2a37e81..b08b6dee 100644 --- a/yazi-actor/src/mgr/tab_swap.rs +++ b/yazi-actor/src/mgr/tab_swap.rs @@ -16,7 +16,7 @@ impl Actor for TabSwap { fn act(cx: &mut Ctx, form: Self::Form) -> Result { let tabs = cx.tabs_mut(); - let new = form.step.add(tabs.cursor, tabs.len(), 0); + let new = form.step.add(tabs.cursor, tabs.len(), 0, 0, 0); if new == tabs.cursor { succ!(); } diff --git a/yazi-actor/src/mgr/toggle.rs b/yazi-actor/src/mgr/toggle.rs index b9c129a4..f68285a0 100644 --- a/yazi-actor/src/mgr/toggle.rs +++ b/yazi-actor/src/mgr/toggle.rs @@ -15,14 +15,14 @@ impl Actor for Toggle { fn act(cx: &mut Ctx, form: Self::Form) -> Result { let tab = cx.tab_mut(); - let Some(url) = form.url.or_else(|| tab.hovered().map(|h| h.url.clone())) else { + let Some(file) = form.file.or_else(|| tab.hovered().cloned()) else { succ!(); }; let b = match form.state { - Some(true) => render_and!(tab.selected.add(&url)), - Some(false) => render_and!(tab.selected.remove(&url)) | true, - None => render_and!(tab.selected.remove(&url) || tab.selected.add(&url)), + Some(true) => render_and!(tab.selected.add(file)), + Some(false) => render_and!(tab.selected.remove(&file.url)) | true, + None => render_and!(tab.selected.remove(&file.url) || tab.selected.add(file)), }; if !b { diff --git a/yazi-actor/src/mgr/toggle_all.rs b/yazi-actor/src/mgr/toggle_all.rs index 1364abe5..9b9c8efd 100644 --- a/yazi-actor/src/mgr/toggle_all.rs +++ b/yazi-actor/src/mgr/toggle_all.rs @@ -17,14 +17,14 @@ impl Actor for ToggleAll { use either::Either::*; let tab = cx.tab_mut(); - let it = tab.current.files.iter().map(|f| &f.url); + let it = tab.current.entries.iter(); let either = match form.state { - Some(true) if form.urls.is_empty() => Left((vec![], it.collect())), - Some(true) => Right((vec![], form.urls)), - Some(false) if form.urls.is_empty() => Left((it.collect(), vec![])), - Some(false) => Right((form.urls, vec![])), - None if form.urls.is_empty() => Left(it.partition(|&u| tab.selected.contains(u))), - None => Right(form.urls.into_iter().partition(|u| tab.selected.contains(u))), + Some(true) if form.files.is_empty() => Left((vec![], it.collect())), + Some(true) => Right((vec![], form.files)), + Some(false) if form.files.is_empty() => Left((it.collect(), vec![])), + Some(false) => Right((form.files, vec![])), + None if form.files.is_empty() => Left(it.partition(|&f| tab.selected.contains(f))), + None => Right(form.files.into_iter().partition(|f| tab.selected.contains(f))), }; let warn = match either { @@ -34,14 +34,14 @@ impl Actor for ToggleAll { } Right((removal, addition)) => { render!(tab.selected.remove_many(&removal) > 0); - render!(tab.selected.add_many(&addition), > 0) != addition.len() + addition.len() != render!(tab.selected.add_many(&addition), > 0) } }; if warn { NotifyProxy::push_warn( "Toggle all", - "Some files cannot be selected, due to path nesting conflict.", + "Some files cannot be selected due to path nesting conflict.", ); } succ!(); diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index 97b77ad5..ff4a7b3a 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -16,7 +16,7 @@ impl Actor for UpdateFiles { const NAME: &str = "update_files"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { - let revision = cx.current().files.revision; + let revision = cx.current().entries.revision; let linked: Vec<_> = LINKED.read().from_dir(form.op.cwd()).map(|u| form.op.chdir(u)).collect(); for op in [form.op].into_iter().chain(linked) { @@ -28,7 +28,7 @@ impl Actor for UpdateFiles { act!(mgr:hidden, cx).ok(); act!(mgr:sort, cx).ok(); - if revision != cx.current().files.revision { + if revision != cx.current().entries.revision { act!(mgr:hover, cx)?; act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; @@ -80,7 +80,7 @@ impl UpdateFiles { } if calc { - cx.tasks.prework_sorted(&cx.current().files); + cx.tasks.prework_sorted(&cx.current().entries); } succ!(); } diff --git a/yazi-actor/src/mgr/update_yanked.rs b/yazi-actor/src/mgr/update_yanked.rs index 60a0148a..ef9182fa 100644 --- a/yazi-actor/src/mgr/update_yanked.rs +++ b/yazi-actor/src/mgr/update_yanked.rs @@ -14,11 +14,11 @@ impl Actor for UpdateYanked { const NAME: &str = "update_yanked"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { - if form.urls.is_empty() && cx.mgr.yanked.is_empty() { + if form.files.is_empty() && cx.mgr.yanked.is_empty() { succ!(); } - cx.mgr.yanked = Yanked::new(form.cut, form.0.urls.into_owned()); + cx.mgr.yanked = Yanked::new(form.cut, form.0.files.into_owned()); succ!(render!()); } } diff --git a/yazi-actor/src/mgr/visual_mode.rs b/yazi-actor/src/mgr/visual_mode.rs index 0fe8a524..13edde08 100644 --- a/yazi-actor/src/mgr/visual_mode.rs +++ b/yazi-actor/src/mgr/visual_mode.rs @@ -1,7 +1,5 @@ -use std::collections::BTreeSet; - use anyhow::Result; -use yazi_core::tab::Mode; +use yazi_core::tab::{Mode, Visual}; use yazi_macro::{render, succ}; use yazi_parser::mgr::VisualModeForm; use yazi_shared::data::Data; @@ -20,9 +18,9 @@ impl Actor for VisualMode { let idx = tab.current.cursor; if form.unset { - tab.mode = Mode::Unset(idx, BTreeSet::from([idx])); + tab.mode = Mode::Unset(Visual::new(idx)); } else { - tab.mode = Mode::Select(idx, BTreeSet::from([idx])); + tab.mode = Mode::Select(Visual::new(idx)); }; succ!(render!()); diff --git a/yazi-actor/src/mgr/yank.rs b/yazi-actor/src/mgr/yank.rs index 99ac6a6b..3180863e 100644 --- a/yazi-actor/src/mgr/yank.rs +++ b/yazi-actor/src/mgr/yank.rs @@ -2,7 +2,7 @@ use anyhow::Result; use yazi_core::mgr::Yanked; use yazi_macro::{act, render}; use yazi_parser::mgr::YankForm; -use yazi_shared::{data::Data, url::UrlBufCov}; +use yazi_shared::data::Data; use crate::{Actor, Ctx}; @@ -17,7 +17,7 @@ impl Actor for Yank { act!(mgr:escape_visual, cx)?; cx.mgr.yanked = - Yanked::new(form.cut, cx.tab().selected_or_hovered().cloned().map(UrlBufCov).collect()); + Yanked::new(form.cut, cx.tab().selected_or_hovered_files().map(Into::into).collect()); render!(cx.mgr.yanked.catchup_revision(true)); act!(mgr:escape_select, cx) diff --git a/yazi-actor/src/spot/arrow.rs b/yazi-actor/src/spot/arrow.rs index d7f85903..c8725e40 100644 --- a/yazi-actor/src/spot/arrow.rs +++ b/yazi-actor/src/spot/arrow.rs @@ -16,7 +16,7 @@ impl Actor for Arrow { let spot = &mut cx.tab_mut().spot; let Some(lock) = &mut spot.lock else { succ!() }; - let new = form.step.add(spot.skip, lock.len().unwrap_or(u16::MAX as _), 0); + let new = form.step.add(spot.skip, lock.len().unwrap_or(u16::MAX as _), 0, 0, 0); let Some(old) = lock.selected() else { return act!(mgr:spot, cx, new); }; diff --git a/yazi-actor/src/tasks/arrow.rs b/yazi-actor/src/tasks/arrow.rs index ebcf200e..474e2969 100644 --- a/yazi-actor/src/tasks/arrow.rs +++ b/yazi-actor/src/tasks/arrow.rs @@ -17,7 +17,7 @@ impl Actor for Arrow { let tasks = &mut cx.tasks; let old = tasks.cursor; - tasks.cursor = form.step.add(tasks.cursor, tasks.snaps.len(), Tasks::limit()); + tasks.cursor = form.step.add(tasks.cursor, tasks.snaps.len(), Tasks::limit(), 0, 0); succ!(render!(tasks.cursor != old)); } diff --git a/yazi-actor/src/which/activate.rs b/yazi-actor/src/which/activate.rs index cd6f8f05..a63ab7d1 100644 --- a/yazi-actor/src/which/activate.rs +++ b/yazi-actor/src/which/activate.rs @@ -23,8 +23,9 @@ impl Actor for Activate { let which = &mut cx.which; which.tx = opt.tx; - which.times = opt.times; + which.layer = opt.layer; which.cands = opt.cands; + which.times = opt.times; which.active = true; which.silent = opt.silent; diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 4cc27b8e..e4fc91fd 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -30,6 +30,6 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } # External dependencies clap = { workspace = true } -clap_complete = "4.6.5" +clap_complete = "4.6.7" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.6.0" diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 09e86f9b..67fb422a 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -42,7 +42,7 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } # External build dependencies anyhow = { workspace = true } clap = { workspace = true } -clap_complete = "4.6.5" +clap_complete = "4.6.7" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.6.0" serde = { workspace = true } diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index 77427710..4dfcf0dc 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -18,5 +18,5 @@ proc-macro = true [dependencies] # External dependencies proc-macro2 = "1" -quote = "1.0.45" +quote = "1.0.46" syn = { version = "2.0.118", features = [ "full" ] } diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index 900cf806..d39c00e8 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -13,13 +13,17 @@ keymap = [ { on = "", run = "close", desc = "Close the current tab, or quit if it's last" }, { on = "", run = "suspend", desc = "Suspend the process" }, - # Hopping - { on = "k", run = "arrow prev", desc = "Previous file" }, - { on = "j", run = "arrow next", desc = "Next file" }, - + # Hop around + { on = "k", run = "arrow prev", desc = "Previous file" }, + { on = "j", run = "arrow next", desc = "Next file" }, { on = "", run = "arrow prev", desc = "Previous file" }, { on = "", run = "arrow next", desc = "Next file" }, + { on = [ "g", "g" ], run = "arrow top", desc = "Go to top" }, + { on = "G", run = "arrow bot", desc = "Go to bottom" }, + { on = "", run = "arrow top", desc = "Go to top" }, + { on = "", run = "arrow bot", desc = "Go to bottom" }, + { on = "", run = "arrow -50%", desc = "Move cursor up half page" }, { on = "", run = "arrow 50%", desc = "Move cursor down half page" }, { on = "", run = "arrow -100%", desc = "Move cursor up one page" }, @@ -30,9 +34,6 @@ keymap = [ { on = "", run = "arrow -100%", desc = "Move cursor up one page" }, { on = "", run = "arrow 100%", desc = "Move cursor down one page" }, - { on = [ "g", "g" ], run = "arrow top", desc = "Go to top" }, - { on = "G", run = "arrow bot", desc = "Go to bottom" }, - # Navigation { on = "h", run = "leave", desc = "Back to the parent directory" }, { on = "l", run = "enter", desc = "Enter the child directory" }, @@ -360,9 +361,10 @@ keymap = [ [help] keymap = [ - { on = "", run = "escape", desc = "Clear the filter, or hide the help" }, - { on = "", run = "escape", desc = "Clear the filter, or hide the help" }, - { on = "", run = "close", desc = "Hide the help" }, + { on = "", run = "escape", desc = "Enter normal mode, or hide help menu" }, + { on = "", run = "escape", desc = "Enter normal mode, or hide help menu" }, + { on = "", run = "close", desc = "Close help menu" }, + { on = "", run = "close --submit", desc = "Close help menu and run selected action(s)" }, # Navigation { on = "k", run = "arrow prev", desc = "Previous line" }, @@ -371,6 +373,6 @@ keymap = [ { on = "", run = "arrow prev", desc = "Previous line" }, { on = "", run = "arrow next", desc = "Next line" }, - # Filtering - { on = "f", run = "filter", desc = "Filter help items" }, + { on = "", run = "arrow prev", desc = "Previous line" }, + { on = "", run = "arrow next", desc = "Next line" }, ] diff --git a/yazi-config/preset/theme-dark.toml b/yazi-config/preset/theme-dark.toml index acfe6527..90d6eca6 100644 --- a/yazi-config/preset/theme-dark.toml +++ b/yazi-config/preset/theme-dark.toml @@ -226,11 +226,10 @@ hovered = { fg = "magenta", bold = true } # : Help menu {{{ [help] -on = { fg = "cyan" } -run = { fg = "magenta" } -desc = {} +border = { fg = "blue" } +chord = { fg = "cyan" } +action = {} hovered = { reversed = true, bold = true } -footer = { fg = "black", bg = "white" } # : }}} diff --git a/yazi-config/preset/theme-light.toml b/yazi-config/preset/theme-light.toml index 7730c866..43cf9552 100644 --- a/yazi-config/preset/theme-light.toml +++ b/yazi-config/preset/theme-light.toml @@ -226,11 +226,10 @@ hovered = { fg = "magenta", bold = true } # : Help menu {{{ [help] -on = { fg = "cyan" } -run = { fg = "magenta" } -desc = {} +border = { fg = "blue" } +chord = { fg = "cyan" } +action = {} hovered = { reversed = true, bold = true } -footer = { fg = "black", bg = "white" } # : }}} diff --git a/yazi-config/src/inject.rs b/yazi-config/src/inject.rs index 7039e902..ba4858c2 100644 --- a/yazi-config/src/inject.rs +++ b/yazi-config/src/inject.rs @@ -6,10 +6,15 @@ use crate::THEME; inventory::submit! { FileInventory { register: |registry| { - registry.add_method("icon", |_, me, ()| { + registry.add_method("icon", |lua, me, ()| { + yazi_binding::deprecate!( + lua, + "{}: `File:icon()` is deprecated, use `th.icon:match(file)` instead" + ); // TODO: use a cache Ok(THEME.icon.matches(me, false)) }); - } + }, + borrow: |_, _| Err(mlua::Error::UserDataTypeMismatch), } } diff --git a/yazi-config/src/keymap/chord.rs b/yazi-config/src/keymap/chord.rs index 44c21135..ec74eb98 100644 --- a/yazi-config/src/keymap/chord.rs +++ b/yazi-config/src/keymap/chord.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer, de}; use serde_with::{DeserializeAs, DisplayFromStr, OneOrMany}; use yazi_binding::Iter; use yazi_codegen::DeserializeOver2; -use yazi_shared::{Layer, event::{Actions, deserialize_actions}, id::Id}; +use yazi_shared::{event::{Actions, deserialize_actions}, id::Id}; use super::{Key, ids::chord_id}; use crate::{Mixable, Platform, keymap::{ChordArc, Chords}}; @@ -14,12 +14,12 @@ use crate::{Mixable, Platform, keymap::{ChordArc, Chords}}; static RE: OnceLock = OnceLock::new(); #[derive(Debug, Default, Deserialize, DeserializeOver2)] -pub struct Chord { +pub struct Chord { #[serde(skip, default = "chord_id")] pub id: Id, #[serde(deserialize_with = "deserialize_on")] pub on: Vec, - #[serde(deserialize_with = "deserialize_actions::")] + #[serde(deserialize_with = "deserialize_actions")] pub run: Actions, #[serde(default)] pub desc: String, @@ -27,7 +27,7 @@ pub struct Chord { pub r#for: Platform, } -impl Clone for Chord { +impl Clone for Chord { fn clone(&self) -> Self { Self { id: chord_id(), @@ -39,17 +39,21 @@ impl Clone for Chord { } } -impl PartialEq for Chord { +impl AsRef for Chord { + fn as_ref(&self) -> &Self { self } +} + +impl PartialEq for Chord { fn eq(&self, other: &Self) -> bool { self.on == other.on } } -impl Eq for Chord {} +impl Eq for Chord {} -impl Hash for Chord { +impl Hash for Chord { fn hash(&self, state: &mut H) { self.on.hash(state) } } -impl Chord { +impl Chord { pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() } pub fn run(&self) -> String { @@ -66,20 +70,13 @@ impl Chord { pub fn desc_or_run(&self) -> Cow<'_, str> { self.desc().unwrap_or_else(|| self.run().into()) } - pub fn contains(&self, s: &str) -> bool { - let s = s.to_lowercase(); - self.desc().map(|d| d.to_lowercase().contains(&s)) == Some(true) - || self.run().to_lowercase().contains(&s) - || self.on().to_lowercase().contains(&s) - } - #[inline] pub(super) fn noop(&self) -> bool { self.run.len() == 1 && self.run[0].name == "noop" && self.run[0].args.is_empty() } } -impl Mixable for Chord { +impl Mixable for Chord { fn filter(&self) -> bool { self.r#for.matches() && !self.noop() } } diff --git a/yazi-config/src/keymap/chord_arc.rs b/yazi-config/src/keymap/chord_arc.rs index 2dd3f7e4..8b81a8eb 100644 --- a/yazi-config/src/keymap/chord_arc.rs +++ b/yazi-config/src/keymap/chord_arc.rs @@ -1,37 +1,40 @@ use std::{ops::{Deref, DerefMut}, sync::Arc}; -use mlua::{UserData, UserDataFields, Value}; +use mlua::{FromLua, Lua, UserData, UserDataFields, Value}; use serde::Deserialize; -use yazi_codegen::FromLuaOwned; use yazi_shared::{Layer, event::ActionCow}; use yazi_shim::mlua::UserDataFieldsExt; use crate::{Mixable, keymap::Chord}; #[repr(transparent)] -#[derive(Clone, Debug, Default, Deserialize, FromLuaOwned)] -pub struct ChordArc(Arc>); +#[derive(Clone, Debug, Default, Deserialize)] +pub struct ChordArc(Arc); -impl Deref for ChordArc { - type Target = Arc>; +impl Deref for ChordArc { + type Target = Arc; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for ChordArc { +impl DerefMut for ChordArc { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl From<&Self> for ChordArc { +impl AsRef for ChordArc { + fn as_ref(&self) -> &Chord { self } +} + +impl From<&Self> for ChordArc { fn from(value: &Self) -> Self { value.clone() } } -impl From> for ChordArc { - fn from(value: Chord) -> Self { ChordArc(Arc::new(value)).into_erased() } +impl From for ChordArc { + fn from(value: Chord) -> Self { Self(Arc::new(value)) } } -impl From> for Chord { - fn from(value: ChordArc) -> Self { +impl From for Chord { + fn from(value: ChordArc) -> Self { match Arc::try_unwrap(value.0) { Ok(c) => c, Err(arc) => Self::clone(&arc), @@ -39,60 +42,53 @@ impl From> for Chord { } } -impl From<&ChordArc> for Chord { - fn from(value: &ChordArc) -> Self { Self::clone(value) } +impl From<&ChordArc> for Chord { + fn from(value: &ChordArc) -> Self { Self::clone(value) } } -impl TryFrom<(Value, Layer)> for ChordArc { - type Error = mlua::Error; - - fn try_from((value, layer): (Value, Layer)) -> Result { - use Chord as C; - use Layer as L; - - let de = mlua::serde::Deserializer::new(value); - Ok(match layer { - L::Null => C::<{ L::Null as u8 }>::deserialize(de)?.into(), - L::App => C::<{ L::App as u8 }>::deserialize(de)?.into(), - L::Mgr => C::<{ L::Mgr as u8 }>::deserialize(de)?.into(), - L::Tasks => C::<{ L::Tasks as u8 }>::deserialize(de)?.into(), - L::Spot => C::<{ L::Spot as u8 }>::deserialize(de)?.into(), - L::Pick => C::<{ L::Pick as u8 }>::deserialize(de)?.into(), - L::Input => C::<{ L::Input as u8 }>::deserialize(de)?.into(), - L::Confirm => C::<{ L::Confirm as u8 }>::deserialize(de)?.into(), - L::Help => C::<{ L::Help as u8 }>::deserialize(de)?.into(), - L::Cmp => C::<{ L::Cmp as u8 }>::deserialize(de)?.into(), - L::Which => C::<{ L::Which as u8 }>::deserialize(de)?.into(), - L::Notify => C::<{ L::Notify as u8 }>::deserialize(de)?.into(), - }) - } -} - -impl ChordArc { - pub fn as_erased(&self) -> &ChordArc { - unsafe { &*(self as *const Self as *const ChordArc) } +impl ChordArc { + pub fn to_seq(&self, layer: Layer) -> Vec { + self + .run + .iter() + .rev() + .cloned() + .map(|mut a| { + a.layer = a.layer.or(layer); + a.into() + }) + .collect() } - pub fn into_erased(self) -> ChordArc { - ChordArc(unsafe { Arc::from_raw(Arc::into_raw(self.0) as *const Chord) }) - } - - pub fn to_seq(&self) -> Vec { - self.run.iter().rev().cloned().map(Into::into).collect() - } - - pub fn into_seq(self) -> Vec { + pub fn into_seq(self, layer: Layer) -> Vec { match Arc::try_unwrap(self.0) { - Ok(c) => c.run.into_iter().rev().map(Into::into).collect(), - Err(arc) => Self(arc).to_seq(), + Ok(c) => c + .run + .into_iter() + .rev() + .map(|mut a| { + a.layer = a.layer.or(layer); + a.into() + }) + .collect(), + Err(arc) => Self(arc).to_seq(layer), } } } -impl Mixable for ChordArc { +impl Mixable for ChordArc { fn filter(&self) -> bool { self.0.filter() } } +impl FromLua for ChordArc { + fn from_lua(value: Value, _: &Lua) -> mlua::Result { + Ok(match value { + Value::UserData(ud) => ud.take()?, + _ => Chord::deserialize(mlua::serde::Deserializer::new(value))?.into(), + }) + } +} + impl UserData for ChordArc { fn add_fields>(fields: &mut F) { fields.add_field_method_get("id", |_, me| Ok(me.id)); diff --git a/yazi-config/src/keymap/chords.rs b/yazi-config/src/keymap/chords.rs index 56bf096a..1b1bdc40 100644 --- a/yazi-config/src/keymap/chords.rs +++ b/yazi-config/src/keymap/chords.rs @@ -1,53 +1,36 @@ use std::{ops::Deref, sync::Arc}; use arc_swap::ArcSwap; -use mlua::{ExternalError, ExternalResult, MetaMethod, Table, UserData, UserDataMethods, Value}; +use mlua::{ExternalError, ExternalResult, MetaMethod, Table, UserData, UserDataMethods}; use serde::Deserialize; -use yazi_shared::{Layer, event::Actions}; +use yazi_shared::event::Actions; use yazi_shim::{arc_swap::{ArcSwapExt, IntoPointee}, mlua::DeserializeOverLua, vec::{IndexAtError, VecExt}}; use crate::keymap::{Chord, ChordArc, ChordIter, ChordMatcher}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Default, Deserialize)] #[serde(transparent)] -pub struct Chords { - pub chords: ArcSwap>>, - #[serde(skip, default = "layer_default::")] - pub layer: Layer, +pub struct Chords(ArcSwap>); + +impl Deref for Chords { + type Target = ArcSwap>; + + fn deref(&self) -> &Self::Target { &self.0 } } -impl Default for Chords { - fn default() -> Self { - Self { chords: ArcSwap::default(), layer: Layer::from_repr(L).unwrap_or_default() } - } +impl From> for Chords { + fn from(inner: Vec) -> Self { Self(inner.into_pointee()) } } -impl Deref for Chords { - type Target = ArcSwap>>; - - fn deref(&self) -> &Self::Target { &self.chords } -} - -impl From>> for Chords { - fn from(inner: Vec>) -> Self { - Self { chords: inner.into_pointee(), layer: Layer::from_repr(L).unwrap_or_default() } - } -} - -impl Chords { - pub fn as_erased(&self) -> Arc>> { - let chords = self.chords.load_full(); - unsafe { Arc::from_raw(Arc::into_raw(chords) as *const Vec>) } - } - +impl Chords { pub fn insert(&self, index: isize, rule: ChordArc) -> Result<(), IndexAtError> { - self.chords.try_rcu(|rules| { + self.0.try_rcu(|rules| { let (before, after) = rules.split_at(rules.index_at(index)?); Ok( before .iter() .cloned() - .chain([rule.as_erased().clone()]) + .chain([rule.clone()]) .chain(after.iter().cloned()) .collect::>(), ) @@ -57,9 +40,9 @@ impl Chords { } pub fn remove(&self, matcher: ChordMatcher) { - self.chords.rcu(|chords| { + self.0.rcu(|chords| { let mut next = Vec::clone(chords); - next.retain(|arc| !matcher.matches(arc.as_erased())); + next.retain(|arc| !matcher.matches(arc)); next }); } @@ -69,11 +52,11 @@ impl Chords { matcher: ChordMatcher, f: impl Fn(Chord) -> Result, ) -> Result<(), E> { - self.chords.try_rcu(|rules| { + self.0.try_rcu(|rules| { let mut next = Vec::clone(rules); for arc in &mut next { - if matcher.matches(arc.as_erased()) { - *arc = f(Chord::clone(arc.as_erased()))?.into(); + if matcher.matches(arc) { + *arc = f(Chord::clone(arc))?.into(); } } Ok(Arc::new(next)) @@ -82,8 +65,8 @@ impl Chords { Ok(()) } - pub(crate) fn unwrap_unchecked(self) -> Vec> { - Arc::try_unwrap(self.chords.into_inner()).expect("unique chords arc") + pub(crate) fn unwrap_unchecked(self) -> Vec { + Arc::try_unwrap(self.0.into_inner()).expect("unique chords arc") } } @@ -91,21 +74,19 @@ impl UserData for &Chords { fn add_methods>(methods: &mut M) { methods.add_method("match", |_, &me, matcher: Option| { Ok(match matcher { - Some(matcher) => ChordIter { chords: me.as_erased(), matcher, ..Default::default() }, + Some(matcher) => ChordIter { chords: me.load_full(), matcher, ..Default::default() }, None => me.into(), }) }); - methods.add_method("insert", |_, me, (index, value): (isize, Value)| { + methods.add_method("insert", |_, me, (index, chord): (isize, ChordArc)| { let index = match index { 1.. => index - 1, 0 => return Err("index must be 1-based or negative".into_lua_err()), _ => index, }; - let chord = ChordArc::try_from((value, me.layer))?; me.insert(index, chord.clone()).into_lua_err()?; - Ok(chord) }); @@ -118,7 +99,7 @@ impl UserData for &Chords { let mut run: Option = table.raw_get("run")?; if let Some(run) = &mut run { table.raw_remove("run")?; - run.set(me.layer, yazi_shared::Source::Key); + run.set_source(yazi_shared::Source::Key); } me.update(matcher, |mut chord| { @@ -133,5 +114,3 @@ impl UserData for &Chords { methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.load().len())); } } - -pub(super) fn layer_default() -> Layer { Layer::from_repr(L).unwrap_or_default() } diff --git a/yazi-config/src/keymap/keymap.rs b/yazi-config/src/keymap/keymap.rs index 5fc87882..c0fe0ff6 100644 --- a/yazi-config/src/keymap/keymap.rs +++ b/yazi-config/src/keymap/keymap.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, sync::Arc}; +use std::sync::Arc; use anyhow::{Context, Result}; use serde::Deserialize; @@ -11,20 +11,20 @@ use crate::keymap::ChordArc; #[derive(Deserialize, DeserializeOver, DeserializeOver1)] pub struct Keymap { - pub mgr: KeymapSection<{ Layer::Mgr as u8 }>, - pub tasks: KeymapSection<{ Layer::Tasks as u8 }>, - pub spot: KeymapSection<{ Layer::Spot as u8 }>, - pub pick: KeymapSection<{ Layer::Pick as u8 }>, - pub input: KeymapSection<{ Layer::Input as u8 }>, - pub confirm: KeymapSection<{ Layer::Confirm as u8 }>, - pub help: KeymapSection<{ Layer::Help as u8 }>, - pub cmp: KeymapSection<{ Layer::Cmp as u8 }>, + pub mgr: KeymapSection, + pub tasks: KeymapSection, + pub spot: KeymapSection, + pub pick: KeymapSection, + pub input: KeymapSection, + pub confirm: KeymapSection, + pub help: KeymapSection, + pub cmp: KeymapSection, } impl Keymap { pub fn chords(&self, layer: Layer) -> Arc> { match self.section(layer) { - Some(s) => s.deref().as_erased(), + Some(s) => s.load_full(), None => Arc::new(Vec::new()), } } @@ -34,14 +34,14 @@ impl Keymap { Some(match layer { L::Null | L::App => None?, - L::Mgr => self.mgr.as_erased(), - L::Tasks => self.tasks.as_erased(), - L::Spot => self.spot.as_erased(), - L::Pick => self.pick.as_erased(), - L::Input => self.input.as_erased(), - L::Confirm => self.confirm.as_erased(), - L::Help => self.help.as_erased(), - L::Cmp => self.cmp.as_erased(), + L::Mgr => &self.mgr, + L::Tasks => &self.tasks, + L::Spot => &self.spot, + L::Pick => &self.pick, + L::Input => &self.input, + L::Confirm => &self.confirm, + L::Help => &self.help, + L::Cmp => &self.cmp, L::Which => None?, L::Notify => None?, }) diff --git a/yazi-config/src/keymap/section.rs b/yazi-config/src/keymap/section.rs index 583cb566..e24ae207 100644 --- a/yazi-config/src/keymap/section.rs +++ b/yazi-config/src/keymap/section.rs @@ -4,53 +4,45 @@ use hashbrown::HashSet; use mlua::{UserData, UserDataFields}; use serde::Deserialize; use yazi_codegen::DeserializeOver2; -use yazi_shared::Layer; use yazi_shim::{mlua::UserDataFieldsExt, toml::DeserializeOverHook}; -use super::{Key, Chord, Chords, chords::layer_default}; +use super::{Chord, Chords, Key}; use crate::{keymap::ChordArc, mix}; #[derive(Default, Deserialize, DeserializeOver2)] -pub struct KeymapSection { - keymap: Chords, +pub struct KeymapSection { + keymap: Chords, #[serde(default)] - prepend_keymap: Vec>, + prepend_keymap: Vec, #[serde(default)] - append_keymap: Vec>, - #[serde(default = "layer_default::")] - layer: Layer, + append_keymap: Vec, } -impl Deref for KeymapSection { - type Target = Chords; +impl Deref for KeymapSection { + type Target = Chords; fn deref(&self) -> &Self::Target { &self.keymap } } -impl KeymapSection { - pub fn as_erased(&self) -> &KeymapSection { - unsafe { &*(self as *const Self as *const KeymapSection) } - } -} - -impl DeserializeOverHook for KeymapSection { +impl DeserializeOverHook for KeymapSection { fn deserialize_over_hook(self) -> Result { #[inline] - fn on(Chord { on, .. }: &Chord) -> [Key; 2] { - [on.first().copied().unwrap_or_default(), on.get(1).copied().unwrap_or_default()] + fn on>(chord: T) -> [Key; 2] { + let c = chord.as_ref(); + [c.on.first().copied().unwrap_or_default(), c.on.get(1).copied().unwrap_or_default()] } let keymap = self.keymap.unwrap_unchecked(); let a_seen: HashSet<_> = self.prepend_keymap.iter().map(on).collect(); - let b_seen: HashSet<_> = keymap.iter().map(|c| on(c)).collect(); + let b_seen: HashSet<_> = keymap.iter().map(on).collect(); - let keymap: Vec> = mix( + let keymap: Vec = mix( self.prepend_keymap, keymap.into_iter().filter(|v| !a_seen.contains(&on(v))), self.append_keymap.into_iter().filter(|v| !b_seen.contains(&on(v))), ); - Ok(Self { keymap: keymap.into(), layer: self.layer, ..Default::default() }) + Ok(Self { keymap: keymap.into(), ..Default::default() }) } } diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 1dab24a7..7468d153 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -4,6 +4,7 @@ yazi_macro::mod_flat!(icon inject layout mixing pattern platform preset priority use std::io::{Read, Write}; +use yazi_macro::writef; use yazi_shim::{cell::{RoCell, SyncCell}, toml::{DeserializeOver, DeserializeOverWith}}; use yazi_tty::{TTY, sequence::SetSgr}; @@ -67,14 +68,14 @@ pub fn build_flavor(light: bool, merge: bool) -> anyhow::Result { } fn wait_for_key(e: anyhow::Error) -> anyhow::Result<()> { - let stdout = &mut *TTY.lockout(); + let mut stdout = &mut *TTY.lockout(); writeln!(stdout, "{e}")?; if let Some(src) = e.source() { writeln!(stdout, "\nCaused by:\n{src}")?; } - writeln!( + writef!( stdout, "{}{}Press to continue with preset settings...{}", SetSgr::Reverse, diff --git a/yazi-config/src/mgr/ratio.rs b/yazi-config/src/mgr/ratio.rs index 2840b96e..d71f894b 100644 --- a/yazi-config/src/mgr/ratio.rs +++ b/yazi-config/src/mgr/ratio.rs @@ -1,5 +1,7 @@ use anyhow::bail; +use mlua::{ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value}; use serde::{Deserialize, Serialize}; +use yazi_binding::deprecate; #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(try_from = "[u16; 3]")] @@ -29,3 +31,83 @@ impl TryFrom<[u16; 3]> for MgrRatio { }) } } + +// TODO: remove --- +impl MgrRatio { + fn at(self, idx: usize) -> Option { + match idx { + 0 => Some(self.parent), + 1 => Some(self.current), + 2 => Some(self.preview), + _ => None, + } + } +} + +impl FromLua for MgrRatio { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + match value { + Value::UserData(ud) => Ok(*ud.borrow::()?), + Value::Table(t) => from_table(&t, lua), + _ => Err("expected a table".into_lua_err()), + } + } +} + +fn from_table(t: &Table, lua: &Lua) -> mlua::Result { + if t.contains_key("parent")? || t.contains_key("current")? || t.contains_key("preview")? { + deprecate!( + lua, + "{}: the table form of `rt.mgr.ratio` is deprecated, use an array `[parent, current, preview]` instead" + ); + MgrRatio::try_from([t.get("parent")?, t.get("current")?, t.get("preview")?]) + .map_err(|e| e.into_lua_err()) + } else { + MgrRatio::try_from([t.get(1)?, t.get(2)?, t.get(3)?]).map_err(|e| e.into_lua_err()) + } +} + +impl UserData for MgrRatio { + fn add_methods>(methods: &mut M) { + methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(3usize)); + + methods.add_meta_method(MetaMethod::Index, |lua, me, key: Value| { + match key { + Value::Integer(i) if (1..=3).contains(&i) => me.at((i - 1) as usize).into_lua(lua), + Value::String(s) => match &*s.as_bytes() { + b"parent" => { + deprecate!( + lua, + "{}: `rt.mgr.ratio.parent` is deprecated, use `rt.mgr.ratio[1]` instead" + ); + me.parent.into_lua(lua) + } + b"current" => { + deprecate!( + lua, + "{}: `rt.mgr.ratio.current` is deprecated, use `rt.mgr.ratio[2]` instead" + ); + me.current.into_lua(lua) + } + b"preview" => { + deprecate!( + lua, + "{}: `rt.mgr.ratio.preview` is deprecated, use `rt.mgr.ratio[3]` instead" + ); + me.preview.into_lua(lua) + } + b"all" => { + deprecate!( + lua, + "{}: `rt.mgr.ratio.all` is deprecated, use `rt.mgr.ratio[1] + rt.mgr.ratio[2] + rt.mgr.ratio[3]` instead" + ); + me.all.into_lua(lua) + } + _ => Ok(Value::Nil), + }, + _ => Ok(Value::Nil), + } + }); + } +} +// --- TODO: remove diff --git a/yazi-config/src/open/open_rule_matcher.rs b/yazi-config/src/open/open_rule_matcher.rs index 78882c0a..1a3fdc48 100644 --- a/yazi-config/src/open/open_rule_matcher.rs +++ b/yazi-config/src/open/open_rule_matcher.rs @@ -60,7 +60,7 @@ impl TryFrom for OpenRuleMatcher<'static> { Ok(Self { rules: YAZI.open.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/plugin/fetcher.rs b/yazi-config/src/plugin/fetcher.rs index 8b5085bc..f2d658bf 100644 --- a/yazi-config/src/plugin/fetcher.rs +++ b/yazi-config/src/plugin/fetcher.rs @@ -95,7 +95,7 @@ impl TryFrom
for FetcherMatcher<'static> { Ok(Self { fetchers: YAZI.plugin.fetchers.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/plugin/preloader.rs b/yazi-config/src/plugin/preloader.rs index e546af01..a4d216d7 100644 --- a/yazi-config/src/plugin/preloader.rs +++ b/yazi-config/src/plugin/preloader.rs @@ -97,7 +97,7 @@ impl TryFrom
for PreloaderMatcher<'static> { Ok(Self { preloaders: YAZI.plugin.preloaders.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/plugin/previewer.rs b/yazi-config/src/plugin/previewer.rs index 6a0a2ebb..7de385a6 100644 --- a/yazi-config/src/plugin/previewer.rs +++ b/yazi-config/src/plugin/previewer.rs @@ -90,7 +90,7 @@ impl TryFrom
for PreviewerMatcher<'static> { Ok(Self { previewers: YAZI.plugin.previewers.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/plugin/spotter.rs b/yazi-config/src/plugin/spotter.rs index f52a64a7..d39e3ac6 100644 --- a/yazi-config/src/plugin/spotter.rs +++ b/yazi-config/src/plugin/spotter.rs @@ -90,7 +90,7 @@ impl TryFrom
for SpotterMatcher<'static> { Ok(Self { spotters: YAZI.plugin.spotters.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/popup/help.rs b/yazi-config/src/popup/help.rs new file mode 100644 index 00000000..4cfdc2f3 --- /dev/null +++ b/yazi-config/src/popup/help.rs @@ -0,0 +1,9 @@ +use yazi_binding::position::{Offset, Origin, Position}; + +pub struct Help; + +impl Help { + pub fn position() -> Position { + Position::new(Origin::Center, Offset { x: 0, y: 0, width: 80, height: 25 }) + } +} diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index e28608ac..8d04c6f8 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -47,6 +47,7 @@ pub struct Input { impl Input { pub fn cd(&self, cwd: Url) -> InputOpt { InputOpt { + name: "cd".to_owned(), title: self.cd_title.clone(), value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() }, position: Position::new(self.cd_origin, self.cd_offset), @@ -57,14 +58,16 @@ impl Input { pub fn create(&self, dir: bool) -> InputOpt { InputOpt { + name: format!("create-{}", if dir { "dir" } else { "file" }), title: self.create_title[dir as usize].clone(), position: Position::new(self.create_origin, self.create_offset), ..Default::default() } } - pub fn rename(&self) -> InputOpt { + pub fn rename(&self, is_dir: bool) -> InputOpt { InputOpt { + name: format!("rename-{}", if is_dir { "dir" } else { "file" }), title: self.rename_title.clone(), position: Position::new(self.rename_origin, self.rename_offset), ..Default::default() @@ -73,6 +76,7 @@ impl Input { pub fn filter(&self) -> InputOpt { InputOpt { + name: "filter".to_owned(), title: self.filter_title.clone(), position: Position::new(self.filter_origin, self.filter_offset), realtime: true, @@ -82,6 +86,7 @@ impl Input { pub fn find(&self, prev: bool) -> InputOpt { InputOpt { + name: "find".to_owned(), title: self.find_title[prev as usize].clone(), position: Position::new(self.find_origin, self.find_offset), realtime: true, @@ -91,6 +96,7 @@ impl Input { pub fn search(&self, name: &str) -> InputOpt { InputOpt { + name: "search".to_owned(), title: self.search_title.replace("{n}", name), position: Position::new(self.search_origin, self.search_offset), ..Default::default() @@ -99,6 +105,7 @@ impl Input { pub fn shell(&self, block: bool) -> InputOpt { InputOpt { + name: "shell".to_owned(), title: self.shell_title[block as usize].clone(), position: Position::new(self.shell_origin, self.shell_offset), ..Default::default() @@ -107,6 +114,7 @@ impl Input { pub fn tab_rename(&self) -> InputOpt { InputOpt { + name: "tab-rename".to_owned(), title: "Rename tab:".to_owned(), position: Position::new(Origin::TopCenter, Offset { x: 0, diff --git a/yazi-config/src/popup/mod.rs b/yazi-config/src/popup/mod.rs index a723fb30..961d4a3c 100644 --- a/yazi-config/src/popup/mod.rs +++ b/yazi-config/src/popup/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(confirm input options pick); +yazi_macro::mod_flat!(confirm help input options pick); diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs index e9906020..7b2587b8 100644 --- a/yazi-config/src/popup/options.rs +++ b/yazi-config/src/popup/options.rs @@ -1,10 +1,13 @@ -use ratatui_core::text::{Line, Text}; +use std::slice; + +use ratatui_core::text::{Line, Span, Text}; use ratatui_widgets::paragraph::{Paragraph, Wrap}; use yazi_binding::position::Position; +use yazi_fs::file::File; use yazi_macro::impl_data_any; -use yazi_shared::{strand::ToStrand, url::UrlBuf}; +use yazi_shared::strand::ToStrand; -use crate::YAZI; +use crate::{THEME, YAZI}; // --- PickCfg #[derive(Clone, Debug, Default)] @@ -42,30 +45,30 @@ impl ConfirmCfg { } } - pub fn trash(urls: &[yazi_shared::url::UrlBuf]) -> Self { + pub fn trash(files: &[File]) -> Self { Self::new( - Self::replace_number(&YAZI.confirm.trash_title, urls.len()), + Self::replace_number(&YAZI.confirm.trash_title, files.len()), YAZI.confirm.trash_position(), None, - Self::truncate_list(urls, urls.len(), 100), + Self::truncate_files(files, 100), ) } - pub fn delete(urls: &[yazi_shared::url::UrlBuf]) -> Self { + pub fn delete(files: &[File]) -> Self { Self::new( - Self::replace_number(&YAZI.confirm.delete_title, urls.len()), + Self::replace_number(&YAZI.confirm.delete_title, files.len()), YAZI.confirm.delete_position(), None, - Self::truncate_list(urls, urls.len(), 100), + Self::truncate_files(files, 100), ) } - pub fn overwrite(url: &UrlBuf) -> Self { + pub fn overwrite(file: &File) -> Self { Self::new( YAZI.confirm.overwrite_title.clone(), YAZI.confirm.overwrite_position(), Some(Text::raw(&YAZI.confirm.overwrite_body)), - Some(url.to_strand().into_string_lossy().into()), + Self::truncate_files(slice::from_ref(file), 1), ) } @@ -74,7 +77,7 @@ impl ConfirmCfg { Self::replace_number(&YAZI.confirm.quit_title, len), YAZI.confirm.quit_position(), Some(Text::raw(&YAZI.confirm.quit_body)), - Self::truncate_list(names, len, 10), + Self::truncate_lines(names, len, 10), ) } @@ -82,7 +85,7 @@ impl ConfirmCfg { tpl.replace("{n}", &n.to_string()).replace("{s}", if n > 1 { "s" } else { "" }) } - fn truncate_list(it: I, len: usize, max: usize) -> Option> + fn truncate_lines(it: I, len: usize, max: usize) -> Option> where I: IntoIterator, I::Item: ToStrand, @@ -97,4 +100,22 @@ impl ConfirmCfg { } Some(Text::from_iter(lines)) } + + fn truncate_files(files: &[File], max: usize) -> Option> { + let mut lines = Vec::with_capacity(files.len().min(max + 1)); + for (i, f) in files.iter().enumerate() { + if i >= max { + lines.push(Line::raw(format!("... and {} more", files.len() - max))); + break; + } + + lines.push(Line::default()); + if let Some(icon) = THEME.icon.matches(f, false) { + lines[i].push_span(Span::styled(icon.text, icon.style)); + lines[i].push_span(" "); + } + lines[i].push_span(f.url.to_strand().into_string_lossy()); + } + Some(lines.into()) + } } diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index 5f507347..4998eae1 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -9,6 +9,7 @@ use yazi_fs::{Xdg, ok_or_not_found, path::sanitize_path}; use yazi_shim::{arc_swap::IntoPointee, cell::SyncCell}; use super::{Custom, Filetype, Flavor, Icon}; +use crate::YAZI; #[derive(Deserialize, DeserializeOver, DeserializeOver1, Overlay)] pub struct Theme { @@ -232,7 +233,11 @@ pub struct Input { impl From<&Input> for yazi_widgets::input::InputStyles { fn from(input: &Input) -> Self { - Self { normal: Some(input.value.get().into()), selected: Some(input.selected.get().into()) } + Self { + normal: Some(input.value.get().into()), + selected: Some(input.selected.get().into()), + blink: Some(YAZI.input.cursor_blink), + } } } @@ -259,12 +264,10 @@ pub struct Tasks { // --- Help #[derive(Deserialize, DeserializeOver, DeserializeOver2, Overlay)] pub struct Help { - pub on: SyncCell, - pub run: SyncCell, - pub desc: SyncCell, - + pub border: SyncCell, + pub chord: SyncCell, + pub action: SyncCell, pub hovered: SyncCell, - pub footer: SyncCell, } fn deserialize_syntect_theme<'de, D>(deserializer: D) -> Result, D::Error> diff --git a/yazi-core/src/core.rs b/yazi-core/src/core.rs index ad6ca18c..112bc231 100644 --- a/yazi-core/src/core.rs +++ b/yazi-core/src/core.rs @@ -33,6 +33,11 @@ impl Core { } pub fn cursor(&self) -> Option<(Position, SetCursorStyle)> { + if let Some(cursor) = self.help.cursor() { + let Rect { x, y, .. } = self.mgr.area(self.help.position).padding(self.help.padding()); + return Some((Position { x: x + cursor, y }, self.help.cursor_shape()?)); + } + if let Some(guard) = self.input.lock() { let Rect { x, y, .. } = match &guard { InputGuard::Main(_) => self.mgr.area(self.input.position()?).padding(self.input.padding()), @@ -41,10 +46,6 @@ impl Core { return Some((Position { x: x + guard.cursor(), y }, guard.cursor_shape())); } - if let Some((x, y)) = self.help.cursor() { - return Some((Position { x, y }, self.help.cursor_shape()?)); - } - None } diff --git a/yazi-core/src/help/help.rs b/yazi-core/src/help/help.rs index bbde8a98..fbe8fbfb 100644 --- a/yazi-core/src/help/help.rs +++ b/yazi-core/src/help/help.rs @@ -1,46 +1,33 @@ use anyhow::Result; -use unicode_width::UnicodeWidthStr; +use ratatui_widgets::block::Padding; +use yazi_binding::position::Position; use yazi_config::{KEYMAP, keymap::ChordArc}; -use yazi_macro::{act, render, render_and}; +use yazi_macro::render; use yazi_shared::Layer; -use yazi_term::{TERM, event::{KeyCode, KeyEvent}}; +use yazi_term::event::KeyEvent; use yazi_tty::sequence::SetCursorStyle; -use yazi_widgets::Scrollable; - -use crate::help::HELP_MARGIN; +use yazi_widgets::{Scrollable, input::Input}; #[derive(Default)] pub struct Help { - pub visible: bool, - pub layer: Layer, - pub(super) bindings: Vec, + pub visible: bool, + pub layer: Layer, + pub position: Position, + pub bindings: Vec, // Filter - pub keyword: String, - pub in_filter: Option, + pub input: Input, + pub keyword: String, pub offset: usize, pub cursor: usize, + pub height: u16, } impl Help { pub fn r#type(&mut self, key: KeyEvent) -> Result { - let Some(input) = &mut self.in_filter else { return Ok(false) }; - match key { - KeyEvent { code: KeyCode::Escape, modifiers, .. } if modifiers.is_empty() => { - self.in_filter = None; - render!(); - } - KeyEvent { code: KeyCode::Enter, modifiers, .. } if modifiers.is_empty() => { - self.in_filter = None; - return Ok(render_and!(true)); // Don't do the `filter_apply` below, since we already have the filtered results. - } - KeyEvent { code: KeyCode::Backspace, modifiers, .. } if modifiers.is_empty() => { - act!(backspace, input)?; - } - _ => { - input.r#type(key)?; - } + if !self.input.r#type(key)? { + return Ok(false); } self.filter_apply(); @@ -48,31 +35,48 @@ impl Help { } pub fn filter_apply(&mut self) { - let kw = self.in_filter.as_ref().map_or("", |i| i.value()); + let kw = self.input.value(); if kw.is_empty() { - self.keyword = String::new(); + self.keyword.clear(); self.bindings = KEYMAP.chords(self.layer).iter().cloned().collect(); } else if self.keyword != kw { self.keyword = kw.to_owned(); - self.bindings = - KEYMAP.chords(self.layer).iter().filter(|&c| c.contains(kw)).cloned().collect(); + self.bindings = Self::filter_chords(&KEYMAP.chords(self.layer), kw); } render!(self.scroll(0)); } + + fn filter_chords(chords: &[ChordArc], kw: &str) -> Vec { + let lowercased = kw.to_lowercase(); + let mut exact = vec![]; + let mut icase = vec![]; + let mut contains = vec![]; + let mut desc = vec![]; + + for c in chords { + let on = c.on(); + if on.starts_with(kw) { + exact.push(c.clone()); + } else if on.to_lowercase().starts_with(&lowercased) { + icase.push(c.clone()); + } else if on.to_lowercase().contains(&lowercased) { + contains.push(c.clone()); + } else if c.desc_or_run().to_lowercase().contains(&lowercased) { + desc.push(c.clone()); + } + } + + exact.extend(icase); + exact.extend(contains); + exact.extend(desc); + exact + } } impl Help { - // --- Keyword - pub fn keyword(&self) -> Option { - self - .in_filter - .as_ref() - .map(|i| i.value()) - .or(Some(self.keyword.as_str()).filter(|&s| !s.is_empty())) - .map(|s| format!("Filter: {s}")) - } + pub fn padding(&self) -> Padding { Padding::new(1, 1, 1, 1) } // --- Bindings pub fn window(&self) -> &[ChordArc] { @@ -81,27 +85,22 @@ impl Help { } // --- Cursor - pub fn cursor(&self) -> Option<(u16, u16)> { - if !self.visible || self.in_filter.is_none() { - return None; - } - if let Some(kw) = self.keyword() { - return Some((kw.width() as u16, TERM.dimension().rows)); - } - None - } + pub fn cursor(&self) -> Option { self.visible.then_some(self.input.cursor()) } pub fn rel_cursor(&self) -> usize { self.cursor - self.offset } pub fn cursor_shape(&self) -> Option { - Some(self.in_filter.as_ref()?.cursor_shape()) + self.visible.then_some(self.input.cursor_shape()) } } impl Scrollable for Help { fn total(&self) -> usize { self.bindings.len() } - fn limit(&self) -> usize { TERM.dimension().rows.saturating_sub(HELP_MARGIN) as usize } + fn limit(&self) -> usize { + let p = self.padding(); + self.height.saturating_sub(p.top + /* input */ 1 + /* divider */ 1 + p.bottom) as usize + } fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor } diff --git a/yazi-core/src/help/mod.rs b/yazi-core/src/help/mod.rs index 386cd482..a0cc5fda 100644 --- a/yazi-core/src/help/mod.rs +++ b/yazi-core/src/help/mod.rs @@ -1,3 +1 @@ yazi_macro::mod_flat!(help); - -const HELP_MARGIN: u16 = 1; diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 4ee51058..425cc277 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -52,9 +52,10 @@ impl Input { #[derive(Default)] pub struct InputMain { inner: yazi_widgets::input::Input, - pub visible: bool, + pub name: String, pub title: String, pub position: Position, + pub visible: bool, } impl Deref for InputMain { diff --git a/yazi-core/src/mgr/mgr.rs b/yazi-core/src/mgr/mgr.rs index 6a96c988..99f60a3c 100644 --- a/yazi-core/src/mgr/mgr.rs +++ b/yazi-core/src/mgr/mgr.rs @@ -67,7 +67,7 @@ impl Splatable for Mgr { tab .checked_sub(1) .and_then(|tab| self.tabs.get(tab)) - .map(|tab| tab.selected_or_hovered()) + .map(|tab| tab.selected_or_hovered_urls()) .unwrap_or_else(|| Box::new(iter::empty())) .skip(idx.unwrap_or(0)) .take(if idx.is_some() { 1 } else { usize::MAX }) @@ -82,5 +82,5 @@ impl Splatable for Mgr { .map(|h| h.url.as_url()) } - fn yanked(&self) -> impl Iterator> { self.yanked.iter().map(|u| u.as_url()) } + fn yanked(&self) -> impl Iterator> { self.yanked.iter().map(|f| f.url.as_url()) } } diff --git a/yazi-core/src/mgr/yanked.rs b/yazi-core/src/mgr/yanked.rs index 22db97b9..babb92be 100644 --- a/yazi-core/src/mgr/yanked.rs +++ b/yazi-core/src/mgr/yanked.rs @@ -1,32 +1,34 @@ use std::ops::Deref; use hashbrown::HashSet; -use indexmap::IndexSet; +use indexmap::{IndexSet, set::MutableValues}; use yazi_dds::Pubsub; -use yazi_fs::FilesOp; +use yazi_fs::{FilesOp, file::FileCov}; use yazi_macro::err; -use yazi_shared::url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlLike}; +use yazi_shared::url::{Url, UrlBuf, UrlCov, UrlLike}; #[derive(Debug, Default)] pub struct Yanked { pub cut: bool, - urls: IndexSet, + files: IndexSet, version: u64, revision: u64, } impl Deref for Yanked { - type Target = IndexSet; + type Target = IndexSet; - fn deref(&self) -> &Self::Target { &self.urls } + fn deref(&self) -> &Self::Target { &self.files } } impl Yanked { - pub fn new(cut: bool, urls: IndexSet) -> Self { - Self { cut, urls, ..Default::default() } + pub fn new(cut: bool, files: IndexSet) -> Self { + Self { cut, files, ..Default::default() } } + pub fn urls(&self) -> impl Iterator { self.files.iter().map(|f| &f.url) } + pub fn remove_many<'a, I, T>(&mut self, urls: I) where I: IntoIterator, @@ -37,45 +39,47 @@ impl Yanked { return; } - let old = self.urls.len(); - self.urls.retain(|u| !urls.contains(&UrlCov::new(u))); - self.revision += (old != self.urls.len()) as u64; + let old = self.files.len(); + self.files.retain(|f| !urls.contains(&UrlCov::new(&f.url))); + self.revision += (old != self.files.len()) as u64; } pub fn clear(&mut self) { - if self.urls.is_empty() { + if self.files.is_empty() { return; } - self.urls.clear(); + self.files.clear(); self.revision += 1; } pub fn contains<'a>(&self, url: impl Into>) -> bool { - self.urls.contains(&UrlCov::new(url)) + self.files.contains(&UrlCov::new(url)) } pub fn contains_in(&self, dir: &UrlBuf) -> bool { - self.urls.iter().any(|u| { - let mut it = u.components(); + self.files.iter().any(|f| { + let mut it = f.url.components(); it.next_back().is_some() && it.covariant(&dir.components()) - && u.parent().is_some_and(|p| p == *dir) + && f.url.parent().is_some_and(|p| p == *dir) }) } pub fn apply_op(&mut self, op: &FilesOp) { let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); if !removal.is_empty() { - let old = self.urls.len(); - self.urls.retain(|u| !removal.contains(u)); - self.revision += (old != self.urls.len()) as u64; + let old = self.files.len(); + self.files.retain(|f| !removal.iter().any(|u| f.url.covariant(u))); + self.revision += (old != self.files.len()) as u64; } - if !addition.is_empty() { - let old = self.urls.len(); - self.urls.extend(addition.into_iter().map(UrlBufCov)); - self.revision += (old != self.urls.len()) as u64; + let old = self.files.len(); + self.files.extend(addition.into_iter().map(FileCov)); + self.revision += (old != self.files.len()) as u64; + } + for f in op.files() { + self.files.get_full_mut2(&UrlCov::new(&f.url)).map(|(_, v)| *v = f.into()); } } @@ -85,7 +89,7 @@ impl Yanked { } self.version = self.revision; - err!(Pubsub::pub_after_yank(self.cut, &self.urls)); + err!(Pubsub::pub_after_yank(self.cut, &self.files)); true } } diff --git a/yazi-core/src/spot/lock.rs b/yazi-core/src/spot/lock.rs index f2dda3df..30f11ae5 100644 --- a/yazi-core/src/spot/lock.rs +++ b/yazi-core/src/spot/lock.rs @@ -22,14 +22,16 @@ impl TryFrom
for SpotLock { fn try_from(t: Table) -> Result { let file: FileRef = t.raw_get("file")?; - Ok(Self { - url: file.url_owned(), - cha: file.cha, - mime: t.raw_get("mime")?, + file.borrow(|f| { + Ok(Self { + url: f.url_owned(), + cha: f.cha, + mime: t.raw_get("mime")?, - id: t.raw_get("id")?, - skip: t.raw_get("skip")?, - data: Default::default(), + id: t.raw_get("id")?, + skip: t.raw_get("skip")?, + data: Default::default(), + }) }) } } diff --git a/yazi-core/src/tab/finder.rs b/yazi-core/src/tab/finder.rs index 506eb86e..fa8111e0 100644 --- a/yazi-core/src/tab/finder.rs +++ b/yazi-core/src/tab/finder.rs @@ -1,6 +1,6 @@ use anyhow::Result; use hashbrown::HashMap; -use yazi_fs::{Files, Filter, FilterCase}; +use yazi_fs::{Entries, Filter, FilterCase}; use yazi_shared::{path::{AsPath, PathBufDyn}, url::UrlBuf}; use crate::tab::Folder; @@ -26,10 +26,10 @@ impl Finder { }) } - pub fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option { - for i in !include as usize..files.len() { - let idx = (cursor + files.len() - i) % files.len(); - if let Some(s) = files[idx].name() + pub fn prev(&self, entries: &Entries, cursor: usize, include: bool) -> Option { + for i in !include as usize..entries.len() { + let idx = (cursor + entries.len() - i) % entries.len(); + if let Some(s) = entries[idx].name() && self.filter.matches(s) { return Some(idx as isize - cursor as isize); @@ -38,10 +38,10 @@ impl Finder { None } - pub fn next(&self, files: &Files, cursor: usize, include: bool) -> Option { - for i in !include as usize..files.len() { - let idx = (cursor + i) % files.len(); - if let Some(s) = files[idx].name() + pub fn next(&self, entries: &Entries, cursor: usize, include: bool) -> Option { + for i in !include as usize..entries.len() { + let idx = (cursor + i) % entries.len(); + if let Some(s) = entries[idx].name() && self.filter.matches(s) { return Some(idx as isize - cursor as isize); @@ -57,7 +57,7 @@ impl Finder { self.matched.clear(); let mut i = 0u8; - for file in folder.files.iter() { + for file in folder.entries.iter() { if file.name().is_none_or(|s| !self.filter.matches(s)) { continue; } @@ -87,12 +87,12 @@ impl Finder { // --- Lock impl From<&Folder> for FinderLock { fn from(value: &Folder) -> Self { - Self { cwd: value.url.clone(), revision: value.files.revision } + Self { cwd: value.url.clone(), revision: value.entries.revision } } } impl PartialEq for FinderLock { fn eq(&self, other: &Folder) -> bool { - self.revision == other.files.revision && self.cwd == other.url + self.revision == other.entries.revision && self.cwd == other.url } } diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index 470749b4..49d8d63a 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -2,7 +2,7 @@ use std::mem; use yazi_config::{LAYOUT, YAZI}; use yazi_dds::Pubsub; -use yazi_fs::{Files, FilesOp, FolderStage, cha::Cha, file::File}; +use yazi_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File}; use yazi_macro::err; use yazi_shared::{id::Id, path::{AsPath, PathBufDyn, PathDyn}, url::UrlBuf}; use yazi_widgets::{Scrollable, Step}; @@ -10,10 +10,10 @@ use yazi_widgets::{Scrollable, Step}; use crate::MgrProxy; pub struct Folder { - pub url: UrlBuf, - pub cha: Cha, - pub files: Files, - pub stage: FolderStage, + pub url: UrlBuf, + pub cha: Cha, + pub entries: Entries, + pub stage: FolderStage, pub offset: usize, pub cursor: usize, @@ -25,14 +25,14 @@ pub struct Folder { impl Default for Folder { fn default() -> Self { Self { - url: Default::default(), - cha: Default::default(), - files: Files::new(YAZI.mgr.show_hidden.get()), - stage: Default::default(), - offset: Default::default(), - cursor: Default::default(), - page: Default::default(), - trace: Default::default(), + url: Default::default(), + cha: Default::default(), + entries: Entries::new(YAZI.mgr.show_hidden.get()), + stage: Default::default(), + offset: Default::default(), + cursor: Default::default(), + page: Default::default(), + trace: Default::default(), } } } @@ -43,7 +43,7 @@ impl> From for Folder { impl Folder { pub fn update(&mut self, op: FilesOp) -> bool { - let (stage, revision) = (self.stage.clone(), self.files.revision); + let (stage, revision) = (self.stage.clone(), self.entries.revision); match op { FilesOp::Full(_, _, cha) => { (self.cha, self.stage) = (cha, FolderStage::Loaded); @@ -51,10 +51,10 @@ impl Folder { FilesOp::Part(_, ref files, _) if files.is_empty() => { (self.cha, self.stage) = (Cha::default(), FolderStage::Loading); } - FilesOp::Part(_, _, ticket) if ticket == self.files.ticket() => { + FilesOp::Part(_, _, ticket) if ticket == self.entries.ticket() => { self.stage = FolderStage::Loading; } - FilesOp::Done(_, cha, ticket) if ticket == self.files.ticket() => { + FilesOp::Done(_, cha, ticket) if ticket == self.entries.ticket() => { (self.cha, self.stage) = (cha, FolderStage::Loaded); } FilesOp::IOErr(_, ref err) => { @@ -65,23 +65,23 @@ impl Folder { let mut deleted = vec![]; match op { - FilesOp::Full(_, files, _) => self.files.update_full(files), - FilesOp::Part(_, files, ticket) => self.files.update_part(files, ticket), + FilesOp::Full(_, files, _) => self.entries.update_full(files), + FilesOp::Part(_, files, ticket) => self.entries.update_part(files, ticket), FilesOp::Done(..) => {} - FilesOp::Size(_, sizes) => self.files.update_size(sizes), - FilesOp::IOErr(..) => self.files.update_ioerr(), + FilesOp::Size(_, sizes) => self.entries.update_size(sizes), + FilesOp::IOErr(..) => self.entries.update_ioerr(), - FilesOp::Creating(_, files) => self.files.update_creating(files), - FilesOp::Deleting(_, urns) => deleted = self.files.update_deleting(urns), - FilesOp::Updating(_, files) => _ = self.files.update_updating(files), - FilesOp::Upserting(_, files) => self.files.update_upserting(files), + FilesOp::Creating(_, files) => self.entries.update_creating(files), + FilesOp::Deleting(_, urns) => deleted = self.entries.update_deleting(urns), + FilesOp::Updating(_, files) => _ = self.entries.update_updating(files), + FilesOp::Upserting(_, files) => self.entries.update_upserting(files), }; - self.trace.take_if(|_| self.files.is_empty() && !self.stage.is_loading()); + self.trace.take_if(|_| self.entries.is_empty() && !self.stage.is_loading()); self.arrow(-(deleted.into_iter().filter(|&i| i < self.cursor).count() as isize)); self.repos(None); - (&stage, revision) != (&self.stage, self.files.revision) + (&stage, revision) != (&self.stage, self.entries.revision) } pub fn update_pub(&mut self, tab: Id, op: FilesOp) -> bool { @@ -93,7 +93,7 @@ impl Folder { } pub fn arrow(&mut self, step: impl Into) -> bool { - let mut b = if self.files.is_empty() { + let mut b = if self.entries.is_empty() { (mem::take(&mut self.cursor), mem::take(&mut self.offset)) != (0, 0) } else { self.scroll(step) @@ -109,7 +109,7 @@ impl Folder { return self.arrow(0); } - let new = self.files.position(urn).unwrap_or(self.cursor) as isize; + let new = self.entries.position(urn).unwrap_or(self.cursor) as isize; let b = self.arrow(new - self.cursor as isize); self.retrace(); @@ -146,7 +146,7 @@ impl Folder { fn squeeze_offset(&mut self) -> bool { let old = self.offset; - let len = self.files.len(); + let len = self.entries.len(); let limit = LAYOUT.get().folder_limit(); let scrolloff = (limit / 2).min(YAZI.mgr.scrolloff.get() as usize); @@ -164,23 +164,23 @@ impl Folder { impl Folder { #[inline] - pub fn hovered(&self) -> Option<&File> { self.files.get(self.cursor) } + pub fn hovered(&self) -> Option<&File> { self.entries.get(self.cursor) } #[inline] - pub fn hovered_mut(&mut self) -> Option<&mut File> { self.files.get_mut(self.cursor) } + pub fn hovered_mut(&mut self) -> Option<&mut File> { self.entries.get_mut(self.cursor) } pub fn paginate(&self, page: usize) -> &[File] { - let len = self.files.len(); + let len = self.entries.len(); let limit = LAYOUT.get().folder_limit(); let start = (page.saturating_sub(1) * limit).min(len.saturating_sub(1)); let end = ((page + 2) * limit).min(len); - &self.files[start..end] + &self.entries[start..end] } } impl Scrollable for Folder { - fn total(&self) -> usize { self.files.len() } + fn total(&self) -> usize { self.entries.len() } fn limit(&self) -> usize { LAYOUT.get().folder_limit() } diff --git a/yazi-core/src/tab/mod.rs b/yazi-core/src/tab/mod.rs index 4678c783..8f7666e1 100644 --- a/yazi-core/src/tab/mod.rs +++ b/yazi-core/src/tab/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(backstack finder folder history mode preference preview preview_lock selected tab); +yazi_macro::mod_flat!(backstack finder folder history visual mode preference preview preview_lock selected tab); diff --git a/yazi-core/src/tab/mode.rs b/yazi-core/src/tab/mode.rs index fa5e4a46..8d261365 100644 --- a/yazi-core/src/tab/mode.rs +++ b/yazi-core/src/tab/mode.rs @@ -1,41 +1,43 @@ -use std::{collections::BTreeSet, fmt::Display, mem}; +use std::{fmt::{self, Display}, mem}; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +use strum::EnumIs; + +use super::Visual; +use crate::tab::VisualIndices; + +#[derive(Clone, Debug, Default, EnumIs, Eq, PartialEq)] pub enum Mode { #[default] Normal, - Select(usize, BTreeSet), - Unset(usize, BTreeSet), + Select(Visual), + Unset(Visual), } impl Mode { - pub fn visual_mut(&mut self) -> Option<(usize, &mut BTreeSet)> { + pub fn visual(&self) -> Option { match self { Self::Normal => None, - Self::Select(start, indices) => Some((*start, indices)), - Self::Unset(start, indices) => Some((*start, indices)), + Self::Select(visual) | Self::Unset(visual) => Some(*visual), } } - pub fn take_visual(&mut self) -> Option<(usize, BTreeSet)> { + pub fn visual_mut(&mut self) -> Option<&mut Visual> { + match self { + Self::Normal => None, + Self::Select(visual) | Self::Unset(visual) => Some(visual), + } + } + + pub fn take_visual(&mut self, end: usize, len: usize) -> Option { match mem::take(self) { Self::Normal => None, - Self::Select(start, indices) => Some((start, indices)), - Self::Unset(start, indices) => Some((start, indices)), + Self::Select(visual) | Self::Unset(visual) => Some(visual.indices(end, len)), } } } -impl Mode { - pub fn is_select(&self) -> bool { matches!(self, Self::Select(..)) } - - pub fn is_unset(&self) -> bool { matches!(self, Self::Unset(..)) } - - pub fn is_visual(&self) -> bool { matches!(self, Self::Select(..) | Self::Unset(..)) } -} - impl Display for Mode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::Normal => "normal", Self::Select(..) => "select", diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index 687808a4..b2b1816e 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -4,7 +4,7 @@ use tokio::{pin, task::JoinHandle}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_adapter::ADAPTOR; use yazi_config::{LAYOUT, YAZI}; -use yazi_fs::{Files, FilesOp, cha::Cha, file::File}; +use yazi_fs::{Entries, FilesOp, cha::Cha, file::File}; use yazi_macro::render; use yazi_runner::{ RUNNER, @@ -14,7 +14,7 @@ use yazi_shared::{ pool::Symbol, url::{UrlBuf, UrlLike}, }; -use yazi_vfs::{VfsFiles, VfsFilesOp}; +use yazi_vfs::{VfsEntries, VfsFilesOp}; use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock}; @@ -68,9 +68,9 @@ impl Preview { self.folder_lock = Some(wd.clone()); self.folder_loader.take().map(|h| h.abort()); self.folder_loader = Some(tokio::spawn(async move { - let Some(new) = Files::assert_stale(&wd, dir.unwrap_or_default()).await else { return }; + let Some(new) = Entries::assert_stale(&wd, dir.unwrap_or_default()).await else { return }; - let rx = match Files::from_dir(&wd).await { + let rx = match Entries::from_dir(&wd).await { Ok(rx) => rx, Err(e) => return FilesOp::issue_error(&wd, e).await, }; diff --git a/yazi-core/src/tab/preview_lock.rs b/yazi-core/src/tab/preview_lock.rs index 38506220..f78904b4 100644 --- a/yazi-core/src/tab/preview_lock.rs +++ b/yazi-core/src/tab/preview_lock.rs @@ -24,15 +24,17 @@ impl TryFrom
for PreviewLock { fn try_from(t: Table) -> Result { let file: FileRef = t.raw_get("file")?; - Ok(Self { - url: file.url_owned(), - cha: file.cha, - mime: t.raw_get::("mime")?.to_str()?.intern(), + file.borrow(|f| { + Ok(Self { + url: f.url_owned(), + cha: f.cha, + mime: t.raw_get::("mime")?.to_str()?.intern(), - skip: t.raw_get("skip")?, - search_idx: t.raw_get("search_idx")?, - area: t.raw_get("area")?, - data: Default::default(), + skip: t.raw_get("skip")?, + search_idx: t.raw_get("search_idx")?, + area: t.raw_get("area")?, + data: Default::default(), + }) }) } } diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs index 105ba9e4..5805d022 100644 --- a/yazi-core/src/tab/selected.rs +++ b/yazi-core/src/tab/selected.rs @@ -1,13 +1,11 @@ -use std::ops::Deref; - use hashbrown::HashMap; -use indexmap::IndexMap; -use yazi_fs::FilesOp; -use yazi_shared::{timestamp_us, url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlCovMapExt}}; +use indexmap::{IndexMap, map::MutableKeys}; +use yazi_fs::{FilesOp, file::{File, FileCov}}; +use yazi_shared::{timestamp_us, url::{AsUrl, Url, UrlBuf, UrlBufCov, UrlCov, UrlCovMapExt, UrlLike, UrlMapExt}}; #[derive(Default)] pub struct Selected { - inner: IndexMap, + inner: IndexMap, parents: HashMap, } @@ -16,64 +14,70 @@ impl Selected { pub fn is_empty(&self) -> bool { self.inner.is_empty() } - pub fn values(&self) -> impl Iterator { self.inner.keys().map(Deref::deref) } + pub fn files(&self) -> impl Iterator { self.inner.keys().map(|f| &f.0) } - pub fn contains<'a>(&self, url: impl Into>) -> bool { - self.inner.contains_key(&UrlCov::new(url)) + pub fn urls(&self) -> impl Iterator { self.inner.keys().map(|f| &f.url) } + + pub fn contains(&self, url: impl AsUrl) -> bool { + self.inner.contains_key(&UrlCov::new(url.as_url())) } - pub fn add<'a>(&mut self, url: impl Into>) -> bool { self.add_same([url]) == 1 } + pub fn add(&mut self, file: impl AsUrl + Into) -> bool { self.add_same([file]) == 1 } - pub fn add_many<'a, I, T>(&mut self, urls: I) -> usize + pub fn add_many(&mut self, files: I) -> usize where I: IntoIterator, - T: Into>, + T: AsUrl + Into, { let mut grouped: IndexMap<_, Vec<_>> = Default::default(); - for url in urls.into_iter().map(Into::into) { - if let Some(p) = url.parent() { - grouped.entry(p).or_default().push(url); + for file in files { + if let Some(p) = file.as_url().parent() { + grouped.get_or_insert_default(p).push(file); } } grouped.into_values().map(|v| self.add_same(v)).sum() } - fn add_same<'a, I, T>(&mut self, urls: I) -> usize + fn add_same(&mut self, files: I) -> usize where I: IntoIterator, - T: Into>, + T: AsUrl + Into, { // If it has appeared as a parent - let urls: Vec<_> = - urls.into_iter().map(UrlCov::new).filter(|u| !self.parents.contains_key(u)).collect(); - if urls.is_empty() { + let files: Vec<_> = + files.into_iter().filter(|f| !self.parents.contains_key(&UrlCov::new(f.as_url()))).collect(); + if files.is_empty() { return 0; } // If it has appeared as a child - let mut parent = urls[0].parent(); - let mut parents = vec![]; + let mut parent = files[0].as_url().parent(); while let Some(u) = parent { if self.inner.contains_key(&UrlCov::new(u)) { return 0; } - parent = u.parent(); - parents.push(u); } - let (now, len) = (timestamp_us(), self.inner.len()); - self.inner.extend(urls.iter().enumerate().map(|(i, u)| (u.into(), now + i as u64))); + // Mark the files as selected children with a timestamp + let (now, fl, il) = (timestamp_us(), files.len(), self.inner.len()); + self + .inner + .extend(files.into_iter().enumerate().map(|(i, f)| (FileCov(f.into()), now + i as u64))); - for u in parents { - *self.parents.get_or_insert_default(UrlCov::new(u)) += self.inner.len() - len; + // Update the parent counts + if let Some((first, _)) = self.inner.get_index(il) { + let mut parent = first.url.parent(); + while let Some(u) = parent { + *self.parents.get_or_insert_default(UrlCov::new(u)) += self.inner.len() - il; + parent = u.parent(); + } } - urls.len() + + fl } - pub fn remove<'a>(&mut self, url: impl Into> + Clone) -> bool { - self.remove_same([url]) == 1 - } + pub fn remove(&mut self, url: impl AsUrl) -> bool { self.remove_same([url]) == 1 } pub fn remove_many<'a, I, T>(&mut self, urls: I) -> usize where @@ -95,21 +99,21 @@ impl Selected { affected } - fn remove_same<'a, I, T>(&mut self, urls: I) -> usize + fn remove_same(&mut self, urls: I) -> usize where I: IntoIterator, - T: Into> + Clone, + T: AsUrl, { - let mut it = urls.into_iter().peekable(); - let Some(first) = it.peek().cloned().map(UrlCov::new) else { return 0 }; + let mut it = urls.into_iter(); + let Some(first) = it.next() else { return 0 }; - let count = it.filter_map(|u| self.inner.swap_remove(&UrlCov::new(u))).count(); + let count = self.inner.swap_remove(&UrlCov::new(first.as_url())).is_some() as usize + + it.filter_map(|u| self.inner.swap_remove(&UrlCov::new(u.as_url()))).count(); if count == 0 { return 0; } - // FIXME: use UrlCov::parent() instead - let mut parent = first.parent(); + let mut parent = first.as_url().parent(); while let Some(u) = parent { let n = self.parents.get_mut(&UrlCov::new(u)).unwrap(); @@ -134,23 +138,28 @@ impl Selected { self.remove_many(&removal); } if !addition.is_empty() { - self.add_many(&addition); + self.add_many(addition); + } + for f in op.files() { + self.inner.get_full_mut2(&UrlCov::new(&f.url)).map(|(_, k, _)| *k = f.into()); } } } #[cfg(test)] mod tests { - use std::path::Path; + use std::{ffi::OsStr, path::Path}; use super::*; + fn f + ?Sized>(s: &S) -> File { File::from_dummy(Path::new(s), None) } + #[test] fn test_insert_non_conflicting() { let mut s = Selected::default(); - assert!(s.add(Path::new("/a/b"))); - assert!(s.add(Path::new("/c/d"))); + assert!(s.add(f("/a/b"))); + assert!(s.add(f("/c/d"))); assert_eq!(s.inner.len(), 2); } @@ -158,24 +167,24 @@ mod tests { fn test_insert_conflicting_parent() { let mut s = Selected::default(); - assert!(s.add(Path::new("/a"))); - assert!(!s.add(Path::new("/a/b"))); + assert!(s.add(f("/a"))); + assert!(!s.add(f("/a/b"))); } #[test] fn test_insert_conflicting_child() { let mut s = Selected::default(); - assert!(s.add(Path::new("/a/b/c"))); - assert!(!s.add(Path::new("/a/b"))); - assert!(s.add(Path::new("/a/b/d"))); + assert!(s.add(f("/a/b/c"))); + assert!(!s.add(f("/a/b"))); + assert!(s.add(f("/a/b/d"))); } #[test] fn test_remove() { let mut s = Selected::default(); - assert!(s.add(Path::new("/a/b"))); + assert!(s.add(f("/a/b"))); assert!(!s.remove(Path::new("/a/c"))); assert!(s.remove(Path::new("/a/b"))); assert!(!s.remove(Path::new("/a/b"))); @@ -184,92 +193,83 @@ mod tests { } #[test] - fn insert_many_success() { + fn add_many_success() { let mut s = Selected::default(); - assert_eq!( - 3, - s.add_same([ - Path::new("/parent/child1"), - Path::new("/parent/child2"), - Path::new("/parent/child3") - ]) - ); + assert_eq!(3, s.add_same([f("/parent/child1"), f("/parent/child2"), f("/parent/child3")])); } #[test] - fn insert_many_with_existing_parent_fails() { + fn add_many_with_existing_parent_fails() { let mut s = Selected::default(); - s.add(Path::new("/parent")); - assert_eq!(0, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); + s.add(f("/parent")); + assert_eq!(0, s.add_same([f("/parent/child1"), f("/parent/child2")])); } #[test] - fn insert_many_with_existing_child_fails() { + fn add_many_with_existing_child_fails() { let mut s = Selected::default(); - s.add(Path::new("/parent/child1")); - assert_eq!(2, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); + s.add(f("/parent/child1")); + assert_eq!(2, s.add_same([f("/parent/child1"), f("/parent/child2")])); } #[test] - fn insert_many_empty_urls_list() { + fn add_many_empty_files_list() { let mut s = Selected::default(); - assert_eq!(0, s.add_same([] as [Url; 0])); + assert_eq!(0, s.add_same([] as [File; 0])); } #[test] - fn insert_many_with_parent_as_child_of_another_url() { + fn add_many_with_parent_as_child_of_another_url() { let mut s = Selected::default(); - s.add(Path::new("/parent/child")); - assert_eq!( - 0, - s.add_same([Path::new("/parent/child/child1"), Path::new("/parent/child/child2")]) - ); - } - #[test] - fn insert_many_with_direct_parent_fails() { - let mut s = Selected::default(); - - s.add(Path::new("/a")); - assert_eq!(0, s.add_same([Path::new("/a/b")])); + s.add(f("/parent/child")); + assert_eq!(0, s.add_same([f("/parent/child/child1"), f("/parent/child/child2")])); } #[test] - fn insert_many_with_nested_child_fails() { + fn add_many_with_direct_parent_fails() { let mut s = Selected::default(); - s.add(Path::new("/a/b")); - assert_eq!(0, s.add_same([Path::new("/a")])); - assert_eq!(1, s.add_same([Path::new("/b"), Path::new("/a")])); + s.add(f("/a")); + assert_eq!(0, s.add_same([f("/a/b")])); } #[test] - fn insert_many_sibling_directories_success() { + fn add_many_with_nested_child_fails() { let mut s = Selected::default(); - assert_eq!(2, s.add_same([Path::new("/a/b"), Path::new("/a/c")])); + s.add(f("/a/b")); + assert_eq!(0, s.add_same([f("/a")])); + assert_eq!(1, s.add_same([f("/b"), f("/a")])); } #[test] - fn insert_many_with_grandchild_fails() { + fn add_many_sibling_directories_success() { let mut s = Selected::default(); - s.add(Path::new("/a/b")); - assert_eq!(0, s.add_same([Path::new("/a/b/c")])); + assert_eq!(2, s.add_same([f("/a/b"), f("/a/c")])); } #[test] - fn test_insert_many_with_remove() { + fn add_many_with_grandchild_fails() { + let mut s = Selected::default(); + + s.add(f("/a/b")); + assert_eq!(0, s.add_same([f("/a/b/c")])); + } + + #[test] + fn test_add_many_with_remove() { let mut s = Selected::default(); let child1 = Path::new("/parent/child1"); let child2 = Path::new("/parent/child2"); let child3 = Path::new("/parent/child3"); - assert_eq!(3, s.add_same([child1, child2, child3])); + assert_eq!(3, s.add_same([f(child1), f(child2), f(child3)])); assert!(s.remove(child1)); assert_eq!(s.inner.len(), 2); @@ -282,4 +282,13 @@ mod tests { assert!(s.inner.is_empty()); assert!(s.parents.is_empty()); } + + #[test] + fn add_same_all_duplicates() { + let mut s = Selected::default(); + + s.add(f("/a/b")); + assert_eq!(1, s.add_same([f("/a/b")])); + assert_eq!(s.inner.len(), 1); + } } diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 8dd15cf3..6fd8660d 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -82,7 +82,7 @@ impl Tab { pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() } pub fn hovered_rect(&self) -> Option { - let y = self.current.files.position(self.hovered()?.urn())? - self.current.offset; + let y = self.current.entries.position(self.hovered()?.urn())? - self.current.offset; let mut rect = LAYOUT.get().current; rect.y = rect.y.saturating_sub(1) + y as u16; @@ -99,22 +99,30 @@ impl Tab { } } - pub fn selected_or_hovered(&self) -> Box + '_> { + pub fn selected_or_hovered_files(&self) -> Box + '_> { + if self.selected.is_empty() { + Box::new(self.hovered().into_iter()) + } else { + Box::new(self.selected.files()) + } + } + + pub fn selected_or_hovered_urls(&self) -> Box + '_> { if self.selected.is_empty() { Box::new(self.hovered().map(|h| &h.url).into_iter()) } else { - Box::new(self.selected.values()) + Box::new(self.selected.urls()) } } pub fn hovered_and_selected(&self) -> Box + '_> { let Some(h) = self.hovered() else { - return Box::new([UrlBuf::new()].into_iter().chain(self.selected.values())); + return Box::new([UrlBuf::new()].into_iter().chain(self.selected.urls())); }; if self.selected.is_empty() { Box::new([&h.url, &h.url].into_iter()) } else { - Box::new([&h.url].into_iter().chain(self.selected.values())) + Box::new([&h.url].into_iter().chain(self.selected.urls())) } } diff --git a/yazi-core/src/tab/visual.rs b/yazi-core/src/tab/visual.rs new file mode 100644 index 00000000..2be30241 --- /dev/null +++ b/yazi-core/src/tab/visual.rs @@ -0,0 +1,49 @@ +use std::{iter::Chain, ops::Range}; + +use yazi_widgets::Step; + +pub type VisualIndices = Chain, Range>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Visual { + start: usize, + wraps: isize, +} + +impl Visual { + pub fn new(start: usize) -> Self { Self { start, wraps: 0 } } + + pub fn arrow(&mut self, step: Step, old: usize, new: usize) { + self.wraps += match step { + Step::Prev if new > old => -1, + Step::Next if new < old => 1, + _ => 0, + } + } + + pub fn contains(&self, index: usize, end: usize, len: usize) -> bool { + let (first, second) = self.ranges(end, len); + first.contains(&index) || second.contains(&index) + } + + pub fn indices(&self, end: usize, len: usize) -> VisualIndices { + let (first, second) = self.ranges(end, len); + first.chain(second) + } + + fn ranges(&self, end: usize, len: usize) -> (Range, Range) { + if len == 0 { + return (0..0, 0..0); + } + + let start = self.start.min(len - 1); + let end = end.min(len - 1); + + match self.wraps { + 0 => (start.min(end)..start.max(end) + 1, 0..0), + 1 if start > end + 1 => (0..end + 1, start..len), + -1 if end > start + 1 => (0..start + 1, end..len), + _ => (0..len, 0..0), + } + } +} diff --git a/yazi-core/src/tasks/file.rs b/yazi-core/src/tasks/file.rs index c956c76e..c92f9cc2 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -1,7 +1,6 @@ -use indexmap::IndexSet; use tracing::debug; use yazi_scheduler::file::FileInCut; -use yazi_shared::url::{UrlBuf, UrlBufCov, UrlLike}; +use yazi_shared::url::{UrlBuf, UrlLike}; use super::Tasks; use crate::mgr::Yanked; @@ -10,15 +9,15 @@ impl Tasks { pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) { self.scheduler.behavior.reset(); - for u in src.iter() { + for u in src.urls() { let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { debug!("file_cut: cannot join {u:?} with {dest:?}"); continue; }; - if force && *u == to { + if force && u == to { debug!("file_cut: same file, skip {to:?}"); } else { - self.scheduler.file_cut(FileInCut::new(u.0.clone(), to, force)); + self.scheduler.file_cut(FileInCut::new(u.clone(), to, force)); } } } @@ -26,47 +25,47 @@ impl Tasks { pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) { self.scheduler.behavior.reset(); - for u in src.iter() { + for u in src.urls() { let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { debug!("file_copy: cannot join {u:?} with {dest:?}"); continue; }; - if force && *u == to { + if force && u == to { debug!("file_copy: same file, skip {to:?}"); } else { - self.scheduler.file_copy(u.0.clone(), to, force, follow); + self.scheduler.file_copy(u.clone(), to, force, follow); } } } - pub fn file_link(&self, src: &IndexSet, dest: &UrlBuf, relative: bool, force: bool) { + pub fn file_link(&self, src: &Yanked, dest: &UrlBuf, relative: bool, force: bool) { self.scheduler.behavior.reset(); - for u in src { + for u in src.urls() { let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { debug!("file_link: cannot join {u:?} with {dest:?}"); continue; }; - if force && *u == to { + if force && u == to { debug!("file_link: same file, skip {to:?}"); } else { - self.scheduler.file_link(u.0.clone(), to, relative, force); + self.scheduler.file_link(u.clone(), to, relative, force); } } } - pub fn file_hardlink(&self, src: &IndexSet, dest: &UrlBuf, force: bool, follow: bool) { + pub fn file_hardlink(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) { self.scheduler.behavior.reset(); - for u in src { + for u in src.urls() { let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { debug!("file_hardlink: cannot join {u:?} with {dest:?}"); continue; }; - if force && *u == to { + if force && u == to { debug!("file_hardlink: same file, skip {to:?}"); } else { - self.scheduler.file_hardlink(u.0.clone(), to, force, follow); + self.scheduler.file_hardlink(u.clone(), to, force, follow); } } } diff --git a/yazi-core/src/tasks/prework.rs b/yazi-core/src/tasks/prework.rs index b1fa4523..be641480 100644 --- a/yazi-core/src/tasks/prework.rs +++ b/yazi-core/src/tasks/prework.rs @@ -1,5 +1,5 @@ use yazi_config::{YAZI, plugin::MAX_FETCHERS}; -use yazi_fs::{Files, FsHash64, SortBy, file::File}; +use yazi_fs::{Entries, FsHash64, SortBy, file::File}; use super::Tasks; use crate::mgr::Mimetype; @@ -44,7 +44,7 @@ impl Tasks { } } - pub fn prework_sorted(&self, targets: &Files) { + pub fn prework_sorted(&self, targets: &Entries) { if targets.sorter().by != SortBy::Size { return; } diff --git a/yazi-core/src/which/option.rs b/yazi-core/src/which/option.rs index 1b763feb..51cef8bc 100644 --- a/yazi-core/src/which/option.rs +++ b/yazi-core/src/which/option.rs @@ -7,9 +7,10 @@ use yazi_shared::{Layer, event::ActionCow}; #[derive(Clone, Debug)] pub struct WhichOpt { pub tx: Option>>, + pub layer: Layer, pub cands: Vec, - pub silent: bool, pub times: usize, + pub silent: bool, } impl_data_any!(WhichOpt); @@ -24,19 +25,21 @@ impl TryFrom for WhichOpt { Ok(Self { tx: a.take_any2("tx").transpose()?, + layer: a.str("layer").parse()?, cands: a.take_any_iter().collect(), - silent: a.bool("silent"), times: a.get("times").unwrap_or(0), + silent: a.bool("silent"), }) } } -impl From<(Layer, Key)> for WhichOpt { - fn from((layer, key): (Layer, Key)) -> Self { +impl WhichOpt { + pub fn new(src: Layer, dist: Layer, key: Key) -> Self { Self { tx: None, + layer: dist, cands: KEYMAP - .chords(layer) + .chords(src) .iter() .filter(|&c| c.on.len() > 1 && c.on[0] == key) .cloned() @@ -55,6 +58,7 @@ impl FromLua for WhichOpt { Ok(Self { tx: t.raw_get::>("tx").ok().map(|t| t.0), + layer: t.raw_get("layer")?, cands: t.raw_get::
("cands")?.sequence_values().collect::>>()?, times: t.raw_get("times").unwrap_or_default(), silent: t.raw_get("silent")?, @@ -68,6 +72,7 @@ impl IntoLua for WhichOpt { lua .create_table_from([ ("tx", self.tx.map(yazi_binding::MpscUnboundedTx).into_lua(lua)?), + ("layer", self.layer.into_lua(lua)?), ("cands", lua.create_sequence_from(self.cands)?.into_lua(lua)?), ("times", self.times.into_lua(lua)?), ("silent", self.silent.into_lua(lua)?), diff --git a/yazi-core/src/which/which.rs b/yazi-core/src/which/which.rs index a046cfcd..d310e2e1 100644 --- a/yazi-core/src/which/which.rs +++ b/yazi-core/src/which/which.rs @@ -1,12 +1,14 @@ use tokio::sync::mpsc; use yazi_config::keymap::{ChordArc, Key}; use yazi_macro::{emit, render_and}; +use yazi_shared::Layer; #[derive(Default)] pub struct Which { pub tx: Option>>, - pub times: usize, + pub layer: Layer, pub cands: Vec, + pub times: usize, // Active state pub active: bool, @@ -32,8 +34,8 @@ impl Which { } pub fn dismiss(&mut self, chord: Option) { - self.times = 0; self.cands.clear(); + self.times = 0; self.active = false; self.silent = false; @@ -42,7 +44,7 @@ impl Which { _ = tx.send(chord.as_ref().map(Into::into)); } if let Some(chord) = chord { - emit!(Seq(chord.into_seq())); + emit!(Seq(chord.into_seq(self.layer))); } } } diff --git a/yazi-dds/src/ember/yank.rs b/yazi-dds/src/ember/yank.rs index 2150d5b3..dbf5e775 100644 --- a/yazi-dds/src/ember/yank.rs +++ b/yazi-dds/src/ember/yank.rs @@ -3,34 +3,31 @@ use std::borrow::Cow; use indexmap::IndexSet; use mlua::{AnyUserData, FromLua, IntoLua, Lua, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods, Value}; use serde::{Deserialize, Serialize}; +use yazi_fs::file::FileCov; use yazi_macro::impl_data_any; -use yazi_shared::url::{UrlBuf, UrlBufCov}; use yazi_shim::mlua::get_metatable; use super::Ember; -type Iter = yazi_binding::Iter< - std::iter::Map, fn(UrlBufCov) -> UrlBuf>, - UrlBuf, ->; +type Iter = yazi_binding::Iter, FileCov>; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct EmberYank<'a> { - pub cut: bool, - pub urls: Cow<'a, IndexSet>, + pub cut: bool, + pub files: Cow<'a, IndexSet>, } impl_data_any!(EmberYank<'static>, from_into_lua = inherit); impl<'a> EmberYank<'a> { - pub fn borrowed(cut: bool, urls: &'a IndexSet) -> Ember<'a> { - Self { cut, urls: Cow::Borrowed(urls) }.into() + pub fn borrowed(cut: bool, files: &'a IndexSet) -> Ember<'a> { + Self { cut, files: Cow::Borrowed(files) }.into() } } impl EmberYank<'static> { - pub fn owned(cut: bool, _: &IndexSet) -> Ember<'static> { - Self { cut, urls: Default::default() }.into() + pub fn owned(cut: bool, _: &IndexSet) -> Ember<'static> { + Self { cut, files: Default::default() }.into() } } @@ -53,8 +50,8 @@ impl FromLua for EmberYank<'static> { impl IntoLua for EmberYank<'_> { fn into_lua(self, lua: &Lua) -> mlua::Result { - let len = self.urls.len(); - let iter = Iter::new(self.urls.into_owned().into_iter().map(UrlBuf::from), Some(len)); + let len = self.files.len(); + let iter = Iter::new(self.files.into_owned().into_iter(), Some(len)); EmberYankIter { cut: self.cut, len, inner: lua.create_userdata(iter)? }.into_lua(lua) } } @@ -69,15 +66,8 @@ pub struct EmberYankIter { impl EmberYankIter { pub fn collect(self, lua: &Lua) -> mlua::Result> { Ok(EmberYank { - cut: self.cut, - urls: Cow::Owned( - self - .inner - .take::()? - .into_iter(lua) - .map(|result| result.map(Into::into)) - .collect::>()?, - ), + cut: self.cut, + files: Cow::Owned(self.inner.take::()?.into_iter(lua).collect::>()?), }) } } diff --git a/yazi-dds/src/pubsub.rs b/yazi-dds/src/pubsub.rs index 42428a59..6fb77e3a 100644 --- a/yazi-dds/src/pubsub.rs +++ b/yazi-dds/src/pubsub.rs @@ -4,8 +4,8 @@ use indexmap::IndexSet; use mlua::Function; use parking_lot::RwLock; use yazi_boot::BOOT; -use yazi_fs::FolderStage; -use yazi_shared::{id::Id, url::{Url, UrlBuf, UrlBufCov}}; +use yazi_fs::{FolderStage, file::FileCov}; +use yazi_shared::{id::Id, url::{Url, UrlBuf}}; use yazi_shim::cell::RoCell; use crate::{Client, ID, PEERS, ember::{Ember, EmberBulkRename, EmberDuplicateItem, EmberHi, EmberMoveItem}}; @@ -162,7 +162,7 @@ impl Pubsub { pub_after!(rename(tab: Id, from: &UrlBuf, to: &UrlBuf), (tab, from, to)); - pub_after!(@yank(cut: bool, urls: &IndexSet), (cut, urls)); + pub_after!(@yank(cut: bool, files: &IndexSet), (cut, files)); pub_after!(duplicate(items: Vec), (&items), (items)); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 2ed1b401..431b97b0 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -321,16 +321,18 @@ impl<'a> Executor<'a> { on!(escape); on!(arrow); - on!(filter); + on!(close); match action.name.as_ref() { - // Help - "close" => act!(help:toggle, cx, Layer::Help), // Plugin "plugin" => act!(app:plugin, cx, action), // Lua "lua" => act!(app:lua, cx, action), - _ => succ!(), + _ => { + cx.help.input.execute(action)?; + cx.help.filter_apply(); + succ!() + } } } diff --git a/yazi-fm/src/help/bindings.rs b/yazi-fm/src/help/bindings.rs index e121653f..a234040f 100644 --- a/yazi-fm/src/help/bindings.rs +++ b/yazi-fm/src/help/bindings.rs @@ -18,26 +18,18 @@ impl Widget for Bindings<'_> { return; } - // On + // Chord let col1: Vec<_> = - bindings.iter().map(|c| ListItem::new(c.on()).style(THEME.help.on.get())).collect(); + bindings.iter().map(|c| ListItem::new(c.on()).style(THEME.help.chord.get())).collect(); - // Run - let col2: Vec<_> = - bindings.iter().map(|c| ListItem::new(c.run()).style(THEME.help.run.get())).collect(); - - // Desc - let col3: Vec<_> = bindings + // Action + let col2: Vec<_> = bindings .iter() - .map(|c| ListItem::new(c.desc().unwrap_or("-".into())).style(THEME.help.desc.get())) + .map(|c| ListItem::new(c.desc_or_run()).style(THEME.help.action.get())) .collect(); - let chunks = layout::Layout::horizontal([ - Constraint::Ratio(2, 10), - Constraint::Ratio(3, 10), - Constraint::Ratio(5, 10), - ]) - .split(area); + let chunks = + layout::Layout::horizontal([Constraint::Length(20), Constraint::Fill(1)]).split(area); let cursor = self.core.help.rel_cursor() as u16; buf.set_style( @@ -47,6 +39,5 @@ impl Widget for Bindings<'_> { List::new(col1).render(chunks[0], buf); List::new(col2).render(chunks[1], buf); - List::new(col3).render(chunks[2], buf); } } diff --git a/yazi-fm/src/help/help.rs b/yazi-fm/src/help/help.rs index 24185d43..221481a0 100644 --- a/yazi-fm/src/help/help.rs +++ b/yazi-fm/src/help/help.rs @@ -1,6 +1,8 @@ -use ratatui_core::{buffer::Buffer, layout::{self, Constraint, Rect}, text::Line, widgets::Widget}; -use yazi_config::{KEYMAP, THEME}; +use ratatui_core::{buffer::Buffer, layout::{Alignment, Constraint, Layout, Rect}, symbols::merge::MergeStrategy, widgets::Widget}; +use ratatui_widgets::{block::{Block, Padding}, borders::BorderType}; +use yazi_config::THEME; use yazi_core::Core; +use yazi_shim::ratatui::Padable; use super::Bindings; @@ -10,27 +12,39 @@ pub(crate) struct Help<'a> { impl<'a> Help<'a> { pub fn new(core: &'a Core) -> Self { Self { core } } - - fn tips() -> String { - match KEYMAP.help.load().iter().find(|&c| c.run.iter().any(|a| a.name == "filter")) { - Some(c) => format!(" (Press `{}` to filter)", c.on()), - None => String::new(), - } - } } impl Widget for Help<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { + fn render(self, _: Rect, buf: &mut Buffer) { let help = &self.core.help; + let area = self.core.mgr.area(help.position); + let padding = help.padding(); + yazi_widgets::clear::Clear::default().render(area, buf); - let chunks = layout::Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).split(area); - Line::styled( - help.keyword().unwrap_or_else(|| format!("{}.help{}", help.layer, Self::tips())), - THEME.help.footer.get(), - ) - .render(chunks[1], buf); + Block::bordered() + .title(format!("{}.help", help.layer)) + .title_alignment(Alignment::Center) + .border_type(BorderType::Rounded) + .border_style(THEME.help.border.get()) + .render(area, buf); - Bindings::new(self.core).render(chunks[0], buf); + let chunks = + Layout::vertical([Constraint::Length(1), Constraint::Length(1), Constraint::Fill(1)]) + .split(area.padding(Padding { left: 0, right: 0, ..padding })); + + // Input + help.input.render(chunks[0].padding(Padding { top: 0, bottom: 0, ..padding }), buf); + + // Divider + Block::bordered() + .border_type(BorderType::Rounded) + .border_style(THEME.help.border.get()) + .merge_borders(MergeStrategy::Fuzzy) + .render(chunks[1], buf); + + // Bindings + Bindings::new(self.core) + .render(chunks[2].padding(Padding { top: 0, bottom: 0, ..padding }), buf); } } diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index c2edf160..973ec3ea 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -1,7 +1,11 @@ +use std::path::Path; + use ratatui_core::{buffer::Buffer, layout::{Margin, Rect}, text::Line, widgets::Widget}; use ratatui_widgets::{block::Block, borders::BorderType}; -use yazi_config::THEME; +use yazi_config::{Icon, THEME}; use yazi_core::Core; +use yazi_fs::{cha::{Cha, ChaKind}, file::File}; +use yazi_shim::path::CROSS_SEPARATOR; pub(crate) struct Input<'a> { core: &'a Core, @@ -9,21 +13,51 @@ pub(crate) struct Input<'a> { impl<'a> Input<'a> { pub(crate) fn new(core: &'a Core) -> Self { Self { core } } + + fn icon(&self) -> Option { + let input = &self.core.input.main; + + let is_dir = input.value().ends_with(CROSS_SEPARATOR); + let is_hovered = input.name.starts_with("rename-"); + + let (path, mode): (&str, u16) = match input.name.as_str() { + "cd" => (input.value(), if is_dir { 0o40755 } else { 0o100644 }), + "create-file" => (input.value(), if is_dir { 0o40755 } else { 0o100644 }), + "create-dir" => (input.value(), 0o40755), + "rename-file" => (input.value(), 0o100644), + "rename-dir" => (input.value(), 0o40755), + "shell" => ("icon.sh", 0o100644), + _ => return None, + }; + + THEME.icon.matches( + &File { + url: Path::new(path).into(), + cha: Cha { kind: ChaKind::empty(), mode: mode.try_into().ok()?, ..Default::default() }, + link_to: None, + }, + is_hovered, + ) + } } impl Widget for Input<'_> { fn render(self, _: Rect, buf: &mut Buffer) { - let input = &self.core.input; + let input = &self.core.input.main; - let outer = self.core.mgr.area(input.main.position); + let outer = self.core.mgr.area(input.position); yazi_widgets::clear::Clear::default().render(outer, buf); - Block::bordered() + let mut block = Block::bordered() .border_type(BorderType::Rounded) .border_style(THEME.input.border.get()) - .title(Line::styled(&input.main.title, THEME.input.title.get())) - .render(outer, buf); + .title(Line::styled(&input.title, THEME.input.title.get())); - input.main.render(outer.inner(Margin::new(1, 1)), buf); + if let Some(i) = self.icon() { + block = block.title_bottom(Line::raw(format!("{} ", i.text)).style(i.style).right_aligned()); + } + + block.render(outer, buf); + input.render(outer.inner(Margin::new(1, 1)), buf); } } diff --git a/yazi-fm/src/router.rs b/yazi-fm/src/router.rs index 2cc863f7..d28812a6 100644 --- a/yazi-fm/src/router.rs +++ b/yazi-fm/src/router.rs @@ -1,6 +1,7 @@ use anyhow::Result; use yazi_actor::Ctx; use yazi_config::{KEYMAP, keymap::{Chord, Key}}; +use yazi_core::which::WhichOpt; use yazi_macro::act; use yazi_shared::Layer; use yazi_term::event::KeyEvent; @@ -32,16 +33,17 @@ impl<'a> Router<'a> { let key = Key::from(key); Ok(match layer { L::Null | L::App | L::Notify => unreachable!(), - L::Mgr | L::Tasks | L::Spot | L::Pick | L::Input | L::Confirm | L::Help => { - self.matches(layer, key) + L::Mgr | L::Tasks | L::Spot | L::Pick | L::Input | L::Confirm => { + self.matches(layer, layer, key) } - L::Cmp => self.matches(L::Cmp, key) || self.matches(L::Input, key), + L::Help => self.matches(L::Help, L::Help, key) || self.matches(L::Input, L::Help, key), + L::Cmp => self.matches(L::Cmp, L::Cmp, key) || self.matches(L::Input, L::Input, key), L::Which => core.which.r#type(key), }) } - fn matches(&mut self, layer: Layer, key: Key) -> bool { - for chord in &*KEYMAP.chords(layer) { + fn matches(&mut self, src: Layer, dist: Layer, key: Key) -> bool { + for chord in &*KEYMAP.chords(src) { let Chord { on, .. } = chord.as_ref(); if on.is_empty() || on[0] != key { continue; @@ -49,9 +51,9 @@ impl<'a> Router<'a> { if on.len() > 1 { let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term); - act!(which:activate, cx, (layer, key)).ok(); + act!(which:activate, cx, WhichOpt::new(src, dist, key)).ok(); } else { - Dispatcher::new(self.app).dispatch_seq(chord.to_seq()); + Dispatcher::new(self.app).dispatch_seq(chord.to_seq(dist)); } return true; } diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index deb11563..0ee64e82 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -2,13 +2,14 @@ use std::{fs::Metadata, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}}; use anyhow::bail; use mlua::FromLua; +use serde::{Deserialize, Serialize}; use yazi_macro::{unix_either, win_either}; use yazi_shared::{strand::AsStrand, url::AsUrl}; use super::ChaKind; use crate::cha::{ChaMode, ChaType}; -#[derive(Clone, Copy, Debug, Eq, FromLua, PartialEq)] +#[derive(Clone, Copy, Debug, Deserialize, Eq, FromLua, PartialEq, Serialize)] pub struct Cha { pub kind: ChaKind, pub mode: ChaMode, diff --git a/yazi-fs/src/cha/kind.rs b/yazi-fs/src/cha/kind.rs index 7a20ce58..2bb4833c 100644 --- a/yazi-fs/src/cha/kind.rs +++ b/yazi-fs/src/cha/kind.rs @@ -1,10 +1,11 @@ use std::fs::Metadata; use bitflags::bitflags; +use serde::{Deserialize, Serialize}; use yazi_shared::strand::AsStrand; bitflags! { - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct ChaKind: u8 { const FOLLOW = 0b0000_0001; const HIDDEN = 0b0000_0010; diff --git a/yazi-fs/src/cha/mode.rs b/yazi-fs/src/cha/mode.rs index fc498bfb..fd2d2db7 100644 --- a/yazi-fs/src/cha/mode.rs +++ b/yazi-fs/src/cha/mode.rs @@ -2,11 +2,12 @@ use std::ops::Deref; use anyhow::{anyhow, bail}; use bitflags::bitflags; +use serde::{Deserialize, Serialize}; use crate::cha::ChaType; bitflags! { - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct ChaMode: u16 { // File type const T_MASK = 0b1111_0000_0000_0000; diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/entries.rs similarity index 98% rename from yazi-fs/src/files.rs rename to yazi-fs/src/entries.rs index f8ba5d95..84c7c426 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/entries.rs @@ -7,7 +7,7 @@ use super::{FilesSorter, Filter}; use crate::{FILES_TICKET, SortBy, file::File}; #[derive(Default)] -pub struct Files { +pub struct Entries { hidden: Vec, items: Vec, ticket: Id, @@ -21,17 +21,17 @@ pub struct Files { show_hidden: bool, } -impl Deref for Files { +impl Deref for Entries { type Target = Vec; fn deref(&self) -> &Self::Target { &self.items } } -impl DerefMut for Files { +impl DerefMut for Entries { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items } } -impl Files { +impl Entries { pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } } pub fn update_full(&mut self, files: Vec) { @@ -267,7 +267,7 @@ impl Files { } } -impl Files { +impl Entries { // --- Items #[inline] pub fn position(&self, urn: PathDyn) -> Option { self.iter().position(|f| urn == f.urn()) } diff --git a/yazi-fs/src/file/data.rs b/yazi-fs/src/file/data.rs new file mode 100644 index 00000000..17cf0e4b --- /dev/null +++ b/yazi-fs/src/file/data.rs @@ -0,0 +1,23 @@ +use anyhow::anyhow; +use yazi_macro::impl_data_any; +use yazi_shared::data::Data; + +use crate::file::File; + +impl_data_any!(File, from_into_lua = inherit); + +impl TryFrom for File { + type Error = anyhow::Error; + + fn try_from(value: Data) -> Result { + value.into_any::().ok_or_else(|| anyhow!("not a File")) + } +} + +impl TryFrom<&Data> for File { + type Error = anyhow::Error; + + fn try_from(value: &Data) -> Result { + value.as_any::().cloned().ok_or_else(|| anyhow!("not a File")) + } +} diff --git a/yazi-fs/src/file/file.rs b/yazi-fs/src/file/file.rs index 02191ada..1318dbf1 100644 --- a/yazi-fs/src/file/file.rs +++ b/yazi-fs/src/file/file.rs @@ -1,10 +1,11 @@ use std::{borrow::Cow, hash::{Hash, Hasher}, ops::Deref, path::Path}; -use yazi_shared::{path::{PathBufDyn, PathDyn}, strand::Strand, url::{UrlBuf, UrlLike}}; +use serde::{Deserialize, Serialize}; +use yazi_shared::{path::{PathBufDyn, PathDyn}, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}}; use crate::cha::{Cha, ChaType}; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct File { pub url: UrlBuf, pub cha: Cha, @@ -29,6 +30,14 @@ impl<'a> From<&'a File> for Cow<'a, File> { fn from(value: &'a File) -> Self { Cow::Borrowed(value) } } +impl AsUrl for File { + fn as_url(&self) -> Url<'_> { self.url.as_url() } +} + +impl AsUrl for &File { + fn as_url(&self) -> Url<'_> { self.url.as_url() } +} + impl File { #[inline] pub fn from_dummy(url: impl Into, r#type: Option) -> Self { diff --git a/yazi-fs/src/file/file_cov.rs b/yazi-fs/src/file/file_cov.rs new file mode 100644 index 00000000..69fb7258 --- /dev/null +++ b/yazi-fs/src/file/file_cov.rs @@ -0,0 +1,80 @@ +use std::{hash::{Hash, Hasher}, ops::Deref}; + +use hashbrown::Equivalent; +use mlua::{FromLua, IntoLua, Lua, Value}; +use serde::{Deserialize, Serialize}; +use yazi_shared::url::{UrlBuf, UrlBufCov, UrlCov}; + +use crate::file::File; + +/// A newtype around [`File`] that hashes and compares by URL only +/// (covariantly), via [`UrlCov`]. Useful as a key in [`IndexMap`] / [`HashMap`] +/// when the file's metadata should not affect identity. +#[derive(Clone, Debug, Default)] +pub struct FileCov(pub File); + +impl Deref for FileCov { + type Target = File; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From for FileCov { + fn from(value: File) -> Self { Self(value) } +} + +impl From<&File> for FileCov { + fn from(value: &File) -> Self { Self(value.clone()) } +} + +impl From for File { + fn from(value: FileCov) -> Self { value.0 } +} + +impl From<&FileCov> for File { + fn from(value: &FileCov) -> Self { value.0.clone() } +} + +impl Hash for FileCov { + fn hash(&self, state: &mut H) { UrlCov::from(&self.url).hash(state); } +} + +impl PartialEq for FileCov { + fn eq(&self, other: &Self) -> bool { UrlCov::from(&self.url).eq(&UrlCov::from(&other.url)) } +} + +impl PartialEq for FileCov { + fn eq(&self, other: &UrlBufCov) -> bool { UrlCov::from(&self.url).eq(other) } +} + +impl PartialEq for FileCov { + fn eq(&self, other: &UrlBuf) -> bool { UrlCov::from(&self.url).eq(&UrlCov::from(other)) } +} + +impl Eq for FileCov {} + +impl Equivalent for UrlCov<'_> { + fn equivalent(&self, key: &FileCov) -> bool { self.eq(&UrlCov::from(&key.url)) } +} + +impl Serialize for FileCov { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for FileCov { + fn deserialize>(deserializer: D) -> Result { + File::deserialize(deserializer).map(Self) + } +} + +impl IntoLua for FileCov { + fn into_lua(self, lua: &Lua) -> mlua::Result { self.0.into_lua(lua) } +} + +impl FromLua for FileCov { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + File::from_lua(value, lua).map(Self) + } +} diff --git a/yazi-fs/src/file/file_ref.rs b/yazi-fs/src/file/file_ref.rs new file mode 100644 index 00000000..86e581e0 --- /dev/null +++ b/yazi-fs/src/file/file_ref.rs @@ -0,0 +1,49 @@ +use std::borrow::Cow; + +use mlua::{AnyUserData, ExternalError, FromLua, Lua, Value}; + +use crate::file::{File, FileInventory}; + +pub struct FileRef(pub(super) AnyUserData); + +const EXPECTED: &str = "expected a table or File"; + +impl TryFrom for File { + type Error = mlua::Error; + + fn try_from(value: FileRef) -> Result { value.borrow(|f| Ok(f.clone())) } +} + +impl<'a> TryFrom for Cow<'a, File> { + type Error = mlua::Error; + + fn try_from(value: FileRef) -> Result { File::try_from(value).map(Cow::Owned) } +} + +impl FileRef { + pub fn borrow(&self, mut f: impl FnMut(&File) -> mlua::Result) -> mlua::Result { + let mut result = None; + for inv in inventory::iter::() { + match (inv.borrow)(&self.0, &mut |file| Ok(result = Some(f(file)?))) { + Ok(()) => return Ok(result.unwrap()), + Err(mlua::Error::UserDataTypeMismatch) => continue, + Err(e) => return Err(e), + } + } + + match self.0.borrow::() { + Ok(file) => f(&file), + Err(mlua::Error::UserDataTypeMismatch) => Err(mlua::Error::UserDataTypeMismatch), + Err(e) => Err(e), + } + } +} + +impl FromLua for FileRef { + fn from_lua(value: Value, _: &Lua) -> mlua::Result { + match value { + Value::UserData(ud) => Ok(Self(ud)), + _ => Err(EXPECTED.into_lua_err())?, + } + } +} diff --git a/yazi-fs/src/file/files.rs b/yazi-fs/src/file/files.rs new file mode 100644 index 00000000..62434be8 --- /dev/null +++ b/yazi-fs/src/file/files.rs @@ -0,0 +1,30 @@ +use std::ops::{Deref, DerefMut}; + +use yazi_shared::url::UrlBuf; + +use crate::file::File; + +#[derive(Clone, Debug, Default)] +pub struct Files(pub Vec); + +impl Deref for Files { + type Target = Vec; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Files { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl From> for Files { + fn from(value: Vec) -> Self { Self(value) } +} + +impl From for Vec { + fn from(value: Files) -> Self { value.0 } +} + +impl From for Vec { + fn from(value: Files) -> Self { value.0.into_iter().map(|f| f.url).collect() } +} diff --git a/yazi-fs/src/file/inventory.rs b/yazi-fs/src/file/inventory.rs index 624fbd0c..b91595e3 100644 --- a/yazi-fs/src/file/inventory.rs +++ b/yazi-fs/src/file/inventory.rs @@ -1,9 +1,10 @@ -use mlua::UserDataRegistry; +use mlua::{AnyUserData, UserDataRegistry}; use crate::file::File; pub struct FileInventory { pub register: fn(&mut UserDataRegistry), + pub borrow: fn(&AnyUserData, &mut dyn FnMut(&File) -> mlua::Result<()>) -> mlua::Result<()>, } inventory::collect!(FileInventory); diff --git a/yazi-fs/src/file/lua.rs b/yazi-fs/src/file/lua.rs index a6e93728..32d19bf4 100644 --- a/yazi-fs/src/file/lua.rs +++ b/yazi-fs/src/file/lua.rs @@ -1,9 +1,7 @@ -use mlua::{AnyUserData, ExternalError, FromLua, Lua, ObjectLike, Table, UserData, UserDataMethods, UserDataRef, UserDataRegistry, Value}; +use mlua::{AnyUserData, ExternalError, FromLua, Lua, Table, UserData, UserDataMethods, UserDataRegistry, Value}; use yazi_binding::{impl_file_fields, impl_file_methods}; -use crate::file::{File, FileInventory}; - -pub type FileRef = UserDataRef; +use crate::file::{File, FileInventory, FileRef}; const EXPECTED: &str = "expected a table, File, or fs::File"; @@ -25,7 +23,11 @@ impl TryFrom for File { type Error = mlua::Error; fn try_from(value: AnyUserData) -> Result { - Ok(if let Ok(me) = value.take::() { me } else { value.get("bare")? }) + match value.take::() { + Ok(me) => Ok(me), + Err(mlua::Error::UserDataTypeMismatch) => FileRef(value).try_into(), + Err(e) => Err(e), + } } } diff --git a/yazi-fs/src/file/mod.rs b/yazi-fs/src/file/mod.rs index 69b498b3..7e2916c3 100644 --- a/yazi-fs/src/file/mod.rs +++ b/yazi-fs/src/file/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(file inventory lua); +yazi_macro::mod_flat!(data file_cov file file_ref files inventory lua); diff --git a/yazi-fs/src/hash.rs b/yazi-fs/src/hash.rs index 852abadb..16df2a42 100644 --- a/yazi-fs/src/hash.rs +++ b/yazi-fs/src/hash.rs @@ -1,7 +1,7 @@ use std::hash::{BuildHasher, Hash}; use mlua::UserDataMethods; -use yazi_shared::url::{AsUrl, UrlBufInventory}; +use yazi_shared::url::{AsUrl, UrlBuf, UrlBufInventory}; use yazi_shim::Twox128; use crate::{cha::Cha, file::File}; @@ -14,7 +14,7 @@ impl FsHash64 for File { fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } } -impl FsHash64 for T { +impl FsHash64 for UrlBuf { fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self.as_url()) } } diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index bfce7b20..d6532551 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -2,7 +2,7 @@ extern crate self as yazi_fs; yazi_macro::mod_pub!(cha file mounts path provider); -yazi_macro::mod_flat!(cwd files filter fns hash op scheme sorter sorting splatter stage url xdg); +yazi_macro::mod_flat!(cwd entries filter fns hash op scheme sorter sorting splatter stage url xdg); pub fn init() { CWD.init(<_>::default()); diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index 4187f3c8..421f7452 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{iter, path::Path}; use hashbrown::{HashMap, HashSet}; use yazi_macro::{impl_data_any, relay}; @@ -25,7 +25,6 @@ pub enum FilesOp { impl_data_any!(FilesOp); impl FilesOp { - #[inline] pub fn cwd(&self) -> &UrlBuf { match self { Self::Full(u, ..) => u, @@ -41,7 +40,21 @@ impl FilesOp { } } - #[inline] + pub fn files(&self) -> Box + '_> { + match self { + Self::Full(_, files, _) => Box::new(files.iter()), + Self::Part(_, files, _) => Box::new(files.iter()), + Self::Done(..) => Box::new(iter::empty()), + Self::Size(..) => Box::new(iter::empty()), + Self::IOErr(..) => Box::new(iter::empty()), + + Self::Creating(_, files) => Box::new(files.iter()), + Self::Deleting(..) => Box::new(iter::empty()), + Self::Updating(_, map) => Box::new(map.values()), + Self::Upserting(_, map) => Box::new(map.values()), + } + } + pub fn emit(self) { yazi_shared::event::Event::Call(relay!(mgr:update_files).with_any("op", self).into()).emit(); } @@ -134,7 +147,7 @@ impl FilesOp { } } - pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec, Vec) { + pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec, Vec) { match self { Self::Deleting(cwd, urns) => { (urns.iter().filter_map(|u| cwd.try_join(u).ok()).collect(), vec![]) @@ -144,7 +157,7 @@ impl FilesOp { .filter(|&(u, f)| u != f.urn()) .filter_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f))) .filter(|(u, _)| contains(u)) - .map(|(u, f)| (u, f.url_owned())) + .map(|(u, f)| (u, f.clone())) .unzip(), _ => (vec![], vec![]), } diff --git a/yazi-macro/src/data.rs b/yazi-macro/src/data.rs index 80c3f56e..173dfcd5 100644 --- a/yazi-macro/src/data.rs +++ b/yazi-macro/src/data.rs @@ -99,10 +99,10 @@ macro_rules! impl_data_any { } }; (@from_lua_inherit) => { - fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result> { + fn from_lua(value: &mlua::Value, lua: &mlua::Lua) -> mlua::Result> { use mlua::FromLua; - ::from_lua(value, lua) + ::from_lua(value.clone(), lua) .map(|v| Box::new(v) as Box) } }; diff --git a/yazi-parser/src/help/close.rs b/yazi-parser/src/help/close.rs new file mode 100644 index 00000000..8f782f98 --- /dev/null +++ b/yazi-parser/src/help/close.rs @@ -0,0 +1,25 @@ +use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value}; +use serde::{Deserialize, Serialize}; +use yazi_shared::event::ActionCow; +use yazi_shim::mlua::SER_OPT; + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct CloseForm { + pub submit: bool, +} + +impl From for CloseForm { + fn from(a: ActionCow) -> Self { Self { submit: a.bool("submit") } } +} + +impl From for CloseForm { + fn from(submit: bool) -> Self { Self { submit } } +} + +impl FromLua for CloseForm { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { lua.from_value(value) } +} + +impl IntoLua for CloseForm { + fn into_lua(self, lua: &Lua) -> mlua::Result { lua.to_value_with(&self, SER_OPT) } +} diff --git a/yazi-parser/src/help/mod.rs b/yazi-parser/src/help/mod.rs index dbc5333d..8aa19c1e 100644 --- a/yazi-parser/src/help/mod.rs +++ b/yazi-parser/src/help/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(toggle); +yazi_macro::mod_flat!(close toggle); diff --git a/yazi-parser/src/mgr/create.rs b/yazi-parser/src/mgr/create.rs index bf8dc1ab..fc8c1cf8 100644 --- a/yazi-parser/src/mgr/create.rs +++ b/yazi-parser/src/mgr/create.rs @@ -1,13 +1,15 @@ use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use serde::Deserialize; -use yazi_shared::event::ActionCow; +use yazi_shared::{event::ActionCow, strand::StrandBuf}; #[derive(Debug, Deserialize)] pub struct CreateForm { + #[serde(alias = "0", default)] + pub target: StrandBuf, #[serde(default)] - pub dir: bool, + pub dir: bool, #[serde(default)] - pub force: bool, + pub force: bool, } impl TryFrom for CreateForm { diff --git a/yazi-parser/src/mgr/mod.rs b/yazi-parser/src/mgr/mod.rs index 60bfeab5..188af862 100644 --- a/yazi-parser/src/mgr/mod.rs +++ b/yazi-parser/src/mgr/mod.rs @@ -21,6 +21,7 @@ yazi_macro::mod_flat!( paste peek remove + remove_do rename reveal search diff --git a/yazi-parser/src/mgr/remove.rs b/yazi-parser/src/mgr/remove.rs index cf41c029..0eb28b18 100644 --- a/yazi-parser/src/mgr/remove.rs +++ b/yazi-parser/src/mgr/remove.rs @@ -1,21 +1,19 @@ use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_shared::{event::ActionCow, url::UrlBuf}; +use yazi_shared::event::ActionCow; #[derive(Debug)] pub struct RemoveForm { pub force: bool, pub permanently: bool, pub hovered: bool, - pub targets: Vec, } impl From for RemoveForm { - fn from(mut a: ActionCow) -> Self { + fn from(a: ActionCow) -> Self { Self { force: a.bool("force"), permanently: a.bool("permanently"), hovered: a.bool("hovered"), - targets: a.take_any("targets").unwrap_or_default(), } } } diff --git a/yazi-parser/src/mgr/remove_do.rs b/yazi-parser/src/mgr/remove_do.rs new file mode 100644 index 00000000..4f8ed033 --- /dev/null +++ b/yazi-parser/src/mgr/remove_do.rs @@ -0,0 +1,25 @@ +use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use yazi_shared::{event::ActionCow, url::UrlBuf}; + +#[derive(Debug)] +pub struct RemoveDoForm { + pub permanently: bool, + pub targets: Vec, +} + +impl From for RemoveDoForm { + fn from(mut a: ActionCow) -> Self { + Self { + permanently: a.bool("permanently"), + targets: a.take_any("targets").unwrap_or_default(), + } + } +} + +impl FromLua for RemoveDoForm { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for RemoveDoForm { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} diff --git a/yazi-parser/src/mgr/toggle.rs b/yazi-parser/src/mgr/toggle.rs index 168dcdb2..3dc34b17 100644 --- a/yazi-parser/src/mgr/toggle.rs +++ b/yazi-parser/src/mgr/toggle.rs @@ -1,16 +1,17 @@ use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_shared::{event::ActionCow, url::UrlBuf}; +use yazi_fs::file::File; +use yazi_shared::event::ActionCow; #[derive(Debug)] pub struct ToggleForm { - pub url: Option, + pub file: Option, pub state: Option, } impl From for ToggleForm { fn from(mut a: ActionCow) -> Self { Self { - url: a.take_first().ok(), + file: a.take_first().ok(), state: match a.get("state") { Ok("on") => Some(true), Ok("off") => Some(false), diff --git a/yazi-parser/src/mgr/toggle_all.rs b/yazi-parser/src/mgr/toggle_all.rs index 21a121b6..c361e1ef 100644 --- a/yazi-parser/src/mgr/toggle_all.rs +++ b/yazi-parser/src/mgr/toggle_all.rs @@ -1,16 +1,17 @@ use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_shared::{event::ActionCow, url::UrlBuf}; +use yazi_fs::file::File; +use yazi_shared::event::ActionCow; #[derive(Debug)] pub struct ToggleAllForm { - pub urls: Vec, + pub files: Vec, pub state: Option, } impl From for ToggleAllForm { fn from(mut a: ActionCow) -> Self { Self { - urls: a.take_seq(), + files: a.take_seq(), state: match a.get("state") { Ok("on") => Some(true), Ok("off") => Some(false), @@ -21,7 +22,7 @@ impl From for ToggleAllForm { } impl From> for ToggleAllForm { - fn from(state: Option) -> Self { Self { urls: vec![], state } } + fn from(state: Option) -> Self { Self { files: vec![], state } } } impl FromLua for ToggleAllForm { diff --git a/yazi-parser/src/spark/spark.rs b/yazi-parser/src/spark/spark.rs index 01c94ba1..46745ac6 100644 --- a/yazi-parser/src/spark/spark.rs +++ b/yazi-parser/src/spark/spark.rs @@ -66,7 +66,7 @@ pub enum Spark<'a> { Quit(crate::app::QuitForm), Refresh(crate::VoidForm), Remove(crate::mgr::RemoveForm), - RemoveDo(crate::mgr::RemoveForm), + RemoveDo(crate::mgr::RemoveDoForm), Rename(crate::mgr::RenameForm), Reveal(crate::mgr::RevealForm), Search(crate::mgr::SearchForm), @@ -110,8 +110,8 @@ pub enum Spark<'a> { // Help HelpArrow(crate::ArrowForm), + HelpClose(crate::help::CloseForm), HelpEscape(crate::VoidForm), - HelpFilter(crate::VoidForm), HelpToggle(crate::help::ToggleForm), // Input @@ -303,8 +303,8 @@ impl<'a> IntoLua for Spark<'a> { // Help Self::HelpArrow(b) => b.into_lua(lua), + Self::HelpClose(b) => b.into_lua(lua), Self::HelpEscape(b) => b.into_lua(lua), - Self::HelpFilter(b) => b.into_lua(lua), Self::HelpToggle(b) => b.into_lua(lua), // Input @@ -395,6 +395,7 @@ try_from_spark!(crate::cmp::ShowForm, cmp:show); try_from_spark!(crate::cmp::TriggerForm, cmp:trigger); try_from_spark!(crate::confirm::CloseForm, confirm:close); try_from_spark!(crate::confirm::ShowForm, confirm:show); +try_from_spark!(crate::help::CloseForm, help:close); try_from_spark!(crate::help::ToggleForm, help:toggle); try_from_spark!(crate::input::CloseForm, input:close); try_from_spark!(crate::mgr::BulkExitForm, mgr:bulk_exit); @@ -418,7 +419,8 @@ try_from_spark!(crate::mgr::OpenDoForm, mgr:open_do); try_from_spark!(crate::mgr::OpenForm, mgr:open); try_from_spark!(crate::mgr::PasteForm, mgr:paste); try_from_spark!(crate::mgr::PeekForm, mgr:peek); -try_from_spark!(crate::mgr::RemoveForm, mgr:remove, mgr:remove_do); +try_from_spark!(crate::mgr::RemoveForm, mgr:remove); +try_from_spark!(crate::mgr::RemoveDoForm, mgr:remove_do); try_from_spark!(crate::mgr::RenameForm, mgr:rename); try_from_spark!(crate::mgr::RevealForm, mgr:reveal); try_from_spark!(crate::mgr::SearchForm, mgr:search, mgr:search_do); diff --git a/yazi-parser/src/which/activate.rs b/yazi-parser/src/which/activate.rs index 3f2acc31..f9685f92 100644 --- a/yazi-parser/src/which/activate.rs +++ b/yazi-parser/src/which/activate.rs @@ -1,15 +1,14 @@ use mlua::{FromLua, IntoLua, Lua, Value}; -use yazi_config::keymap::Key; use yazi_core::which::WhichOpt; -use yazi_shared::{Layer, event::ActionCow}; +use yazi_shared::event::ActionCow; #[derive(Clone, Debug)] pub struct ActivateForm { pub opt: WhichOpt, } -impl From<(Layer, Key)> for ActivateForm { - fn from(value: (Layer, Key)) -> Self { Self { opt: value.into() } } +impl From for ActivateForm { + fn from(opt: WhichOpt) -> Self { Self { opt } } } impl TryFrom for ActivateForm { diff --git a/yazi-plugin/preset/components/entity.lua b/yazi-plugin/preset/components/entity.lua index 99c61581..7baaa249 100644 --- a/yazi-plugin/preset/components/entity.lua +++ b/yazi-plugin/preset/components/entity.lua @@ -26,7 +26,7 @@ function Entity:padding() end function Entity:icon() - local icon = self._file:icon() + local icon = th.icon:match(self._file, { hovered = self._file.is_hovered }) if not icon then return "" elseif self._file.is_hovered then diff --git a/yazi-plugin/preset/components/rail.lua b/yazi-plugin/preset/components/rail.lua index 305f9694..2a2ba346 100644 --- a/yazi-plugin/preset/components/rail.lua +++ b/yazi-plugin/preset/components/rail.lua @@ -42,7 +42,7 @@ function Rail:drag(event) end local r = rt.mgr.ratio - if r.parent ~= parent or r.current ~= current or r.preview ~= preview then + if r[1] ~= parent or r[2] ~= current or r[3] ~= preview then rt.mgr.ratio = { parent, current, preview } ui.render() end diff --git a/yazi-plugin/preset/components/tab.lua b/yazi-plugin/preset/components/tab.lua index 357abaab..3e759760 100644 --- a/yazi-plugin/preset/components/tab.lua +++ b/yazi-plugin/preset/components/tab.lua @@ -11,12 +11,13 @@ end function Tab:layout() local ratio = rt.mgr.ratio + local all = ratio[1] + ratio[2] + ratio[3] self._chunks = ui.Layout() :direction(ui.Layout.HORIZONTAL) :constraints({ - ui.Constraint.Ratio(ratio.parent, ratio.all), - ui.Constraint.Ratio(ratio.current, ratio.all), - ui.Constraint.Ratio(ratio.preview, ratio.all), + ui.Constraint.Ratio(ratio[1], all), + ui.Constraint.Ratio(ratio[2], all), + ui.Constraint.Ratio(ratio[3], all), }) :split(self._area) end diff --git a/yazi-plugin/preset/plugins/archive.lua b/yazi-plugin/preset/plugins/archive.lua index 00572ccf..8788a887 100644 --- a/yazi-plugin/preset/plugins/archive.lua +++ b/yazi-plugin/preset/plugins/archive.lua @@ -20,10 +20,10 @@ function M:peek(job) local left, right = {}, {} for i = job.skip + 1, #items do local f = items[i] - local icon = File({ + local icon = th.icon:match(File { url = Url(f.path), cha = Cha { mode = tonumber(f.is_dir and "40700" or "100644", 8) }, - }):icon() + }) if f.size > 0 then right[#right + 1] = " " .. ya.readable_size(f.size) .. " " diff --git a/yazi-plugin/preset/plugins/dnd.lua b/yazi-plugin/preset/plugins/dnd.lua index 552c26de..b18ae139 100644 --- a/yazi-plugin/preset/plugins/dnd.lua +++ b/yazi-plugin/preset/plugins/dnd.lua @@ -2,8 +2,8 @@ local M = {} function M.selected_uri_list() local paths = {} - for _, u in pairs(cx.active.selected) do - paths[#paths + 1] = "file://" .. ya.percent_encode(tostring(u.path)) + for _, f in pairs(cx.active.selected) do + paths[#paths + 1] = "file://" .. ya.percent_encode(tostring(f.path)) end if #paths == 0 and cx.active.current.hovered then paths[1] = "file://" .. ya.percent_encode(tostring(cx.active.current.hovered.path)) diff --git a/yazi-plugin/preset/plugins/fzf.lua b/yazi-plugin/preset/plugins/fzf.lua index 6842d26a..8d5552e3 100644 --- a/yazi-plugin/preset/plugins/fzf.lua +++ b/yazi-plugin/preset/plugins/fzf.lua @@ -2,8 +2,8 @@ local M = {} local state = ya.sync(function() local selected = {} - for _, url in pairs(cx.active.selected) do - selected[#selected + 1] = url + for _, f in pairs(cx.active.selected) do + selected[#selected + 1] = f.url end return cx.active.current.cwd, selected end) @@ -25,12 +25,20 @@ function M:entry() end local urls = M.split_urls(cwd, output) - if #urls == 1 then + if #urls == 0 then + return + elseif #urls == 1 then local cha = #selected == 0 and fs.cha(urls[1]) - ya.emit(cha and cha.is_dir and "cd" or "reveal", { urls[1], raw = true }) - elseif #urls > 1 then - urls.state = #selected > 0 and "off" or "on" - ya.emit("toggle_all", urls) + return ya.emit(cha and cha.is_dir and "cd" or "reveal", { urls[1], raw = true }) + end + + local files = {} + for _, url in ipairs(urls) do + files[#files + 1] = fs.file(url) + end + if #files > 0 then + files.state = #selected > 0 and "off" or "on" + ya.emit("toggle_all", files) end end @@ -69,11 +77,7 @@ function M.split_urls(cwd, output) local t = {} for line in output:gmatch("[^\r\n]+") do local u = Url(line) - if u.is_absolute then - t[#t + 1] = u - else - t[#t + 1] = cwd:join(u) - end + t[#t + 1] = u.is_absolute and u or cwd:join(u) end return t end diff --git a/yazi-plugin/preset/plugins/multi.lua b/yazi-plugin/preset/plugins/multi.lua index 9951140f..bf45ba89 100644 --- a/yazi-plugin/preset/plugins/multi.lua +++ b/yazi-plugin/preset/plugins/multi.lua @@ -2,8 +2,8 @@ local M = {} local selected = ya.sync(function() local urls = {} - for _, u in pairs(cx.active.selected) do - urls[#urls + 1] = u + for _, f in pairs(cx.active.selected) do + urls[#urls + 1] = f.url end return urls end) diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index b75c043b..7d7d8311 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -19,6 +19,7 @@ pub fn compose() -> Composer { b"create" => create(lua)?, b"cwd" => cwd(lua)?, b"expand_url" => expand_url(lua)?, + b"file" => file(lua)?, b"op" => op(lua)?, b"partitions" => partitions(lua)?, b"read_dir" => read_dir(lua)?, @@ -121,6 +122,15 @@ fn expand_url(lua: &Lua) -> mlua::Result { }) } +fn file(lua: &Lua) -> mlua::Result { + lua.create_async_function(|lua, url: UrlRef| async move { + match File::new(&*url).await { + Ok(file) => file.into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + } + }) +} + fn op(lua: &Lua) -> mlua::Result { lua.create_function(|lua, (name, t): (mlua::String, Table)| match &*name.as_bytes() { b"part" => super::FilesOp::part(lua, t), diff --git a/yazi-plugin/src/runtime/runtime.rs b/yazi-plugin/src/runtime/runtime.rs index 0de71d41..5db8af6d 100644 --- a/yazi-plugin/src/runtime/runtime.rs +++ b/yazi-plugin/src/runtime/runtime.rs @@ -1,7 +1,7 @@ -use mlua::{IntoLua, Lua, LuaSerdeExt, Value}; +use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value}; use yazi_binding::{Composer, ComposerGet, ComposerSet, elements::Wrap}; use yazi_boot::ARGS; -use yazi_config::YAZI; +use yazi_config::{YAZI, mgr::MgrRatio}; use yazi_shared::url::UrlBuf; use yazi_shim::mlua::SER_OPT; use yazi_tty::TTY; @@ -60,7 +60,7 @@ fn mgr() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { let m = &YAZI.mgr; match key { - b"ratio" => lua.to_value_with(&m.ratio, SER_OPT)?, + b"ratio" => m.ratio.get().into_lua(lua)?, b"sort_by" => lua.to_value_with(&m.sort_by, SER_OPT)?, b"sort_sensitive" => m.sort_sensitive.get().into_lua(lua)?, @@ -83,7 +83,7 @@ fn mgr() -> Composer { let m = &YAZI.mgr; Ok(match key { b"ratio" => { - m.ratio.set(lua.from_value(value)?); + m.ratio.set(MgrRatio::from_lua(value, lua)?); Value::Nil } _ => value, diff --git a/yazi-plugin/src/theme/icon.rs b/yazi-plugin/src/theme/icon.rs new file mode 100644 index 00000000..716a5298 --- /dev/null +++ b/yazi-plugin/src/theme/icon.rs @@ -0,0 +1,23 @@ +use mlua::{IntoLua, Lua, Table, Value}; +use yazi_binding::{Composer, ComposerGet, ComposerSet}; +use yazi_config::THEME; +use yazi_fs::file::FileRef; + +pub(super) fn icon() -> Composer { + fn get(lua: &Lua, key: &[u8]) -> mlua::Result { + match key { + b"match" => lua + .create_function(|_, (_, file, opts): (Value, FileRef, Option
)| { + let hovered = opts.map(|t| t.raw_get("hovered")).transpose()?.unwrap_or(false); + file.borrow(|f| Ok(THEME.icon.matches(f, hovered))) + })? + .into_lua(lua), + + _ => Ok(Value::Nil), + } + } + + fn set(_: &Lua, _: &[u8], value: Value) -> mlua::Result { Ok(value) } + + Composer::new(get, set) +} diff --git a/yazi-plugin/src/theme/mod.rs b/yazi-plugin/src/theme/mod.rs index c0a6b7f5..daa06c77 100644 --- a/yazi-plugin/src/theme/mod.rs +++ b/yazi-plugin/src/theme/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(theme); +yazi_macro::mod_flat!(icon theme); diff --git a/yazi-plugin/src/theme/theme.rs b/yazi-plugin/src/theme/theme.rs index c13b47fc..64df33b2 100644 --- a/yazi-plugin/src/theme/theme.rs +++ b/yazi-plugin/src/theme/theme.rs @@ -3,7 +3,7 @@ use yazi_binding::{Composer, ComposerGet, ComposerSet, style::Style}; use yazi_config::{THEME, theme::CustomSectionArc}; use yazi_shared::url::UrlBuf; -use crate::LUA; +use crate::{LUA, theme::icon}; pub fn compose() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { @@ -23,6 +23,7 @@ pub fn compose() -> Composer { b"cmp" => cmp(), b"tasks" => tasks(), b"help" => help(), + b"icon" => icon(), _ => return custom(lua, key), } .into_lua(lua) @@ -361,12 +362,10 @@ fn help() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { let t = &THEME.help; match key { - b"on" => Style::from(&t.on).into_lua(lua), - b"run" => Style::from(&t.run).into_lua(lua), - b"desc" => Style::from(&t.desc).into_lua(lua), - + b"border" => Style::from(&t.border).into_lua(lua), + b"chord" => Style::from(&t.chord).into_lua(lua), + b"action" => Style::from(&t.action).into_lua(lua), b"hovered" => Style::from(&t.hovered).into_lua(lua), - b"footer" => Style::from(&t.footer).into_lua(lua), _ => Ok(Value::Nil), } diff --git a/yazi-plugin/src/utils/cache.rs b/yazi-plugin/src/utils/cache.rs index 58d5fe63..f8f80ba0 100644 --- a/yazi-plugin/src/utils/cache.rs +++ b/yazi-plugin/src/utils/cache.rs @@ -12,18 +12,20 @@ impl Utils { pub(super) fn file_cache(lua: &Lua) -> mlua::Result { lua.create_function(|_, t: Table| { let file: FileRef = t.raw_get("file")?; - if file.url.parent() == Some(yazi_shared::url::Url::regular(&YAZI.preview.cache_dir)) { - return Ok(None); - } + file.borrow(|f| { + if f.url.parent() == Some(yazi_shared::url::Url::regular(&YAZI.preview.cache_dir)) { + return Ok(None); + } - let hex = { - let mut h = Twox128::default(); - file.hash(&mut h); - t.raw_get("skip").unwrap_or(0usize).hash(&mut h); - format!("{:x}", h.finish_128()) - }; + let hex = { + let mut h = Twox128::default(); + f.hash(&mut h); + t.raw_get("skip").unwrap_or(0usize).hash(&mut h); + format!("{:x}", h.finish_128()) + }; - Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(hex)))) + Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(hex)))) + }) }) } } diff --git a/yazi-plugin/src/utils/layer.rs b/yazi-plugin/src/utils/layer.rs index 16d6c775..1057189b 100644 --- a/yazi-plugin/src/utils/layer.rs +++ b/yazi-plugin/src/utils/layer.rs @@ -25,7 +25,7 @@ impl Utils { .enumerate() .map(|(i, cand)| { let cand = cand?; - Ok(ChordArc::from(Chord::<{ Layer::Null as u8 }> { + Ok(ChordArc::from(Chord { id: yazi_config::keymap::chord_id(), on: Self::parse_keys(cand.raw_get("on")?)?, run: relay!(which:callback, [i + 1]).into(), diff --git a/yazi-proxy/src/mgr.rs b/yazi-proxy/src/mgr.rs index dae29a97..9c9d8c15 100644 --- a/yazi-proxy/src/mgr.rs +++ b/yazi-proxy/src/mgr.rs @@ -36,9 +36,9 @@ impl MgrProxy { emit!(Call(relay!(mgr:open_do).with_any("opt", opt))); } - pub fn remove_do(targets: Vec, permanently: bool) { + pub fn remove_do(permanently: bool, targets: impl Into>) { emit!(Call( - relay!(mgr:remove_do).with("permanently", permanently).with_any("targets", targets) + relay!(mgr:remove_do).with("permanently", permanently).with_any("targets", targets.into()) )); } diff --git a/yazi-proxy/src/which.rs b/yazi-proxy/src/which.rs index baf25ef0..5ef649aa 100644 --- a/yazi-proxy/src/which.rs +++ b/yazi-proxy/src/which.rs @@ -2,6 +2,7 @@ use tokio::sync::mpsc; use yazi_config::keymap::ChordArc; use yazi_core::which::WhichOpt; use yazi_macro::{emit, relay}; +use yazi_shared::Layer; pub struct WhichProxy; @@ -10,9 +11,10 @@ impl WhichProxy { let (tx, mut rx) = mpsc::unbounded_channel(); emit!(Call(relay!(which:activate).with_any("opt", WhichOpt { tx: Some(tx), + layer: Layer::Null, cands, - silent, times: 0, + silent, }))); rx.recv().await? } diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 15e4434c..56cc7e1a 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -24,6 +24,7 @@ dyn-clone = { workspace = true } foldhash = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } +indexmap = { workspace = true } inventory = { workspace = true } memchr = "2.8.2" mlua = { workspace = true } diff --git a/yazi-shared/src/data/any.rs b/yazi-shared/src/data/any.rs index 4c4c159c..951eeb38 100644 --- a/yazi-shared/src/data/any.rs +++ b/yazi-shared/src/data/any.rs @@ -14,7 +14,7 @@ pub trait DataAny: Any + Send + Sync + DynClone { fn to_lua(&self, lua: &Lua) -> mlua::Result { dyn_clone::clone_box(self).into_lua(lua) } - fn from_lua(_value: Value, _lua: &Lua) -> mlua::Result> + fn from_lua(_value: &Value, _lua: &Lua) -> mlua::Result> where Self: Sized, { diff --git a/yazi-shared/src/data/inventory.rs b/yazi-shared/src/data/inventory.rs index b4114236..79f036d3 100644 --- a/yazi-shared/src/data/inventory.rs +++ b/yazi-shared/src/data/inventory.rs @@ -3,7 +3,7 @@ use mlua::{Lua, Value}; use crate::data::DataAny; pub struct DataInventory { - pub from_lua: fn(Value, &Lua) -> mlua::Result>, + pub from_lua: fn(&Value, &Lua) -> mlua::Result>, } inventory::collect!(DataInventory); diff --git a/yazi-shared/src/data/sendable.rs b/yazi-shared/src/data/sendable.rs index 39e0c8b4..e804ef5c 100644 --- a/yazi-shared/src/data/sendable.rs +++ b/yazi-shared/src/data/sendable.rs @@ -61,7 +61,7 @@ impl Sendable { } for inv in inventory::iter:: { - match (inv.from_lua)(value.clone(), lua) { + match (inv.from_lua)(&value, lua) { Ok(data) => return Ok(Data::Any(data)), Err(mlua::Error::UserDataTypeMismatch) => continue, Err(e) => return Err(e), diff --git a/yazi-shared/src/event/actions.rs b/yazi-shared/src/event/actions.rs index 01b71916..70259752 100644 --- a/yazi-shared/src/event/actions.rs +++ b/yazi-shared/src/event/actions.rs @@ -1,10 +1,10 @@ use std::{ops::{Deref, DerefMut}, slice, vec}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use serde::{Deserializer, de}; +use serde::Deserializer; use serde_with::{DeserializeAs, DisplayFromStr, OneOrMany}; -use crate::{Layer, Source, event::Action}; +use crate::{Source, event::Action}; #[derive(Clone, Debug, Default)] pub struct Actions(pub Vec); @@ -28,12 +28,9 @@ impl From> for Actions { } impl Actions { - pub fn set(&mut self, layer: Layer, source: Source) { + pub fn set_source(&mut self, source: Source) { for action in &mut self.0 { action.source = source; - if action.layer == Layer::Null { - action.layer = layer; - } } } } @@ -68,16 +65,12 @@ impl IntoLua for Actions { } } -pub fn deserialize_actions<'de, const L: u8, D>(deserializer: D) -> Result +pub fn deserialize_actions<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let Some(layer) = Layer::from_repr(L) else { - return Err(de::Error::custom(format!("invalid keymap layer const: {L}"))); - }; - let mut actions = Actions(OneOrMany::::deserialize_as(deserializer)?); - actions.set(layer, Source::Key); + actions.set_source(Source::Key); Ok(actions) } diff --git a/yazi-shared/src/layer.rs b/yazi-shared/src/layer.rs index f3e10377..53ef3f91 100644 --- a/yazi-shared/src/layer.rs +++ b/yazi-shared/src/layer.rs @@ -1,4 +1,4 @@ -use mlua::{MetaMethod, UserData, UserDataMethods}; +use mlua::{ExternalError, ExternalResult, FromLua, MetaMethod, UserData, UserDataMethods, Value}; use serde::Deserialize; use strum::{Display, EnumString, FromRepr, IntoStaticStr}; use yazi_shim::strum::IntoStr; @@ -17,8 +17,8 @@ use yazi_shim::strum::IntoStr; IntoStaticStr, PartialEq, )] -#[serde(rename_all = "kebab-case")] -#[strum(serialize_all = "kebab-case")] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] #[repr(u8)] pub enum Layer { #[default] @@ -36,6 +36,20 @@ pub enum Layer { Notify, } +impl Layer { + pub fn or(self, other: Self) -> Self { if self == Self::Null { other } else { self } } +} + +impl FromLua for Layer { + fn from_lua(value: Value, _: &mlua::Lua) -> mlua::Result { + Ok(match value { + Value::String(s) => s.to_str()?.parse().into_lua_err()?, + Value::UserData(ud) => *ud.borrow::()?, + _ => Err("expected a string or a Layer".into_lua_err())?, + }) + } +} + impl UserData for Layer { fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.into_str())); diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 5abaedde..b0e0f20a 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, ffi::OsString, hash::{Hash, Hasher}}; use hashbrown::Equivalent; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use yazi_codegen::FromLuaOwned; use yazi_shim::wtf8::FromWtf8Vec; @@ -189,3 +190,29 @@ impl PathBufDyn { } } } + +impl Serialize for PathBufDyn { + fn serialize(&self, serializer: S) -> Result { + #[derive(Serialize)] + struct Shadow<'a> { + kind: PathKind, + path: &'a [u8], + } + + let path = self.as_path(); + Shadow { kind: path.kind(), path: path.encoded_bytes() }.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PathBufDyn { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct Shadow { + kind: PathKind, + path: Vec, + } + + let Shadow { kind, path } = Shadow::deserialize(deserializer)?; + Self::with(kind, path).map_err(de::Error::custom) + } +} diff --git a/yazi-shared/src/path/kind.rs b/yazi-shared/src/path/kind.rs index c66781cd..fb2c9a1f 100644 --- a/yazi-shared/src/path/kind.rs +++ b/yazi-shared/src/path/kind.rs @@ -1,5 +1,9 @@ +use serde::{Deserialize, Serialize}; + use crate::scheme::SchemeKind; +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] pub enum PathKind { Os, Unix, diff --git a/yazi-shared/src/predictor.rs b/yazi-shared/src/predictor.rs index 4603e2e9..6007c573 100644 --- a/yazi-shared/src/predictor.rs +++ b/yazi-shared/src/predictor.rs @@ -12,6 +12,11 @@ pub trait Utf8BytePredictor { pub struct AnyAsciiChar<'a>(&'a [u8]); impl<'a> AnyAsciiChar<'a> { + #[cfg(windows)] + pub const SEP: Self = Self(b"/\\"); + #[cfg(not(windows))] + pub const SEP: Self = Self(b"/"); + pub fn new(chars: &'a [u8]) -> Option { if chars.iter().all(|&b| b <= 0x7f) { Some(Self(chars)) } else { None } } diff --git a/yazi-shared/src/strand/de.rs b/yazi-shared/src/strand/de.rs new file mode 100644 index 00000000..0f6ccff8 --- /dev/null +++ b/yazi-shared/src/strand/de.rs @@ -0,0 +1,40 @@ +use std::fmt; + +use serde::{Deserialize, Deserializer, de::{self, Visitor}}; + +use crate::strand::StrandBuf; + +impl<'de> Deserialize<'de> for StrandBuf { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct V; + + impl<'de> Visitor<'de> for V { + type Value = StrandBuf; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("a string or bytes") + } + + fn visit_str(self, v: &str) -> Result { + Ok(StrandBuf::Utf8(v.to_owned())) + } + + fn visit_string(self, v: String) -> Result { + Ok(StrandBuf::Utf8(v)) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(StrandBuf::Bytes(v.to_owned())) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(StrandBuf::Bytes(v)) + } + } + + deserializer.deserialize_any(V) + } +} diff --git a/yazi-shared/src/strand/like.rs b/yazi-shared/src/strand/like.rs index 297fc32f..e6463dae 100644 --- a/yazi-shared/src/strand/like.rs +++ b/yazi-shared/src/strand/like.rs @@ -17,6 +17,8 @@ pub trait StrandLike: AsStrand { fn encoded_bytes(&self) -> &[u8] { self.as_strand().encoded_bytes() } + fn ends_with(&self, needle: impl AsStrand) -> bool { self.as_strand().ends_with(needle) } + fn eq_ignore_ascii_case(&self, other: impl AsStrand) -> bool { self.as_strand().eq_ignore_ascii_case(other) } diff --git a/yazi-shared/src/strand/mod.rs b/yazi-shared/src/strand/mod.rs index af6a08c4..ccbb363f 100644 --- a/yazi-shared/src/strand/mod.rs +++ b/yazi-shared/src/strand/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(buf conversion cow error extensions kind like strand view); +yazi_macro::mod_flat!(buf conversion cow de error extensions kind like strand view); diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs index 4337bf29..b66d341f 100644 --- a/yazi-shared/src/strand/strand.rs +++ b/yazi-shared/src/strand/strand.rs @@ -156,6 +156,10 @@ impl<'a> Strand<'a> { } } + pub fn ends_with(self, needle: impl AsStrand) -> bool { + self.encoded_bytes().ends_with(needle.as_strand().encoded_bytes()) + } + pub fn eq_ignore_ascii_case(self, other: impl AsStrand) -> bool { self.encoded_bytes().eq_ignore_ascii_case(other.as_strand().encoded_bytes()) } diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index bb5d69fe..c1ea5387 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -1,6 +1,7 @@ use std::{hash::BuildHasher, path::{Path, PathBuf}}; use hashbrown::{HashMap, hash_map::EntryRef}; +use indexmap::{IndexMap, map::RawEntryApiV1}; use crate::{loc::Loc, url::{Url, UrlBuf, UrlBufCov, UrlCov, UrlCow}}; @@ -127,6 +128,28 @@ where } } +impl UrlMapExt for IndexMap +where + S: BuildHasher, +{ + fn get_or_insert_default(&mut self, url: U) -> &mut V + where + U: AsUrl, + V: Default, + { + self.get_or_insert_with(url, |_| Default::default()) + } + + fn get_or_insert_with(&mut self, url: U, default: F) -> &mut V + where + U: AsUrl, + F: FnOnce(Url<'_>) -> V, + { + let url = url.as_url(); + self.raw_entry_mut_v1().from_key(&url).or_insert_with(|| (url.into(), default(url))).1 + } +} + // --- UrlCovMapExt pub trait UrlCovMapExt { fn get_or_insert_default(&mut self, url: UrlCov<'_>) -> &mut V diff --git a/yazi-term/src/restorer/windows.rs b/yazi-term/src/restorer/windows.rs index 4571638b..da5291f8 100644 --- a/yazi-term/src/restorer/windows.rs +++ b/yazi-term/src/restorer/windows.rs @@ -1,6 +1,6 @@ use std::{io::{self, Write}, os::windows::io::AsRawHandle}; -use windows_sys::Win32::System::Console::{CONSOLE_MODE, GetConsoleCP, GetConsoleMode, GetConsoleOutputCP, SetConsoleCP, SetConsoleMode, SetConsoleOutputCP}; +use windows_sys::Win32::System::Console::{CONSOLE_MODE, FlushConsoleInputBuffer, GetConsoleCP, GetConsoleMode, GetConsoleOutputCP, SetConsoleCP, SetConsoleMode, SetConsoleOutputCP}; use yazi_shim::{bool_ok, nz_ok}; use yazi_tty::Tty; @@ -29,6 +29,7 @@ impl Restorer { pub fn restore(&self, tty: &Tty) { tty.writer().flush().ok(); unsafe { + FlushConsoleInputBuffer(tty.reader().as_raw_handle()); SetConsoleCP(self.input_cp); SetConsoleMode(tty.reader().as_raw_handle(), self.input_mode); SetConsoleOutputCP(self.output_cp); diff --git a/yazi-version/Cargo.toml b/yazi-version/Cargo.toml index 44dfdecc..fc35b54c 100644 --- a/yazi-version/Cargo.toml +++ b/yazi-version/Cargo.toml @@ -13,4 +13,4 @@ rust-version.workspace = true workspace = true [build-dependencies] -vergen-gitcl = { version = "10.0.0", features = [ "build", "rustc" ] } +vergen-gitcl = { version = "10.0.1", features = [ "build", "rustc" ] } diff --git a/yazi-vfs/src/files.rs b/yazi-vfs/src/entries.rs similarity index 93% rename from yazi-vfs/src/files.rs rename to yazi-vfs/src/entries.rs index 7b5168b6..da47837b 100644 --- a/yazi-vfs/src/files.rs +++ b/yazi-vfs/src/entries.rs @@ -1,12 +1,12 @@ use std::io; use tokio::{select, sync::mpsc::{self, UnboundedReceiver}}; -use yazi_fs::{Files, FilesOp, cha::Cha, file::File, mounts::PARTITIONS, provider::{DirReader, FileHolder}}; +use yazi_fs::{Entries, FilesOp, cha::Cha, file::File, mounts::PARTITIONS, provider::{DirReader, FileHolder}}; use yazi_shared::url::UrlBuf; use crate::{VfsCha, VfsFile, VfsFilesOp, provider::{self, DirEntry}}; -pub trait VfsFiles { +pub trait VfsEntries { fn from_dir(dir: &UrlBuf) -> impl Future>>; fn from_dir_bulk(dir: &UrlBuf) -> impl Future>>; @@ -14,7 +14,7 @@ pub trait VfsFiles { fn assert_stale(dir: &UrlBuf, cha: Cha) -> impl Future>; } -impl VfsFiles for Files { +impl VfsEntries for Entries { async fn from_dir(dir: &UrlBuf) -> std::io::Result> { let mut it = provider::read_dir(dir).await?; let (tx, rx) = mpsc::unbounded_channel(); diff --git a/yazi-vfs/src/file.rs b/yazi-vfs/src/file.rs index 06d29965..26323a2d 100644 --- a/yazi-vfs/src/file.rs +++ b/yazi-vfs/src/file.rs @@ -1,22 +1,35 @@ -use anyhow::Result; +use std::io; + use yazi_fs::{cha::Cha, file::File}; use yazi_shared::url::{UrlBuf, UrlCow}; use crate::{VfsCha, provider}; pub trait VfsFile: Sized { - fn new<'a>(url: impl Into>) -> impl Future>; + fn new<'a>(url: impl Into>) -> impl Future>; + + fn maybe_new<'a>(url: impl Into>) -> impl Future>>; fn from_follow(url: UrlBuf, cha: Cha) -> impl Future; } impl VfsFile for File { - async fn new<'a>(url: impl Into>) -> Result { + async fn new<'a>(url: impl Into>) -> io::Result { let url = url.into(); let cha = provider::symlink_metadata(&url).await?; Ok(Self::from_follow(url.into_owned(), cha).await) } + async fn maybe_new<'a>(url: impl Into>) -> io::Result> { + let url = url.into(); + let cha = match provider::symlink_metadata(&url).await { + Ok(cha) => cha, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(e), + }; + Ok(Some(Self::from_follow(url.into_owned(), cha).await)) + } + async fn from_follow(url: UrlBuf, cha: Cha) -> Self { let link_to = if cha.is_link() { provider::read_link(&url).await.ok() } else { None }; diff --git a/yazi-vfs/src/lib.rs b/yazi-vfs/src/lib.rs index 498f440f..79d5c69a 100644 --- a/yazi-vfs/src/lib.rs +++ b/yazi-vfs/src/lib.rs @@ -1,5 +1,5 @@ yazi_macro::mod_pub!(config provider); -yazi_macro::mod_flat!(cha file files fns op); +yazi_macro::mod_flat!(cha entries file fns op); pub fn init() { provider::init(); } diff --git a/yazi-widgets/src/input/event.rs b/yazi-widgets/src/input/event.rs index 52f77d0d..82ed4fa6 100644 --- a/yazi-widgets/src/input/event.rs +++ b/yazi-widgets/src/input/event.rs @@ -2,6 +2,7 @@ use strum::IntoStaticStr; use yazi_shared::id::Id; #[derive(Debug, IntoStaticStr)] +#[strum(serialize_all = "lowercase")] pub enum InputEvent { Submit(String), Cancel(String), @@ -11,11 +12,21 @@ pub enum InputEvent { } impl InputEvent { + pub fn is_submit(&self) -> bool { matches!(self, Self::Submit(_)) } + pub fn value(&self) -> &str { match self { Self::Submit(v) | Self::Cancel(v) | Self::Type(v) | Self::Trigger(v, _) => v.as_str(), } } - pub fn is_submit(&self) -> bool { matches!(self, Self::Submit(_)) } + pub fn map(self, f: F) -> Option + where + F: FnOnce(String) -> T, + { + match self { + Self::Submit(v) => Some(f(v)), + _ => None, + } + } } diff --git a/yazi-widgets/src/input/input.rs b/yazi-widgets/src/input/input.rs index b4f9591e..62d47ead 100644 --- a/yazi-widgets/src/input/input.rs +++ b/yazi-widgets/src/input/input.rs @@ -16,7 +16,6 @@ pub struct Input { pub snaps: InputSnaps, pub styles: InputStyles, pub obscure: bool, - pub blinking: bool, pub realtime: bool, pub completion: bool, @@ -30,7 +29,6 @@ impl Input { snaps: InputSnaps::new(opt.value, opt.obscure), styles: opt.styles, obscure: opt.obscure, - blinking: opt.blinking, realtime: opt.realtime, completion: opt.completion, @@ -133,13 +131,14 @@ impl Input { pub fn cursor_shape(&self) -> SetCursorStyle { use InputMode as M; + let blink = self.styles.blink.unwrap_or(false); match self.mode() { - M::Normal if self.blinking => SetCursorStyle::BlinkingBlock, - M::Normal if !self.blinking => SetCursorStyle::SteadyBlock, - M::Insert if self.blinking => SetCursorStyle::BlinkingBar, - M::Insert if !self.blinking => SetCursorStyle::SteadyBar, - M::Replace if self.blinking => SetCursorStyle::BlinkingUnderline, - M::Replace if !self.blinking => SetCursorStyle::SteadyUnderline, + M::Normal if blink => SetCursorStyle::BlinkingBlock, + M::Normal if !blink => SetCursorStyle::SteadyBlock, + M::Insert if blink => SetCursorStyle::BlinkingBar, + M::Insert if !blink => SetCursorStyle::SteadyBar, + M::Replace if blink => SetCursorStyle::BlinkingUnderline, + M::Replace if !blink => SetCursorStyle::SteadyUnderline, M::Normal | M::Insert | M::Replace => unreachable!(), } } diff --git a/yazi-widgets/src/input/input_arc.rs b/yazi-widgets/src/input/input_arc.rs index ae0b48ad..71f132ef 100644 --- a/yazi-widgets/src/input/input_arc.rs +++ b/yazi-widgets/src/input/input_arc.rs @@ -29,6 +29,7 @@ impl InputArc { let new = lua.create_function(move |_, (_, mut opt): (Table, InputOpt)| { opt.styles.normal = opt.styles.normal.or(styles.normal); opt.styles.selected = opt.styles.selected.or(styles.selected); + opt.styles.blink = opt.styles.blink.or(styles.blink); opt = opt.with_cb(|e| err!(Pubsub::pub_after_input((&e).into_str(), e.value()))); Ok(Self::from(Input::new(opt)?)) })?; diff --git a/yazi-widgets/src/input/option.rs b/yazi-widgets/src/input/option.rs index 3cb3c1c5..d3d50c65 100644 --- a/yazi-widgets/src/input/option.rs +++ b/yazi-widgets/src/input/option.rs @@ -6,6 +6,7 @@ use crate::input::{InputCallback, InputStyles}; #[derive(Clone, Debug, Default)] pub struct InputOpt { + pub name: String, pub title: String, pub value: String, pub styles: InputStyles, @@ -42,6 +43,7 @@ impl TryFrom<&Table> for InputOpt { fn try_from(t: &Table) -> Result { Ok(Self { + name: t.raw_get("name").unwrap_or_default(), title: t.raw_get("title").unwrap_or_default(), value: t.raw_get("value").unwrap_or_default(), styles: t.raw_get("styles")?, diff --git a/yazi-widgets/src/input/styles.rs b/yazi-widgets/src/input/styles.rs index c402bdcd..bc32688e 100644 --- a/yazi-widgets/src/input/styles.rs +++ b/yazi-widgets/src/input/styles.rs @@ -5,6 +5,7 @@ use ratatui_core::style::Style; pub struct InputStyles { pub normal: Option