Compare commits

...

58 commits

Author SHA1 Message Date
三咲雅 misaki masa
9accf929f4
feat: trash bin (#4144)
Some checks are pending
Cachix / Publish Flake (push) Waiting to run
Check / clippy (push) Waiting to run
Check / rustfmt (push) Waiting to run
Check / stylua (push) Waiting to run
Draft / build-unix (gcc-aarch64-linux-gnu, ubuntu-latest, aarch64-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (gcc-i686-linux-gnu, ubuntu-latest, i686-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (gcc-riscv64-linux-gnu, ubuntu-latest, riscv64gc-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (gcc-sparc64-linux-gnu, ubuntu-latest, sparc64-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (macos-latest, aarch64-apple-darwin) (push) Waiting to run
Draft / build-unix (macos-latest, x86_64-apple-darwin) (push) Waiting to run
Draft / build-unix (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Waiting to run
Draft / build-windows (windows-latest, aarch64-pc-windows-msvc) (push) Waiting to run
Draft / build-windows (windows-latest, x86_64-pc-windows-msvc) (push) Waiting to run
Draft / build-musl (aarch64-unknown-linux-musl) (push) Waiting to run
Draft / build-musl (x86_64-unknown-linux-musl) (push) Waiting to run
Draft / build-snap (amd64, ubuntu-latest) (push) Waiting to run
Draft / build-snap (arm64, ubuntu-24.04-arm) (push) Waiting to run
Draft / snap (push) Blocked by required conditions
Draft / draft (push) Blocked by required conditions
Draft / nightly (push) Blocked by required conditions
Test / test (macos-latest) (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
Test / test (windows-latest) (push) Waiting to run
2026-07-22 03:13:58 +08:00
nabsei
6aa75594db
docs: add a note about following the contribution guidelines in the PR template (#4140)
Co-authored-by: 三咲雅 misaki masa <sxyazi@gmail.com>
2026-07-21 08:59:20 +08:00
Omenabaka
60d94608cb
fix: magick preview of multiframe files (#4024)
Co-authored-by: 三咲雅 misaki masa <sxyazi@gmail.com>
2026-07-19 23:58:16 +08:00
三咲雅 misaki masa
52640fcece
feat: new hub VFS kind (#4131) 2026-07-17 03:04:19 +08:00
三咲雅 misaki masa
b25ae9f82d
fix: fail when package source is missing (#4124) 2026-07-14 01:11:34 +08:00
三咲雅 misaki masa
4dab480347
fix: preserve search results on refresh (#4123) 2026-07-13 22:51:42 +08:00
三咲雅 misaki masa
d39637f40c
feat!: support VFS services with same name but different schemes (#4120) 2026-07-13 20:01:47 +08:00
三咲雅 misaki masa
ddc6632505
fix: flush last input before submitting the completion (#4119) 2026-07-13 09:01:59 +08:00
三咲雅 misaki masa
6d84921e70
feat!: custom VFS provider (#4118) 2026-07-13 02:01:37 +08:00
三咲雅 misaki masa
dbb0cc0d55
feat: experimental %y, %Y, %t, %T, %yN, %YN, %tN, %TN shell formatting parameters (#4108) 2026-07-08 17:04:12 +08:00
三咲雅 misaki masa
aa850f9a3b
feat: input history (#4104) 2026-07-06 18:43:08 +08:00
三咲雅 misaki masa
c92c4aba88
feat: make visual mode support wraparound scrolling (#4101) 2026-07-05 18:06:25 +08:00
ebugusey
6e0aaee822
feat: add ya CLI completions to Windows package (#4099) 2026-07-03 22:09:57 +08:00
三咲雅 misaki masa
b8ceaea940
feat!: show file icons in trash/delete/overwrite confirmations (#4096) 2026-07-02 04:09:12 +08:00
三咲雅 misaki masa
21550a7eb5
feat: new target parameter for create action (#4088) 2026-06-28 02:10:23 +08:00
三咲雅 misaki masa
39527f488f
perf: switch memory allocator back to jemalloc (#4084) 2026-06-26 22:38:48 +08:00
三咲雅 misaki masa
de611b0e6b
perf: switch memory allocator to mimalloc (#4083) 2026-06-26 14:34:28 +08:00
三咲雅 misaki masa
ef654f4420
feat: context-aware icons for inputs (#4080) 2026-06-24 23:14:16 +08:00
Alastair
2a106aa231
fix: filter keybindings by chord in command palette (#4078)
Co-authored-by: Alastair Scheuermann <alastairsounds@users.noreply.github.com>
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-06-24 21:50:51 +08:00
SpiritCroc
b1ee2e72ff
feat: H/M/L Vim-like motion for moving cursor relative to viewport (#3970)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-06-24 17:42:20 +08:00
三咲雅 misaki masa
581886a355
feat!: make help menu a command palette (#4074) 2026-06-22 23:57:29 +08:00
三咲雅 misaki masa
4f45ddb514
fix: ignore NumLock state when comparing keybindings (#4071) 2026-06-20 23:57:12 +08:00
三咲雅 misaki masa
0ba1811af3
fix: use cleaned and normalized $PWD as startup CWD to avoid MSYS2/git-bash path quirks (#4068) 2026-06-20 12:47:00 +08:00
三咲雅 misaki masa
feeea673f1
fix: normalize \\?\-prefixed Verbatim paths when creating relative symlinks on Windows (#4067) 2026-06-20 12:07:33 +08:00
三咲雅 misaki masa
ef5db6e257
fix: sort interactive cd candidates case-insensitively so short exact matches match first (#4066) 2026-06-20 11:15:40 +08:00
三咲雅 misaki masa
0daa5df644
fix: use WebP as magick preset preloader cache format to keep image transparency (#4065) 2026-06-20 10:37:24 +08:00
三咲雅 misaki masa
6144d2748e
fix: keep package hashes indifferent to line endings when ya pkg pulls packages (#4064) 2026-06-20 09:34:41 +08:00
三咲雅 misaki masa
84a5be0b6a
feat: Lua API for printing escape sequences to the terminal (#4059) 2026-06-19 23:14:13 +08:00
Alastair
ab8d634f8e
fix: split graceful vs passive stop in terminal stream (#4055)
Co-authored-by: Alastair Scheuermann <alastairsounds@users.noreply.github.com>
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-06-19 09:30:52 +08:00
三咲雅 misaki masa
85132ff343
fix: quit once event stream closes in case terminal won't send a SIGHUP signal (#4052) 2026-06-18 21:21:38 +08:00
三咲雅 misaki masa
53d36b5186
refactor: switch to ratatui-core (#4050) 2026-06-18 16:23:26 +08:00
三咲雅 misaki masa
446a5721bf
refactor: move Lua bindings down into each subcrates (#4049) 2026-06-18 15:06:31 +08:00
三咲雅 misaki masa
f1e93d7f52
feat: new input DDS event watching changes in input content (#4043) 2026-06-14 19:58:23 +08:00
三咲雅 misaki masa
b37be0529a
feat: new ui.Input element (#4040) 2026-06-13 19:37:29 +08:00
三咲雅 misaki masa
9b920ed2e3
refactor: unify rendering behavior with a new renderer (#4038) 2026-06-11 20:44:22 +08:00
三咲雅 misaki masa
07a5deb109
feat: dynamic keymap Lua API (#4031) 2026-06-10 15:09:49 +08:00
Biswapriyo Nath
4086fbb166
feat(nix): shell completions for ya CLI (#4030) 2026-06-10 13:22:31 +08:00
sxyazi
9cfeb67db7
refactor: make Lua bindings Send (#4027) 2026-06-09 22:52:43 +08:00
三咲雅 misaki masa
f6113d3865
feat: advanced app overall style (#4023) 2026-06-07 21:46:07 +08:00
三咲雅 misaki masa
ea7fa2127c
perf: eliminate hacks in image and UI conflict resolution (#4022) 2026-06-06 14:35:12 +08:00
cyanyux
8a4b0b208e
fix(snap): make bundled magick find libgomp at runtime (#4016) 2026-06-06 13:53:59 +08:00
三咲雅 misaki masa
9bdd6777e4
feat: keymap Lua bindings (#4021) 2026-06-06 13:45:52 +08:00
三咲雅 misaki masa
8e80798984
feat: new gait for input backward and forward actions (#4012) 2026-06-01 13:16:01 +08:00
三咲雅 misaki masa
e892bf7d90
fix: decouple yazi_boot crate init from yazi_term init (#4011) 2026-05-31 13:07:11 +08:00
三咲雅 misaki masa
601cb10b32
feat: Unix domain socket on Windows as the underlying transport layer for DDS (#4009) 2026-05-29 15:33:03 +08:00
三咲雅 misaki masa
6c30430666
fix: a TOCTOU race condition while creating temporary directories (#4008) 2026-05-28 18:07:31 +08:00
三咲雅 misaki masa
7a51626505
feat: drag and drop (#4005) 2026-05-28 12:05:06 +08:00
三咲雅 misaki masa
cf8b54179e
feat: terminal drag and drop protocol (#3999) 2026-05-25 10:30:04 +08:00
三咲雅 misaki masa
f7fea2b19a
docs: update contributing guidelines 2026-05-25 09:31:04 +08:00
Dongda Li
d9cd1907d9
feat: image preview with Überzug++ on Niri (#3990)
Co-authored-by: 三咲雅 misaki masa <sxyazi@gmail.com>
2026-05-23 11:02:40 +08:00
JinHyuk Sung
5d643ee01a
fix: restrict preview cache directory permissions (#3984)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-05-22 18:56:19 +08:00
三咲雅 misaki masa
c037855c4f
feat!: new virtual terminal backend (#3989) 2026-05-22 17:00:06 +08:00
三咲雅 misaki masa
3f5cc47a48
fix: normalize main entry IDs for consistent internal sharing (#3966) 2026-05-15 07:30:38 +08:00
Daniel Vincent
fde563380b
feat: bulk create (#3793)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-05-12 02:12:08 +08:00
三咲雅 misaki masa
247f925e53
fix: get rid of the Cargo curse (#3952) 2026-05-10 00:57:57 +08:00
Immanuel Tikhonov
22fb9e0d09
fix: keep repeated ! negations in a condition right-associative (#3946)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-05-09 22:32:18 +08:00
三咲雅 misaki masa
92b9ea3794
fix: make the input component sync and reactive (#3949) 2026-05-09 09:26:07 +08:00
三咲雅 misaki masa
3968c4799c
docs: rename id of the fetcher rule to group (#3943) 2026-05-08 08:57:50 +08:00
961 changed files with 20528 additions and 12411 deletions

View file

@ -22,10 +22,10 @@ body:
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: debug id: env
attributes: attributes:
label: "`yazi --debug` output" label: "`ya env` output"
description: Please run `yazi --debug` and paste the debug information here. description: Run `ya env` and paste the environment information here.
render: Shell render: Shell
validations: validations:
required: true required: true

View file

@ -26,10 +26,10 @@ body:
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: debug id: env
attributes: attributes:
label: "`yazi --debug` output" label: "`ya env` output"
description: Please run `yazi --debug` and paste the debug information here. description: Run `ya env` and paste the environment information here.
render: Shell render: Shell
validations: validations:
required: true required: true
@ -65,6 +65,6 @@ body:
options: options:
- label: I tried the [latest nightly build](https://yazi-rs.github.io/docs/installation#binaries), and the issue is still reproducible - label: I tried the [latest nightly build](https://yazi-rs.github.io/docs/installation#binaries), and the issue is still reproducible
required: true required: true
- label: I updated the debug information (`yazi --debug`) input box to the nightly that I tried - label: I updated the environment information (`ya env`) field to the nightly that I tried
required: true required: true
- label: I can reproduce it after disabling all custom configs/plugins (`mv ~/.config/yazi ~/.config/yazi-backup`) - label: I can reproduce it after disabling all custom configs/plugins (`mv ~/.config/yazi ~/.config/yazi-backup`)

View file

@ -4,10 +4,10 @@ labels: [feature]
assignees: [] assignees: []
body: body:
- type: textarea - type: textarea
id: debug id: env
attributes: attributes:
label: "`yazi --debug` output" label: "`ya env` output"
description: Please run `yazi --debug` and paste the debug information here. description: Run `ya env` and paste the environment information here.
render: Shell render: Shell
validations: validations:
required: true required: true

View file

@ -15,3 +15,10 @@ A clear and concise description of the rationale of the changes, to help our rev
If it has already been detailed in the associated issue, please skip this section. If it has already been detailed in the associated issue, please skip this section.
--> -->
## Checklist
- [ ] I have read [CONTRIBUTING.md](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md)
- [ ] I confirm this PR follows the [AI Policy](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md#ai-policy)
<!-- AI bots are not allowed to open PRs in this repository. All PRs must be made by humans and comply with the AI policy. -->

View file

@ -15,7 +15,7 @@ jobs:
os: [ubuntu-latest, macos-latest] os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v31

View file

@ -17,7 +17,7 @@ jobs:
clippy: clippy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup Rust toolchain - name: Setup Rust toolchain
run: | run: |
@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Setup Rust toolchain - name: Setup Rust toolchain
run: | run: |
@ -45,12 +45,12 @@ jobs:
uses: mozilla-actions/sccache-action@v0.0.10 uses: mozilla-actions/sccache-action@v0.0.10
- name: Rustfmt - name: Rustfmt
run: rustfmt +nightly --check **/*.rs run: find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} +
stylua: stylua:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: JohnnyMorganz/stylua-action@v5 - uses: JohnnyMorganz/stylua-action@v5
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -42,7 +42,7 @@ jobs:
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Install gcc - name: Install gcc
if: matrix.gcc != '' if: matrix.gcc != ''
@ -81,7 +81,7 @@ jobs:
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
@ -97,10 +97,11 @@ jobs:
env: env:
TARGET_NAME: yazi-${{ matrix.target }} TARGET_NAME: yazi-${{ matrix.target }}
run: | 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\ya.exe" -Destination ${env:TARGET_NAME}
Copy-Item -Path "target\${{ matrix.target }}\release-windows\yazi.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} Copy-Item -Path "README.md", "LICENSE" -Destination ${env:TARGET_NAME}
Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip" Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip"
@ -120,7 +121,7 @@ jobs:
container: container:
image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
@ -150,7 +151,7 @@ jobs:
arch: arm64 arch: arm64
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@ -233,7 +234,7 @@ jobs:
echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v8
with: with:

View file

@ -21,7 +21,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup Rust toolchain - name: Setup Rust toolchain
run: rustup toolchain install stable --profile minimal run: rustup toolchain install stable --profile minimal

View file

@ -1,4 +1,4 @@
name: Validate Form name: Validate Issue
on: on:
issues: issues:
@ -12,10 +12,10 @@ jobs:
permissions: permissions:
issues: write issues: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
with: with:
node-version: 20 node-version: 20
@ -24,7 +24,7 @@ jobs:
cd scripts/validate-form cd scripts/validate-form
npm ci npm ci
- name: Validate Form - name: Validate Issue
uses: actions/github-script@v9 uses: actions/github-script@v9
with: with:
script: | script: |

36
.github/workflows/validate-pr.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Validate PR
on:
pull_request:
types: [opened, edited, reopened, synchronize]
permissions:
contents: read
pull-requests: read
jobs:
check-list:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.repository.default_branch }}
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 20
- name: Install Dependencies
run: |
cd scripts/validate-form
npm ci
- name: Validate PR
uses: actions/github-script@v9
with:
script: |
const script = require('./scripts/validate-form/main.js')
await script({github, context, core})
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -12,6 +12,51 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
## [Unreleased] ## [Unreleased]
### Added
- Drag and drop ([#4005])
- Trash bin ([#4144])
- Bulk create ([#3793])
- Make help menu a command palette ([#4074])
- Input history ([#4104])
- Experimental `%y`, `%Y`, `%t`, `%T`, `%yN`, `%YN`, `%tN`, `%TN` shell formatting parameters ([#4108])
- Custom VFS provider ([#4118])
- 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])
- New gait for input `backward` and `forward` actions ([#4012])
### Changed
- Rename SFTP sections in `vfs.toml` from `[services.domain]` to `[sftp.domain]` ([#4120]).
- Rename `<BackTab>` to `<S-Tab>` ([#3989])
- Remove `Url.is_archive` - `archive://` is no longer built in and can now be registered by plugins ([#4118])
- 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])
- Deprecate `Url.is_regular`, `Url.is_search`, and `Url.domain` in favor of `Url.spec.is_regular`, `Url.spec.is_search`, and `Url.spec.domain`, respectively ([#4118])
### Fixed
- Normalize `\\?\`-prefixed Verbatim paths when creating relative symlinks on Windows ([#4067])
- Keep package hashes indifferent to line endings when `ya pkg` pulls packages ([#4064])
- Use WebP as `magick` preset preloader cache format to keep image transparency ([#4065])
- Use cleaned and normalized `$PWD` as startup CWD to avoid MSYS2/git-bash path quirks ([#4068])
### Improved
- Eliminate hacks in image and UI conflict resolution ([#4022])
## [v26.5.6] ## [v26.5.6]
### Added ### Added
@ -52,6 +97,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Make `ui.Style` immutable ([#3862]) - Make `ui.Style` immutable ([#3862])
- Remove `title_format` in favor of new `ind-app-title` DDS event for flexible title customization ([#3684]) - Remove `title_format` in favor of new `ind-app-title` DDS event for flexible title customization ([#3684])
- Remove `micro_workers` and `macro_workers` in favor of finer control over concurrent workers ([#3661]) - Remove `micro_workers` and `macro_workers` in favor of finer control over concurrent workers ([#3661])
- Rename `id` of the fetcher rule to `group` ([#3943])
### Deprecated ### Deprecated
@ -1068,7 +1114,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[v25.12.29]: https://github.com/sxyazi/yazi/compare/v25.5.31...v25.12.29 [v25.12.29]: https://github.com/sxyazi/yazi/compare/v25.5.31...v25.12.29
[v26.1.4]: https://github.com/sxyazi/yazi/compare/v25.12.29...v26.1.4 [v26.1.4]: https://github.com/sxyazi/yazi/compare/v25.12.29...v26.1.4
[v26.1.22]: https://github.com/sxyazi/yazi/compare/v26.1.4...v26.1.22 [v26.1.22]: https://github.com/sxyazi/yazi/compare/v26.1.4...v26.1.22
[v26.5.6]: https://github.com/sxyazi/yazi/compare/v26.5.6...v26.5.6 [v26.5.6]: https://github.com/sxyazi/yazi/compare/v26.1.22...v26.5.6
[#4]: https://github.com/sxyazi/yazi/pull/4 [#4]: https://github.com/sxyazi/yazi/pull/4
[#5]: https://github.com/sxyazi/yazi/pull/5 [#5]: https://github.com/sxyazi/yazi/pull/5
[#6]: https://github.com/sxyazi/yazi/pull/6 [#6]: https://github.com/sxyazi/yazi/pull/6
@ -1703,6 +1749,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3780]: https://github.com/sxyazi/yazi/pull/3780 [#3780]: https://github.com/sxyazi/yazi/pull/3780
[#3781]: https://github.com/sxyazi/yazi/pull/3781 [#3781]: https://github.com/sxyazi/yazi/pull/3781
[#3792]: https://github.com/sxyazi/yazi/pull/3792 [#3792]: https://github.com/sxyazi/yazi/pull/3792
[#3793]: https://github.com/sxyazi/yazi/pull/3793
[#3804]: https://github.com/sxyazi/yazi/pull/3804 [#3804]: https://github.com/sxyazi/yazi/pull/3804
[#3813]: https://github.com/sxyazi/yazi/pull/3813 [#3813]: https://github.com/sxyazi/yazi/pull/3813
[#3846]: https://github.com/sxyazi/yazi/pull/3846 [#3846]: https://github.com/sxyazi/yazi/pull/3846
@ -1714,3 +1761,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3901]: https://github.com/sxyazi/yazi/pull/3901 [#3901]: https://github.com/sxyazi/yazi/pull/3901
[#3906]: https://github.com/sxyazi/yazi/pull/3906 [#3906]: https://github.com/sxyazi/yazi/pull/3906
[#3934]: https://github.com/sxyazi/yazi/pull/3934 [#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
[#4012]: https://github.com/sxyazi/yazi/pull/4012
[#4022]: https://github.com/sxyazi/yazi/pull/4022
[#4031]: https://github.com/sxyazi/yazi/pull/4031
[#4040]: https://github.com/sxyazi/yazi/pull/4040
[#4064]: https://github.com/sxyazi/yazi/pull/4064
[#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
[#4104]: https://github.com/sxyazi/yazi/pull/4104
[#4108]: https://github.com/sxyazi/yazi/pull/4108
[#4118]: https://github.com/sxyazi/yazi/pull/4118
[#4120]: https://github.com/sxyazi/yazi/pull/4120
[#4144]: https://github.com/sxyazi/yazi/pull/4144

View file

@ -11,6 +11,7 @@ This guide will help you understand how to contribute to the project.
3. [Development Setup](#development-setup) 3. [Development Setup](#development-setup)
4. [How to Contribute](#how-to-contribute) 4. [How to Contribute](#how-to-contribute)
5. [Pull Requests](#pull-requests) 5. [Pull Requests](#pull-requests)
6. [AI Policy](#ai-policy)
## Getting Started ## Getting Started
@ -164,3 +165,10 @@ We want you to succeed, and it can be discouraging to find that a lot of re-work
- Your code passes all tests and lints. - Your code passes all tests and lints.
- Your pull request description clearly explains the changes and why they are needed. - Your pull request description clearly explains the changes and why they are needed.
4. Address any review comments. Make sure to push updates to the same branch on your fork. 4. Address any review comments. Make sure to push updates to the same branch on your fork.
## AI Policy
1. All issue, PR, and discussion descriptions must be written by humans, not AI.
2. Any use of AI must be disclosed. You must declare which model you used and the extent of AI assistance.
3. Any AI-generated code must be reviewed, tested, and simplified by a human before publishing. This requires you to fully understand how it interacts with the greater system without AI assistance.
4. Any AI tools used must explicitly state they do not assert copyright over the work.

2854
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -36,47 +36,51 @@ debug = false
[workspace.dependencies] [workspace.dependencies]
ansi-to-tui = "8.0.1" ansi-to-tui = "8.0.1"
anyhow = "1.0.102" anyhow = "1.0.104"
arc-swap = { version = "1.9.1", features = [ "serde" ] } arc-swap = { version = "1.9.2", features = [ "serde" ] }
base64 = "0.22.1" base64 = "0.22.1"
bitflags = { version = "2.11.1", features = [ "serde" ] } bitflags = { version = "2.13.1", features = [ "serde" ] }
chrono = "0.4.44" chrono = "0.4.45"
clap = { version = "4.6.1", features = [ "derive" ] } clap = { version = "4.6.2", features = [ "derive" ] }
compact_str = { version = "0.10.0", features = [ "serde" ] }
core-foundation-sys = "0.8.7" core-foundation-sys = "0.8.7"
crossterm = { version = "0.29.0", features = [ "event-stream" ] } data-encoding = "2.11.0"
dirs = "6.0.0" dirs = "6.0.0"
dyn-clone = "1.0.20" dyn-clone = "1.0.20"
either = { version = "1.15.0" } either = { version = "1.16.0" }
foldhash = "0.2.0" foldhash = "0.2.0"
futures = "0.3.32" futures = "0.3.33"
globset = "0.4.18" globset = "0.4.19"
hashbrown = { version = "0.17.0", features = [ "serde" ] } hashbrown = { version = "0.17.1", features = [ "serde" ] }
image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] }
indexmap = { version = "2.14.0", features = [ "serde" ] } indexmap = { version = "2.14.0", features = [ "serde" ] }
inventory = "0.3.24"
libc = "0.2.186" libc = "0.2.186"
lru = "0.18.0" lru = "0.18.1"
mlua = { version = "0.11.6", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] } mlua = { version = "0.12.0", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] }
objc2 = "0.6.4" objc2 = "0.6.4"
ordered-float = { version = "5.3.0", features = [ "serde" ] } ordered-float = { version = "5.3.0", features = [ "serde" ] }
parking_lot = "0.12.5" parking_lot = "0.12.5"
paste = "1.0.15" paste = "1.0.15"
percent-encoding = "2.3.2" 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 = { version = "0.30.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] } ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] }
regex = "1.12.3" ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] }
russh = { version = "0.60.2", default-features = false, features = [ "ring", "rsa" ] } regex = "1.13.1"
russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] } serde = { version = "1.0.229", features = [ "derive" ] }
serde_json = "1.0.149" serde_json = "1.0.151"
serde_with = "3.19.0" serde_with = "3.21.0"
strum = { version = "0.28.0", features = [ "derive" ] } strum = { version = "0.28.0", features = [ "derive" ] }
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.18" thiserror = "2.0.19"
tokio = { version = "1.52.2", features = [ "full" ] } tokio = { version = "1.53.0", features = [ "full" ] }
tokio-stream = "0.1.18" tokio-stream = "0.1.18"
tokio-util = "0.7.18" tokio-util = "0.7.18"
toml = { version = "1.1.2" } toml = { version = "1.1.3" }
tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] } tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
typed-path = "0.12.3" typed-path = "0.12.3"
unicode-width = { version = "0.2.2", default-features = false } unicode-width = { version = "0.2.2", default-features = false }
uzers = "0.12.2" uzers = "0.12.2"

View file

@ -23,8 +23,8 @@ Yazi (means "duck") is a terminal file manager written in Rust, based on non-blo
- 🧰 Integration with ripgrep, fd, fzf, zoxide - 🧰 Integration with ripgrep, fd, fzf, zoxide
- 💫 Vim-like input/pick/confirm/which/notify component, auto-completion for cd paths - 💫 Vim-like input/pick/confirm/which/notify component, auto-completion for cd paths
- 🏷️ Multi-Tab Support, Cross-directory selection, Scrollable Preview (for videos, PDFs, archives, code, directories, etc.) - 🏷️ Multi-Tab Support, Cross-directory selection, Scrollable Preview (for videos, PDFs, archives, code, directories, etc.)
- 🔄 Bulk Renaming, Archive Extraction, Visual Mode, File Chooser, [Git Integration](https://github.com/yazi-rs/plugins/tree/main/git.yazi), [Mount Manager](https://github.com/yazi-rs/plugins/tree/main/mount.yazi) - 🔄 Bulk Rename/Create, Archive Extraction, Visual Mode, File Chooser, [Git Integration](https://github.com/yazi-rs/plugins/tree/main/git.yazi), [Mount Manager](https://github.com/yazi-rs/plugins/tree/main/mount.yazi)
- 🎨 Theme System, Mouse Support, Trash Bin, Custom Layouts, CSI u, OSC 52 - 🎨 Theme System, Mouse Support, Drag and Drop, Trash Bin, Custom Layouts, CSI u, OSC 52
- ... and more! - ... and more!
https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7 https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7

View file

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

12
flake.lock generated
View file

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1776329215, "lastModified": 1783279667,
"narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=", "narHash": "sha256-/NAkDSsve+GNM0Bt6tleJdCGfsTlK89nPjkVOzZMo0s=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b86751bc4085f48661017fa226dee99fab6c651b", "rev": "f205b5574fd0cb7da5b702a2da51507b7f4fdd1b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -48,11 +48,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1776741231, "lastModified": 1783320166,
"narHash": "sha256-k9G98qzn+7npROUaks8VqCFm7cFtEG8ulQLBBo5lItg=", "narHash": "sha256-l7C/OsjcnWDOk2K3ssj+SBduwL67LashjBqis9+t468=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "02061303f7c4c964f7b4584dabd9e985b4cd442b", "rev": "20ee15370c9256669d66968b89ee20a4b0a4e673",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -28,9 +28,6 @@ rustPlatform.buildRustPackage (finalAttrs: {
cargoLock = { cargoLock = {
lockFile = "${src}/Cargo.lock"; lockFile = "${src}/Cargo.lock";
#outputHashes = {
# "mlua-0.10.0" = "sha256-Xg6/jc+UP8tbJJ6x1sbAgt8ZHt051xEBBcjmikQqYlw=";
#};
}; };
env = { env = {
@ -54,6 +51,11 @@ rustPlatform.buildRustPackage (finalAttrs: {
--fish ./yazi-boot/completions/yazi.fish \ --fish ./yazi-boot/completions/yazi.fish \
--zsh ./yazi-boot/completions/_yazi --zsh ./yazi-boot/completions/_yazi
installShellCompletion --cmd ya \
--bash ./yazi-cli/completions/ya.bash \
--fish ./yazi-cli/completions/ya.fish \
--zsh ./yazi-cli/completions/_ya
# Resize logo # Resize logo
for RES in 16 24 32 48 64 128 256; do for RES in 16 24 32 48 64 128 256; do
mkdir -p $out/share/icons/hicolor/"$RES"x"$RES"/apps mkdir -p $out/share/icons/hicolor/"$RES"x"$RES"/apps
@ -75,7 +77,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
hash = "sha256-kEVXejDg4ChFoMNBvKlwdFEyUuTcY2VuK9j0PdafKus="; hash = "sha256-kEVXejDg4ChFoMNBvKlwdFEyUuTcY2VuK9j0PdafKus=";
}; };
}; };
meta = { meta = {
description = "Blazing fast terminal file manager written in Rust, based on async I/O"; description = "Blazing fast terminal file manager written in Rust, based on async I/O";

View file

@ -2,6 +2,15 @@ const LABEL_NAME = "needs info"
const RE_VERSION = /Yazi\s+Version\s*:\s\d+\.\d+\.\d+\s\(/gm const RE_VERSION = /Yazi\s+Version\s*:\s\d+\.\d+\.\d+\s\(/gm
const RE_DEPENDENCIES = /Dependencies\s+[/a-z]+\s*:\s/gm const RE_DEPENDENCIES = /Dependencies\s+[/a-z]+\s*:\s/gm
const RE_CHECKLIST = /#{3}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm const RE_CHECKLIST = /#{3}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm
const RE_PR_CHECKLIST = /#{2}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm
function pullRequestBody(content) {
if (RE_PR_CHECKLIST.test(content)) {
return null
}
return "All required checklist items must be checked in the PR description."
}
function bugReportBody(creator, content, hash) { function bugReportBody(creator, content, hash) {
if (RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content) && new RegExp(` \\(${hash}[a-f0-9]? `).test(content)) { if (RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content) && new RegExp(` \\(${hash}[a-f0-9]? `).test(content)) {
@ -13,7 +22,7 @@ function bugReportBody(creator, content, hash) {
I noticed that you did not correctly follow the issue template. Please ensure that: I noticed that you did not correctly follow the issue template. Please ensure that:
- The bug can still be reproduced on the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries). - The bug can still be reproduced on the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries).
- The debug information (\`yazi --debug\`) is updated for the newest nightly. - The environment info (\`ya env\`) is updated for the newest nightly.
- The non-optional items in the checklist are checked. - The non-optional items in the checklist are checked.
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
@ -32,7 +41,7 @@ function featureRequestBody(creator, content) {
I noticed that you did not correctly follow the issue template. Please ensure that: I noticed that you did not correctly follow the issue template. Please ensure that:
- The requested feature does not exist in the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries). - The requested feature does not exist in the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries).
- The debug information (\`yazi --debug\`) is updated for the newest nightly. - The environment info (\`ya env\`) is updated for the newest nightly.
- The non-optional items in the checklist are checked. - The non-optional items in the checklist are checked.
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
@ -41,6 +50,12 @@ Our maintainers work on Yazi in their free time, this helps them work efficientl
` `
} }
function skipValidation(context) {
const login = context.payload.issue?.user?.login || context.payload.pull_request?.user?.login
const owner = context.payload.repository?.owner?.login || context.repo.owner
return !!login && login === owner
}
module.exports = async ({ github, context, core }) => { module.exports = async ({ github, context, core }) => {
async function nightlyHash() { async function nightlyHash() {
try { try {
@ -214,19 +229,23 @@ Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.y
} }
async function main() { async function main() {
const hash = await nightlyHash()
if (!hash) return
if (context.eventName === "schedule") { if (context.eventName === "schedule") {
await closeOldIssues() await closeOldIssues()
return return
} }
if (skipValidation(context)) {
return
}
if (context.eventName === "issues") { if (context.eventName === "issues") {
const id = context.payload.issue.number const id = context.payload.issue.number
const content = context.payload.issue.body || "" const content = context.payload.issue.body || ""
const creator = context.payload.issue.user.login const creator = context.payload.issue.user.login
const hash = await nightlyHash()
if (!hash) return
if (await hasLabel(id, "bug")) { if (await hasLabel(id, "bug")) {
const body = bugReportBody(creator, content, hash) const body = bugReportBody(creator, content, hash)
await updateLabels(id, !!body, body) await updateLabels(id, !!body, body)
@ -236,6 +255,13 @@ Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.y
} else if (context.payload.action === "opened") { } else if (context.payload.action === "opened") {
await closeUnsupportedIssue(id) await closeUnsupportedIssue(id)
} }
} else if (context.eventName === "pull_request" || context.eventName === "pull_request_target") {
const content = context.payload.pull_request.body || ""
const body = pullRequestBody(content)
if (body) {
core.setFailed(body)
}
} }
} }

View file

@ -238,9 +238,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.25.0", "version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"

View file

@ -133,9 +133,16 @@ parts:
- --prefix=/usr - --prefix=/usr
build-attributes: build-attributes:
- enable-patchelf - enable-patchelf
organize:
# `libgomp1` installs libgomp into the multiarch directory, but the
# `magick` binary's RPATH only searches `$ORIGIN/../lib` (i.e. usr/lib).
# Move it alongside the Magick libraries so the bundled binary can load
# it at runtime; otherwise magick fails with:
# "libgomp.so.1: cannot open shared object file: No such file or directory"
usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libgomp.so*: usr/lib/
prime: prime:
- usr/bin/magick - usr/bin/magick
- usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libgomp.so.1 - usr/lib/libgomp.so*
- usr/lib/libMagickCore-7.Q16HDRI.so* - usr/lib/libMagickCore-7.Q16HDRI.so*
- usr/lib/libMagickWand-7.Q16HDRI.so* - usr/lib/libMagickWand-7.Q16HDRI.so*

View file

@ -33,28 +33,26 @@ yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
yazi-tui = { path = "../yazi-tui", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" } yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
crossterm = { workspace = true } either = { workspace = true }
either = { workspace = true } futures = { workspace = true }
futures = { workspace = true } hashbrown = { workspace = true }
hashbrown = { workspace = true } indexmap = { workspace = true }
indexmap = { workspace = true } inventory = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
ratatui = { workspace = true } ratatui-core = { workspace = true }
scopeguard = { workspace = true } scopeguard = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-stream = { workspace = true } tokio-stream = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
[target."cfg(unix)".dependencies] [target."cfg(unix)".dependencies]
libc = { workspace = true } libc = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -29,7 +29,7 @@ impl Actor for AcceptPayload {
drop(lock); drop(lock);
let kind = kind.to_owned(); let kind = kind.to_owned();
succ!(Lives::scope(cx.core, || { succ!(Lives::scope(cx.core, |_| {
let body = payload.body.into_lua(&LUA)?; let body = payload.body.into_lua(&LUA)?;
for (id, cb) in handlers { for (id, cb) in handlers {
if let Err(e) = runtime_scope!(LUA, &id, cb.call::<()>(body.clone())) { if let Err(e) = runtime_scope!(LUA, &id, cb.call::<()>(body.clone())) {

43
yazi-actor/src/app/dnd.rs Normal file
View file

@ -0,0 +1,43 @@
use anyhow::Result;
use mlua::{ObjectLike, Table};
use tracing::error;
use yazi_actor::lives::Lives;
use yazi_binding::runtime_scope;
use yazi_macro::succ;
use yazi_parser::app::DndForm;
use yazi_plugin::LUA;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Dnd;
impl Actor for Dnd {
type Form = DndForm;
const NAME: &str = "dnd";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
let area = yazi_binding::elements::Rect::from(size);
let result = Lives::scope(cx.core, move |_| {
runtime_scope!(LUA, "root", {
let root = LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area)?;
if form.event.is_drag() {
root.call_method::<()>("drag", form.event)?;
} else {
root.call_method::<()>("drop", form.event)?;
}
Ok(())
})
});
if let Err(ref e) = result {
error!("{e}");
}
succ!(result?);
}
}

View file

@ -1,10 +1,9 @@
use anyhow::Result; use anyhow::Result;
use yazi_binding::runtime_scope; use yazi_binding::runtime_scope;
use yazi_dds::Sendable;
use yazi_macro::succ; use yazi_macro::succ;
use yazi_parser::app::LuaForm; use yazi_parser::app::LuaForm;
use yazi_plugin::LUA; use yazi_plugin::LUA;
use yazi_shared::data::Data; use yazi_shared::data::{Data, Sendable};
use crate::{Actor, Ctx, lives::Lives}; use crate::{Actor, Ctx, lives::Lives};
@ -17,7 +16,7 @@ impl Actor for Lua {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let chunk = LUA.load(&*form.code).set_name("anonymous"); let chunk = LUA.load(&*form.code).set_name("anonymous");
let result = Lives::scope(cx.core, || { let result = Lives::scope(cx.core, |_| {
runtime_scope!(LUA, "inline", Sendable::value_to_data(&LUA, chunk.eval()?)) runtime_scope!(LUA, "inline", Sendable::value_to_data(&LUA, chunk.eval()?))
}); });
succ!(result?); succ!(result?);

View file

@ -2,6 +2,7 @@ yazi_macro::mod_flat!(
accept_payload accept_payload
bootstrap bootstrap
deprecate deprecate
dnd
focus focus
lua lua
mouse mouse

View file

@ -1,5 +1,4 @@
use anyhow::Result; use anyhow::Result;
use crossterm::event::MouseEventKind;
use mlua::{ObjectLike, Table}; use mlua::{ObjectLike, Table};
use tracing::error; use tracing::error;
use yazi_actor::lives::Lives; use yazi_actor::lives::Lives;
@ -8,6 +7,7 @@ use yazi_macro::succ;
use yazi_parser::app::MouseForm; use yazi_parser::app::MouseForm;
use yazi_plugin::LUA; use yazi_plugin::LUA;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_term::event::MouseEventKind;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -19,31 +19,29 @@ impl Actor for Mouse {
const NAME: &str = "mouse"; const NAME: &str = "mouse";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let event = yazi_binding::MouseEvent::from(form.event);
let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() }; let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
let area = yazi_binding::elements::Rect::from(size); let area = yazi_binding::elements::Rect::from(size);
let result = Lives::scope(cx.core, move || { let result = Lives::scope(cx.core, move |_| {
let root = runtime_scope!(LUA, "root", { runtime_scope!(LUA, "root", {
LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area) let root = LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area)?;
})?;
match event.kind { match form.event.kind {
MouseEventKind::Down(_) => root.call_method("click", (event, false))?, MouseEventKind::Down(_) => root.call_method("click", (form.event, false))?,
MouseEventKind::Up(_) => root.call_method("click", (event, true))?, MouseEventKind::Up(_) => root.call_method("click", (form.event, true))?,
MouseEventKind::ScrollDown => root.call_method("scroll", (event, 1))?, MouseEventKind::ScrollDown => root.call_method("scroll", (form.event, 1))?,
MouseEventKind::ScrollUp => root.call_method("scroll", (event, -1))?, MouseEventKind::ScrollUp => root.call_method("scroll", (form.event, -1))?,
MouseEventKind::ScrollRight => root.call_method("touch", (event, 1))?, MouseEventKind::ScrollRight => root.call_method("touch", (form.event, 1))?,
MouseEventKind::ScrollLeft => root.call_method("touch", (event, -1))?, MouseEventKind::ScrollLeft => root.call_method("touch", (form.event, -1))?,
MouseEventKind::Moved => root.call_method("move", event)?, MouseEventKind::Moved => root.call_method("move", form.event)?,
MouseEventKind::Drag(_) => root.call_method("drag", event)?, MouseEventKind::Drag(_) => root.call_method("drag", form.event)?,
} }
Ok(()) Ok(())
})
}); });
if let Err(ref e) = result { if let Err(ref e) = result {

View file

@ -43,7 +43,7 @@ impl Actor for PluginDo {
}; };
drop(loader); drop(loader);
let result = Lives::scope(cx.core, || { let result = Lives::scope(cx.core, |_| {
if let Some(cb) = opt.callback { if let Some(cb) = opt.callback {
cb(&LUA, plugin) cb(&LUA, plugin)
} else { } else {

View file

@ -1,9 +1,11 @@
use std::process;
use anyhow::Result; use anyhow::Result;
use yazi_boot::ARGS; use yazi_boot::ARGS;
use yazi_fs::provider::{Provider, local::Local}; use yazi_fs::engine::{Engine, local::Local};
use yazi_parser::app::QuitForm; use yazi_parser::app::QuitForm;
use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}}; use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}};
use yazi_term::Term; use yazi_tui::Raterm;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -27,7 +29,8 @@ impl Actor for Quit {
); );
}); });
Term::goodbye(|| opt.code); Raterm::stop();
process::exit(opt.code);
} }
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use mlua::Value; use mlua::{LuaString, Value};
use ratatui::layout::Position; use ratatui_core::layout::Position;
use tracing::error; use tracing::error;
use yazi_actor::lives::Lives; use yazi_actor::lives::Lives;
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
@ -21,7 +21,7 @@ impl Actor for Reflow {
let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() }; let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
let mut layout = LAYOUT.get(); let mut layout = LAYOUT.get();
let result = Lives::scope(cx.core, || { let result = Lives::scope(cx.core, |_| {
let comps = (form.reflow)((Position::ORIGIN, size).into())?; let comps = (form.reflow)((Position::ORIGIN, size).into())?;
for v in comps.sequence_values::<Value>() { for v in comps.sequence_values::<Value>() {
@ -30,7 +30,7 @@ impl Actor for Reflow {
continue; continue;
}; };
let id: mlua::String = t.get("_id")?; let id: LuaString = t.get("_id")?;
match &*id.as_bytes() { match &*id.as_bytes() {
b"current" => layout.current = *t.raw_get::<yazi_binding::elements::Rect>("_area")?, b"current" => layout.current = *t.raw_get::<yazi_binding::elements::Rect>("_area")?,
b"preview" => layout.preview = *t.raw_get::<yazi_binding::elements::Rect>("_area")?, b"preview" => layout.preview = *t.raw_get::<yazi_binding::elements::Rect>("_area")?,

View file

@ -1,28 +1,28 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::app::ResumeForm; use yazi_parser::app::ReflowForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_term::Term; use yazi_tui::Raterm;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Resume; pub struct Resume;
impl Actor for Resume { impl Actor for Resume {
type Form = ResumeForm; type Form = ReflowForm;
const NAME: &str = "resume"; const NAME: &str = "resume";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.active_mut().preview.reset(); cx.active_mut().preview.reset();
*cx.term = Some(Term::start()?);
drop(cx.term.take());
*cx.term = Some(Raterm::start()?);
// While the app resumes, it's possible that the terminal size has changed. // While the app resumes, it's possible that the terminal size has changed.
// We need to trigger a resize, and render the UI based on the resized area. // We need to trigger a resize, and render the UI based on the resized area.
act!(app:resize, cx, form.reflow)?; act!(app:resize, cx, form.reflow)?;
form.tx.send((true, form.replier))?;
act!(app:title, cx).ok(); act!(app:title, cx).ok();
succ!(render!()); succ!(render!());
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::succ; use yazi_macro::succ;
use yazi_parser::app::StopForm; use yazi_parser::VoidForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -8,20 +8,15 @@ use crate::{Actor, Ctx};
pub struct Stop; pub struct Stop;
impl Actor for Stop { impl Actor for Stop {
type Form = StopForm; type Form = VoidForm;
const NAME: &str = "stop"; const NAME: &str = "stop";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
cx.active_mut().preview.reset_image(); cx.active_mut().preview.reset_image();
// We need to destroy the `term` first before stopping the `signals`
// to prevent any signal from triggering the term to render again
// while the app is being suspended.
*cx.term = None; *cx.term = None;
form.tx.send((false, form.replier))?;
succ!(); succ!();
} }
} }

View file

@ -1,11 +1,10 @@
use anyhow::Result; use anyhow::Result;
use crossterm::{execute, terminal::SetTitle};
use yazi_actor::Ctx; use yazi_actor::Ctx;
use yazi_macro::succ; use yazi_macro::{succ, writef};
use yazi_parser::{app::TitleForm, spark::SparkKind}; use yazi_parser::{app::TitleForm, spark::SparkKind};
use yazi_shared::{Source, data::Data}; use yazi_shared::{Source, data::Data};
use yazi_term::TermState; use yazi_tty::{TTY, sequence::SetTitle};
use yazi_tty::TTY; use yazi_tui::RatermState;
use crate::Actor; use crate::Actor;
@ -18,9 +17,9 @@ impl Actor for Title {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let s = form.value.unwrap_or_else(|| format!("Yazi: {}", cx.tab().name()).into()); let s = form.value.unwrap_or_else(|| format!("Yazi: {}", cx.tab().name()).into());
execute!(TTY.writer(), SetTitle(&s))?; writef!(TTY.writer(), "{}", SetTitle(&s))?;
yazi_term::STATE.set(TermState { title: !s.is_empty(), ..yazi_term::STATE.get() }); yazi_tui::STATE.set(RatermState { title: !s.is_empty(), ..yazi_tui::STATE.get() });
succ!() succ!()
} }

View file

@ -16,6 +16,10 @@ impl Actor for Close {
const NAME: &str = "close"; const NAME: &str = "close";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
if form.submit && cx.cmp.visible {
Self::flush_last_input(cx)?;
}
let cmp = &mut cx.cmp; let cmp = &mut cx.cmp;
if let Some(item) = cmp.selected().filter(|_| form.submit).cloned() { if let Some(item) = cmp.selected().filter(|_| form.submit).cloned() {
return act!(input:complete, cx, CompleteOpt { name: item.name, is_dir: item.is_dir, ticket: cmp.ticket }); return act!(input:complete, cx, CompleteOpt { name: item.name, is_dir: item.is_dir, ticket: cmp.ticket });
@ -27,3 +31,17 @@ impl Actor for Close {
succ!(render!(mem::replace(&mut cmp.visible, false))); succ!(render!(mem::replace(&mut cmp.visible, false)));
} }
} }
impl Close {
fn flush_last_input(cx: &mut Ctx) -> Result<Data> {
let Some(guard) = cx.input.lock() else { succ!() };
if cx.cmp.ticket == guard.ticket.current() {
succ!();
}
let before = guard.partition().0.to_owned();
drop(guard);
act!(cmp:trigger, cx, before)
}
}

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use yazi_core::cmp::CmpItem; use yazi_core::cmp::CmpItem;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::cmp::ShowForm; use yazi_parser::cmp::ShowForm;
use yazi_shared::{data::Data, path::{AsPath, PathDyn}, strand::StrandLike}; use yazi_shared::{data::Data, path::{DynPath, PathDyn}, strand::StrandLike};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -30,7 +30,7 @@ impl Actor for Show {
succ!(); succ!();
}; };
cmp.matches = Self::match_candidates(opt.word.as_path(), cache); cmp.matches = Self::match_candidates(opt.word.dyn_path(), cache);
if cmp.matches.is_empty() { if cmp.matches.is_empty() {
succ!(render!(mem::replace(&mut cmp.visible, false))); succ!(render!(mem::replace(&mut cmp.visible, false)));
} }

View file

@ -2,12 +2,12 @@ use std::{io, mem};
use anyhow::Result; use anyhow::Result;
use yazi_core::cmp::{CmpItem, CmpOpt}; use yazi_core::cmp::{CmpItem, CmpOpt};
use yazi_fs::{path::clean_url, provider::{DirReader, FileHolder}}; use yazi_fs::{engine::{DirReader, FileHolder}, path::clean_url};
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::cmp::TriggerForm; use yazi_parser::cmp::TriggerForm;
use yazi_proxy::CmpProxy; use yazi_proxy::CmpProxy;
use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{DynPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}};
use yazi_vfs::provider; use yazi_vfs::engine;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -22,7 +22,7 @@ impl Actor for Trigger {
if form.ticket.is_some_and(|t| t != cx.cmp.ticket) { if form.ticket.is_some_and(|t| t != cx.cmp.ticket) {
succ!(); succ!();
} else if form.ticket.is_none() { } else if form.ticket.is_none() {
cx.cmp.ticket = cx.input.ticket.current(); cx.cmp.ticket = cx.input.lock().map(|g| g.ticket.current()).unwrap_or_default();
} }
cx.cmp.handle.take().map(|h| h.abort()); cx.cmp.handle.take().map(|h| h.abort());
@ -36,7 +36,7 @@ impl Actor for Trigger {
} }
cx.cmp.handle = Some(tokio::spawn(async move { cx.cmp.handle = Some(tokio::spawn(async move {
let mut dir = provider::read_dir(&parent).await?; let mut dir = engine::read_dir(&parent).await?;
let mut cache = vec![]; let mut cache = vec![];
// "/" is both a directory separator and the root directory per se // "/" is both a directory separator and the root directory per se
@ -53,7 +53,7 @@ impl Actor for Trigger {
if !cache.is_empty() { if !cache.is_empty() {
cache cache
.sort_unstable_by(|a, b| natsort(a.name.encoded_bytes(), b.name.encoded_bytes(), false)); .sort_unstable_by(|a, b| natsort(a.name.encoded_bytes(), b.name.encoded_bytes(), true));
CmpProxy::show(CmpOpt { cache, cache_name: parent, word, ticket }); CmpProxy::show(CmpOpt { cache, cache_name: parent, word, ticket });
} }
@ -66,32 +66,26 @@ impl Actor for Trigger {
impl Trigger { impl Trigger {
fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> { fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> {
let sep = if cfg!(windows) { let (spec, path) = Spec::parse(s.as_bytes()).ok()?;
AnyAsciiChar::new(b"/\\").unwrap() if path.is_empty() && !AnyAsciiChar::SEP.predicate(s.bytes().last()?) {
} else {
AnyAsciiChar::new(b"/").unwrap()
};
let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?;
if path.is_empty() && !sep.predicate(s.bytes().last()?) {
return None; // We don't complete a `sftp://test`, but `sftp://test/` return None; // We don't complete a `sftp://test`, but `sftp://test/`
} }
// Scheme // Spec
let scheme = scheme.zeroed(); let spec = spec.zeroed();
if scheme.is_local() && path.as_strand() == "~" { if spec.kind.is_local() && path.as_strand() == "~" {
return None; // We don't complete a `~`, but `~/` return None; // We don't complete a `~`, but `~/`
} }
// Child // 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.dyn_path(), |(_, c)| c).to_owned();
// Parent // Parent
let url = UrlCow::try_from((scheme.clone().zeroed(), path)).ok()?; let url = UrlCow::try_from((spec.clone().zeroed(), path)).ok()?;
let abs = if let Some(u) = provider::try_absolute(&url) { u } else { url }; let abs = if let Some(u) = engine::try_absolute(&url) { u } else { url };
let parent = abs.loc().try_strip_suffix(&child).ok()?; let parent = abs.loc().try_strip_suffix(&child).ok()?;
Some((clean_url(UrlCow::try_from((scheme, parent)).ok()?), child)) Some((clean_url(UrlCow::try_from((spec, parent)).ok()?), child))
} }
} }
@ -114,10 +108,11 @@ mod tests {
#[test] #[test]
fn test_split() { fn test_split() {
yazi_shared::init_tests(); yazi_shared::init_tests();
yazi_config::init_tests();
yazi_fs::init(); yazi_fs::init();
assert_eq!(Trigger::split_url(""), None); assert_eq!(Trigger::split_url(""), None);
assert_eq!(Trigger::split_url("sftp://test"), None); assert_eq!(Trigger::split_url("sftp://vps"), None);
compare(" ", "", " "); compare(" ", "", " ");
compare("/", "/", ""); compare("/", "/", "");
@ -133,16 +128,18 @@ mod tests {
compare("/foo/bar", "/foo/", "bar"); compare("/foo/bar", "/foo/", "bar");
compare("///foo/bar", "/foo/", "bar"); compare("///foo/bar", "/foo/", "bar");
CWD.set(&"sftp://test".parse::<UrlBuf>().unwrap(), || {}); CWD.set(&"sftp://vps".parse::<UrlBuf>().unwrap(), || {});
compare("sftp://test/a", "sftp://test/.", "a"); compare("sftp://vps/a", "sftp://vps/.", "a");
compare("sftp://test//a", "sftp://test//", "a"); compare("sftp://vps//a", "sftp://vps//", "a");
compare("sftp://test2/a", "sftp://test2/.", "a"); compare("test-scope://aws/a", "test-scope://aws/.", "a");
compare("sftp://test2//a", "sftp://test2//", "a"); compare("test-scope://aws//a", "test-scope://aws//", "a");
} }
#[cfg(windows)] #[cfg(windows)]
#[test] #[test]
fn test_split() { fn test_split() {
yazi_shared::init_tests();
yazi_config::init_tests();
yazi_fs::init(); yazi_fs::init();
compare("foo", "", "foo"); compare("foo", "", "foo");

View file

@ -19,7 +19,7 @@ impl Actor for Arrow {
let len = confirm.list.line_count(area.width); let len = confirm.list.line_count(area.width);
let old = confirm.offset; 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)); succ!(render!(old != confirm.offset));
} }

View file

@ -2,13 +2,13 @@ use std::ops::{Deref, DerefMut};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}}; use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}};
use yazi_fs::File; use yazi_fs::file::File;
use yazi_shared::{Id, Source, event::Action, url::UrlBuf}; use yazi_shared::{Source, event::Action, id::Id, url::UrlBuf};
use yazi_term::Term; use yazi_tui::Raterm;
pub struct Ctx<'a> { pub struct Ctx<'a> {
pub core: &'a mut Core, pub core: &'a mut Core,
pub term: &'a mut Option<Term>, pub term: &'a mut Option<Raterm>,
pub tab: usize, pub tab: usize,
pub level: usize, pub level: usize,
pub source: Source, pub source: Source,
@ -27,7 +27,7 @@ impl DerefMut for Ctx<'_> {
} }
impl<'a> Ctx<'a> { impl<'a> Ctx<'a> {
pub fn new(action: &Action, core: &'a mut Core, term: &'a mut Option<Term>) -> Result<Self> { pub fn new(action: &Action, core: &'a mut Core, term: &'a mut Option<Raterm>) -> Result<Self> {
let tab = if let Ok(id) = action.get::<Id>("tab") { let tab = if let Ok(id) = action.get::<Id>("tab") {
core core
.mgr .mgr
@ -63,7 +63,7 @@ impl<'a> Ctx<'a> {
} }
} }
pub fn active(core: &'a mut Core, term: &'a mut Option<Term>) -> Self { pub fn active(core: &'a mut Core, term: &'a mut Option<Raterm>) -> Self {
let tab = core.mgr.tabs.cursor; let tab = core.mgr.tabs.cursor;
Self { Self {
core, core,
@ -108,6 +108,9 @@ impl<'a> Ctx<'a> {
#[inline] #[inline]
pub fn hovered(&self) -> Option<&File> { self.tab().hovered() } pub fn hovered(&self) -> Option<&File> { self.tab().hovered() }
#[inline]
pub fn hovered_url(&self) -> Option<&UrlBuf> { self.tab().hovered_url() }
#[inline] #[inline]
pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() } pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() }

View file

@ -16,7 +16,7 @@ impl Preflight {
return Ok(opt.1); return Ok(opt.1);
}; };
Ok(Lives::scope(cx.core, || { Ok(Lives::scope(cx.core, |_| {
let mut body = opt.1.into_lua(&LUA)?; let mut body = opt.1.into_lua(&LUA)?;
for (id, cb) in handlers { for (id, cb) in handlers {
match runtime_scope!(LUA, &id, cb.call::<Value>(&body)) { match runtime_scope!(LUA, &id, cb.call::<Value>(&body)) {

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_macro::{act, render, succ};
use yazi_parser::VoidForm; use yazi_parser::VoidForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::input::InputMode;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -13,15 +14,11 @@ impl Actor for Escape {
const NAME: &str = "escape"; const NAME: &str = "escape";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
if cx.help.keyword().is_none() { if cx.help.input.mode() == InputMode::Normal {
return act!(help:toggle, cx, cx.help.layer); return act!(help:close, cx);
} }
let help = &mut cx.help; act!(escape, cx.help.input)?;
help.keyword = String::new();
help.in_filter = None;
help.filter_apply();
succ!(render!()); 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 anyhow::Result;
use ratatui_core::layout::Margin;
use yazi_config::popup::Help;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::help::ToggleForm; use yazi_parser::help::ToggleForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::input::Input;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -13,17 +16,24 @@ impl Actor for Toggle {
const NAME: &str = "toggle"; const NAME: &str = "toggle";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { 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; let help = &mut cx.help;
help.visible = true;
help.visible = !help.visible;
help.layer = form.layer; help.layer = form.layer;
help.position = position;
help.height = area.height;
help.keyword = String::new(); help.input = Input::default();
help.in_filter = None; help.input.repos(input_area);
help.filter_apply();
help.keyword.clear();
help.offset = 0; help.offset = 0;
help.cursor = 0; help.cursor = 0;
help.filter_apply();
succ!(render!()); succ!(render!());
} }
} }

View file

@ -1,7 +1,8 @@
use anyhow::Result; use anyhow::Result;
use yazi_core::input::InputMutGuard;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::input::CloseForm; use yazi_parser::{input::CloseForm, spark::SparkKind};
use yazi_shared::data::Data; use yazi_shared::{Source, data::Data};
use yazi_widgets::input::InputEvent; use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -14,16 +15,33 @@ impl Actor for Close {
const NAME: &str = "close"; const NAME: &str = "close";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let input = &mut cx.input; let Some(mut guard) = cx.input.lock_mut() else {
input.visible = false; succ!();
input.ticket.next(); };
if let Some(tx) = input.tx.take() { guard.ticket.next();
let value = input.snap().value.clone(); if let Some(cb) = guard.cb.take() {
_ = tx.send(if form.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) }); let value = guard.value().to_owned();
cb(if form.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) });
} }
if form.submit
&& let InputMutGuard::Main(input) = guard
{
input.histories.remember(&input.main.history.name, input.main.value());
}
cx.input.main.visible = false;
act!(cmp:close, cx)?; act!(cmp:close, cx)?;
succ!(render!()); succ!(render!());
} }
fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> {
match cx.source() {
Source::Key => Some(SparkKind::KeyInputClose),
Source::Ind => Some(SparkKind::IndInputClose),
_ => None,
}
}
} }

View file

@ -13,11 +13,14 @@ impl Actor for Complete {
const NAME: &str = "complete"; const NAME: &str = "complete";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let input = &mut cx.input; let Some(mut guard) = cx.input.lock_mut() else {
if !input.visible || input.ticket.current() != form.ticket { succ!();
};
if guard.ticket.current() != form.ticket {
succ!(); succ!();
} }
act!(complete, input, form) act!(complete, guard, form)
} }
} }

View file

@ -15,16 +15,20 @@ impl Actor for Escape {
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
use yazi_widgets::input::InputMode as M; use yazi_widgets::input::InputMode as M;
let input = &mut cx.input; let Some(mut guard) = cx.input.lock_mut() else {
succ!();
};
let mode = input.snap().mode; let (mode, op) = (guard.snap().mode, guard.snap().op);
act!(escape, guard)?;
drop(guard);
match mode { match mode {
M::Normal if input.snap_mut().op == InputOp::None => act!(input:close, cx), M::Normal if op == InputOp::None => act!(input:close, cx),
M::Insert => act!(cmp:close, cx), M::Insert => act!(cmp:close, cx),
M::Normal | M::Replace => Ok(().into()), M::Normal | M::Replace => Ok(().into()),
}?; }?;
act!(escape, cx.input)?;
succ!(render!()); succ!(render!());
} }
} }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(close complete escape show); yazi_macro::mod_flat!(close complete escape recall remember show);

View file

@ -0,0 +1,32 @@
use anyhow::Result;
use yazi_core::input::InputMutGuard;
use yazi_macro::succ;
use yazi_parser::input::RecallForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Recall;
impl Actor for Recall {
type Form = RecallForm;
const NAME: &str = "recall";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some(input) = cx.input.lock_mut() else {
succ!();
};
match input {
InputMutGuard::Main(input) => {
let entries = input.histories.get(&input.main.history.name);
input.main.recall(entries, form.step)
}
InputMutGuard::Alt(input, mut guard) => {
let entries = input.histories.get(&guard.history.name);
guard.recall(entries, form.step)
}
}
}
}

View file

@ -0,0 +1,33 @@
use anyhow::Result;
use yazi_core::input::InputMutGuard;
use yazi_macro::succ;
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Remember;
impl Actor for Remember {
type Form = VoidForm;
const NAME: &str = "remember";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let Some(mut input) = cx.input.lock_mut() else {
succ!();
};
match &mut input {
InputMutGuard::Main(input) => {
input.histories.remember(&input.main.history.name, input.main.value());
}
InputMutGuard::Alt(input, guard) => {
input.histories.remember(&guard.history.name, guard.value());
}
}
input.history.take();
succ!();
}
}

View file

@ -1,27 +1,35 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use anyhow::Result; use anyhow::Result;
use yazi_config::{THEME, YAZI};
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::input::ShowForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::input::InputOpt; use yazi_shim::ratatui::Padable;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Show; pub struct Show;
impl Actor for Show { impl Actor for Show {
type Form = InputOpt; type Form = ShowForm;
const NAME: &str = "show"; const NAME: &str = "show";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, Self::Form { mut opt }: Self::Form) -> Result<Data> {
act!(input:close, cx)?; act!(input:close, cx)?;
let area = cx.mgr.area(opt.position).padding(cx.input.padding());
let input = &mut cx.input; let input = &mut cx.input;
input.visible = true; input.main.name = opt.name.clone();
input.title = form.cfg.title.clone(); input.main.title = opt.title.clone();
input.position = form.cfg.position; input.main.position = opt.position;
*input.deref_mut() = yazi_widgets::input::Input::new(form)?; input.main.visible = true;
opt.styles = (&THEME.input).into();
opt.blinking = YAZI.input.cursor_blink;
*input.main.deref_mut() = yazi_widgets::input::Input::new(opt)?;
input.main.repos(area);
succ!(render!()); succ!(render!());
} }

View file

@ -3,3 +3,5 @@ extern crate self as yazi_actor;
yazi_macro::mod_pub!(app cmp confirm core help input lives mgr notify pick spot tasks which); yazi_macro::mod_pub!(app cmp confirm core help input lives mgr notify pick spot tasks which);
yazi_macro::mod_flat!(actor context); yazi_macro::mod_flat!(actor context);
pub fn init() { lives::init(); }

View file

@ -0,0 +1,29 @@
use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataMethods};
use super::{Lives, PtrCell};
pub(super) struct Behavior {
inner: PtrCell<yazi_scheduler::Behavior>,
}
impl Deref for Behavior {
type Target = yazi_scheduler::Behavior;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Behavior {
pub(super) fn make(inner: &yazi_scheduler::Behavior) -> mlua::Result<AnyUserData> {
let inner = PtrCell::from(inner);
Lives::scoped_userdata(Self { inner })
}
}
impl UserData for Behavior {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("reset", |_, me, ()| Ok(me.reset()));
}
}

View file

@ -1,10 +1,12 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, IntoLua, MetaMethod, UserData, UserDataMethods, Value}; use mlua::{AnyUserData, IntoLua, LuaString, MetaMethod, UserData, UserDataMethods, UserDataRef, Value};
use paste::paste; use paste::paste;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
pub(super) type CoreRef = UserDataRef<Core>;
pub(super) struct Core { pub(super) struct Core {
inner: PtrCell<yazi_core::Core>, inner: PtrCell<yazi_core::Core>,
@ -12,8 +14,9 @@ pub(super) struct Core {
c_tabs: Option<Value>, c_tabs: Option<Value>,
c_tasks: Option<Value>, c_tasks: Option<Value>,
c_yanked: Option<Value>, c_yanked: Option<Value>,
c_layer: Option<Value>, c_input: Option<Value>,
c_which: Option<Value>, c_which: Option<Value>,
c_layer: Option<Value>,
} }
impl Deref for Core { impl Deref for Core {
@ -31,15 +34,16 @@ impl Core {
c_tabs: None, c_tabs: None,
c_tasks: None, c_tasks: None,
c_yanked: None, c_yanked: None,
c_layer: None, c_input: None,
c_which: None, c_which: None,
c_layer: None,
}) })
} }
} }
impl UserData for Core { impl UserData for Core {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: mlua::String| { methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: LuaString| {
macro_rules! reuse { macro_rules! reuse {
($key:ident, $value:expr) => { ($key:ident, $value:expr) => {
match paste! { &me.[<c_ $key>] } { match paste! { &me.[<c_ $key>] } {
@ -57,10 +61,9 @@ impl UserData for Core {
b"tabs" => reuse!(tabs, super::Tabs::make(&me.mgr.tabs)), b"tabs" => reuse!(tabs, super::Tabs::make(&me.mgr.tabs)),
b"tasks" => reuse!(tasks, super::Tasks::make(&me.tasks)), b"tasks" => reuse!(tasks, super::Tasks::make(&me.tasks)),
b"yanked" => reuse!(yanked, super::Yanked::make(&me.mgr.yanked)), b"yanked" => reuse!(yanked, super::Yanked::make(&me.mgr.yanked)),
b"layer" => { b"input" => reuse!(input, super::Input::make(&me.input)),
reuse!(layer, Ok::<_, mlua::Error>(yazi_binding::Layer::from(me.layer())))
}
b"which" => reuse!(which, super::Which::make(&me.which)), b"which" => reuse!(which, super::Which::make(&me.which)),
b"layer" => reuse!(layer, Ok::<_, mlua::Error>(me.layer())),
_ => Value::Nil, _ => Value::Nil,
}) })
}); });

View file

@ -1,37 +1,35 @@
use std::ops::{Deref, Range}; use std::ops::{Deref, Range};
use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods};
use yazi_binding::cached_field; use yazi_shim::mlua::UserDataFieldsExt;
use super::{File, Filter, Lives, PtrCell}; use super::{File, Filter, Lives, PtrCell};
pub(super) struct Files { pub(super) struct Entries {
window: Range<usize>, window: Range<usize>,
folder: PtrCell<yazi_core::tab::Folder>, folder: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>, tab: PtrCell<yazi_core::tab::Tab>,
v_filter: Option<Value>,
} }
impl Deref for Files { impl Deref for Entries {
type Target = yazi_fs::Files; 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( pub(super) fn make(
window: Range<usize>, window: Range<usize>,
folder: &yazi_core::tab::Folder, folder: &yazi_core::tab::Folder,
tab: &yazi_core::tab::Tab, tab: &yazi_core::tab::Tab,
) -> mlua::Result<AnyUserData> { ) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { window, folder: folder.into(), tab: tab.into(), v_filter: None }) Lives::scoped_userdata(Self { window, folder: folder.into(), tab: tab.into() })
} }
} }
impl UserData for Files { impl UserData for Entries {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, filter, |_, me| me.filter().map(Filter::make).transpose()); fields.add_static_field("filter", |_, me| me.filter().map(Filter::make).transpose());
} }
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -1,37 +1,28 @@
use std::{ops::Deref, ptr}; use std::{ops::Deref, ptr};
use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods, Value}; use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Range, Style, cached_field}; use yazi_binding::{Range, style::Style};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_shared::{path::AsPath, url::UrlLike}; use yazi_fs::file::FileInventory;
use yazi_shared::{path::DynPath, url::UrlLike};
use super::Lives; use super::{FILE_CACHE, Lives};
use crate::lives::PtrCell; use crate::lives::{CoreRef, PtrCell};
pub(super) struct File { pub(super) struct File {
idx: usize, idx: usize,
folder: PtrCell<yazi_core::tab::Folder>, folder: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>, tab: PtrCell<yazi_core::tab::Tab>,
v_cha: Option<Value>,
v_url: Option<Value>,
v_link_to: Option<Value>,
v_name: Option<Value>,
v_path: Option<Value>,
v_cache: Option<Value>,
v_bare: Option<Value>,
} }
impl Deref for File { impl Deref for File {
type Target = yazi_fs::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> for File { impl AsRef<yazi_fs::file::File> for File {
fn as_ref(&self) -> &yazi_fs::File { self } fn as_ref(&self) -> &yazi_fs::file::File { self }
} }
impl File { impl File {
@ -42,28 +33,16 @@ impl File {
) -> mlua::Result<AnyUserData> { ) -> mlua::Result<AnyUserData> {
use hashbrown::hash_map::Entry; use hashbrown::hash_map::Entry;
Ok(match super::FILE_CACHE.borrow_mut().entry(PtrCell(&folder.files[idx])) { Ok(
Entry::Occupied(oe) => oe.into_mut().clone(), match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.entries[idx])) {
Entry::Vacant(ve) => { Entry::Occupied(oe) => oe.into_mut().clone(),
let ud = Lives::scoped_userdata(Self { Entry::Vacant(ve) => {
idx, let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into() })?;
folder: folder.into(), ve.insert(ud.clone());
tab: tab.into(), ud
}
v_cha: None, },
v_url: None, )
v_link_to: None,
v_name: None,
v_path: None,
v_cache: None,
v_bare: None,
})?;
ve.insert(ud.clone());
ud
}
})
} }
#[inline] #[inline]
@ -73,7 +52,6 @@ impl File {
impl UserData for File { impl UserData for File {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
yazi_binding::impl_file_fields!(fields); yazi_binding::impl_file_fields!(fields);
cached_field!(fields, bare, |_, me| Ok(yazi_binding::File::new(&**me)));
fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1));
fields.add_field_method_get("is_hovered", |_, me| Ok(me.is_hovered())); fields.add_field_method_get("is_hovered", |_, me| Ok(me.is_hovered()));
@ -86,18 +64,24 @@ impl UserData for File {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
yazi_binding::impl_file_methods!(methods); yazi_binding::impl_file_methods!(methods);
methods.add_method("icon", |_, me, ()| { methods.add_method("icon", |lua, me, ()| {
use yazi_binding::Icon; yazi_binding::deprecate!(
lua,
"{}: `File:icon()` is deprecated, use `th.icon:match(file)` instead"
);
// TODO: use a cache // TODO: use a cache
Ok(yazi_config::THEME.icon.matches(me, me.is_hovered()).map(Icon::from)) Ok(yazi_config::THEME.icon.matches(me, me.is_hovered()))
}); });
methods.add_method("size", |_, me, ()| { 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.entry_key()).copied()
} else {
Some(me.len)
})
}); });
methods.add_method("mime", |lua, me, ()| { methods.add_method("mime", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
core.mgr.mimetype.get(&me.url).map(|s| lua.create_string(s)).transpose() core.mgr.mimetype.get(&me.url).map(|s| lua.create_string(s)).transpose()
})?
}); });
methods.add_method("prefix", |lua, me, ()| { methods.add_method("prefix", |lua, me, ()| {
if !me.url.has_trail() { if !me.url.has_trail() {
@ -106,64 +90,68 @@ impl UserData for File {
let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components(); let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components();
comp.next_back(); comp.next_back();
Some(lua.create_string(comp.as_path().encoded_bytes())).transpose() Some(lua.create_string(comp.dyn_path().encoded_bytes())).transpose()
}); });
methods.add_method("style", |lua, me, ()| { methods.add_method("style", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
let mime = core.mgr.mimetype.get(&me.url).unwrap_or_default(); let mime = core.mgr.mimetype.get(&me.url).unwrap_or_default();
THEME.filetype.match_style(me, mime).map(Style::from) Ok(THEME.filetype.match_style(me, mime).map(Style::from))
})
}); });
methods.add_method("is_yanked", |lua, me, ()| { methods.add_method("is_yanked", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
if !core.mgr.yanked.contains(&me.url) { Ok(if !core.mgr.yanked.contains(&me.url) {
0u8 0u8
} else if core.mgr.yanked.cut { } else if core.mgr.yanked.cut {
2u8 2u8
} else { } else {
1u8 1u8
}
}) })
}); });
methods.add_method("is_marked", |_, me, ()| { methods.add_method("is_marked", |_, me, ()| {
use yazi_core::tab::Mode::*; let Some(visual) = me.tab.mode.visual() else {
if !me.tab.mode.is_visual() || me.folder.url != me.tab.current.url { return Ok(0u8);
};
if !visual.contains(me.idx, me.tab.current.cursor, me.folder.entries.len()) {
return Ok(0u8); return Ok(0u8);
} }
if me.folder.url != me.tab.current.url {
Ok(match &me.tab.mode { return Ok(0u8);
Select(_, indices) if indices.contains(&me.idx) => 1u8, }
Unset(_, indices) if indices.contains(&me.idx) => 2u8, Ok(if me.tab.mode.is_select() { 1u8 } else { 2u8 })
_ => 0u8,
})
}); });
methods.add_method("is_selected", |_, me, ()| Ok(me.tab.selected.contains(&me.url))); methods.add_method("is_selected", |_, me, ()| Ok(me.tab.selected.contains(&me.url)));
methods.add_method("found", |lua, me, ()| { methods.add_method("found", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
let Some(finder) = &core.active().finder else { let Some(finder) = &core.active().finder else {
return Ok(None); return Ok(None);
}; };
let Some(idx) = finder.matched_idx(&me.folder, me.urn()) else { let Some(idx) = finder.matched_idx(&me.folder, me.entry_key()) else {
return Ok(None); return Ok(None);
}; };
Some(lua.create_sequence_from([idx.into_lua(lua)?, finder.matched.len().into_lua(lua)?])) lua.create_sequence_from([idx.into_lua(lua)?, finder.matched.len().into_lua(lua)?]).map(Some)
.transpose()
})
}); });
methods.add_method("highlights", |lua, me, ()| { methods.add_method("highlights", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
let Some(finder) = &core.active().finder else { let Some(finder) = &core.active().finder else {
return None; return Ok(None);
}; };
if me.folder.url != me.tab.current.url { if me.folder.url != me.tab.current.url {
return None; return Ok(None);
} }
let Some(Some(h)) = me.url.name().map(|s| finder.filter.highlighted(s)) else {
return Ok(None);
};
let h = finder.filter.highlighted(me.url.name()?)?; lua.create_sequence_from(h.into_iter().map(Range::from)).map(Some)
Some(h.into_iter().map(Range::from).collect::<Vec<_>>())
})
}); });
} }
} }
inventory::submit! {
FileInventory {
register: |_| {},
borrow: |ud, f| f(&*ud.borrow::<File>()?),
}
}

View file

@ -1,21 +1,15 @@
use std::ops::{Deref, Range}; use std::ops::{Deref, Range};
use mlua::{AnyUserData, UserData, UserDataFields, Value}; use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_binding::{FolderStage, Url, cached_field};
use yazi_config::LAYOUT; 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 { pub(super) struct Folder {
window: Range<usize>, window: Range<usize>,
inner: PtrCell<yazi_core::tab::Folder>, inner: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>, tab: PtrCell<yazi_core::tab::Tab>,
v_cwd: Option<Value>,
v_files: Option<Value>,
v_stage: Option<Value>,
v_window: Option<Value>,
v_hovered: Option<Value>,
} }
impl Deref for Folder { impl Deref for Folder {
@ -34,34 +28,24 @@ impl Folder {
Some(w) => w, Some(w) => w,
None => { None => {
let limit = LAYOUT.get().preview.height as usize; 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)
} }
}; };
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self { window, inner: inner.into(), tab: tab.into() })
window,
inner: inner.into(),
tab: tab.into(),
v_cwd: None,
v_files: None,
v_stage: None,
v_window: None,
v_hovered: None,
})
} }
} }
impl UserData for Folder { impl UserData for Folder {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, cwd, |_, me| Ok(Url::new(&me.url))); fields.add_cached_field("cwd", |_, me| Ok(me.url.clone()));
cached_field!(fields, 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));
cached_field!(fields, stage, |_, me| Ok(FolderStage::new(me.stage.clone()))); fields.add_cached_field("stage", |_, me| Ok(me.stage.clone()));
cached_field!(fields, 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("offset", |_, me| Ok(me.offset));
fields.add_field_method_get("cursor", |_, me| Ok(me.cursor)); fields.add_field_method_get("cursor", |_, me| Ok(me.cursor));
cached_field!(fields, hovered, |_, me| { fields.add_static_field("hovered", |_, me| {
me.hovered().map(|_| File::make(me.cursor, me, &me.tab)).transpose() me.hovered().map(|_| File::make(me.cursor, me, &me.tab)).transpose()
}); });
} }

View file

@ -0,0 +1,29 @@
use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Lives, PtrCell};
use crate::lives::InputAlt;
pub(super) struct Input {
inner: PtrCell<yazi_core::input::Input>,
}
impl Deref for Input {
type Target = yazi_core::input::Input;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Input {
pub(super) fn make(inner: &yazi_core::input::Input) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into() })
}
}
impl UserData for Input {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("alt", |_, me| me.alt.as_ref().map(InputAlt::make).transpose());
}
}

View file

@ -0,0 +1,26 @@
use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Lives, PtrCell};
pub(super) struct InputAlt(PtrCell<yazi_core::input::InputAlt>);
impl Deref for InputAlt {
type Target = yazi_core::input::InputAlt;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl InputAlt {
pub(super) fn make(inner: &yazi_core::input::InputAlt) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self(inner.into()))
}
}
impl UserData for InputAlt {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("value", |lua, me| lua.create_string(me.lock().value()));
}
}

View file

@ -1,39 +1,38 @@
use std::cell::RefCell; use std::mem::MaybeUninit;
use hashbrown::HashMap; use hashbrown::HashMap;
use mlua::{AnyUserData, UserData}; use mlua::{AnyUserData, UserData};
use scopeguard::defer; use scopeguard::defer;
use tracing::error; use tracing::error;
use yazi_plugin::LUA; use yazi_plugin::LUA;
use yazi_shim::cell::RoCell;
use super::{Core, PtrCell}; use super::{Core, PtrCell};
use crate::lives::MutCell;
static TO_DESTROY: RoCell<RefCell<Vec<AnyUserData>>> = RoCell::new_const(RefCell::new(Vec::new())); pub(super) static TO_DESTROY: MutCell<Vec<AnyUserData>> = MutCell::new(Vec::new());
pub(super) static FILE_CACHE: RoCell<RefCell<HashMap<PtrCell<yazi_fs::File>, AnyUserData>>> = pub(super) static FILE_CACHE: MutCell<
RoCell::new(); MaybeUninit<HashMap<PtrCell<yazi_fs::file::File>, AnyUserData>>,
> = MutCell::new(MaybeUninit::uninit());
pub struct Lives; pub struct Lives;
impl Lives { impl Lives {
pub fn scope<T, F>(core: &yazi_core::Core, f: F) -> mlua::Result<T> pub fn scope<T, F>(core: &mut yazi_core::Core, f: F) -> mlua::Result<T>
where where
F: FnOnce() -> mlua::Result<T>, F: FnOnce(&mut yazi_core::Core) -> mlua::Result<T>,
{ {
FILE_CACHE.init(Default::default()); defer! {
defer! { FILE_CACHE.drop(); } unsafe {
(*FILE_CACHE.get()).assume_init_mut().clear();
let result = LUA.scope(|scope| { for ud in (*TO_DESTROY.get()).drain(..) {
scope.add_destructor(|| {
for ud in TO_DESTROY.borrow_mut().drain(..) {
ud.destroy().expect("failed to destruct scoped userdata"); ud.destroy().expect("failed to destruct scoped userdata");
} }
}); }
}
LUA.set_named_registry_value("cx", scope.create_any_userdata_ref(core)?)?; LUA.set_named_registry_value("cx", Core::make(core)?)?;
LUA.globals().raw_set("cx", Core::make(core)?)?; LUA.globals().raw_set("cx", Core::make(core)?)?;
f() let result = f(core);
});
if let Err(ref e) = result { if let Err(ref e) = result {
error!("{e}"); error!("{e}");
@ -46,7 +45,7 @@ impl Lives {
T: UserData + 'static, T: UserData + 'static,
{ {
let ud = LUA.create_userdata(data)?; let ud = LUA.create_userdata(data)?;
TO_DESTROY.borrow_mut().push(ud.clone()); unsafe { &mut *TO_DESTROY.get() }.push(ud.clone());
Ok(ud) Ok(ud)
} }
} }

View file

@ -1 +1,5 @@
yazi_macro::mod_flat!(core file files filter finder folder lives mode 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 std::ops::Deref;
use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods}; use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods};
use yazi_binding::deprecate;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
@ -22,9 +23,13 @@ impl Mode {
impl UserData for Mode { impl UserData for Mode {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { 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_select", |_, me| Ok(me.is_select()));
fields.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); 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) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -0,0 +1,11 @@
use std::cell::UnsafeCell;
pub(super) struct MutCell<T>(UnsafeCell<T>);
unsafe impl<T> Sync for MutCell<T> {}
impl<T> MutCell<T> {
pub(super) const fn new(value: T) -> Self { Self(UnsafeCell::new(value)) }
pub(super) fn get(&self) -> *mut T { self.0.get() }
}

View file

@ -1,18 +1,12 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields, Value}; use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_binding::cached_field; use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr};
use yazi_shim::strum::IntoStr;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
pub(super) struct Preference { pub(super) struct Preference {
inner: PtrCell<yazi_core::tab::Preference>, inner: PtrCell<yazi_core::tab::Preference>,
v_name: Option<Value>,
v_linemode: Option<Value>,
v_sort_by: Option<Value>,
} }
impl Deref for Preference { impl Deref for Preference {
@ -23,26 +17,19 @@ impl Deref for Preference {
impl Preference { impl Preference {
pub(super) fn make(inner: &yazi_core::tab::Preference) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_core::tab::Preference) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self { inner: inner.into() })
inner: inner.into(),
v_name: None,
v_linemode: None,
v_sort_by: None,
})
} }
} }
impl UserData for Preference { impl UserData for Preference {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
// Display // Display
cached_field!(fields, name, |lua, me| lua.create_string(&me.name)); fields.add_cached_field("name", |lua, me| lua.create_string(&me.name));
cached_field!(fields, linemode, |lua, me| lua.create_string(&*me.linemode)); fields.add_cached_field("linemode", |lua, me| lua.create_string(&*me.linemode));
fields.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden)); fields.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden));
// Sorting // Sorting
cached_field!(fields, sort_by, |_, me| Ok(me.sort_by.into_str())); fields.add_cached_field("sort_by", |_, me| Ok(me.sort_by.into_str()));
fields.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive)); fields.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive));
fields.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse)); fields.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse));
fields.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); fields.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first));

View file

@ -1,15 +1,13 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields, Value}; use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_binding::cached_field;
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Folder, Lives, PtrCell}; use super::{Folder, Lives, PtrCell};
pub(super) struct Preview { pub(super) struct Preview {
tab: PtrCell<yazi_core::tab::Tab>, tab: PtrCell<yazi_core::tab::Tab>,
v_folder: Option<Value>,
} }
impl Deref for Preview { impl Deref for Preview {
@ -20,19 +18,19 @@ impl Deref for Preview {
impl Preview { impl Preview {
pub(super) fn make(tab: &yazi_core::tab::Tab) -> mlua::Result<AnyUserData> { pub(super) fn make(tab: &yazi_core::tab::Tab) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { tab: tab.into(), v_folder: None }) Lives::scoped_userdata(Self { tab: tab.into() })
} }
} }
impl UserData for Preview { impl UserData for Preview {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("skip", |_, me| Ok(me.skip)); fields.add_field_method_get("skip", |_, me| Ok(me.skip));
cached_field!(fields, folder, |_, me| { fields.add_static_field("folder", |_, me| {
me.tab me.tab
.hovered_folder() .hovered_folder()
.map(|f| { .map(|f| {
let limit = LAYOUT.get().preview.height as usize; let limit = LAYOUT.get().preview.height as usize;
Folder::make(Some(me.skip..f.files.len().min(me.skip + limit)), f, &me.tab) Folder::make(Some(me.skip..f.entries.len().min(me.skip + limit)), f, &me.tab)
}) })
.transpose() .transpose()
}); });

View file

@ -1,7 +1,6 @@
use mlua::AnyUserData; use mlua::AnyUserData;
use super::Lives; use super::{Lives, PtrCell};
use crate::lives::PtrCell;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(super) struct Selected; pub(super) struct Selected;
@ -11,7 +10,7 @@ impl Selected {
let inner = PtrCell::from(inner); let inner = PtrCell::from(inner);
Lives::scoped_userdata(yazi_binding::Iter::new( Lives::scoped_userdata(yazi_binding::Iter::new(
inner.as_static().values().map(yazi_binding::Url::new), inner.as_static().files().cloned(),
Some(inner.len()), Some(inner.len()),
)) ))
} }

View file

@ -1,21 +1,13 @@
use std::{borrow::Cow, ops::Deref}; use std::{borrow::Cow, ops::Deref};
use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods, Value}; use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Id, UrlRef, cached_field}; use yazi_shared::url::UrlRef;
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected}; use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected};
pub(super) struct Tab { pub(super) struct Tab {
inner: PtrCell<yazi_core::tab::Tab>, inner: PtrCell<yazi_core::tab::Tab>,
v_name: Option<Value>,
v_mode: Option<Value>,
v_pref: Option<Value>,
v_current: Option<Value>,
v_parent: Option<Value>,
v_selected: Option<Value>,
v_preview: Option<Value>,
v_finder: Option<Value>,
} }
impl Deref for Tab { impl Deref for Tab {
@ -26,42 +18,29 @@ impl Deref for Tab {
impl Tab { impl Tab {
pub(super) fn make(inner: &yazi_core::tab::Tab) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_core::tab::Tab) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self { inner: inner.into() })
inner: inner.into(),
v_name: None,
v_mode: None,
v_pref: None,
v_current: None,
v_parent: None,
v_selected: None,
v_preview: None,
v_finder: None,
})
} }
} }
impl UserData for Tab { impl UserData for Tab {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id))); fields.add_field_method_get("id", |_, me| Ok(me.id));
cached_field!(fields, name, |lua, me| { fields.add_cached_field("name", |lua, me| match me.name() {
match me.name() { Cow::Borrowed(s) => lua.create_string(s),
Cow::Borrowed(s) => lua.create_string(s), Cow::Owned(s) => lua.create_external_string(s),
Cow::Owned(s) => lua.create_external_string(s),
}
}); });
cached_field!(fields, mode, |_, me| Mode::make(&me.mode)); fields.add_static_field("mode", |_, me| Mode::make(&me.mode));
cached_field!(fields, pref, |_, me| Preference::make(&me.pref)); fields.add_static_field("pref", |_, me| Preference::make(&me.pref));
cached_field!(fields, current, |_, me| Folder::make(None, &me.current, me)); fields.add_static_field("current", |_, me| Folder::make(None, &me.current, me));
cached_field!(fields, parent, |_, me| { fields.add_static_field("parent", |_, me| {
me.parent.as_ref().map(|f| Folder::make(None, f, me)).transpose() me.parent.as_ref().map(|f| Folder::make(None, f, me)).transpose()
}); });
cached_field!(fields, selected, |_, me| Selected::make(&me.selected)); fields.add_static_field("selected", |_, me| Selected::make(&me.selected));
cached_field!(fields, preview, |_, me| Preview::make(me)); fields.add_static_field("preview", |_, me| Preview::make(me));
cached_field!(fields, finder, |_, me| me.finder.as_ref().map(Finder::make).transpose()); fields.add_static_field("finder", |_, me| me.finder.as_ref().map(Finder::make).transpose());
} }
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -1,16 +1,13 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value}; use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields};
use yazi_binding::{SER_OPT, cached_field};
use yazi_scheduler::Progress; use yazi_scheduler::Progress;
use yazi_shim::mlua::{SER_OPT, UserDataFieldsExt};
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
pub(super) struct TaskSnap { pub(super) struct TaskSnap {
inner: PtrCell<yazi_scheduler::TaskSnap>, inner: PtrCell<yazi_scheduler::TaskSnap>,
v_title: Option<Value>,
v_prog: Option<Value>,
} }
impl Deref for TaskSnap { impl Deref for TaskSnap {
@ -21,14 +18,14 @@ impl Deref for TaskSnap {
impl TaskSnap { impl TaskSnap {
pub(super) fn make(inner: &yazi_scheduler::TaskSnap) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_scheduler::TaskSnap) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into(), v_title: None, v_prog: None }) Lives::scoped_userdata(Self { inner: inner.into() })
} }
} }
impl UserData for TaskSnap { impl UserData for TaskSnap {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, title, |lua, me| lua.create_string(&me.title)); fields.add_cached_field("title", |lua, me| lua.create_string(&me.title));
cached_field!(fields, prog, |lua, me| lua.to_value_with(&me.prog, SER_OPT)); fields.add_cached_field("prog", |lua, me| lua.to_value_with(&me.prog, SER_OPT));
fields.add_field_method_get("running", |_, me| Ok(me.prog.running())); fields.add_field_method_get("running", |_, me| Ok(me.prog.running()));
fields.add_field_method_get("cooked", |_, me| Ok(me.prog.cooked())); fields.add_field_method_get("cooked", |_, me| Ok(me.prog.cooked()));

View file

@ -1,16 +1,13 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value}; use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields};
use yazi_binding::{SER_OPT, cached_field}; use yazi_shim::mlua::{SER_OPT, UserDataFieldsExt};
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
use crate::lives::TaskSnap; use crate::lives::{Behavior, TaskSnap};
pub(super) struct Tasks { pub(super) struct Tasks {
inner: PtrCell<yazi_core::tasks::Tasks>, inner: PtrCell<yazi_core::tasks::Tasks>,
v_snaps: Option<Value>,
v_summary: Option<Value>,
} }
impl Deref for Tasks { impl Deref for Tasks {
@ -21,12 +18,7 @@ impl Deref for Tasks {
impl Tasks { impl Tasks {
pub(super) fn make(inner: &yazi_core::tasks::Tasks) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_core::tasks::Tasks) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self { inner: inner.into() })
inner: inner.into(),
v_snaps: None,
v_summary: None,
})
} }
} }
@ -34,7 +26,9 @@ impl UserData for Tasks {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cursor", |_, me| Ok(me.cursor)); fields.add_field_method_get("cursor", |_, me| Ok(me.cursor));
cached_field!(fields, snaps, |lua, me| { fields.add_static_field("behavior", |_, me| Behavior::make(&me.scheduler.behavior));
fields.add_static_field("snaps", |lua, me| {
let tbl = lua.create_table_with_capacity(me.snaps.len(), 0)?; let tbl = lua.create_table_with_capacity(me.snaps.len(), 0)?;
for snap in &me.snaps { for snap in &me.snaps {
tbl.raw_push(TaskSnap::make(snap)?)?; tbl.raw_push(TaskSnap::make(snap)?)?;
@ -42,6 +36,6 @@ impl UserData for Tasks {
Ok(tbl) Ok(tbl)
}); });
cached_field!(fields, summary, |lua, me| lua.to_value_with(&me.summary, SER_OPT)); fields.add_cached_field("summary", |lua, me| lua.to_value_with(&me.summary, SER_OPT));
} }
} }

View file

@ -1,15 +1,12 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields, Value}; use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_binding::cached_field; use yazi_shim::mlua::UserDataFieldsExt;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
pub(super) struct Which { pub(super) struct Which {
inner: PtrCell<yazi_core::which::Which>, inner: PtrCell<yazi_core::which::Which>,
v_tx: Option<Value>,
v_cands: Option<Value>,
} }
impl Deref for Which { impl Deref for Which {
@ -20,17 +17,17 @@ impl Deref for Which {
impl Which { impl Which {
pub(super) fn make(inner: &yazi_core::which::Which) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_core::which::Which) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into(), v_tx: None, v_cands: None }) Lives::scoped_userdata(Self { inner: inner.into() })
} }
} }
impl UserData for Which { impl UserData for Which {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, tx, |_, me| Ok(me.tx.clone().map(yazi_binding::MpscUnboundedTx))); 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| {
cached_field!(fields, cands, |lua, me| { lua.create_sequence_from(me.inner.cands.iter().cloned())
lua.create_sequence_from(me.inner.cands.iter().cloned().map(yazi_binding::ChordCow))
}); });
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("active", |_, me| Ok(me.inner.active));
fields.add_field_method_get("silent", |_, me| Ok(me.inner.silent)); fields.add_field_method_get("silent", |_, me| Ok(me.inner.silent));

View file

@ -1,7 +1,9 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods}; use mlua::{AnyUserData, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Iter, get_metatable}; use yazi_binding::Iter;
use yazi_fs::file::File;
use yazi_shim::mlua::get_metatable;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
@ -23,7 +25,7 @@ impl Yanked {
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self {
inner, inner,
iter: Lives::scoped_userdata(Iter::new( iter: Lives::scoped_userdata(Iter::new(
inner.as_static().iter().map(yazi_binding::Url::new), inner.as_static().iter().map(File::from),
Some(inner.len()), Some(inner.len()),
))?, ))?,
}) })

View file

@ -14,6 +14,7 @@ impl Actor for Arrow {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let tab = cx.tab_mut(); let tab = cx.tab_mut();
let old = tab.current.cursor;
if !tab.current.arrow(form.step) { if !tab.current.arrow(form.step) {
succ!(); succ!();
} }
@ -22,9 +23,8 @@ impl Actor for Arrow {
tab.current.retrace(); tab.current.retrace();
// Visual selection // Visual selection
if let Some((start, items)) = tab.mode.visual_mut() { if let Some(visual) = tab.mode.visual_mut() {
let end = tab.current.cursor; visual.arrow(form.step, old, tab.current.cursor);
*items = (start.min(end)..=end.max(start)).collect();
} }
act!(mgr:hover, cx)?; act!(mgr:hover, cx)?;

View file

@ -0,0 +1,175 @@
use std::{fmt::{self, Display}, io::{self, Read, Write}, path::{MAIN_SEPARATOR, Path}};
use anyhow::{Result, anyhow};
use scopeguard::defer;
use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRuleArc};
use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}};
use yazi_macro::{succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt};
use yazi_shared::{data::Data, strand::Strand, url::{UrlBuf, UrlLike}};
use yazi_shim::path::CROSS_SEPARATOR;
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_tty::{TTY, sequence::EraseScreen};
use yazi_vfs::engine;
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
pub struct BulkCreate;
impl Actor for BulkCreate {
type Form = VoidForm;
const NAME: &str = "bulk_create";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let Some(opener) = Self::opener() else {
succ!(NotifyProxy::push_warn("Bulk create", "No text opener found"));
};
let cwd = cx.cwd().clone();
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk-create");
let file = engine::create_new(&tmp).await?.file().await?;
defer! {
let tmp = tmp.clone();
tokio::spawn(async move {
Local::regular(&tmp).remove_file().await
});
}
TasksProxy::process_exec(ShellOpt {
cwd: cwd.clone(),
cmd: Splatter::new(&[file]).splat(&opener.run),
block: opener.block,
orphan: opener.orphan,
})
.await;
let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume());
AppProxy::stop().await;
let content = Local::regular(&tmp).read_to_string().await?;
Self::r#do(cwd, content.lines().filter_map(Entry::parse).collect()).await
});
succ!()
}
}
impl BulkCreate {
async fn r#do(cwd: UrlBuf, todo: Vec<Entry<'_>>) -> Result<()> {
writef!(TTY.writer(), "{EraseScreen}\n")?;
if todo.is_empty() {
return Ok(());
} else if !Self::ask_continue(&todo, None)? {
return Ok(()); // TODO: support `bulk_exit`?
}
let _permit = WATCHER.acquire().await.unwrap();
let (mut failed, mut succeeded) = (vec![], Vec::with_capacity(todo.len()));
for entry in todo {
let Ok(dist) = cwd.try_join(entry.path) else {
failed.push((entry, anyhow!("Invalid path")));
continue;
};
let result: io::Result<()> = if entry.is_dir {
engine::create_dir_all(&dist).await
} else if let Some(parent) = dist.parent() {
engine::create_dir_all(parent).await.ok();
engine::create_new(&dist).await.map(|_| ())
} else {
Err(io::Error::other("No parent directory"))
};
if let Err(e) = result {
failed.push((entry, e.into()));
} else if let Ok(f) = engine::file(dist).await {
succeeded.push(f);
} else {
failed.push((entry, anyhow!("Failed to retrieve file info")));
}
}
if !succeeded.is_empty() {
// err!(Pubsub::pub_after_bulk_create(it)); // FIXME
FilesOp::create(succeeded);
}
drop(_permit);
if !failed.is_empty() {
Self::output_failed(failed).await?;
}
Ok(())
}
fn opener() -> Option<OpenerRuleArc> {
YAZI
.open
.match_dummy(Path::new("bulk-create.txt"), "text/plain")
.and_then(|r| YAZI.opener.block(&r))
}
fn ask_continue(todo: &[Entry], decision: Option<bool>) -> Result<bool> {
if let Some(decision) = decision {
return Ok(decision);
}
{
let mut w = TTY.lockout();
for entry in todo {
writeln!(w, "{entry}")?;
}
write!(w, "Continue to create? (y/N): ")?;
w.flush()?;
}
let mut buf = [0; 10];
_ = TTY.reader().read(&mut buf)?;
Ok(buf[0] == b'y' || buf[0] == b'Y')
}
async fn output_failed(failed: Vec<(Entry<'_>, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout();
writeln!(stdout, "{EraseScreen}")?;
writeln!(stdout, "Failed to create:")?;
for (entry, err) in failed {
writeln!(stdout, "{entry}: {err}")?;
}
writeln!(stdout, "\nPress ENTER to exit")?;
stdout.flush()?;
TTY.reader().read_exact(&mut [0])?;
Ok(())
}
}
// --- Entry
struct Entry<'a> {
path: Strand<'a>,
is_dir: bool,
}
impl<'a> Entry<'a> {
fn parse(s: &'a str) -> Option<Self> {
let (path, is_dir) = match s.strip_suffix(CROSS_SEPARATOR) {
Some(p) => (p, true),
None => (s, false),
};
(!path.is_empty()).then_some(Self { path: path.into(), is_dir })
}
}
impl Display for Entry<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_dir {
write!(f, "{}{MAIN_SEPARATOR}", self.path.display())
} else {
self.path.display().fmt(f)
}
}
}

View file

@ -1,22 +1,21 @@
use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path, sync::Arc}; use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use crossterm::{execute, style::Print};
use hashbrown::HashMap; use hashbrown::HashMap;
use scopeguard::defer; use scopeguard::defer;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use yazi_binding::Permit; use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRule}; use yazi_config::{YAZI, opener::OpenerRuleArc};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}}; use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::Local}, max_common_root, path::skip_url};
use yazi_macro::{err, succ}; use yazi_macro::{err, succ, writef};
use yazi_parser::VoidForm; use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy; use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy}; use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt};
use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_term::YIELD_TO_SUBPROCESS; use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_tty::TTY; use yazi_tty::{TTY, sequence::EraseScreen};
use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_vfs::{engine::{self, Demand}, maybe_exists};
use yazi_watcher::WATCHER; use yazi_watcher::WATCHER;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -33,7 +32,7 @@ impl Actor for BulkRename {
succ!(NotifyProxy::push_warn("Bulk rename", "No text opener found")); 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() { if selected.is_empty() {
succ!(NotifyProxy::push_warn("Bulk rename", "No files selected")); succ!(NotifyProxy::push_warn("Bulk rename", "No files selected"));
} }
@ -45,15 +44,10 @@ impl Actor for BulkRename {
let cwd = cx.cwd().clone(); let cwd = cx.cwd().clone();
let batcher = cx.core.mgr.batcher.clone(); let batcher = cx.core.mgr.batcher.clone();
tokio::spawn(async move { tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk"); let tmp = YAZI.preview.tmpfile("bulk-rename");
Gate::default() let mut rw = Demand::default().write(true).create_new(true).open(&tmp).await?;
.write(true) rw.write_all(old.join(Strand::Utf8("\n")).encoded_bytes()).await?;
.create_new(true)
.open(&tmp)
.await?
.write_all(old.join(Strand::Utf8("\n")).encoded_bytes())
.await?;
defer! { defer! {
let tmp = tmp.clone(); let tmp = tmp.clone();
@ -64,13 +58,12 @@ impl Actor for BulkRename {
} }
batcher.prime(&tmp); batcher.prime(&tmp);
TasksProxy::process_exec( TasksProxy::process_exec(ShellOpt {
cwd.into(), cwd,
Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run), cmd: Splatter::new(&[rw.into_file().await?]).splat(&opener.run),
vec![UrlCow::default(), UrlBuf::from(&tmp).into()], block: opener.block,
opener.block, orphan: opener.orphan,
opener.orphan, })
)
.await; .await;
let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume()); let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume());
@ -100,11 +93,10 @@ impl BulkRename {
selected: Vec<UrlBuf>, selected: Vec<UrlBuf>,
decision: Option<bool>, decision: Option<bool>,
) -> Result<()> { ) -> Result<()> {
terminal_clear(TTY.writer())?; writef!(TTY.writer(), "{EraseScreen}\n")?;
if old.len() != new.len() { if old.len() != new.len() {
#[rustfmt::skip] #[rustfmt::skip]
let s = format!("Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len()); writef!(TTY.writer(), "Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len())?;
execute!(TTY.writer(), Print(s))?;
TTY.reader().read_exact(&mut [0])?; TTY.reader().read_exact(&mut [0])?;
return Ok(()); return Ok(());
@ -130,11 +122,11 @@ impl BulkRename {
continue; continue;
}; };
if maybe_exists(&new).await && !provider::must_identical(&old, &new).await { if maybe_exists(&new).await && !engine::must_identical(&old, &new).await {
failed.push((o, n, anyhow!("Destination already exists"))); failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = provider::rename(&old, &new).await { } else if let Err(e) = engine::rename(&old, &new).await {
failed.push((o, n, e.into())); failed.push((o, n, e.into()));
} else if let Ok(f) = File::new(new).await { } else if let Ok(f) = engine::file(new).await {
succeeded.insert(old, f); succeeded.insert(old, f);
} else { } else {
failed.push((o, n, anyhow!("Failed to retrieve file info"))); failed.push((o, n, anyhow!("Failed to retrieve file info")));
@ -143,7 +135,7 @@ impl BulkRename {
if !succeeded.is_empty() { if !succeeded.is_empty() {
let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url())); let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url()));
err!(Pubsub::pub_after_bulk(it)); err!(Pubsub::pub_after_bulk_rename(it));
FilesOp::rename(succeeded); FilesOp::rename(succeeded);
} }
drop(permit); drop(permit);
@ -154,7 +146,7 @@ impl BulkRename {
Ok(()) Ok(())
} }
fn opener() -> Option<Arc<OpenerRule>> { fn opener() -> Option<OpenerRuleArc> {
YAZI YAZI
.open .open
.match_dummy(Path::new("bulk-rename.txt"), "text/plain") .match_dummy(Path::new("bulk-rename.txt"), "text/plain")
@ -186,7 +178,7 @@ impl BulkRename {
async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout(); let mut stdout = TTY.lockout();
terminal_clear(&mut *stdout)?; writeln!(stdout, "{EraseScreen}")?;
writeln!(stdout, "Failed to rename:")?; writeln!(stdout, "Failed to rename:")?;
for (old, new, err) in failed { for (old, new, err) in failed {

View file

@ -3,15 +3,15 @@ use std::{mem, time::Duration};
use anyhow::Result; use anyhow::Result;
use tokio::pin; use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::popup::InputCfg; use yazi_config::YAZI;
use yazi_core::mgr::CdSource; use yazi_core::mgr::CdSource;
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, path::{clean_url, expand_url}}; use yazi_fs::{FilesOp, path::{clean_url, expand_url}};
use yazi_macro::{act, err, render, succ}; use yazi_macro::{act, err, input, render, succ};
use yazi_parser::mgr::CdForm; use yazi_parser::mgr::CdForm;
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; use yazi_proxy::{CmpProxy, MgrProxy};
use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, provider}; use yazi_vfs::engine;
use yazi_widgets::input::InputEvent; use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -62,8 +62,8 @@ impl Actor for Cd {
} }
impl Cd { impl Cd {
fn cd_interactive(cx: &Ctx) -> Result<Data> { fn cd_interactive(cx: &mut Ctx) -> Result<Data> {
let input = InputProxy::show(InputCfg::cd(cx.cwd().as_url())); let input = input!(cx, YAZI.input.cd(cx.cwd().as_url()))?;
tokio::spawn(async move { tokio::spawn(async move {
let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50)); let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50));
@ -73,16 +73,16 @@ impl Cd {
match result { match result {
InputEvent::Submit(s) => { InputEvent::Submit(s) => {
let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return }; let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return };
let Ok(url) = provider::absolute(&url).await else { return }; let Ok(url) = engine::absolute(&url).await else { return };
let url = clean_url(url); let url = clean_url(url);
let Ok(file) = File::new(&url).await else { return }; let Ok(file) = engine::file(&url).await else { return };
if file.is_dir() { if file.is_dir() {
return MgrProxy::cd(&url, CdSource::Cd); return MgrProxy::cd(&url, CdSource::Cd);
} }
if let Some(p) = url.parent() { if let Some((p, k)) = url.pair2() {
FilesOp::Upserting(p.into(), [(url.urn().into(), file)].into()).emit(); FilesOp::Upserting(p.into(), [(k.into(), file)].into()).emit();
} }
MgrProxy::reveal(url); MgrProxy::reveal(url);
} }

View file

@ -21,6 +21,6 @@ impl Actor for Close {
} }
fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> { fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> {
Some(SparkKind::KeyClose).filter(|_| cx.source().is_key()) cx.source().is_key().then_some(SparkKind::KeyClose)
} }
} }

View file

@ -18,9 +18,9 @@ impl Actor for Copy {
let mut s = Vec::<u8>::new(); let mut s = Vec::<u8>::new();
let mut it = if form.hovered { let mut it = if form.hovered {
Box::new(cx.hovered().map(|h| &h.url).into_iter()) Box::new(cx.hovered_url().into_iter())
} else { } else {
cx.tab().selected_or_hovered() cx.tab().selected_or_hovered_urls()
} }
.peekable(); .peekable();

View file

@ -1,13 +1,16 @@
use std::pin::Pin;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use yazi_config::popup::{ConfirmCfg, InputCfg}; use futures::{Stream, StreamExt};
use yazi_fs::{File, FilesOp}; use tokio_stream::wrappers::UnboundedReceiverStream;
use yazi_macro::{ok_or_not_found, succ}; 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_parser::mgr::CreateForm;
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; use yazi_proxy::{ConfirmProxy, MgrProxy};
use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, strand::{StrandBuf, StrandLike}, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_vfs::{VfsFile, engine};
use yazi_watcher::WATCHER; use yazi_watcher::WATCHER;
use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -18,28 +21,37 @@ impl Actor for Create {
const NAME: &str = "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 cwd = cx.cwd().to_owned();
let mut input = InputProxy::show(InputCfg::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 { 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() { if name.is_empty() {
return; return Ok(());
} }
let Ok(new) = cwd.try_join(&name) else { let Ok(new) = cwd.try_join(&name) else {
return; bail!("Failed to join new name with CWD");
}; };
if !form.force if !force
&& maybe_exists(&new).await && let Some(file) = File::maybe_new(&new).await?
&& !ConfirmProxy::show(ConfirmCfg::overwrite(&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!(); succ!();
} }
@ -50,26 +62,26 @@ impl Create {
let _permit = WATCHER.acquire().await.unwrap(); let _permit = WATCHER.acquire().await.unwrap();
if dir { if dir {
provider::create_dir_all(&new).await?; engine::create_dir_all(&new).await?;
} else if let Ok(real) = provider::casefold(&new).await } else if let Ok(real) = engine::casefold(&new).await
&& let Some((parent, urn)) = real.pair() && let Some((parent, key)) = real.pair2()
{ {
ok_or_not_found!(provider::remove_file(&new).await); ok_or_not_found!(engine::remove_file(&new).await);
FilesOp::Deleting(parent.into(), [urn.into()].into()).emit(); FilesOp::Deleting(parent.into(), [key.into()].into()).emit();
provider::create(&new).await?; engine::create(&new).await?;
} else if let Some(parent) = new.parent() { } else if let Some(parent) = new.parent() {
provider::create_dir_all(parent).await.ok(); engine::create_dir_all(parent).await.ok();
ok_or_not_found!(provider::remove_file(&new).await); ok_or_not_found!(engine::remove_file(&new).await);
provider::create(&new).await?; engine::create(&new).await?;
} else { } else {
bail!("Cannot create file at root"); bail!("Cannot create file at root");
} }
if let Ok(real) = provider::casefold(&new).await if let Ok(real) = engine::casefold(&new).await
&& let Some((parent, urn)) = real.pair() && let Some((parent, key)) = real.pair2()
{ {
let file = File::new(&real).await?; let file = engine::file(&real).await?;
FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()).emit(); FilesOp::Upserting(parent.into(), [(key.into(), file)].into()).emit();
MgrProxy::reveal(&real); MgrProxy::reveal(&real);
} }

View file

@ -4,7 +4,7 @@ use yazi_macro::succ;
use yazi_parser::VoidForm; use yazi_parser::VoidForm;
use yazi_proxy::MgrProxy; use yazi_proxy::MgrProxy;
use yazi_shared::{data::Data, url::UrlLike}; use yazi_shared::{data::Data, url::UrlLike};
use yazi_vfs::provider; use yazi_vfs::engine;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -24,7 +24,7 @@ impl Actor for Displace {
let from = cx.cwd().to_owned(); let from = cx.cwd().to_owned();
tokio::spawn(async move { tokio::spawn(async move {
MgrProxy::displace_do(tab, DisplaceOpt { MgrProxy::displace_do(tab, DisplaceOpt {
to: provider::canonicalize(&from).await.map_err(Into::into), to: engine::canonicalize(&from).await.map_err(Into::into),
from, from,
}); });
}); });

View file

@ -4,12 +4,12 @@ use anyhow::Result;
use futures::{StreamExt, stream::FuturesUnordered}; use futures::{StreamExt, stream::FuturesUnordered};
use hashbrown::HashSet; use hashbrown::HashSet;
use yazi_core::mgr::OpenOpt; use yazi_core::mgr::OpenOpt;
use yazi_fs::{File, FsScheme, provider::{Provider, local::Local}}; use yazi_fs::{FsSpec, engine::{Engine, local::Local}};
use yazi_macro::succ; use yazi_macro::succ;
use yazi_parser::mgr::DownloadForm; use yazi_parser::mgr::DownloadForm;
use yazi_proxy::MgrProxy; use yazi_proxy::MgrProxy;
use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}};
use yazi_vfs::VfsFile; use yazi_vfs::engine;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -42,7 +42,7 @@ impl Actor for Download {
continue; continue;
} }
let Ok(f) = File::new(&url).await else { continue }; let Ok(f) = engine::file(&url).await else { continue };
urls.push(url); urls.push(url);
files.push(f); files.push(f);
@ -74,7 +74,7 @@ impl Actor for Download {
impl Download { impl Download {
async fn prepare(urls: &[UrlBuf]) { async fn prepare(urls: &[UrlBuf]) {
let roots: HashSet<_> = urls.iter().filter_map(|u| u.scheme().cache()).collect(); let roots: HashSet<_> = urls.iter().filter_map(|u| u.auth().cache()).collect();
for mut root in roots { for mut root in roots {
root.push("%lock"); root.push("%lock");
Local::regular(&root).create_dir_all().await.ok(); Local::regular(&root).create_dir_all().await.ok();

View file

@ -67,15 +67,16 @@ impl Actor for EscapeVisual {
let tab = cx.tab_mut(); let tab = cx.tab_mut();
let select = tab.mode.is_select(); 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!(); render!();
let urls: Vec<_> = let files: Vec<_> = indices.into_iter().filter_map(|i| tab.current.entries.get(i)).collect();
indices.into_iter().filter_map(|i| tab.current.files.get(i)).map(|f| &f.url).collect();
if !select { if !select {
tab.selected.remove_many(urls); tab.selected.remove_many(files);
} else if urls.len() != tab.selected.add_many(urls) { } else if files.len() != tab.selected.add_many(files) {
NotifyProxy::push_warn( NotifyProxy::push_warn(
"Escape visual mode", "Escape visual mode",
"Some files cannot be selected, due to path nesting conflict.", "Some files cannot be selected, due to path nesting conflict.",
@ -96,7 +97,7 @@ impl Actor for EscapeFilter {
const NAME: &str = "escape_filter"; const NAME: &str = "escape_filter";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> { 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); succ!(false);
} }

View file

@ -3,11 +3,11 @@ use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use tokio::pin; use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::popup::InputCfg; use yazi_config::YAZI;
use yazi_core::mgr::FilterOpt; use yazi_core::mgr::FilterOpt;
use yazi_macro::succ; use yazi_macro::{input, succ};
use yazi_parser::mgr::FilterForm; use yazi_parser::mgr::FilterForm;
use yazi_proxy::{InputProxy, MgrProxy}; use yazi_proxy::MgrProxy;
use yazi_shared::{Debounce, data::Data}; use yazi_shared::{Debounce, data::Data};
use yazi_widgets::input::InputEvent; use yazi_widgets::input::InputEvent;
@ -20,8 +20,8 @@ impl Actor for Filter {
const NAME: &str = "filter"; const NAME: &str = "filter";
fn act(_: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
let input = InputProxy::show(InputCfg::filter()); let input = input!(cx, YAZI.input.filter())?;
tokio::spawn(async move { tokio::spawn(async move {
let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50)); let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50));

View file

@ -16,10 +16,10 @@ impl Actor for FilterDo {
fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) }; 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()); let hovered = cx.hovered().map(|f| f.entry_key().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) { if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) {
act!(mgr:hover, cx, hovered)?; act!(mgr:hover, cx, hovered)?;
act!(mgr:peek, cx)?; act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?; act!(mgr:watch, cx)?;

View file

@ -3,11 +3,11 @@ use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use tokio::pin; use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::popup::InputCfg; use yazi_config::YAZI;
use yazi_core::mgr::FindDoOpt; use yazi_core::mgr::FindDoOpt;
use yazi_macro::succ; use yazi_macro::{input, succ};
use yazi_parser::mgr::FindForm; use yazi_parser::mgr::FindForm;
use yazi_proxy::{InputProxy, MgrProxy}; use yazi_proxy::MgrProxy;
use yazi_shared::{Debounce, data::Data}; use yazi_shared::{Debounce, data::Data};
use yazi_widgets::input::InputEvent; use yazi_widgets::input::InputEvent;
@ -20,8 +20,8 @@ impl Actor for Find {
const NAME: &str = "find"; const NAME: &str = "find";
fn act(_: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let input = InputProxy::show(InputCfg::find(form.prev)); let input = input!(cx, YAZI.input.find(form.prev))?;
tokio::spawn(async move { tokio::spawn(async move {
let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50)); let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50));

View file

@ -18,9 +18,9 @@ impl Actor for FindArrow {
render!(finder.catchup(&tab.current)); render!(finder.catchup(&tab.current));
let offset = if form.prev { let offset = if form.prev {
finder.prev(&tab.current.files, tab.current.cursor, false) finder.prev(&tab.current.entries, tab.current.cursor, false)
} else { } else {
finder.next(&tab.current.files, tab.current.cursor, false) finder.next(&tab.current.entries, tab.current.cursor, false)
}; };
if let Some(offset) = offset { if let Some(offset) = offset {

View file

@ -24,9 +24,9 @@ impl Actor for FindDo {
} }
let step = if opt.prev { let step = if opt.prev {
finder.prev(&cx.current().files, cx.current().cursor, true) finder.prev(&cx.current().entries, cx.current().cursor, true)
} else { } else {
finder.next(&cx.current().files, cx.current().cursor, true) finder.next(&cx.current().entries, cx.current().cursor, true)
}; };
if let Some(step) = step { if let Some(step) = step {

View file

@ -16,7 +16,7 @@ impl Actor for Follow {
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let Some(file) = cx.hovered() else { succ!() }; let Some(file) = cx.hovered() else { succ!() };
let Some(link_to) = &file.link_to else { succ!() }; let Some(link_to) = file.extra.link_to() else { succ!() };
let Some(parent) = file.url.parent() else { succ!() }; let Some(parent) = file.url.parent() else { succ!() };
let Ok(joined) = parent.try_join(link_to) else { succ!() }; let Ok(joined) = parent.try_join(link_to) else { succ!() };
act!(mgr:reveal, cx, (clean_url(joined), CdSource::Follow)) act!(mgr:reveal, cx, (clean_url(joined), CdSource::Follow))

View file

@ -4,6 +4,7 @@ use yazi_fs::FolderStage;
use yazi_macro::{act, render, render_and, succ}; use yazi_macro::{act, render, render_and, succ};
use yazi_parser::{mgr::HiddenForm, spark::SparkKind}; use yazi_parser::{mgr::HiddenForm, spark::SparkKind};
use yazi_shared::{Source, data::Data}; use yazi_shared::{Source, data::Data};
use yazi_shim::OptionExt;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -18,21 +19,19 @@ impl Actor for Hidden {
let state = form.state.bool(cx.tab().pref.show_hidden); let state = form.state.bool(cx.tab().pref.show_hidden);
cx.tab_mut().pref.show_hidden = state; cx.tab_mut().pref.show_hidden = state;
let hovered = cx.hovered().map(|f| f.urn().to_owned()); let hovered = cx.hovered().map(|f| f.entry_key()).owned();
let apply = |f: &mut Folder| { let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading { if f.stage == FolderStage::Loading {
render!(); render!();
false false
} else { } else {
f.files.set_show_hidden(state); f.entries.set_show_hidden(state);
render_and!(f.files.catchup_revision()) render_and!(f.entries.catchup_revision())
} }
}; };
// Apply to CWD and parent // Apply to CWD and parent
if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply)) if apply(cx.current_mut()) | cx.parent_mut().is_some_and(apply) {
&& (a | b)
{
act!(mgr:hover, cx)?; act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?; act!(mgr:update_paged, cx)?;
} }
@ -43,7 +42,7 @@ impl Actor for Hidden {
{ {
render!(h.repos(None)); render!(h.repos(None));
act!(mgr:peek, cx, true)?; act!(mgr:peek, cx, true)?;
} else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { } else if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) {
act!(mgr:peek, cx)?; act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?; act!(mgr:watch, cx)?;
} }

View file

@ -18,25 +18,25 @@ impl Actor for Hover {
// Parent should always track CWD // Parent should always track CWD
if let Some(p) = &mut tab.parent { if let Some(p) = &mut tab.parent {
render!(p.repos(tab.current.url.try_strip_prefix(&p.url).ok())); render!(p.repos(Some(tab.current.url.entry_key())));
} }
// Repos CWD // Repos CWD
render!(tab.current.repos(form.urn.as_ref().map(Into::into))); render!(tab.current.repos(form.key.as_ref().map(Into::into)));
// Turn on tracing // Turn on tracing
if let (Some(h), Some(u)) = (tab.hovered(), form.urn) if let (Some(h), Some(key)) = (tab.hovered(), form.key)
&& h.urn() == u && h.entry_key() == key
{ {
// `hover(Some)` occurs after user actions, such as create, rename, reveal, etc. // `hover(Some)` occurs after user actions, such as create, rename, reveal, etc.
// At this point, it's intuitive to track the file location regardless. // At this point, it's intuitive to track the entry regardless.
tab.current.trace = Some(u.clone()); tab.current.trace = Some(key.clone());
cx.tasks.scheduler.behavior.reset(); cx.tasks.scheduler.behavior.reset();
} }
// Publish through DDS // Publish through DDS
let tab = tab!(cx); let tab = tab!(cx);
err!(Pubsub::pub_after_hover(tab.id, tab.hovered().map(|h| &h.url))); err!(Pubsub::pub_after_hover(tab.id, tab.hovered_url()));
succ!(); succ!();
} }
} }

View file

@ -1,6 +1,7 @@
yazi_macro::mod_flat!( yazi_macro::mod_flat!(
arrow arrow
back back
bulk_create
bulk_exit bulk_exit
bulk_rename bulk_rename
cd cd
@ -32,6 +33,7 @@ yazi_macro::mod_flat!(
quit quit
refresh refresh
remove remove
remove_do
rename rename
reveal reveal
search search

View file

@ -3,12 +3,11 @@ use futures::StreamExt;
use hashbrown::HashSet; use hashbrown::HashSet;
use yazi_boot::ARGS; use yazi_boot::ARGS;
use yazi_core::mgr::OpenDoOpt; use yazi_core::mgr::OpenDoOpt;
use yazi_fs::File;
use yazi_macro::{act, succ}; use yazi_macro::{act, succ};
use yazi_parser::mgr::OpenForm; use yazi_parser::mgr::OpenForm;
use yazi_proxy::MgrProxy; use yazi_proxy::MgrProxy;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_vfs::VfsFile; use yazi_vfs::engine;
use crate::{Actor, Ctx, mgr::Quit}; use crate::{Actor, Ctx, mgr::Quit};
@ -24,10 +23,10 @@ impl Actor for Open {
succ!(if !opt.targets.is_empty() { succ!(if !opt.targets.is_empty() {
Quit::with_selected(opt.targets) Quit::with_selected(opt.targets)
} else if opt.hovered { } else if opt.hovered {
Quit::with_selected(cx.hovered().map(|h| &h.url)) Quit::with_selected(cx.hovered_url())
} else { } else {
act!(mgr:escape_visual, cx)?; act!(mgr:escape_visual, cx)?;
Quit::with_selected(cx.tab().selected_or_hovered()) Quit::with_selected(cx.tab().selected_or_hovered_urls())
}); });
} }
@ -36,7 +35,7 @@ impl Actor for Open {
cx.hovered().map(|h| vec![h.url.clone()]).unwrap_or_default() cx.hovered().map(|h| vec![h.url.clone()]).unwrap_or_default()
} else { } else {
act!(mgr:escape_visual, cx)?; 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() { if opt.targets.is_empty() {
@ -59,7 +58,7 @@ impl Actor for Open {
let it = futures::stream::iter(opt.targets) let it = futures::stream::iter(opt.targets)
.enumerate() .enumerate()
.map(|(i, url)| async move { File::new(url).await.ok().map(|file| (i, file)) }) .map(|(i, url)| async move { engine::file(url).await.ok().map(|file| (i, file)) })
.buffered(3) .buffered(3)
.filter_map(|item| async move { item }); .filter_map(|item| async move { item });

View file

@ -1,12 +1,13 @@
use anyhow::Result; use anyhow::Result;
use hashbrown::HashMap; use hashbrown::HashMap;
use indexmap::IndexSet; use indexmap::IndexSet;
use yazi_config::{YAZI, popup::PickCfg}; use yazi_config::{YAZI, opener::OpenerRule};
use yazi_fs::{Splatter, file::File};
use yazi_macro::succ; use yazi_macro::succ;
use yazi_parser::mgr::OpenDoForm; use yazi_parser::mgr::OpenDoForm;
use yazi_proxy::{PickProxy, TasksProxy}; use yazi_proxy::{PickProxy, TasksProxy};
use yazi_scheduler::process::ProcessOpt; use yazi_scheduler::process::ShellOpt;
use yazi_shared::{data::Data, url::{UrlBuf, UrlCow}}; use yazi_shared::{data::Data, url::UrlBuf};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -31,7 +32,7 @@ impl Actor for OpenDo {
if targets.is_empty() { if targets.is_empty() {
succ!(); succ!();
} else if !opt.interactive { } else if !opt.interactive {
succ!(Self::match_and_open(cx, opt.cwd, targets)); succ!(Self::match_and_open(opt.cwd, targets));
} }
let openers: IndexSet<_> = let openers: IndexSet<_> =
@ -40,21 +41,11 @@ impl Actor for OpenDo {
succ!(); succ!();
} }
let pick = PickProxy::show(PickCfg::open(openers.iter().map(|o| o.desc()).collect())); let pick = PickProxy::show(YAZI.pick.open(openers.iter().map(|o| o.desc()).collect()));
let urls: Vec<_> = [UrlCow::default()] let files: Vec<_> = targets.into_iter().map(|(file, _)| file).collect();
.into_iter()
.chain(targets.into_iter().map(|(file, _)| file.url.into()))
.collect();
tokio::spawn(async move { tokio::spawn(async move {
if let Some(choice) = pick.await { if let Some(choice) = pick.await {
TasksProxy::open_shell_compat(ProcessOpt { Self::open_with(&openers[choice], &opt.cwd, &files);
cwd: opt.cwd,
cmd: openers[choice].run.clone().into(),
args: urls,
block: openers[choice].block,
orphan: openers[choice].orphan,
spread: openers[choice].spread,
});
} }
}); });
succ!(); succ!();
@ -62,24 +53,28 @@ impl Actor for OpenDo {
} }
impl OpenDo { impl OpenDo {
// TODO: remove fn match_and_open(cwd: UrlBuf, targets: Vec<(File, &str)>) {
fn match_and_open(cx: &Ctx, cwd: UrlBuf, targets: Vec<(yazi_fs::File, &str)>) { let mut openers: HashMap<_, Vec<_>> = Default::default();
let mut openers = HashMap::new();
for (file, mime) in targets { for (file, mime) in targets {
if let Some(open) = YAZI.open.matches(&file, mime) if let Some(open) = YAZI.open.matches(&file, mime)
&& let Some(opener) = YAZI.opener.first(&open) && let Some(opener) = YAZI.opener.first(&open)
{ {
openers.entry(opener).or_insert_with(|| vec![UrlCow::default()]).push(file.url.into()); openers.entry(opener).or_default().push(file);
} }
} }
for (opener, args) in openers { for (opener, files) in openers {
cx.tasks.open_shell_compat(ProcessOpt { Self::open_with(&opener, &cwd, &files);
cwd: cwd.clone(), }
cmd: opener.run.clone().into(), }
args,
block: opener.block, fn open_with(opener: &OpenerRule, cwd: &UrlBuf, files: &[File]) {
let size = if opener.spread { files.len().max(1) } else { 1 };
for files in files.chunks(size) {
TasksProxy::process_open(ShellOpt {
cwd: cwd.clone(),
cmd: Splatter::new(files).splat(&opener.run),
block: opener.block,
orphan: opener.orphan, orphan: opener.orphan,
spread: opener.spread,
}); });
} }
} }

View file

@ -20,7 +20,7 @@ impl Actor for Paste {
if mgr.yanked.cut { if mgr.yanked.cut {
cx.core.tasks.file_cut(&mgr.yanked, dest, form.force); 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) act!(mgr:unyank, cx)
} else { } else {
succ!(cx.core.tasks.file_copy(&mgr.yanked, dest, form.force, form.follow)); succ!(cx.core.tasks.file_copy(&mgr.yanked, dest, form.force, form.follow));

View file

@ -21,18 +21,14 @@ impl Actor for Peek {
} }
let mime = cx.mgr.mimetype.owned(&hovered.url).unwrap_or_default(); let mime = cx.mgr.mimetype.owned(&hovered.url).unwrap_or_default();
let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.cha)); let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.file.clone()));
if !cx.tab().preview.same_url(&hovered.url) { if !cx.tab().preview.same_url(&hovered.url) {
cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_default(); cx.tab_mut().preview.skip = folder.as_ref().map(|f| f.0).unwrap_or_default();
} }
if !cx.tab().preview.same_file(&hovered, &mime) { if !cx.tab().preview.same_file(&hovered, &mime) {
cx.tab_mut().preview.reset(); cx.tab_mut().preview.reset();
} }
if !cx.tab().preview.same_folder(&hovered.url) {
cx.tab_mut().preview.folder_lock = None;
}
if matches!(form.only_if, Some(u) if u != hovered.url) { if matches!(form.only_if, Some(u) if u != hovered.url) {
succ!(); succ!();
} }
@ -46,11 +42,13 @@ impl Actor for Peek {
} }
} }
if hovered.is_dir() { if let Some((_, file)) = folder {
cx.tab_mut().preview.go_folder(hovered, folder.map(|(_, cha)| cha), mime, form.force); cx.core.mgr.watcher.refresher.refresh([file]);
} else { } else if hovered.is_dir() {
cx.tab_mut().preview.go(hovered, mime, form.force); cx.core.mgr.watcher.refresher.load(&hovered);
} }
cx.tab_mut().preview.go(hovered, mime, form.force);
succ!(); succ!();
} }
} }

View file

@ -59,7 +59,7 @@ impl Actor for Quit {
} }
fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> { fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> {
Some(SparkKind::KeyQuit).filter(|_| cx.source().is_key()) cx.source().is_key().then_some(SparkKind::KeyQuit)
} }
} }

View file

@ -1,10 +1,8 @@
use anyhow::Result; use anyhow::Result;
use yazi_core::tab::Folder; use yazi_fs::CWD;
use yazi_fs::{CWD, Files, FilesOp, cha::Cha};
use yazi_macro::{act, succ}; use yazi_macro::{act, succ};
use yazi_parser::VoidForm; use yazi_parser::VoidForm;
use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; use yazi_shared::{data::Data, url::UrlLike};
use yazi_vfs::{VfsFiles, VfsFilesOp};
use yazi_watcher::MgrProxy; use yazi_watcher::MgrProxy;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -19,17 +17,19 @@ impl Actor for Refresh {
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
CWD.set(cx.cwd(), Self::cwd_changed); CWD.set(cx.cwd(), Self::cwd_changed);
if let Some(p) = cx.parent() { cx.core.mgr.watcher.refresher.refresh(
Self::trigger_dirs(&[cx.current(), p]); [Some(cx.current()), cx.parent()]
} else { .into_iter()
Self::trigger_dirs(&[cx.current()]); .flatten()
} .filter(|f| f.url.is_absolute() && !f.url.is_search())
.map(|f| &f.file),
);
act!(mgr:peek, cx)?; act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?; act!(mgr:watch, cx)?;
act!(mgr:update_paged, cx)?; act!(mgr:update_paged, cx)?;
cx.tasks.prework_sorted(&cx.current().files); cx.tasks.prework_sorted(&cx.current().entries);
succ!(); succ!();
} }
} }
@ -40,26 +40,4 @@ impl Refresh {
MgrProxy::watch(); MgrProxy::watch();
} }
} }
// 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 };
match Files::from_dir_bulk(&dir).await {
Ok(files) => FilesOp::Full(dir, files, cha).emit(),
Err(e) => FilesOp::issue_error(&dir, e).await,
}
}
let futs: Vec<_> = folders
.iter()
.filter(|&f| f.url.is_absolute() && f.url.is_internal())
.map(|&f| go(f.url.clone(), f.cha))
.collect();
if !futs.is_empty() {
tokio::spawn(futures::future::join_all(futs));
}
}
} }

View file

@ -1,8 +1,9 @@
use anyhow::Result; use anyhow::Result;
use yazi_config::popup::ConfirmCfg; use yazi_config::popup::ConfirmCfg;
use yazi_macro::{act, succ}; use yazi_fs::file::Files;
use yazi_parser::mgr::RemoveForm; use yazi_macro::{act, confirm, succ};
use yazi_proxy::{ConfirmProxy, MgrProxy}; use yazi_parser::mgr::{RemoveDoForm, RemoveForm};
use yazi_proxy::MgrProxy;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -14,55 +15,31 @@ impl Actor for Remove {
const NAME: &str = "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)?; act!(mgr:escape_visual, cx)?;
form.targets = if form.hovered { let targets = Files(if form.hovered {
cx.hovered().map_or(vec![], |h| vec![h.url.clone()]) cx.hovered().map_or(vec![], |h| vec![h.clone()])
} else { } else {
cx.tab().selected_or_hovered().cloned().collect() cx.tab().selected_or_hovered_files().cloned().collect()
};
if form.targets.is_empty() {
succ!();
} else if form.force {
return act!(mgr:remove_do, cx, form);
}
let confirm = ConfirmProxy::show(if form.permanently {
ConfirmCfg::delete(&form.targets)
} else {
ConfirmCfg::trash(&form.targets)
}); });
if targets.is_empty() {
succ!();
} else if form.force {
return act!(mgr:remove_do, cx, RemoveDoForm { permanently: form.permanently, targets: targets.into() });
}
let confirm = confirm!(
cx,
if form.permanently { ConfirmCfg::delete(&targets) } else { ConfirmCfg::trash(&targets) }
)?;
tokio::spawn(async move { tokio::spawn(async move {
if confirm.await { if confirm.future().await {
MgrProxy::remove_do(form.targets, form.permanently); MgrProxy::remove_do(form.permanently, targets);
} }
}); });
succ!(); 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,12 +1,12 @@
use anyhow::Result; use anyhow::{Result, bail};
use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_config::{YAZI, popup::ConfirmCfg};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp}; use yazi_fs::{FilesOp, file::File};
use yazi_macro::{act, err, ok_or_not_found, succ}; use yazi_macro::{act, err, input, ok_or_not_found, succ};
use yazi_parser::mgr::RenameForm; use yazi_parser::mgr::RenameForm;
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; use yazi_proxy::{ConfirmProxy, MgrProxy};
use yazi_shared::{Id, data::Data, url::{UrlBuf, UrlLike}}; use yazi_shared::{data::Data, id::Id, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_vfs::{VfsFile, engine};
use yazi_watcher::WATCHER; use yazi_watcher::WATCHER;
use yazi_widgets::input::InputEvent; use yazi_widgets::input::InputEvent;
@ -42,23 +42,23 @@ impl Actor for Rename {
}; };
let (tab, old) = (cx.tab().id, hovered.url_owned()); let (tab, old) = (cx.tab().id, hovered.url_owned());
let mut input = InputProxy::show(InputCfg::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 { 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() { if name.is_empty() {
return; return Ok(());
} }
let Some(Ok(new)) = old.parent().map(|u| u.try_join(name)) else { 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 { if form.force || Self::try_ask(&old, &new).await? {
Self::r#do(tab, old, new).await.ok(); Self::r#do(tab, old, new).await?;
} else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await {
Self::r#do(tab, old, new).await.ok();
} }
Ok::<(), anyhow::Error>(())
}); });
succ!(); succ!();
} }
@ -66,30 +66,30 @@ impl Actor for Rename {
impl Rename { impl Rename {
async fn r#do(tab: Id, old: UrlBuf, new: UrlBuf) -> Result<()> { async fn r#do(tab: Id, old: UrlBuf, new: UrlBuf) -> Result<()> {
let Some((old_p, old_n)) = old.pair() else { return Ok(()) }; let Some((old_p, old_k)) = old.pair2() else { return Ok(()) };
let Some(_) = new.pair() else { return Ok(()) }; let Some(_) = new.pair2() else { return Ok(()) };
let _permit = WATCHER.acquire().await.unwrap(); let _permit = WATCHER.acquire().await.unwrap();
let overwritten = provider::casefold(&new).await; let overwritten = engine::casefold(&new).await;
provider::rename(&old, &new).await?; engine::rename(&old, &new).await?;
if let Ok(u) = overwritten if let Ok(u) = overwritten
&& u != new && u != new
&& let Some((parent, urn)) = u.pair() && let Some((parent, key)) = u.pair2()
{ {
ok_or_not_found!(provider::rename(&u, &new).await); ok_or_not_found!(engine::rename(&u, &new).await);
FilesOp::Deleting(parent.to_owned(), [urn.into()].into()).emit(); FilesOp::Deleting(parent.to_owned(), [key.into()].into()).emit();
} }
let new = provider::casefold(&new).await?; let new = engine::casefold(&new).await?;
let Some((new_p, new_n)) = new.pair() else { return Ok(()) }; let Some((new_p, new_k)) = new.pair2() else { return Ok(()) };
let file = File::new(&new).await?; let file = engine::file(&new).await?;
if new_p == old_p { if new_p == old_p {
FilesOp::Upserting(old_p.into(), [(old_n.into(), file)].into()).emit(); FilesOp::Upserting(old_p.into(), [(old_k.into(), file)].into()).emit();
} else { } else {
FilesOp::Deleting(old_p.into(), [old_n.into()].into()).emit(); FilesOp::Deleting(old_p.into(), [old_k.into()].into()).emit();
FilesOp::Upserting(new_p.into(), [(new_n.into(), file)].into()).emit(); FilesOp::Upserting(new_p.into(), [(new_k.into(), file)].into()).emit();
} }
MgrProxy::reveal(&new); MgrProxy::reveal(&new);
@ -97,6 +97,17 @@ impl Rename {
Ok(()) Ok(())
} }
async fn try_ask(old: &UrlBuf, new: &UrlBuf) -> Result<bool> {
let Some(file) = File::maybe_new(&new).await? else {
return Ok(true);
};
Ok(
engine::must_identical(old, new).await
|| ConfirmProxy::show(ConfirmCfg::overwrite(&file)).await,
)
}
fn empty_url_part(url: &UrlBuf, by: &str) -> String { fn empty_url_part(url: &UrlBuf, by: &str) -> String {
if by == "all" { if by == "all" {
return String::new(); return String::new();

View file

@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use yazi_fs::{File, FilesOp}; use yazi_fs::{FilesOp, file::File};
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::mgr::RevealForm; use yazi_parser::mgr::RevealForm;
use yazi_shared::{data::Data, url::UrlLike}; use yazi_shared::{data::Data, url::UrlLike};
@ -14,7 +14,7 @@ impl Actor for Reveal {
const NAME: &str = "reveal"; const NAME: &str = "reveal";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some((parent, child)) = form.target.pair() else { succ!() }; let Some((parent, child)) = form.target.pair2() else { succ!() };
// Cd to the parent directory // Cd to the parent directory
act!(mgr:cd, cx, (parent, form.source))?; act!(mgr:cd, cx, (parent, form.source))?;
@ -25,7 +25,7 @@ impl Actor for Reveal {
// If the child is not hovered, which means it doesn't exist, // If the child is not hovered, which means it doesn't exist,
// create a dummy file // create a dummy file
if !form.no_dummy && tab.hovered().is_none_or(|f| child != f.urn()) { if !form.no_dummy && tab.hovered().is_none_or(|f| f.entry_key() != child) {
let op = FilesOp::Creating(parent.into(), vec![File::from_dummy(&form.target, None)]); let op = FilesOp::Creating(parent.into(), vec![File::from_dummy(&form.target, None)]);
tab.current.update_pub(tab.id, op); tab.current.update_pub(tab.id, op);
} }

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