Merge remote-tracking branch 'upstream/main' into improve-rg-search

This commit is contained in:
Jed 2026-07-05 18:27:24 +02:00
commit b1d0f10cec
172 changed files with 1803 additions and 1041 deletions

View file

@ -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

View file

@ -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 }}

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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 `<BackTab>` to `<S-Tab>` ([#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

213
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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 }

View file

@ -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()?;

View file

@ -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));
}

View file

@ -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<Data> {
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!());
}
}

View file

@ -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<Data> {
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!());
}
}

View file

@ -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<Data> {
let help = &mut cx.help;
help.in_filter = Some(Default::default());
help.filter_apply();
succ!(render!());
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(arrow escape filter toggle);
yazi_macro::mod_flat!(arrow close escape toggle);

View file

@ -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<Data> {
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!());
}
}

View file

@ -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;

View file

@ -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<usize>,
folder: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>,
}
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<usize>,
folder: &yazi_core::tab::Folder,
@ -27,7 +27,7 @@ impl Files {
}
}
impl UserData for Files {
impl UserData for Entries {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_static_field("filter", |_, me| me.filter().map(Filter::make).transpose());
}

View file

@ -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<yazi_fs::file::File> for File {
@ -32,14 +33,16 @@ impl File {
) -> mlua::Result<AnyUserData> {
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<F: UserDataFields<Self>>(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<M: UserDataMethods<Self>>(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::<File>()?),
}
}

View file

@ -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<usize>,
@ -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<F: UserDataFields<Self>>(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));

View file

@ -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())) };

View file

@ -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<F: UserDataFields<Self>>(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<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -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()
});

View file

@ -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()),
))
}

View file

@ -24,10 +24,10 @@ impl Which {
impl UserData for Which {
fn add_fields<F: UserDataFields<Self>>(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));

View file

@ -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()),
))?,
})

View file

@ -14,6 +14,7 @@ impl Actor for Arrow {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
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)?;

View file

@ -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"));
}

View file

@ -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();

View file

@ -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<Data> {
fn act(cx: &mut Ctx, CreateForm { target, dir, force }: Self::Form) -> Result<Data> {
let cwd = cx.cwd().to_owned();
let mut input = input!(cx, YAZI.input.create(form.dir))?;
let mut target: Pin<Box<dyn Stream<Item = StrandBuf> + 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!();
}

View file

@ -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<Data> {
if cx.current_mut().files.filter().is_none() {
if cx.current_mut().entries.filter().is_none() {
succ!(false);
}

View file

@ -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)?;

View file

@ -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 {

View file

@ -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 {

View file

@ -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())
}
};

View file

@ -33,6 +33,7 @@ yazi_macro::mod_flat!(
quit
refresh
remove
remove_do
rename
reveal
search

View file

@ -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() {

View file

@ -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));

View file

@ -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,
}

View file

@ -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<Data> {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
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<Data> {
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!();
}
}

View file

@ -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<Data> {
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!();
}
}

View file

@ -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<bool> {
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();

View file

@ -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

View file

@ -16,7 +16,7 @@ impl Actor for TabSwap {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
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!();
}

View file

@ -15,14 +15,14 @@ impl Actor for Toggle {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
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 {

View file

@ -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!();

View file

@ -16,7 +16,7 @@ impl Actor for UpdateFiles {
const NAME: &str = "update_files";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
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!();
}

View file

@ -14,11 +14,11 @@ impl Actor for UpdateYanked {
const NAME: &str = "update_yanked";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
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!());
}
}

View file

@ -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!());

View file

@ -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)

View file

@ -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);
};

View file

@ -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));
}

View file

@ -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;

View file

@ -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"

View file

@ -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 }

View file

@ -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" ] }

View file

@ -13,13 +13,17 @@ keymap = [
{ on = "<C-c>", run = "close", desc = "Close the current tab, or quit if it's last" },
{ on = "<C-z>", 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 = "<Up>", run = "arrow prev", desc = "Previous file" },
{ on = "<Down>", 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 = "<Home>", run = "arrow top", desc = "Go to top" },
{ on = "<End>", run = "arrow bot", desc = "Go to bottom" },
{ on = "<C-u>", run = "arrow -50%", desc = "Move cursor up half page" },
{ on = "<C-d>", run = "arrow 50%", desc = "Move cursor down half page" },
{ on = "<C-b>", run = "arrow -100%", desc = "Move cursor up one page" },
@ -30,9 +34,6 @@ keymap = [
{ on = "<PageUp>", run = "arrow -100%", desc = "Move cursor up one page" },
{ on = "<PageDown>", 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 = "<Esc>", run = "escape", desc = "Clear the filter, or hide the help" },
{ on = "<C-[>", run = "escape", desc = "Clear the filter, or hide the help" },
{ on = "<C-c>", run = "close", desc = "Hide the help" },
{ on = "<Esc>", run = "escape", desc = "Enter normal mode, or hide help menu" },
{ on = "<C-[>", run = "escape", desc = "Enter normal mode, or hide help menu" },
{ on = "<C-c>", run = "close", desc = "Close help menu" },
{ on = "<Enter>", 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 = "<Up>", run = "arrow prev", desc = "Previous line" },
{ on = "<Down>", run = "arrow next", desc = "Next line" },
# Filtering
{ on = "f", run = "filter", desc = "Filter help items" },
{ on = "<C-p>", run = "arrow prev", desc = "Previous line" },
{ on = "<C-n>", run = "arrow next", desc = "Next line" },
]

View file

@ -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" }
# : }}}

View file

@ -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" }
# : }}}

View file

@ -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),
}
}

View file

@ -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<Regex> = OnceLock::new();
#[derive(Debug, Default, Deserialize, DeserializeOver2)]
pub struct Chord<const L: u8 = { Layer::Null as u8 }> {
pub struct Chord {
#[serde(skip, default = "chord_id")]
pub id: Id,
#[serde(deserialize_with = "deserialize_on")]
pub on: Vec<Key>,
#[serde(deserialize_with = "deserialize_actions::<L, _>")]
#[serde(deserialize_with = "deserialize_actions")]
pub run: Actions,
#[serde(default)]
pub desc: String,
@ -27,7 +27,7 @@ pub struct Chord<const L: u8 = { Layer::Null as u8 }> {
pub r#for: Platform,
}
impl<const L: u8> Clone for Chord<L> {
impl Clone for Chord {
fn clone(&self) -> Self {
Self {
id: chord_id(),
@ -39,17 +39,21 @@ impl<const L: u8> Clone for Chord<L> {
}
}
impl<const L: u8> PartialEq for Chord<L> {
impl AsRef<Self> for Chord {
fn as_ref(&self) -> &Self { self }
}
impl PartialEq for Chord {
fn eq(&self, other: &Self) -> bool { self.on == other.on }
}
impl<const L: u8> Eq for Chord<L> {}
impl Eq for Chord {}
impl<const L: u8> Hash for Chord<L> {
impl Hash for Chord {
fn hash<H: Hasher>(&self, state: &mut H) { self.on.hash(state) }
}
impl<const L: u8> Chord<L> {
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<const L: u8> Chord<L> {
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<const L: u8> Mixable for Chord<L> {
impl Mixable for Chord {
fn filter(&self) -> bool { self.r#for.matches() && !self.noop() }
}

View file

@ -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<const L: u8 = { Layer::Null as u8 }>(Arc<Chord<L>>);
#[derive(Clone, Debug, Default, Deserialize)]
pub struct ChordArc(Arc<Chord>);
impl<const L: u8> Deref for ChordArc<L> {
type Target = Arc<Chord<L>>;
impl Deref for ChordArc {
type Target = Arc<Chord>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<const L: u8> DerefMut for ChordArc<L> {
impl DerefMut for ChordArc {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl<const L: u8> From<&Self> for ChordArc<L> {
impl AsRef<Chord> for ChordArc {
fn as_ref(&self) -> &Chord { self }
}
impl From<&Self> for ChordArc {
fn from(value: &Self) -> Self { value.clone() }
}
impl<const L: u8, const M: u8> From<Chord<L>> for ChordArc<M> {
fn from(value: Chord<L>) -> Self { ChordArc(Arc::new(value)).into_erased() }
impl From<Chord> for ChordArc {
fn from(value: Chord) -> Self { Self(Arc::new(value)) }
}
impl<const L: u8> From<ChordArc<L>> for Chord<L> {
fn from(value: ChordArc<L>) -> Self {
impl From<ChordArc> 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<const L: u8> From<ChordArc<L>> for Chord<L> {
}
}
impl<const L: u8> From<&ChordArc<L>> for Chord<L> {
fn from(value: &ChordArc<L>) -> 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<Self, Self::Error> {
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<const L: u8> ChordArc<L> {
pub fn as_erased<const M: u8>(&self) -> &ChordArc<M> {
unsafe { &*(self as *const Self as *const ChordArc<M>) }
impl ChordArc {
pub fn to_seq(&self, layer: Layer) -> Vec<ActionCow> {
self
.run
.iter()
.rev()
.cloned()
.map(|mut a| {
a.layer = a.layer.or(layer);
a.into()
})
.collect()
}
pub fn into_erased<const M: u8>(self) -> ChordArc<M> {
ChordArc(unsafe { Arc::from_raw(Arc::into_raw(self.0) as *const Chord<M>) })
}
pub fn to_seq(&self) -> Vec<ActionCow> {
self.run.iter().rev().cloned().map(Into::into).collect()
}
pub fn into_seq(self) -> Vec<ActionCow> {
pub fn into_seq(self, layer: Layer) -> Vec<ActionCow> {
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<const L: u8> Mixable for ChordArc<L> {
impl Mixable for ChordArc {
fn filter(&self) -> bool { self.0.filter() }
}
impl FromLua for ChordArc {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
Ok(match value {
Value::UserData(ud) => ud.take()?,
_ => Chord::deserialize(mlua::serde::Deserializer::new(value))?.into(),
})
}
}
impl UserData for ChordArc {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(me.id));

View file

@ -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<const L: u8 = { Layer::Null as u8 }> {
pub chords: ArcSwap<Vec<ChordArc<L>>>,
#[serde(skip, default = "layer_default::<L>")]
pub layer: Layer,
pub struct Chords(ArcSwap<Vec<ChordArc>>);
impl Deref for Chords {
type Target = ArcSwap<Vec<ChordArc>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<const L: u8> Default for Chords<L> {
fn default() -> Self {
Self { chords: ArcSwap::default(), layer: Layer::from_repr(L).unwrap_or_default() }
}
impl From<Vec<ChordArc>> for Chords {
fn from(inner: Vec<ChordArc>) -> Self { Self(inner.into_pointee()) }
}
impl<const L: u8> Deref for Chords<L> {
type Target = ArcSwap<Vec<ChordArc<L>>>;
fn deref(&self) -> &Self::Target { &self.chords }
}
impl<const L: u8> From<Vec<ChordArc<L>>> for Chords<L> {
fn from(inner: Vec<ChordArc<L>>) -> Self {
Self { chords: inner.into_pointee(), layer: Layer::from_repr(L).unwrap_or_default() }
}
}
impl<const L: u8> Chords<L> {
pub fn as_erased<const M: u8>(&self) -> Arc<Vec<ChordArc<M>>> {
let chords = self.chords.load_full();
unsafe { Arc::from_raw(Arc::into_raw(chords) as *const Vec<ChordArc<M>>) }
}
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::<Vec<_>>(),
)
@ -57,9 +40,9 @@ impl<const L: u8> Chords<L> {
}
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<const L: u8> Chords<L> {
matcher: ChordMatcher,
f: impl Fn(Chord) -> Result<Chord, E>,
) -> 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<const L: u8> Chords<L> {
Ok(())
}
pub(crate) fn unwrap_unchecked(self) -> Vec<ChordArc<L>> {
Arc::try_unwrap(self.chords.into_inner()).expect("unique chords arc")
pub(crate) fn unwrap_unchecked(self) -> Vec<ChordArc> {
Arc::try_unwrap(self.0.into_inner()).expect("unique chords arc")
}
}
@ -91,21 +74,19 @@ impl UserData for &Chords {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("match", |_, &me, matcher: Option<ChordMatcher>| {
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<Actions> = 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<const L: u8>() -> Layer { Layer::from_repr(L).unwrap_or_default() }

View file

@ -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<Vec<ChordArc>> {
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?,
})

View file

@ -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<const L: u8 = { Layer::Null as u8 }> {
keymap: Chords<L>,
pub struct KeymapSection {
keymap: Chords,
#[serde(default)]
prepend_keymap: Vec<Chord<L>>,
prepend_keymap: Vec<Chord>,
#[serde(default)]
append_keymap: Vec<Chord<L>>,
#[serde(default = "layer_default::<L>")]
layer: Layer,
append_keymap: Vec<Chord>,
}
impl<const L: u8> Deref for KeymapSection<L> {
type Target = Chords<L>;
impl Deref for KeymapSection {
type Target = Chords;
fn deref(&self) -> &Self::Target { &self.keymap }
}
impl<const L: u8> KeymapSection<L> {
pub fn as_erased<const M: u8>(&self) -> &KeymapSection<M> {
unsafe { &*(self as *const Self as *const KeymapSection<M>) }
}
}
impl<const L: u8> DeserializeOverHook for KeymapSection<L> {
impl DeserializeOverHook for KeymapSection {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
#[inline]
fn on<const L: u8>(Chord { on, .. }: &Chord<L>) -> [Key; 2] {
[on.first().copied().unwrap_or_default(), on.get(1).copied().unwrap_or_default()]
fn on<T: AsRef<Chord>>(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<ChordArc<L>> = mix(
let keymap: Vec<ChordArc> = 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() })
}
}

View file

@ -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<theme::Theme> {
}
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 <Enter> to continue with preset settings...{}",
SetSgr::Reverse,

View file

@ -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<u16> {
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<Self> {
match value {
Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
Value::Table(t) => from_table(&t, lua),
_ => Err("expected a table".into_lua_err()),
}
}
}
fn from_table(t: &Table, lua: &Lua) -> mlua::Result<MgrRatio> {
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<M: UserDataMethods<Self>>(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

View file

@ -60,7 +60,7 @@ impl TryFrom<Table> 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()
})

View file

@ -95,7 +95,7 @@ impl TryFrom<Table> 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()
})

View file

@ -97,7 +97,7 @@ impl TryFrom<Table> 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()
})

View file

@ -90,7 +90,7 @@ impl TryFrom<Table> 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()
})

View file

@ -90,7 +90,7 @@ impl TryFrom<Table> 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()
})

View file

@ -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 })
}
}

View file

@ -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,

View file

@ -1 +1 @@
yazi_macro::mod_flat!(confirm input options pick);
yazi_macro::mod_flat!(confirm help input options pick);

View file

@ -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<I>(it: I, len: usize, max: usize) -> Option<Text<'static>>
fn truncate_lines<I>(it: I, len: usize, max: usize) -> Option<Text<'static>>
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<Text<'static>> {
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())
}
}

View file

@ -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<StyleFlat>,
pub run: SyncCell<StyleFlat>,
pub desc: SyncCell<StyleFlat>,
pub border: SyncCell<StyleFlat>,
pub chord: SyncCell<StyleFlat>,
pub action: SyncCell<StyleFlat>,
pub hovered: SyncCell<StyleFlat>,
pub footer: SyncCell<StyleFlat>,
}
fn deserialize_syntect_theme<'de, D>(deserializer: D) -> Result<ArcSwap<PathBuf>, D::Error>

View file

@ -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
}

View file

@ -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<ChordArc>,
pub visible: bool,
pub layer: Layer,
pub position: Position,
pub bindings: Vec<ChordArc>,
// Filter
pub keyword: String,
pub in_filter: Option<yazi_widgets::input::Input>,
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<bool> {
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<ChordArc> {
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<String> {
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<u16> { self.visible.then_some(self.input.cursor()) }
pub fn rel_cursor(&self) -> usize { self.cursor - self.offset }
pub fn cursor_shape(&self) -> Option<SetCursorStyle> {
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 }

View file

@ -1,3 +1 @@
yazi_macro::mod_flat!(help);
const HELP_MARGIN: u16 = 1;

View file

@ -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 {

View file

@ -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<Item = Url<'_>> { self.yanked.iter().map(|u| u.as_url()) }
fn yanked(&self) -> impl Iterator<Item = Url<'_>> { self.yanked.iter().map(|f| f.url.as_url()) }
}

View file

@ -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<UrlBufCov>,
files: IndexSet<FileCov>,
version: u64,
revision: u64,
}
impl Deref for Yanked {
type Target = IndexSet<UrlBufCov>;
type Target = IndexSet<FileCov>;
fn deref(&self) -> &Self::Target { &self.urls }
fn deref(&self) -> &Self::Target { &self.files }
}
impl Yanked {
pub fn new(cut: bool, urls: IndexSet<UrlBufCov>) -> Self {
Self { cut, urls, ..Default::default() }
pub fn new(cut: bool, files: IndexSet<FileCov>) -> Self {
Self { cut, files, ..Default::default() }
}
pub fn urls(&self) -> impl Iterator<Item = &UrlBuf> { self.files.iter().map(|f| &f.url) }
pub fn remove_many<'a, I, T>(&mut self, urls: I)
where
I: IntoIterator<Item = T>,
@ -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<Url<'a>>) -> 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
}
}

View file

@ -22,14 +22,16 @@ impl TryFrom<Table> for SpotLock {
fn try_from(t: Table) -> Result<Self, Self::Error> {
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(),
})
})
}
}

View file

@ -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<isize> {
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<isize> {
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<isize> {
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<isize> {
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<Folder> 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
}
}

View file

@ -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<T: Into<UrlBuf>> From<T> 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<Step>) -> 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() }

View file

@ -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);

View file

@ -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<usize>),
Unset(usize, BTreeSet<usize>),
Select(Visual),
Unset(Visual),
}
impl Mode {
pub fn visual_mut(&mut self) -> Option<(usize, &mut BTreeSet<usize>)> {
pub fn visual(&self) -> Option<Visual> {
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<usize>)> {
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<VisualIndices> {
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",

View file

@ -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,
};

View file

@ -24,15 +24,17 @@ impl TryFrom<Table> for PreviewLock {
fn try_from(t: Table) -> Result<Self, Self::Error> {
let file: FileRef = t.raw_get("file")?;
Ok(Self {
url: file.url_owned(),
cha: file.cha,
mime: t.raw_get::<mlua::String>("mime")?.to_str()?.intern(),
file.borrow(|f| {
Ok(Self {
url: f.url_owned(),
cha: f.cha,
mime: t.raw_get::<mlua::String>("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(),
})
})
}
}

View file

@ -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<UrlBufCov, u64>,
inner: IndexMap<FileCov, u64>,
parents: HashMap<UrlBufCov, usize>,
}
@ -16,64 +14,70 @@ impl Selected {
pub fn is_empty(&self) -> bool { self.inner.is_empty() }
pub fn values(&self) -> impl Iterator<Item = &UrlBuf> { self.inner.keys().map(Deref::deref) }
pub fn files(&self) -> impl Iterator<Item = &File> { self.inner.keys().map(|f| &f.0) }
pub fn contains<'a>(&self, url: impl Into<Url<'a>>) -> bool {
self.inner.contains_key(&UrlCov::new(url))
pub fn urls(&self) -> impl Iterator<Item = &UrlBuf> { 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<Url<'a>>) -> bool { self.add_same([url]) == 1 }
pub fn add(&mut self, file: impl AsUrl + Into<File>) -> bool { self.add_same([file]) == 1 }
pub fn add_many<'a, I, T>(&mut self, urls: I) -> usize
pub fn add_many<I, T>(&mut self, files: I) -> usize
where
I: IntoIterator<Item = T>,
T: Into<Url<'a>>,
T: AsUrl + Into<File>,
{
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<I, T>(&mut self, files: I) -> usize
where
I: IntoIterator<Item = T>,
T: Into<Url<'a>>,
T: AsUrl + Into<File>,
{
// 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<Url<'a>> + 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<I, T>(&mut self, urls: I) -> usize
where
I: IntoIterator<Item = T>,
T: Into<Url<'a>> + 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<S: AsRef<OsStr> + ?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);
}
}

View file

@ -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<Rect> {
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<dyn Iterator<Item = &UrlBuf> + '_> {
pub fn selected_or_hovered_files(&self) -> Box<dyn Iterator<Item = &File> + '_> {
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<dyn Iterator<Item = &UrlBuf> + '_> {
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<dyn Iterator<Item = &UrlBuf> + '_> {
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()))
}
}

View file

@ -0,0 +1,49 @@
use std::{iter::Chain, ops::Range};
use yazi_widgets::Step;
pub type VisualIndices = Chain<Range<usize>, Range<usize>>;
#[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<usize>, Range<usize>) {
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),
}
}
}

View file

@ -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<UrlBufCov>, 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<UrlBufCov>, 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);
}
}
}

View file

@ -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;
}

View file

@ -7,9 +7,10 @@ use yazi_shared::{Layer, event::ActionCow};
#[derive(Clone, Debug)]
pub struct WhichOpt {
pub tx: Option<mpsc::UnboundedSender<Option<ChordArc>>>,
pub layer: Layer,
pub cands: Vec<ChordArc>,
pub silent: bool,
pub times: usize,
pub silent: bool,
}
impl_data_any!(WhichOpt);
@ -24,19 +25,21 @@ impl TryFrom<ActionCow> 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::<yazi_binding::MpscUnboundedTx<_>>("tx").ok().map(|t| t.0),
layer: t.raw_get("layer")?,
cands: t.raw_get::<Table>("cands")?.sequence_values().collect::<mlua::Result<Vec<_>>>()?,
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)?),

View file

@ -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<mpsc::UnboundedSender<Option<ChordArc>>>,
pub times: usize,
pub layer: Layer,
pub cands: Vec<ChordArc>,
pub times: usize,
// Active state
pub active: bool,
@ -32,8 +34,8 @@ impl Which {
}
pub fn dismiss(&mut self, chord: Option<ChordArc>) {
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)));
}
}
}

View file

@ -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<indexmap::set::IntoIter<UrlBufCov>, fn(UrlBufCov) -> UrlBuf>,
UrlBuf,
>;
type Iter = yazi_binding::Iter<indexmap::set::IntoIter<FileCov>, FileCov>;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EmberYank<'a> {
pub cut: bool,
pub urls: Cow<'a, IndexSet<UrlBufCov>>,
pub cut: bool,
pub files: Cow<'a, IndexSet<FileCov>>,
}
impl_data_any!(EmberYank<'static>, from_into_lua = inherit);
impl<'a> EmberYank<'a> {
pub fn borrowed(cut: bool, urls: &'a IndexSet<UrlBufCov>) -> Ember<'a> {
Self { cut, urls: Cow::Borrowed(urls) }.into()
pub fn borrowed(cut: bool, files: &'a IndexSet<FileCov>) -> Ember<'a> {
Self { cut, files: Cow::Borrowed(files) }.into()
}
}
impl EmberYank<'static> {
pub fn owned(cut: bool, _: &IndexSet<UrlBufCov>) -> Ember<'static> {
Self { cut, urls: Default::default() }.into()
pub fn owned(cut: bool, _: &IndexSet<FileCov>) -> 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<Value> {
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<EmberYank<'static>> {
Ok(EmberYank {
cut: self.cut,
urls: Cow::Owned(
self
.inner
.take::<Iter>()?
.into_iter(lua)
.map(|result| result.map(Into::into))
.collect::<mlua::Result<_>>()?,
),
cut: self.cut,
files: Cow::Owned(self.inner.take::<Iter>()?.into_iter(lua).collect::<mlua::Result<_>>()?),
})
}
}

View file

@ -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<UrlBufCov>), (cut, urls));
pub_after!(@yank(cut: bool, files: &IndexSet<FileCov>), (cut, files));
pub_after!(duplicate(items: Vec<EmberDuplicateItem>), (&items), (items));

View file

@ -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!()
}
}
}

Some files were not shown because too many files have changed in this diff Show more