Compare commits

..

No commits in common. "main" and "v25.5.31" have entirely different histories.

1537 changed files with 24370 additions and 65732 deletions

View file

@ -1,5 +1,5 @@
[env]
MACOSX_DEPLOYMENT_TARGET = "10.12"
MACOSX_DEPLOYMENT_TARGET = "10.11"
JEMALLOC_SYS_WITH_LG_PAGE = "16"
# environment variable for tikv-jemalloc-sys

View file

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

View file

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

View file

@ -1,8 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 🚧 Build Issues
url: https://github.com/sxyazi/yazi/discussions/new?category=3-build-issues
about: If you have issues building Yazi from source code
- name: 📝 Documentation Improvement
url: https://github.com/yazi-rs/yazi-rs.github.io
about: If you'd like to help improve the documentation

View file

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

View file

@ -1,15 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"
- package-ecosystem: "npm"
directory: "/scripts/validate-form"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"

View file

@ -15,10 +15,3 @@ 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.
-->
## 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

@ -4,9 +4,6 @@ on:
push:
branches: [main]
permissions:
contents: read
jobs:
publish:
name: Publish Flake
@ -15,13 +12,13 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Authenticate with Cachix
uses: cachix/cachix-action@v17
uses: cachix/cachix-action@v16
with:
name: yazi
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"

View file

@ -10,14 +10,11 @@ env:
SCCACHE_GHA_ENABLED: true
RUSTC_WRAPPER: sccache
permissions:
contents: read
jobs:
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Setup Rust toolchain
run: |
@ -25,7 +22,7 @@ jobs:
rustup component add clippy
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.9
- name: Clippy
run: cargo clippy --all
@ -34,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v4
- name: Setup Rust toolchain
run: |
@ -42,16 +39,16 @@ jobs:
rustup component add rustfmt --toolchain nightly
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.9
- name: Rustfmt
run: find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} +
run: rustfmt +nightly --check **/*.rs
stylua:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v7
- uses: JohnnyMorganz/stylua-action@v5
- uses: actions/checkout@v4
- uses: JohnnyMorganz/stylua-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: latest

View file

@ -9,9 +9,6 @@ on:
env:
SCCACHE_GHA_ENABLED: true
permissions:
contents: read
jobs:
build-unix:
strategy:
@ -42,29 +39,26 @@ jobs:
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Install gcc
if: matrix.gcc != ''
run: sudo apt update && sudo apt install -yq ${{ matrix.gcc }}
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Setup Rust toolchain
run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.9
- name: Build
run: ./scripts/build.sh ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: |
yazi-${{ matrix.target }}.zip
yazi-${{ matrix.target }}.deb
name: yazi-${{ matrix.target }}.zip
path: yazi-${{ matrix.target }}.zip
build-windows:
strategy:
@ -81,14 +75,13 @@ jobs:
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Setup Rust toolchain
run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.9
- name: Build
run: cargo build --profile release-windows --locked --target ${{ matrix.target }}
@ -97,18 +90,17 @@ jobs:
env:
TARGET_NAME: yazi-${{ matrix.target }}
run: |
New-Item -ItemType Directory -Path "${env:TARGET_NAME}\completions"
New-Item -ItemType Directory -Path ${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 "yazi-cli\completions\*" -Destination "${env:TARGET_NAME}\completions"
Copy-Item -Path "yazi-boot\completions\*" -Destination "${env:TARGET_NAME}\completions"
Copy-Item -Path "yazi-boot\completions" -Destination ${env:TARGET_NAME} -Recurse
Copy-Item -Path "README.md", "LICENSE" -Destination ${env:TARGET_NAME}
Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip"
- name: Upload artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
name: yazi-${{ matrix.target }}.zip
path: yazi-${{ matrix.target }}.zip
build-musl:
@ -117,29 +109,28 @@ jobs:
matrix:
include:
- target: x86_64-unknown-linux-musl
image: rust-musl-cross:x86_64-musl
- target: aarch64-unknown-linux-musl
image: rust-musl-cross:aarch64-musl
container:
image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge
image: docker://ghcr.io/rust-cross/${{ matrix.image }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Add musl target
run: rustup target add ${{ matrix.target }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.9
- name: Build
run: ./scripts/build.sh ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: |
yazi-${{ matrix.target }}.zip
yazi-${{ matrix.target }}.deb
name: yazi-${{ matrix.target }}.zip
path: yazi-${{ matrix.target }}.zip
build-snap:
strategy:
@ -151,12 +142,12 @@ jobs:
arch: arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup LXD
uses: canonical/setup-lxd@v1
uses: canonical/setup-lxd@v0.1.2
- name: Setup snapcraft
run: sudo snap install --classic snapcraft
@ -168,18 +159,18 @@ jobs:
run: mv yazi_*.snap yazi-${{ matrix.arch }}.snap
- name: Upload artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: snap-${{ matrix.arch }}
name: yazi-${{ matrix.arch }}.snap
path: yazi-${{ matrix.arch }}.snap
snap:
runs-on: ubuntu-latest
needs: [build-snap]
steps:
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v4
with:
pattern: snap-*
pattern: yazi-*.snap
merge-multiple: true
- name: Setup snapcraft
@ -190,14 +181,14 @@ jobs:
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
run: |
parallel 'snapcraft upload -v --release latest/candidate {}' ::: yazi-*.snap || true
parallel 'snapcraft push -v --release latest/candidate {}' ::: yazi-*.snap
- name: Push snap to edge channel
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
run: |
parallel 'snapcraft upload -v --release latest/edge {}' ::: yazi-*.snap || true
parallel 'snapcraft push -v --release latest/edge {}' ::: yazi-*.snap
draft:
if: startsWith(github.ref, 'refs/tags/')
@ -206,17 +197,16 @@ jobs:
runs-on: ubuntu-latest
needs: [build-unix, build-windows, build-musl, build-snap]
steps:
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v4
with:
merge-multiple: true
- name: Draft
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v2
with:
draft: true
files: |
yazi-*.zip
yazi-*.deb
yazi-*.snap
generate_release_notes: true
@ -231,12 +221,11 @@ jobs:
echo 'NIGHTLY_BODY<<EOF' >> $GITHUB_ENV
echo "From commit: ${GITHUB_SHA:0:8}" >> $GITHUB_ENV
echo "Generated on: $(date -u +"%Y-%m-%d %H:%M") UTC" >> $GITHUB_ENV
echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v4
with:
merge-multiple: true
@ -247,13 +236,12 @@ jobs:
git tag --force nightly && git push --force origin tag nightly
- name: Nightly
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v2
with:
tag_name: nightly
prerelease: true
files: |
yazi-*.zip
yazi-*.deb
yazi-*.snap
name: Nightly Build
body: ${{ env.NIGHTLY_BODY }}

View file

@ -16,7 +16,7 @@ jobs:
pull-requests: write
discussions: write
steps:
- uses: dessant/lock-threads@v6
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: "30"
issue-comment: >

View file

@ -4,14 +4,11 @@ on:
release:
types: [published]
permissions:
contents: read
jobs:
winget:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v4
with:
merge-multiple: true
@ -22,13 +19,6 @@ jobs:
installers-regex: 'yazi-(x86_64|aarch64)-pc-windows-msvc\.zip$'
token: ${{ secrets.WINGET_TOKEN }}
snapcraft:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
merge-multiple: true
- name: Promote snap to stable
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}

View file

@ -11,9 +11,6 @@ env:
RUSTC_WRAPPER: sccache
CARGO_TERM_COLOR: always
permissions:
contents: read
jobs:
test:
strategy:
@ -21,13 +18,13 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Setup Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.9
- name: Build
run: cargo build --verbose

View file

@ -1,4 +1,4 @@
name: Validate Issue
name: Validate Form
on:
issues:
@ -12,20 +12,20 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@v4
with:
node-version: 20
node-js: 20
- name: Install Dependencies
run: |
cd scripts/validate-form
npm ci
- name: Validate Issue
uses: actions/github-script@v9
- name: Validate Form
uses: actions/github-script@v7
with:
script: |
const script = require('./scripts/validate-form/main.js')

View file

@ -1,36 +0,0 @@
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

@ -1,9 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime.version": "Lua 5.5",
"runtime.special": {
"fail": "error"
},
"workspace.library": ["~/.config/yazi/plugins/types.yazi/"],
"diagnostics.disable": ["redefined-local"]
}

2
.styluaignore Normal file
View file

@ -0,0 +1,2 @@
# this file caused some issues with the build system
yazi-plugin/preset/plugins/mime.lua

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,6 @@ This guide will help you understand how to contribute to the project.
3. [Development Setup](#development-setup)
4. [How to Contribute](#how-to-contribute)
5. [Pull Requests](#pull-requests)
6. [AI Policy](#ai-policy)
## Getting Started
@ -165,10 +164,3 @@ 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 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.
## 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.

4084
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,8 @@
[workspace]
resolver = "3"
resolver = "2"
members = [ "yazi-*" ]
default-members = [ "yazi-fm", "yazi-cli" ]
[workspace.package]
edition = "2024"
version = "26.5.6"
license = "MIT"
authors = [ "sxyazi <sxyazi@gmail.com>" ]
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
rust-version = "1.95.0"
[profile.dev]
debug = "line-tables-only"
[profile.release]
codegen-units = 1
lto = true
@ -31,67 +19,36 @@ codegen-units = 256
incremental = true
lto = false
[profile.dev.package."*"]
debug = false
[workspace.dependencies]
ansi-to-tui = "8.0.1"
anyhow = "1.0.104"
arc-swap = { version = "1.9.2", features = [ "serde" ] }
ansi-to-tui = "7.0.0"
anyhow = "1.0.98"
base64 = "0.22.1"
bitflags = { version = "2.13.1", features = [ "serde" ] }
chrono = "0.4.45"
clap = { version = "4.6.2", features = [ "derive" ] }
compact_str = { version = "0.10.0", features = [ "serde" ] }
bitflags = "2.9.1"
clap = { version = "4.5.39", features = [ "derive" ] }
core-foundation-sys = "0.8.7"
data-encoding = "2.11.0"
crossterm = { version = "0.29.0", features = [ "event-stream" ] }
dirs = "6.0.0"
dyn-clone = "1.0.20"
either = { version = "1.16.0" }
foldhash = "0.2.0"
futures = "0.3.33"
globset = "0.4.19"
hashbrown = { version = "0.17.1", features = [ "serde" ] }
image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] }
indexmap = { version = "2.14.0", features = [ "serde" ] }
inventory = "0.3.24"
libc = "0.2.186"
lru = "0.18.1"
mlua = { version = "0.12.0", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] }
objc2 = "0.6.4"
ordered-float = { version = "5.3.0", features = [ "serde" ] }
parking_lot = "0.12.5"
foldhash = "0.1.5"
futures = "0.3.31"
globset = "0.4.16"
indexmap = { version = "2.9.0", features = [ "serde" ] }
libc = "0.2.172"
lru = "0.14.0"
mlua = { version = "0.10.5", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] }
objc = "0.2.7"
parking_lot = "0.12.4"
paste = "1.0.15"
percent-encoding = "2.3.2"
rand = { version = "0.10.2", default-features = false, features = [ "std", "sys_rng" ] }
ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] }
ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] }
regex = "1.13.1"
russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] }
ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info" ] }
regex = "1.11.1"
scopeguard = "1.2.0"
serde = { version = "1.0.229", features = [ "derive" ] }
serde_json = "1.0.151"
serde_with = "3.21.0"
strum = { version = "0.28.0", features = [ "derive" ] }
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.19"
tokio = { version = "1.53.0", features = [ "full" ] }
tokio-stream = "0.1.18"
tokio-util = "0.7.18"
toml = { version = "1.1.3" }
tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
typed-path = "0.12.3"
unicode-width = { version = "0.2.2", default-features = false }
uzers = "0.12.2"
[workspace.lints.clippy]
format_push_string = "warn"
if_same_then_else = "allow"
implicit_clone = "warn"
len_without_is_empty = "allow"
missing_safety_doc = "allow"
module_inception = "allow"
option_map_unit_fn = "allow"
unit_arg = "allow"
use_self = "warn"
serde = { version = "1.0.219", features = [ "derive" ] }
serde_json = "1.0.140"
syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
tokio = { version = "1.45.1", features = [ "full" ] }
tokio-stream = "0.1.17"
tokio-util = "0.7.15"
toml = { version = "0.8.22" }
tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.0", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
unicode-width = { version = "0.2.0", default-features = false }
uzers = "0.12.1"

View file

@ -1,11 +1,17 @@
<div align="center">
<sup>Special thanks to:</sup><br>
| <a href="https://go.warp.dev/yazi" target="_blank"><img alt="Warp sponsorship" width=350 src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-02.png"><br><b>Warp, built for coding with multiple AI agents</b><br><sup>Available for macOS, Linux and Windows</sup></a> |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
<sup>Special thanks to:</sup>
<br><br>
<a href="https://www.warp.dev/?utm_source=github&utm_medium=referral&utm_campaign=yazi" target="_blank">
<img alt="Warp sponsorship" width="300" src="https://github.com/user-attachments/assets/c7f141e7-9751-407d-bb0e-d6f2c487b34f">
<br>
<b>Warp, the intelligent terminal</b>
<br>
<sup>Yazi's AI-powered terminal of choice!<br>Available for macOS, Linux and Windows</sup>
</a>
</div>
<br>
## Yazi - ⚡️ Blazing Fast Terminal File Manager
Yazi (means "duck") is a terminal file manager written in Rust, based on non-blocking async I/O. It aims to provide an efficient, user-friendly, and customizable file management experience.
@ -17,14 +23,13 @@ Yazi (means "duck") is a terminal file manager written in Rust, based on non-blo
- 🖼️ **Built-in Support for Multiple Image Protocols**: Also integrated with Überzug++ and Chafa, covering almost all terminals.
- 🌟 **Built-in Code Highlighting and Image Decoding**: Combined with the pre-loading mechanism, greatly accelerates image and normal file loading.
- 🔌 **Concurrent Plugin System**: UI plugins (rewriting most of the UI), functional plugins, custom previewer/preloader/spotter/fetcher; Just some pieces of Lua.
- ☁️ **Virtual Filesystem**: Remote file management, custom search engines.
- 📡 **Data Distribution Service**: Built on a client-server architecture (no additional server process required), integrated with a Lua-based publish-subscribe model, achieving cross-instance communication and state persistence.
- 📦 **Package Manager**: Install plugins and themes with one command, keeping them up-to-date, or pin them to a specific version.
- 🧰 Integration with ripgrep, fd, fzf, zoxide
- 💫 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.)
- 🔄 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, Drag and Drop, Trash Bin, Custom Layouts, CSI u, OSC 52
- 🔄 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)
- 🎨 Theme System, Mouse Support, Trash Bin, Custom Layouts, CSI u, OSC 52
- ... and more!
https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7
@ -47,24 +52,25 @@ Yazi is currently in heavy development, expect breaking changes.
## Image Preview
| Platform | Protocol | Support |
| ---------------------------------------------------------------------------- | -------------------------------------- | -------------------------------------- |
| [kitty](https://github.com/kovidgoyal/kitty) (>= 0.28.0) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [iTerm2](https://iterm2.com) | [Inline images protocol][iip] | ✅ Built-in |
| [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol][iip] | ✅ Built-in |
| [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol][kgp-old] | ✅ Built-in |
| [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Ghostty](https://github.com/ghostty-org/ghostty) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.10352.0) | [Sixel graphics format][sixel] | ✅ Built-in |
| [st with Sixel patch](https://github.com/bakkeby/st-flexipatch) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Warp](https://www.warp.dev) (macOS/Linux only) | [Inline images protocol][iip] | ✅ Built-in |
| [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol][iip] | ✅ Built-in |
| [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol][iip] | ✅ Built-in |
| [Rio](https://github.com/raphamorim/rio) (>= 0.3.9) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Bobcat](https://github.com/ismail-yilmaz/Bobcat) | [Inline images protocol][iip] | ✅ Built-in |
| X11 / Wayland | Window system protocol | ☑️ [Überzug++][ueberzug] required |
| Fallback | [ASCII art (Unicode block)][ascii-art] | ☑️ [Chafa][chafa] required (>= 1.16.0) |
| Platform | Protocol | Support |
| ---------------------------------------------------------------------------- | -------------------------------------- | ----------------------------------------------------- |
| [kitty](https://github.com/kovidgoyal/kitty) (>= 0.28.0) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [iTerm2](https://iterm2.com) | [Inline images protocol][iip] | ✅ Built-in |
| [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol][iip] | ✅ Built-in |
| [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol][kgp-old] | ✅ Built-in |
| [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Ghostty](https://github.com/ghostty-org/ghostty) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.10352.0) | [Sixel graphics format][sixel] | ✅ Built-in |
| [st with Sixel patch](https://github.com/bakkeby/st-flexipatch) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Warp](https://www.warp.dev) | [Inline images protocol][iip] | ✅ Built-in |
| [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol][iip] | ✅ Built-in |
| [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol][iip] | ✅ Built-in |
| [Rio](https://github.com/raphamorim/rio) | [Inline images protocol][iip] | ❌ Rio doesn't correctly clear images [#709][rio-bug] |
| [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Hyper](https://github.com/vercel/hyper) | [Inline images protocol][iip] | ✅ Built-in |
| [Bobcat](https://github.com/ismail-yilmaz/Bobcat) | [Inline images protocol][iip] | ✅ Built-in |
| X11 / Wayland | Window system protocol | ☑️ [Überzug++][ueberzug] required |
| Fallback | [ASCII art (Unicode block)][ascii-art] | ☑️ [Chafa][chafa] required |
See https://yazi-rs.github.io/docs/image-preview for details.
@ -81,13 +87,9 @@ See https://yazi-rs.github.io/docs/image-preview for details.
[ueberzug]: https://github.com/jstkdng/ueberzugpp
[chafa]: https://hpjansson.org/chafa/
## Special Thanks
<!-- Rio bug -->
<img alt="RustRover logo" align="right" width="200" src="https://resources.jetbrains.com/storage/products/company/brand/logos/RustRover.svg">
Thanks to RustRover team for providing open-source licenses to support the maintenance of Yazi.
Active code contributors can contact @sxyazi to get a license (if any are still available).
[rio-bug]: https://github.com/raphamorim/rio/issues/709
## License

View file

@ -1,11 +1,11 @@
[Desktop Entry]
Name=Yazi File Manager
Name=Yazi
Icon=yazi
Comment=Blazing fast terminal file manager written in Rust, based on async I/O
Terminal=true
TryExec=yazi
Exec=yazi %f
Exec=yazi %u
Type=Application
MimeType=inode/directory
Categories=System;FileManager;FileTools;ConsoleOnly
Categories=Utility;Core;System;FileTools;FileManager;ConsoleOnly
Keywords=File;Manager;Explorer;Browser;Launcher

View file

@ -1 +1 @@
{"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"]}
{"version":"0.2","language":"en","flagWords":[],"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"]}

12
flake.lock generated
View file

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1783279667,
"narHash": "sha256-/NAkDSsve+GNM0Bt6tleJdCGfsTlK89nPjkVOzZMo0s=",
"lastModified": 1747312588,
"narHash": "sha256-MmJvj6mlWzeRwKGLcwmZpKaOPZ5nJb/6al5CXqJsgjo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f205b5574fd0cb7da5b702a2da51507b7f4fdd1b",
"rev": "b1bebd0fe266bbd1820019612ead889e96a8fa2d",
"type": "github"
},
"original": {
@ -48,11 +48,11 @@
]
},
"locked": {
"lastModified": 1783320166,
"narHash": "sha256-l7C/OsjcnWDOk2K3ssj+SBduwL67LashjBqis9+t468=",
"lastModified": 1747363019,
"narHash": "sha256-N4dwkRBmpOosa4gfFkFf/LTD8oOcNkAyvZ07JvRDEf0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "20ee15370c9256669d66968b89ee20a4b0a4e673",
"rev": "0e624f2b1972a34be1a9b35290ed18ea4b419b6f",
"type": "github"
},
"original": {

View file

@ -32,7 +32,7 @@
rev = self.shortRev or self.dirtyShortRev or "dirty";
date = self.lastModifiedDate or self.lastModified or "19700101";
version =
(builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.package.version
(builtins.fromTOML (builtins.readFile ./yazi-fm/Cargo.toml)).package.version
+ "pre${builtins.substring 0 8 date}_${rev}";
in
{
@ -45,28 +45,21 @@
rustPlatform
;
};
yazi = pkgs.callPackage ./nix/yazi.nix {
inherit (self.packages.${system}) yazi-unwrapped;
};
yazi = pkgs.callPackage ./nix/yazi.nix { inherit (self.packages.${system}) yazi-unwrapped; };
default = self.packages.${system}.yazi;
};
devShells = {
default = pkgs.callPackage ./nix/shell.nix {
inherit toolchain;
inherit (self.packages.${system}) yazi yazi-unwrapped;
};
default = pkgs.callPackage ./nix/shell.nix { };
};
formatter = pkgs.nixfmt-tree;
formatter = pkgs.nixfmt-rfc-style;
}
)
// {
overlays = {
default = self.overlays.yazi;
yazi = _: prev: {
inherit (self.packages.${prev.stdenv.system}) yazi yazi-unwrapped;
};
yazi = _: prev: { inherit (self.packages.${prev.stdenv.system}) yazi yazi-unwrapped; };
};
};
}

View file

@ -1,14 +1,14 @@
{
mkShell,
yazi,
toolchain,
cspell,
yazi-unwrapped,
callPackage,
rust-bin,
nodePackages,
}:
mkShell {
packages = yazi.passthru.runtimePaths ++ [
(toolchain.override {
let
mainPkg = callPackage ./yazi.nix { };
in
mainPkg.overrideAttrs (oa: {
nativeBuildInputs = [
(rust-bin.stable.latest.default.override {
extensions = [
"rust-src"
"rustfmt"
@ -16,10 +16,9 @@ mkShell {
"clippy"
];
})
cspell
];
inputsFrom = [ yazi-unwrapped ];
nodePackages.cspell
] ++ (oa.nativeBuildInputs or [ ]);
env.RUST_BACKTRACE = "1";
}
})

View file

@ -6,7 +6,8 @@
lib,
installShellFiles,
fetchFromGitHub,
stdenv,
darwin,
rust-jemalloc-sys,
imagemagick,
@ -22,12 +23,15 @@ let
];
};
in
rustPlatform.buildRustPackage (finalAttrs: {
rustPlatform.buildRustPackage rec {
pname = "yazi";
inherit version src;
cargoLock = {
lockFile = "${src}/Cargo.lock";
#outputHashes = {
# "mlua-0.10.0" = "sha256-Xg6/jc+UP8tbJJ6x1sbAgt8ZHt051xEBBcjmikQqYlw=";
#};
};
env = {
@ -43,7 +47,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
buildInputs = [
rust-jemalloc-sys
];
] ++ lib.optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [ Foundation ]);
postInstall = ''
installShellCompletion --cmd yazi \
@ -51,38 +55,20 @@ rustPlatform.buildRustPackage (finalAttrs: {
--fish ./yazi-boot/completions/yazi.fish \
--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
for RES in 16 24 32 48 64 128 256; do
mkdir -p $out/share/icons/hicolor/"$RES"x"$RES"/apps
magick assets/logo.png -resize "$RES"x"$RES" $out/share/icons/hicolor/"$RES"x"$RES"/apps/yazi.png
done
installManPage ${finalAttrs.passthru.srcs.man_src}/yazi{.1,-config.5}
mkdir -p $out/share/applications
install -m644 assets/yazi.desktop $out/share/applications/
'';
passthru.srcs = {
man_src = fetchFromGitHub {
name = "manpages"; # needed to ensure name is unique
owner = "yazi-rs";
repo = "manpages";
rev = "8950e968f4a1ad0b83d5836ec54a070855068dbf";
hash = "sha256-kEVXejDg4ChFoMNBvKlwdFEyUuTcY2VuK9j0PdafKus=";
};
};
meta = {
description = "Blazing fast terminal file manager written in Rust, based on async I/O";
homepage = "https://github.com/sxyazi/yazi";
license = lib.licenses.mit;
mainProgram = "yazi";
};
})
}

View file

@ -12,12 +12,11 @@
# default optional deps
jq,
poppler-utils,
poppler_utils,
_7zz,
ffmpeg,
ffmpegthumbnailer,
fd,
ripgrep,
resvg,
fzf,
zoxide,
imagemagick,
@ -39,12 +38,11 @@ let
defaultDeps = [
jq
poppler-utils
poppler_utils
_7zz
ffmpeg
ffmpegthumbnailer
fd
ripgrep
resvg
fzf
zoxide
imagemagick
@ -96,8 +94,6 @@ runCommand yazi-unwrapped.name
inherit (yazi-unwrapped) pname version meta;
nativeBuildInputs = [ makeWrapper ];
passthru.runtimePaths = runtimePaths;
}
''
mkdir -p $out/bin

View file

@ -4,31 +4,21 @@ set -euo pipefail
export ARTIFACT_NAME="yazi-$1"
export YAZI_GEN_COMPLETIONS=1
# Build the target
# Build for the target
git config --global --add safe.directory "*"
cargo build --release --locked --target "$1"
# Copy the binaries to a known location
mkdir -p "target/release"
cp "target/$1/release/ya" "target/release/ya"
cp "target/$1/release/yazi" "target/release/yazi"
# Package deb
if [[ "$ARTIFACT_NAME" == *-linux-* ]] && { [[ "$ARTIFACT_NAME" == *-aarch64-* ]] || [[ "$ARTIFACT_NAME" == *-x86_64-* ]]; }; then
cargo install cargo-deb
cargo deb -p yazi-packing --no-build --target "$1" -o "$ARTIFACT_NAME.deb"
fi
# Create the artifact
mkdir -p "$ARTIFACT_NAME/completions"
cp "target/release/ya" "$ARTIFACT_NAME"
cp "target/release/yazi" "$ARTIFACT_NAME"
cp "target/$1/release/ya" "$ARTIFACT_NAME"
cp "target/$1/release/yazi" "$ARTIFACT_NAME"
cp yazi-cli/completions/* "$ARTIFACT_NAME/completions"
cp yazi-boot/completions/* "$ARTIFACT_NAME/completions"
cp README.md LICENSE "$ARTIFACT_NAME"
# Zip the artifact
if ! command -v zip &> /dev/null; then
apt-get update && apt-get install -yq zip
if ! command -v zip &> /dev/null
then
sudo apt-get update && sudo apt-get install -yq zip
fi
zip -r "$ARTIFACT_NAME.zip" "$ARTIFACT_NAME"

View file

@ -7,14 +7,7 @@ cd "$SCRIPT_DIR/.."
echo "Bumping version: $1"
TOML_FILES="$(git ls-files '*Cargo.toml')"
perl -pi -e 's/^version .*= .*$/version = "'"$1"'"/' -- $TOML_FILES
perl -pi -e 's/^(yazi-[a-z]+)\s*=\s*{.*$/\1 = { path = "..\/\1", version = "'"$1"'" }/' -- $TOML_FILES
perl -pi -e "s/^version .*= .*\$/version = \"$1\"/" -- $TOML_FILES
perl -pi -e "s/^(yazi-[a-z]+)\\s*=\\s*{.*\$/\\1 = { path = \"..\/\\1\", version = \"$1\" }/" -- $TOML_FILES
# Insert "## [v$1]" after "## [Unreleased]"
perl -0777 -pe "s/^(## \[Unreleased\]\s*)/\\1## [v$1]\n\n/m" -i CHANGELOG.md
# Determine previous version and append compare link
prev_ver=$(grep -oE '^\[v[0-9][^]]+\]' CHANGELOG.md | tail -n1 | tr -d '[]')
link="\[v$1\]: https://github.com/sxyazi/yazi/compare/$prev_ver...v$1"
perl -pi -e 's{(\['"$prev_ver"'\]:[^\n]+\n)}{$1'"$link"'\n}s' CHANGELOG.md
eslint -c ~/.config/rules/eslint/eslint.config.js --fix -- $TOML_FILES
ESLINT_USE_FLAT_CONFIG=true eslint -c ~/.config/rules/eslint/eslint.config.cjs --fix -- $TOML_FILES

20
scripts/publish.sh Executable file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env bash
cargo publish -p yazi-macro && sleep 30
cargo publish -p yazi-codegen && sleep 30
cargo publish -p yazi-shared && sleep 30
cargo publish -p yazi-ffi && sleep 30
cargo publish -p yazi-fs && sleep 30
cargo publish -p yazi-term && sleep 30
cargo publish -p yazi-config && sleep 30
cargo publish -p yazi-proxy && sleep 30
cargo publish -p yazi-adapter && sleep 30
cargo publish -p yazi-boot && sleep 30
cargo publish -p yazi-binding && sleep 30
cargo publish -p yazi-dds && sleep 30
cargo publish -p yazi-scheduler && sleep 30
cargo publish -p yazi-plugin && sleep 30
cargo publish -p yazi-widgets && sleep 30
cargo publish -p yazi-core && sleep 30
cargo publish -p yazi-fm && sleep 30
cargo publish -p yazi-cli

View file

@ -2,32 +2,19 @@ const LABEL_NAME = "needs info"
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_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) {
if (RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content) && new RegExp(` \\(${hash}[a-f0-9]? `).test(content)) {
return null
}
return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that:
return `Hey @${creator}, 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 environment info (\`ya env\`) is updated for the newest nightly.
- The debug information (\`yazi --debug\`) is updated for the newest nightly.
- 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.
Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏
`
}
@ -36,26 +23,16 @@ function featureRequestBody(creator, content) {
return null
}
return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that:
return `Hey @${creator}, 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 environment info (\`ya env\`) is updated for the newest nightly.
- The debug information (\`yazi --debug\`) is updated for the newest nightly.
- 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.
Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏
`
}
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 }) => {
async function nightlyHash() {
try {
@ -89,7 +66,7 @@ module.exports = async ({ github, context, core }) => {
const { data: events } = await github.rest.issues.listEvents({
...context.repo,
issue_number: id,
per_page: 100,
per_page : 100,
})
const all = events.filter(v => v.event === "labeled" && v.label?.name === LABEL_NAME)
@ -105,7 +82,7 @@ module.exports = async ({ github, context, core }) => {
const { data: events } = await github.rest.issues.listEvents({
...context.repo,
issue_number: id,
per_page: 100,
per_page : 100,
})
const all = events.filter(v => v.event === "unlabeled" && v.label?.name === LABEL_NAME)
@ -124,16 +101,14 @@ module.exports = async ({ github, context, core }) => {
await github.rest.issues.removeLabel({
...context.repo,
issue_number: id,
name: LABEL_NAME,
name : LABEL_NAME,
})
await hideOldComments(id)
} else if (mark && !marked && !await removedLabelManually(id)) {
await github.rest.issues.addLabels({
...context.repo,
issue_number: id,
labels: [LABEL_NAME],
labels : [LABEL_NAME],
})
await hideOldComments(id)
await github.rest.issues.createComment({
...context.repo,
issue_number: id,
@ -145,42 +120,11 @@ module.exports = async ({ github, context, core }) => {
}
}
async function hideOldComments(id) {
try {
const comments = await github.paginate(github.rest.issues.listComments, {
...context.repo,
issue_number: id,
per_page: 100,
})
for (const c of comments) {
const byBot = c.user?.login?.endsWith("[bot]") || c.user?.type === "Bot"
const contains = c?.body?.includes("or closed after 2 days of inactivity")
if (!byBot || !contains || !c.node_id) continue
try {
await github.graphql(
`mutation($subjectId: ID!, $classifier: ReportedContentClassifiers!) {
minimizeComment(input: {subjectId: $subjectId, classifier: $classifier}) {
minimizedComment { isMinimized }
}
}`,
{ subjectId: c.node_id, classifier: "OUTDATED" },
)
} catch (e) {
core.error(`Error minimizing comment ${c.id}: ${e.message}`)
}
}
} catch (e) {
core.error(`Error listing comments: ${e.message}`)
}
}
async function closeOldIssues() {
try {
const { data: issues } = await github.rest.issues.listForRepo({
...context.repo,
state: "open",
state : "open",
labels: LABEL_NAME,
})
@ -193,13 +137,13 @@ module.exports = async ({ github, context, core }) => {
await github.rest.issues.update({
...context.repo,
issue_number: issue.number,
state: "closed",
state : "closed",
state_reason: "not_planned",
})
await github.rest.issues.createComment({
...context.repo,
issue_number: issue.number,
body: `This issue has been automatically closed because it was marked as \`${LABEL_NAME}\` for more than 2 days without updates.
body : `This issue has been automatically closed because it was marked as \`${LABEL_NAME}\` for more than 2 days without updates.
If the problem persists, please file a new issue and complete the issue template so we can capture all the details necessary to investigate further.`,
})
}
@ -209,58 +153,26 @@ If the problem persists, please file a new issue and complete the issue template
}
}
async function closeUnsupportedIssue(id) {
try {
await github.rest.issues.update({
...context.repo,
issue_number: id,
state: "closed",
state_reason: "not_planned",
})
await github.rest.issues.createComment({
...context.repo,
issue_number: id,
body: `Unsupported issue template.
Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) or [Feature Request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml) template should be used.`,
})
} catch (e) {
core.error(`Error closing unsupported issue: ${e.message}`)
}
}
async function main() {
const hash = await nightlyHash()
if (!hash) return
if (context.eventName === "schedule") {
await closeOldIssues()
return
}
if (skipValidation(context)) {
return
}
if (context.eventName === "issues") {
const id = context.payload.issue.number
const content = context.payload.issue.body || ""
const creator = context.payload.issue.user.login
const hash = await nightlyHash()
if (!hash) return
if (await hasLabel(id, "bug")) {
const body = bugReportBody(creator, content, hash)
await updateLabels(id, !!body, body)
} else if (await hasLabel(id, "feature")) {
const body = featureRequestBody(creator, content)
await updateLabels(id, !!body, body)
} else if (context.payload.action === "opened") {
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

@ -8,225 +8,247 @@
"name": "validate-form",
"version": "1.0.0",
"dependencies": {
"@actions/core": "^3.0.1",
"@actions/github": "^9.1.1"
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0"
}
},
"node_modules/@actions/core": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz",
"integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"license": "MIT",
"dependencies": {
"@actions/exec": "^3.0.0",
"@actions/http-client": "^4.0.0"
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/exec": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz",
"integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"license": "MIT",
"dependencies": {
"@actions/io": "^3.0.2"
"@actions/io": "^1.0.1"
}
},
"node_modules/@actions/github": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-9.1.1.tgz",
"integrity": "sha512-tL5JbYOBZHc0ngEnCsaDcryUizIUIlQyIMwy1Wkx93H5HzbBJ7TbiPx2PnFjBwZW0Vh05JmfFZhecE6gglYegA==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz",
"integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^3.0.2",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@octokit/request": "^10.0.7",
"@octokit/request-error": "^7.1.0",
"undici": "^6.23.0"
}
},
"node_modules/@actions/github/node_modules/@actions/http-client": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz",
"integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^6.23.0"
"@actions/http-client": "^2.2.0",
"@octokit/core": "^5.0.1",
"@octokit/plugin-paginate-rest": "^9.2.2",
"@octokit/plugin-rest-endpoint-methods": "^10.4.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"undici": "^5.28.5"
}
},
"node_modules/@actions/http-client": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.1.tgz",
"integrity": "sha512-+Nvd1ImaOZBSoPbsUtEhv+1z99H12xzncCkz0a3RuehINE81FZSe2QTj3uvAPTcJX/SCzUQHQ0D1GrPMbrPitg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^6.23.0"
"undici": "^5.25.4"
}
},
"node_modules/@actions/io": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz",
"integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
"license": "MIT"
},
"node_modules/@octokit/auth-token": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"license": "MIT",
"engines": {
"node": ">= 20"
"node": ">=14"
}
},
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
"integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==",
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz",
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
"@octokit/request": "^10.0.6",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"before-after-hook": "^4.0.0",
"universal-user-agent": "^7.0.0"
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 20"
"node": ">= 18"
}
},
"node_modules/@octokit/endpoint": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz",
"integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==",
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.2"
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 20"
"node": ">= 18"
}
},
"node_modules/@octokit/graphql": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
"integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz",
"integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^10.0.6",
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.0"
"@octokit/request": "^8.4.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 20"
"node": ">= 18"
}
},
"node_modules/@octokit/openapi-types": {
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
"integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
"integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz",
"integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
"@octokit/types": "^12.6.0"
},
"engines": {
"node": ">= 20"
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=6"
"@octokit/core": "5"
}
},
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
"integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
"integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^20.0.0"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
"integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz",
"integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
"@octokit/types": "^12.6.0"
},
"engines": {
"node": ">= 20"
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=6"
"@octokit/core": "5"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
"integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
"integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^20.0.0"
}
},
"node_modules/@octokit/request": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz",
"integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz",
"integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.3",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"fast-content-type-parse": "^3.0.0",
"json-with-bigint": "^3.5.3",
"universal-user-agent": "^7.0.2"
"@octokit/endpoint": "^9.0.6",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 20"
"node": ">= 18"
}
},
"node_modules/@octokit/request-error": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz",
"integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
"@octokit/types": "^13.1.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 20"
"node": ">= 18"
}
},
"node_modules/@octokit/types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
"integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^27.0.0"
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"license": "Apache-2.0"
},
"node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"license": "ISC"
},
"node_modules/json-with-bigint": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz",
"integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==",
"license": "MIT"
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/tunnel": {
"version": "0.0.6",
@ -238,18 +260,27 @@
}
},
"node_modules/undici": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
"engines": {
"node": ">=18.17"
"node": ">=14.0"
}
},
"node_modules/universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}

View file

@ -6,7 +6,7 @@
},
"private": true,
"dependencies": {
"@actions/core": "^3.0.1",
"@actions/github": "^9.1.1"
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0"
}
}

View file

@ -18,10 +18,6 @@ apps:
command: yazi
environment:
PATH: $SNAP/bin:$SNAP/usr/bin:$PATH
ya:
command: ya
environment:
PATH: $SNAP/bin:$SNAP/usr/bin:$PATH
parts:
yazi:
@ -34,7 +30,7 @@ parts:
- chafa
- fd-find
- ffmpeg
# - fzf
- fzf
- libglu1-mesa
- libglut3.12
- poppler-utils
@ -123,26 +119,17 @@ parts:
# expected binary without much overhead, and works across platforms.
magick:
plugin: autotools
source: https://github.com/ImageMagick/ImageMagick.git
source-type: git
source-tag: 7.1.2-0
source-depth: 1
source: https://imagemagick.org/archive/ImageMagick.tar.gz
source-type: tar
stage-packages:
- libgomp1
autotools-configure-parameters:
- --prefix=/usr
build-attributes:
- 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:
- usr/bin/magick
- usr/lib/libgomp.so*
- usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libgomp.so.1
- usr/lib/libMagickCore-7.Q16HDRI.so*
- usr/lib/libMagickWand-7.Q16HDRI.so*

View file

@ -1,4 +1,3 @@
syntax = "Lua54"
indent_width = 2
call_parentheses = "NoSingleTable"
collapse_simple_statement = "FunctionOnly"

View file

@ -1,58 +0,0 @@
[package]
name = "yazi-actor"
description = "Yazi actor model"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[features]
default = [ "vendored-lua" ]
vendored-lua = [ "mlua/vendored" ]
[dependencies]
yazi-binding = { path = "../yazi-binding", version = "26.5.6" }
yazi-boot = { path = "../yazi-boot", version = "26.5.6" }
yazi-config = { path = "../yazi-config", version = "26.5.6" }
yazi-core = { path = "../yazi-core", version = "26.5.6" }
yazi-dds = { path = "../yazi-dds", version = "26.5.6" }
yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" }
yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-parser = { path = "../yazi-parser", version = "26.5.6" }
yazi-plugin = { path = "../yazi-plugin", version = "26.5.6" }
yazi-proxy = { path = "../yazi-proxy", version = "26.5.6" }
yazi-runner = { path = "../yazi-runner", version = "26.5.6" }
yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
yazi-tui = { path = "../yazi-tui", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies
anyhow = { workspace = true }
either = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
indexmap = { workspace = true }
inventory = { workspace = true }
mlua = { workspace = true }
paste = { workspace = true }
ratatui-core = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
[target."cfg(unix)".dependencies]
libc = { workspace = true }

View file

@ -1,5 +0,0 @@
# yazi-actor
This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API.
[source]: https://github.com/sxyazi/yazi

View file

@ -1,15 +0,0 @@
use anyhow::Result;
use yazi_parser::spark::SparkKind;
use yazi_shared::data::Data;
use crate::Ctx;
pub trait Actor {
type Form;
const NAME: &str;
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data>;
fn hook(_cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> { None }
}

View file

@ -1,42 +0,0 @@
use anyhow::Result;
use mlua::IntoLua;
use tracing::error;
use yazi_actor::lives::Lives;
use yazi_binding::runtime_scope;
use yazi_dds::{LOCAL, Payload, REMOTE};
use yazi_macro::succ;
use yazi_plugin::LUA;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct AcceptPayload;
impl Actor for AcceptPayload {
type Form = Payload<'static>;
const NAME: &str = "accept_payload";
fn act(cx: &mut Ctx, payload: Payload) -> Result<Data> {
let kind = payload.body.kind();
let lock = if payload.receiver == 0 || payload.receiver != payload.sender {
REMOTE.read()
} else {
LOCAL.read()
};
let Some(handlers) = lock.get(kind).filter(|&m| !m.is_empty()).cloned() else { succ!() };
drop(lock);
let kind = kind.to_owned();
succ!(Lives::scope(cx.core, |_| {
let body = payload.body.into_lua(&LUA)?;
for (id, cb) in handlers {
if let Err(e) = runtime_scope!(LUA, &id, cb.call::<()>(body.clone())) {
error!("Failed to run `{kind}` event handler in your `{id}` plugin: {e}");
}
}
Ok(())
})?);
}
}

View file

@ -1,32 +0,0 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_boot::BOOT;
use yazi_core::mgr::CdSource;
use yazi_macro::{act, succ};
use yazi_parser::VoidForm;
use yazi_shared::{data::Data, strand::StrandLike, url::UrlLike};
use crate::Actor;
pub struct Bootstrap;
impl Actor for Bootstrap {
type Form = VoidForm;
const NAME: &str = "bootstrap";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
cx.mgr.tabs.resize_with(BOOT.files.len(), Default::default);
for (i, file) in BOOT.files.iter().enumerate().rev() {
cx.tab = i;
if file.is_empty() {
act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?;
} else if let Ok(u) = BOOT.cwds[i].try_join(file) {
act!(mgr:reveal, cx, (u, CdSource::Tab))?;
}
}
succ!();
}
}

View file

@ -1,24 +0,0 @@
use anyhow::Result;
use yazi_core::notify::{MessageLevel, MessageOpt};
use yazi_macro::act;
use yazi_parser::app::DeprecateForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Deprecate;
impl Actor for Deprecate {
type Form = DeprecateForm;
const NAME: &str = "deprecate";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
act!(notify:push, cx, MessageOpt {
title: "Deprecated API".to_owned(),
content: form.content.into_owned(),
level: MessageLevel::Warn,
timeout: std::time::Duration::from_secs(20),
})
}
}

View file

@ -1,43 +0,0 @@
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,17 +0,0 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::act;
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use crate::Actor;
pub struct Focus;
impl Actor for Focus {
type Form = VoidForm;
const NAME: &str = "focus";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> { act!(mgr:refresh, cx) }
}

View file

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

View file

@ -1,52 +0,0 @@
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::MouseForm;
use yazi_plugin::LUA;
use yazi_shared::data::Data;
use yazi_term::event::MouseEventKind;
use crate::{Actor, Ctx};
pub struct Mouse;
impl Actor for Mouse {
type Form = MouseForm;
const NAME: &str = "mouse";
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)?;
match form.event.kind {
MouseEventKind::Down(_) => root.call_method("click", (form.event, false))?,
MouseEventKind::Up(_) => root.call_method("click", (form.event, true))?,
MouseEventKind::ScrollDown => root.call_method("scroll", (form.event, 1))?,
MouseEventKind::ScrollUp => root.call_method("scroll", (form.event, -1))?,
MouseEventKind::ScrollRight => root.call_method("touch", (form.event, 1))?,
MouseEventKind::ScrollLeft => root.call_method("touch", (form.event, -1))?,
MouseEventKind::Moved => root.call_method("move", form.event)?,
MouseEventKind::Drag(_) => root.call_method("drag", form.event)?,
}
Ok(())
})
});
if let Err(ref e) = result {
error!("{e}");
}
succ!(result?);
}
}

View file

@ -1,40 +0,0 @@
use anyhow::Result;
use yazi_core::app::PluginMode;
use yazi_macro::{act, succ};
use yazi_parser::app::PluginForm;
use yazi_proxy::AppProxy;
use yazi_runner::loader::LOADER;
use yazi_scheduler::NotifyProxy;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Plugin;
impl Actor for Plugin {
type Form = PluginForm;
const NAME: &str = "plugin";
fn act(cx: &mut Ctx, Self::Form { mut opt }: Self::Form) -> Result<Data> {
let mut hits = false;
if let Some(chunk) = LOADER.read().get(&*opt.id) {
hits = true;
opt.mode = opt.mode.auto_then(chunk.sync_entry);
}
if opt.mode == PluginMode::Async {
succ!(cx.core.tasks.scheduler.plugin_entry(opt.into()));
} else if opt.mode == PluginMode::Sync && hits {
return act!(app:plugin_do, cx, opt);
}
tokio::spawn(async move {
match LOADER.ensure(&opt.id, |_| ()).await {
Ok(()) => AppProxy::plugin_do(opt),
Err(e) => NotifyProxy::push_error("Plugin load failed", e.to_string()),
}
});
succ!();
}
}

View file

@ -1,58 +0,0 @@
use anyhow::Result;
use mlua::ObjectLike;
use scopeguard::defer;
use tracing::{error, warn};
use yazi_binding::runtime_mut;
use yazi_core::app::PluginMode;
use yazi_macro::succ;
use yazi_parser::app::PluginForm;
use yazi_plugin::LUA;
use yazi_runner::{entry::EntryJob, loader::{LOADER, Loader}};
use yazi_scheduler::NotifyProxy;
use yazi_shared::data::Data;
use crate::{Actor, Ctx, lives::Lives};
pub struct PluginDo;
impl Actor for PluginDo {
type Form = PluginForm;
const NAME: &str = "plugin_do";
fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
let loader = LOADER.read();
let Some(chunk) = loader.get(&*opt.id) else {
succ!(warn!("plugin `{}` not found", opt.id));
};
if let Err(e) = Loader::compatible_or_error(&opt.id, chunk) {
succ!(NotifyProxy::push_error("Incompatible plugin", e.to_string()));
}
if opt.mode.auto_then(chunk.sync_entry) != PluginMode::Sync {
succ!(cx.core.tasks.scheduler.plugin_entry(opt.into()));
}
let blocking = runtime_mut!(LUA)?.critical_push(&opt.id, true);
defer! { _ = runtime_mut!(LUA).map(|mut r| r.critical_pop(blocking)) }
let plugin = match LOADER.load_chunk(&LUA, &opt.id, chunk) {
Ok(t) => t,
Err(e) => succ!(warn!("{e}")),
};
drop(loader);
let result = Lives::scope(cx.core, |_| {
if let Some(cb) = opt.callback {
cb(&LUA, plugin)
} else {
plugin.call_method("entry", EntryJob { args: opt.args, ..Default::default() })
}
});
if let Err(ref e) = result {
error!("Sync plugin `{}` failed: {e}", opt.id);
}
succ!(result?);
}
}

View file

@ -1,50 +0,0 @@
use std::process;
use anyhow::Result;
use yazi_boot::ARGS;
use yazi_fs::engine::{Engine, local::Local};
use yazi_parser::app::QuitForm;
use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}};
use yazi_tui::Raterm;
use crate::{Actor, Ctx};
pub struct Quit;
impl Actor for Quit {
type Form = QuitForm;
const NAME: &str = "quit";
fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
cx.tasks.shutdown();
cx.mgr.shutdown();
futures::executor::block_on(async {
_ = futures::join!(
yazi_dds::shutdown(),
yazi_dds::STATE.drain(),
Self::cwd_to_file(cx, opt.no_cwd_file),
Self::selected_to_file(opt.selected)
);
});
Raterm::stop();
process::exit(opt.code);
}
}
impl Quit {
async fn cwd_to_file(cx: &Ctx<'_>, no: bool) {
if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) {
let cwd = cx.mgr.cwd().to_strand();
Local::regular(p).write(cwd.encoded_bytes()).await.ok();
}
}
async fn selected_to_file(selected: Option<StrandBuf>) {
if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) {
Local::regular(p).write(s.encoded_bytes()).await.ok();
}
}
}

View file

@ -1,54 +0,0 @@
use anyhow::Result;
use mlua::{LuaString, Value};
use ratatui_core::layout::Position;
use tracing::error;
use yazi_actor::lives::Lives;
use yazi_config::LAYOUT;
use yazi_macro::{render, succ};
use yazi_parser::app::ReflowForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Reflow;
impl Actor for Reflow {
type Form = ReflowForm;
const NAME: &str = "reflow";
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 mut layout = LAYOUT.get();
let result = Lives::scope(cx.core, |_| {
let comps = (form.reflow)((Position::ORIGIN, size).into())?;
for v in comps.sequence_values::<Value>() {
let Value::Table(t) = v? else {
error!("`reflow()` must return a table of components");
continue;
};
let id: LuaString = t.get("_id")?;
match &*id.as_bytes() {
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"progress" => layout.progress = *t.raw_get::<yazi_binding::elements::Rect>("_area")?,
_ => {}
}
}
Ok(())
});
if layout != LAYOUT.get() {
LAYOUT.set(layout);
render!();
}
if let Err(ref e) = result {
error!("Failed to `reflow()` the `Root` component:\n{e}");
}
succ!();
}
}

View file

@ -1,25 +0,0 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::act;
use yazi_parser::app::ReflowForm;
use yazi_shared::data::Data;
use crate::Actor;
pub struct Resize;
impl Actor for Resize {
type Form = ReflowForm;
const NAME: &str = "resize";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
act!(app:reflow, cx, form)?;
cx.current_mut().arrow(0);
cx.parent_mut().map(|f| f.arrow(0));
cx.current_mut().sync_page(true);
act!(mgr:peek, cx)
}
}

View file

@ -1,29 +0,0 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::app::ReflowForm;
use yazi_shared::data::Data;
use yazi_tui::Raterm;
use crate::{Actor, Ctx};
pub struct Resume;
impl Actor for Resume {
type Form = ReflowForm;
const NAME: &str = "resume";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.active_mut().preview.reset();
drop(cx.term.take());
*cx.term = Some(Raterm::start()?);
// 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.
act!(app:resize, cx, form.reflow)?;
act!(app:title, cx).ok();
succ!(render!());
}
}

View file

@ -1,22 +0,0 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Stop;
impl Actor for Stop {
type Form = VoidForm;
const NAME: &str = "stop";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
cx.active_mut().preview.reset_image();
*cx.term = None;
succ!();
}
}

View file

@ -1,25 +0,0 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_config::{THEME, build_flavor};
use yazi_emulator::EMULATOR;
use yazi_macro::{render, succ};
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use yazi_shim::serde::Overlay;
use crate::Actor;
pub struct Theme;
impl Actor for Theme {
type Form = VoidForm;
const NAME: &str = "theme";
fn act(_cx: &mut Ctx, _: Self::Form) -> Result<Data> {
THEME.overlay(build_flavor(EMULATOR.light, true)?);
yazi_plugin::theme::reset()?;
succ!(render!());
}
}

View file

@ -1,32 +0,0 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::{succ, writef};
use yazi_parser::{app::TitleForm, spark::SparkKind};
use yazi_shared::{Source, data::Data};
use yazi_tty::{TTY, sequence::SetTitle};
use yazi_tui::RatermState;
use crate::Actor;
pub struct Title;
impl Actor for Title {
type Form = TitleForm;
const NAME: &str = "title";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let s = form.value.unwrap_or_else(|| format!("Yazi: {}", cx.tab().name()).into());
writef!(TTY.writer(), "{}", SetTitle(&s))?;
yazi_tui::STATE.set(RatermState { title: !s.is_empty(), ..yazi_tui::STATE.get() });
succ!()
}
fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> {
match cx.source() {
Source::Ind => Some(SparkKind::IndAppTitle),
_ => None,
}
}
}

View file

@ -1,40 +0,0 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::{act, render, render_partial, succ};
use yazi_parser::app::UpdateProgressForm;
use yazi_shared::data::Data;
use crate::Actor;
pub struct UpdateProgress;
impl Actor for UpdateProgress {
type Form = UpdateProgressForm;
const NAME: &str = "update_progress";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
// Update the progress of all tasks.
let tasks = &mut cx.tasks;
let progressed = tasks.summary != form.summary;
tasks.summary = form.summary;
// If the task manager is visible, update the snaps with a full render.
if tasks.visible {
let new = tasks.paginate();
if tasks.snaps != new {
tasks.snaps = new;
act!(tasks:arrow, cx)?;
succ!(render!());
}
}
if !progressed {
succ!()
} else if tasks.summary.total == 0 {
succ!(render!())
} else {
succ!(render_partial!())
}
}
}

View file

@ -1,19 +0,0 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::ArrowForm;
use yazi_shared::data::Data;
use yazi_widgets::Scrollable;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Form = ArrowForm;
const NAME: &str = "arrow";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
succ!(render!(cx.cmp.scroll(form.step)));
}
}

View file

@ -1,47 +0,0 @@
use std::mem;
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::cmp::CloseForm;
use yazi_shared::data::Data;
use yazi_widgets::input::parser::CompleteOpt;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Form = CloseForm;
const NAME: &str = "close";
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;
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 });
}
cmp.caches.clear();
cmp.ticket = Default::default();
cmp.handle.take().map(|h| h.abort());
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

@ -1,76 +0,0 @@
use std::{mem, ops::ControlFlow};
use anyhow::Result;
use yazi_core::cmp::CmpItem;
use yazi_macro::{render, succ};
use yazi_parser::cmp::ShowForm;
use yazi_shared::{data::Data, path::{DynPath, PathDyn}, strand::StrandLike};
use crate::{Actor, Ctx};
const LIMIT: usize = 30;
pub struct Show;
impl Actor for Show {
type Form = ShowForm;
const NAME: &str = "show";
fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
let cmp = &mut cx.cmp;
if cmp.ticket != opt.ticket {
succ!();
}
if !opt.cache.is_empty() {
cmp.caches.insert(opt.cache_name.clone(), opt.cache);
}
let Some(cache) = cmp.caches.get(&opt.cache_name) else {
succ!();
};
cmp.matches = Self::match_candidates(opt.word.dyn_path(), cache);
if cmp.matches.is_empty() {
succ!(render!(mem::replace(&mut cmp.visible, false)));
}
cmp.offset = 0;
cmp.cursor = 0;
cmp.visible = true;
succ!(render!());
}
}
impl Show {
fn match_candidates(word: PathDyn, cache: &[CmpItem]) -> Vec<CmpItem> {
let smart = !word.encoded_bytes().iter().any(|&b| b.is_ascii_uppercase());
let flow = cache.iter().try_fold((Vec::new(), Vec::new()), |(mut exact, mut fuzzy), item| {
let starts_with = if smart {
item.name.starts_with_ignore_ascii_case(word)
} else {
item.name.starts_with(word)
};
if starts_with {
exact.push(item);
if exact.len() >= LIMIT {
return ControlFlow::Break((exact, fuzzy));
}
} else if fuzzy.len() < LIMIT - exact.len() && item.name.contains(word) {
// Here we don't break the control flow, since we want more exact matching.
fuzzy.push(item)
}
ControlFlow::Continue((exact, fuzzy))
});
let (exact, fuzzy) = match flow {
ControlFlow::Continue(v) => v,
ControlFlow::Break(v) => v,
};
let it = fuzzy.into_iter().take(LIMIT - exact.len());
exact.into_iter().chain(it).cloned().collect()
}
}

View file

@ -1,155 +0,0 @@
use std::{io, mem};
use anyhow::Result;
use yazi_core::cmp::{CmpItem, CmpOpt};
use yazi_fs::{engine::{DirReader, FileHolder}, path::clean_url};
use yazi_macro::{act, render, succ};
use yazi_parser::cmp::TriggerForm;
use yazi_proxy::CmpProxy;
use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{DynPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}};
use yazi_vfs::engine;
use crate::{Actor, Ctx};
pub struct Trigger;
impl Actor for Trigger {
type Form = TriggerForm;
const NAME: &str = "trigger";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
if form.ticket.is_some_and(|t| t != cx.cmp.ticket) {
succ!();
} else if form.ticket.is_none() {
cx.cmp.ticket = cx.input.lock().map(|g| g.ticket.current()).unwrap_or_default();
}
cx.cmp.handle.take().map(|h| h.abort());
let Some((parent, word)) = Self::split_url(&form.word) else {
return act!(cmp:close, cx, false);
};
let ticket = cx.cmp.ticket;
if cx.cmp.caches.contains_key(&parent) {
return act!(cmp:show, cx, CmpOpt { cache: vec![], cache_name: parent, word, ticket });
}
cx.cmp.handle = Some(tokio::spawn(async move {
let mut dir = engine::read_dir(&parent).await?;
let mut cache = vec![];
// "/" is both a directory separator and the root directory per se
// As there's no parent directory for the FS root, it is a special case
if parent.loc() == "/" {
cache.push(CmpItem { name: Default::default(), is_dir: true });
}
while let Ok(Some(ent)) = dir.next().await {
if let Ok(ft) = ent.file_type().await {
cache.push(CmpItem { name: ent.name().into_owned(), is_dir: ft.is_dir() });
}
}
if !cache.is_empty() {
cache
.sort_unstable_by(|a, b| natsort(a.name.encoded_bytes(), b.name.encoded_bytes(), true));
CmpProxy::show(CmpOpt { cache, cache_name: parent, word, ticket });
}
Ok::<_, io::Error>(())
}));
succ!(render!(mem::replace(&mut cx.cmp.visible, false)));
}
}
impl Trigger {
fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> {
let (spec, path) = Spec::parse(s.as_bytes()).ok()?;
if path.is_empty() && !AnyAsciiChar::SEP.predicate(s.bytes().last()?) {
return None; // We don't complete a `sftp://test`, but `sftp://test/`
}
// Spec
let spec = spec.zeroed();
if spec.kind.is_local() && path.as_strand() == "~" {
return None; // We don't complete a `~`, but `~/`
}
// Child
let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.dyn_path(), |(_, c)| c).to_owned();
// Parent
let url = UrlCow::try_from((spec.clone().zeroed(), path)).ok()?;
let abs = if let Some(u) = engine::try_absolute(&url) { u } else { url };
let parent = abs.loc().try_strip_suffix(&child).ok()?;
Some((clean_url(UrlCow::try_from((spec, parent)).ok()?), child))
}
}
#[cfg(test)]
mod tests {
use yazi_fs::CWD;
use yazi_shared::url::UrlLike;
use super::*;
fn compare(s: &str, parent: &str, child: &str) {
let (mut p, c) = Trigger::split_url(s).unwrap();
if let Ok(u) = p.try_strip_prefix(yazi_fs::CWD.load().as_ref()) {
p = UrlBuf::Regular(u.as_os().unwrap().into());
}
assert_eq!((p, c.to_str().unwrap()), (parent.parse().unwrap(), child));
}
#[cfg(unix)]
#[test]
fn test_split() {
yazi_shared::init_tests();
yazi_config::init_tests();
yazi_fs::init();
assert_eq!(Trigger::split_url(""), None);
assert_eq!(Trigger::split_url("sftp://vps"), None);
compare(" ", "", " ");
compare("/", "/", "");
compare("//", "/", "");
compare("///", "/", "");
compare("/foo", "/", "foo");
compare("//foo", "/", "foo");
compare("///foo", "/", "foo");
compare("/foo/", "/foo/", "");
compare("//foo/", "/foo/", "");
compare("/foo/bar", "/foo/", "bar");
compare("///foo/bar", "/foo/", "bar");
CWD.set(&"sftp://vps".parse::<UrlBuf>().unwrap(), || {});
compare("sftp://vps/a", "sftp://vps/.", "a");
compare("sftp://vps//a", "sftp://vps//", "a");
compare("test-scope://aws/a", "test-scope://aws/.", "a");
compare("test-scope://aws//a", "test-scope://aws//", "a");
}
#[cfg(windows)]
#[test]
fn test_split() {
yazi_shared::init_tests();
yazi_config::init_tests();
yazi_fs::init();
compare("foo", "", "foo");
compare(r"foo\", r"foo\", "");
compare(r"foo\bar", r"foo\", "bar");
compare(r"foo\bar\", r"foo\bar\", "");
compare(r"C:\", r"C:\", "");
compare(r"C:\foo", r"C:\", "foo");
compare(r"C:\foo\", r"C:\foo\", "");
compare(r"C:\foo\bar", r"C:\foo\", "bar");
}
}

View file

@ -1,26 +0,0 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::ArrowForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Form = ArrowForm;
const NAME: &str = "arrow";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let confirm = &mut cx.core.confirm;
let area = cx.core.mgr.area(confirm.position);
let len = confirm.list.line_count(area.width);
let old = confirm.offset;
confirm.offset = form.step.add(confirm.offset, len, area.height as _, 0, 0);
succ!(render!(old != confirm.offset));
}
}

View file

@ -1,20 +0,0 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::confirm::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, form: Self::Form) -> Result<Data> {
cx.confirm.token.complete(form.submit);
cx.confirm.visible = false;
succ!(render!());
}
}

View file

@ -1,31 +0,0 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::confirm::ShowForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Show;
impl Actor for Show {
type Form = ShowForm;
const NAME: &str = "show";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
act!(confirm:close, cx)?;
let confirm = &mut cx.confirm;
confirm.title = form.cfg.title;
confirm.body = form.cfg.body;
confirm.list = form.cfg.list;
confirm.position = form.cfg.position;
confirm.offset = 0;
confirm.token = form.token;
confirm.visible = true;
succ!(render!());
}
}

View file

@ -1,123 +0,0 @@
use std::ops::{Deref, DerefMut};
use anyhow::{Result, anyhow};
use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}};
use yazi_fs::file::File;
use yazi_shared::{Source, event::Action, id::Id, url::UrlBuf};
use yazi_tui::Raterm;
pub struct Ctx<'a> {
pub core: &'a mut Core,
pub term: &'a mut Option<Raterm>,
pub tab: usize,
pub level: usize,
pub source: Source,
#[cfg(debug_assertions)]
pub backtrace: Vec<&'static str>,
}
impl Deref for Ctx<'_> {
type Target = Core;
fn deref(&self) -> &Self::Target { self.core }
}
impl DerefMut for Ctx<'_> {
fn deref_mut(&mut self) -> &mut Self::Target { self.core }
}
impl<'a> Ctx<'a> {
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") {
core
.mgr
.tabs
.iter()
.position(|t| t.id == id)
.ok_or_else(|| anyhow!("Tab with id {id} not found"))?
} else {
core.mgr.tabs.cursor
};
Ok(Self {
core,
term,
tab,
level: 0,
source: action.source,
#[cfg(debug_assertions)]
backtrace: vec![],
})
}
pub fn renew<'b>(cx: &'a mut Ctx<'b>) -> Self {
let tab = cx.core.mgr.tabs.cursor;
Self {
core: cx.core,
term: cx.term,
tab,
level: cx.level,
source: cx.source,
#[cfg(debug_assertions)]
backtrace: vec![],
}
}
pub fn active(core: &'a mut Core, term: &'a mut Option<Raterm>) -> Self {
let tab = core.mgr.tabs.cursor;
Self {
core,
term,
tab,
level: 0,
source: Source::Unknown,
#[cfg(debug_assertions)]
backtrace: vec![],
}
}
}
impl<'a> Ctx<'a> {
#[inline]
pub fn tabs(&self) -> &Tabs { &self.mgr.tabs }
#[inline]
pub fn tabs_mut(&mut self) -> &mut Tabs { &mut self.mgr.tabs }
#[inline]
pub fn tab(&self) -> &Tab { &self.tabs()[self.tab] }
#[inline]
pub fn tab_mut(&mut self) -> &mut Tab { &mut self.core.mgr.tabs[self.tab] }
#[inline]
pub fn cwd(&self) -> &UrlBuf { self.tab().cwd() }
#[inline]
pub fn parent(&self) -> Option<&Folder> { self.tab().parent.as_ref() }
#[inline]
pub fn parent_mut(&mut self) -> Option<&mut Folder> { self.tab_mut().parent.as_mut() }
#[inline]
pub fn current(&self) -> &Folder { &self.tab().current }
#[inline]
pub fn current_mut(&mut self) -> &mut Folder { &mut self.tab_mut().current }
#[inline]
pub fn hovered(&self) -> Option<&File> { self.tab().hovered() }
#[inline]
pub fn hovered_url(&self) -> Option<&UrlBuf> { self.tab().hovered_url() }
#[inline]
pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() }
#[inline]
pub fn hovered_folder_mut(&mut self) -> Option<&mut Folder> {
self.tab_mut().hovered_folder_mut()
}
pub fn source(&self) -> Source { if self.level != 1 { Source::Ind } else { self.source } }
}

View file

@ -1 +0,0 @@
yazi_macro::mod_flat!(preflight);

View file

@ -1,37 +0,0 @@
use anyhow::Result;
use mlua::{ErrorContext, ExternalError, IntoLua, Value};
use yazi_binding::runtime_scope;
use yazi_dds::LOCAL;
use yazi_parser::spark::{Spark, SparkKind};
use yazi_plugin::LUA;
use crate::{Ctx, lives::Lives};
pub struct Preflight;
impl Preflight {
pub fn act<'a>(cx: &mut Ctx, opt: (SparkKind, Spark<'a>)) -> Result<Spark<'a>> {
let kind = opt.0;
let Some(handlers) = LOCAL.read().get(kind.into()).filter(|&m| !m.is_empty()).cloned() else {
return Ok(opt.1);
};
Ok(Lives::scope(cx.core, |_| {
let mut body = opt.1.into_lua(&LUA)?;
for (id, cb) in handlers {
match runtime_scope!(LUA, &id, cb.call::<Value>(&body)) {
Ok(Value::Nil) => {
Err(format!("`{kind}` event cancelled by `{id}` plugin on preflight").into_lua_err())?
}
Ok(v) => body = v,
Err(e) => Err(
format!("Failed to run `{kind}` event handler in `{id}` plugin: {e}").into_lua_err(),
)?,
};
}
Spark::from_lua(&LUA, kind, body)
.with_context(|e| format!("Unexpected return type from `{kind}` event handlers: {e}"))
})?)
}
}

View file

@ -1,19 +0,0 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::ArrowForm;
use yazi_shared::data::Data;
use yazi_widgets::Scrollable;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Form = ArrowForm;
const NAME: &str = "arrow";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
succ!(render!(cx.help.scroll(form.step)));
}
}

View file

@ -1,25 +0,0 @@
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

@ -1,24 +0,0 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use yazi_widgets::input::InputMode;
use crate::{Actor, Ctx};
pub struct Escape;
impl Actor for Escape {
type Form = VoidForm;
const NAME: &str = "escape";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
if cx.help.input.mode() == InputMode::Normal {
return act!(help:close, cx);
}
act!(escape, cx.help.input)?;
succ!(render!());
}
}

View file

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

View file

@ -1,39 +0,0 @@
use anyhow::Result;
use ratatui_core::layout::Margin;
use yazi_config::popup::Help;
use yazi_macro::{render, succ};
use yazi_parser::help::ToggleForm;
use yazi_shared::data::Data;
use yazi_widgets::input::Input;
use crate::{Actor, Ctx};
pub struct Toggle;
impl Actor for Toggle {
type Form = ToggleForm;
const NAME: &str = "toggle";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let position = Help::position();
let area = cx.mgr.area(position);
let input_area = area.inner(Margin::new(1, 1));
let help = &mut cx.help;
help.visible = true;
help.layer = form.layer;
help.position = position;
help.height = area.height;
help.input = Input::default();
help.input.repos(input_area);
help.keyword.clear();
help.offset = 0;
help.cursor = 0;
help.filter_apply();
succ!(render!());
}
}

View file

@ -1,47 +0,0 @@
use anyhow::Result;
use yazi_core::input::InputMutGuard;
use yazi_macro::{act, render, succ};
use yazi_parser::{input::CloseForm, spark::SparkKind};
use yazi_shared::{Source, data::Data};
use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Form = CloseForm;
const NAME: &str = "close";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some(mut guard) = cx.input.lock_mut() else {
succ!();
};
guard.ticket.next();
if let Some(cb) = guard.cb.take() {
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)?;
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

@ -1,26 +0,0 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_shared::data::Data;
use yazi_widgets::input::parser::CompleteOpt;
use crate::{Actor, Ctx};
pub struct Complete;
impl Actor for Complete {
type Form = CompleteOpt;
const NAME: &str = "complete";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some(mut guard) = cx.input.lock_mut() else {
succ!();
};
if guard.ticket.current() != form.ticket {
succ!();
}
act!(complete, guard, form)
}
}

View file

@ -1,34 +0,0 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use yazi_widgets::input::InputOp;
use crate::{Actor, Ctx};
pub struct Escape;
impl Actor for Escape {
type Form = VoidForm;
const NAME: &str = "escape";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
use yazi_widgets::input::InputMode as M;
let Some(mut guard) = cx.input.lock_mut() else {
succ!();
};
let (mode, op) = (guard.snap().mode, guard.snap().op);
act!(escape, guard)?;
drop(guard);
match mode {
M::Normal if op == InputOp::None => act!(input:close, cx),
M::Insert => act!(cmp:close, cx),
M::Normal | M::Replace => Ok(().into()),
}?;
succ!(render!());
}
}

View file

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

View file

@ -1,32 +0,0 @@
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

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

View file

@ -1,7 +0,0 @@
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_flat!(actor context);
pub fn init() { lives::init(); }

View file

@ -1,29 +0,0 @@
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,157 +0,0 @@
use std::{ops::Deref, ptr};
use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Range, style::Style};
use yazi_config::THEME;
use yazi_fs::file::FileInventory;
use yazi_shared::{path::DynPath, url::UrlLike};
use super::{FILE_CACHE, Lives};
use crate::lives::{CoreRef, PtrCell};
pub(super) struct File {
idx: usize,
folder: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>,
}
impl Deref for File {
type Target = yazi_fs::file::File;
fn deref(&self) -> &Self::Target { &self.folder.entries[self.idx] }
}
impl AsRef<yazi_fs::file::File> for File {
fn as_ref(&self) -> &yazi_fs::file::File { self }
}
impl File {
pub(super) fn make(
idx: usize,
folder: &yazi_core::tab::Folder,
tab: &yazi_core::tab::Tab,
) -> mlua::Result<AnyUserData> {
use hashbrown::hash_map::Entry;
Ok(
match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.entries[idx])) {
Entry::Occupied(oe) => oe.into_mut().clone(),
Entry::Vacant(ve) => {
let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into() })?;
ve.insert(ud.clone());
ud
}
},
)
}
#[inline]
fn is_hovered(&self) -> bool { self.idx == self.folder.cursor }
}
impl UserData for File {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
yazi_binding::impl_file_fields!(fields);
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("in_current", |_, me| Ok(ptr::eq(&*me.folder, &me.tab.current)));
fields.add_field_method_get("in_preview", |_, me| {
Ok(me.idx == me.folder.cursor && me.tab.hovered().is_some_and(|f| f.url == me.folder.url))
});
}
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
yazi_binding::impl_file_methods!(methods);
methods.add_method("icon", |lua, me, ()| {
yazi_binding::deprecate!(
lua,
"{}: `File:icon()` is deprecated, use `th.icon:match(file)` instead"
);
// TODO: use a cache
Ok(yazi_config::THEME.icon.matches(me, me.is_hovered()))
});
methods.add_method("size", |_, me, ()| {
Ok(if me.is_dir() {
me.folder.entries.sizes.get(&me.entry_key()).copied()
} else {
Some(me.len)
})
});
methods.add_method("mime", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?;
core.mgr.mimetype.get(&me.url).map(|s| lua.create_string(s)).transpose()
});
methods.add_method("prefix", |lua, me, ()| {
if !me.url.has_trail() {
return Ok(None);
}
let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components();
comp.next_back();
Some(lua.create_string(comp.dyn_path().encoded_bytes())).transpose()
});
methods.add_method("style", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?;
let mime = core.mgr.mimetype.get(&me.url).unwrap_or_default();
Ok(THEME.filetype.match_style(me, mime).map(Style::from))
});
methods.add_method("is_yanked", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?;
Ok(if !core.mgr.yanked.contains(&me.url) {
0u8
} else if core.mgr.yanked.cut {
2u8
} else {
1u8
})
});
methods.add_method("is_marked", |_, me, ()| {
let Some(visual) = me.tab.mode.visual() else {
return Ok(0u8);
};
if !visual.contains(me.idx, me.tab.current.cursor, me.folder.entries.len()) {
return Ok(0u8);
}
if me.folder.url != me.tab.current.url {
return Ok(0u8);
}
Ok(if me.tab.mode.is_select() { 1u8 } else { 2u8 })
});
methods.add_method("is_selected", |_, me, ()| Ok(me.tab.selected.contains(&me.url)));
methods.add_method("found", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?;
let Some(finder) = &core.active().finder else {
return Ok(None);
};
let Some(idx) = finder.matched_idx(&me.folder, me.entry_key()) else {
return Ok(None);
};
lua.create_sequence_from([idx.into_lua(lua)?, finder.matched.len().into_lua(lua)?]).map(Some)
});
methods.add_method("highlights", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?;
let Some(finder) = &core.active().finder else {
return Ok(None);
};
if me.folder.url != me.tab.current.url {
return Ok(None);
}
let Some(Some(h)) = me.url.name().map(|s| finder.filter.highlighted(s)) else {
return Ok(None);
};
lua.create_sequence_from(h.into_iter().map(Range::from)).map(Some)
});
}
}
inventory::submit! {
FileInventory {
register: |_| {},
borrow: |ud, f| f(&*ud.borrow::<File>()?),
}
}

View file

@ -1,52 +0,0 @@
use std::ops::{Deref, Range};
use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_config::LAYOUT;
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Entries, File, Lives, PtrCell};
pub(super) struct Folder {
window: Range<usize>,
inner: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>,
}
impl Deref for Folder {
type Target = yazi_core::tab::Folder;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Folder {
pub(super) fn make(
window: Option<Range<usize>>,
inner: &yazi_core::tab::Folder,
tab: &yazi_core::tab::Tab,
) -> mlua::Result<AnyUserData> {
let window = match window {
Some(w) => w,
None => {
let limit = LAYOUT.get().preview.height as usize;
inner.offset..inner.entries.len().min(inner.offset + limit)
}
};
Lives::scoped_userdata(Self { window, inner: inner.into(), tab: tab.into() })
}
}
impl UserData for Folder {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("cwd", |_, me| Ok(me.url.clone()));
fields.add_static_field("files", |_, me| Entries::make(0..me.entries.len(), me, &me.tab));
fields.add_cached_field("stage", |_, me| Ok(me.stage.clone()));
fields.add_static_field("window", |_, me| Entries::make(me.window.clone(), me, &me.tab));
fields.add_field_method_get("offset", |_, me| Ok(me.offset));
fields.add_field_method_get("cursor", |_, me| Ok(me.cursor));
fields.add_static_field("hovered", |_, me| {
me.hovered().map(|_| File::make(me.cursor, me, &me.tab)).transpose()
});
}
}

View file

@ -1,29 +0,0 @@
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

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

View file

@ -1,5 +0,0 @@
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,11 +0,0 @@
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,17 +0,0 @@
use mlua::AnyUserData;
use super::{Lives, PtrCell};
#[derive(Clone, Copy)]
pub(super) struct Selected;
impl Selected {
pub(super) fn make(inner: &yazi_core::tab::Selected) -> mlua::Result<AnyUserData> {
let inner = PtrCell::from(inner);
Lives::scoped_userdata(yazi_binding::Iter::new(
inner.as_static().files().cloned(),
Some(inner.len()),
))
}
}

View file

@ -1,51 +0,0 @@
use std::{borrow::Cow, ops::Deref};
use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods};
use yazi_shared::url::UrlRef;
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected};
pub(super) struct Tab {
inner: PtrCell<yazi_core::tab::Tab>,
}
impl Deref for Tab {
type Target = yazi_core::tab::Tab;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Tab {
pub(super) fn make(inner: &yazi_core::tab::Tab) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into() })
}
}
impl UserData for Tab {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(me.id));
fields.add_cached_field("name", |lua, me| match me.name() {
Cow::Borrowed(s) => lua.create_string(s),
Cow::Owned(s) => lua.create_external_string(s),
});
fields.add_static_field("mode", |_, me| Mode::make(&me.mode));
fields.add_static_field("pref", |_, me| Preference::make(&me.pref));
fields.add_static_field("current", |_, me| Folder::make(None, &me.current, me));
fields.add_static_field("parent", |_, me| {
me.parent.as_ref().map(|f| Folder::make(None, f, me)).transpose()
});
fields.add_static_field("selected", |_, me| Selected::make(&me.selected));
fields.add_static_field("preview", |_, me| Preview::make(me));
fields.add_static_field("finder", |_, me| me.finder.as_ref().map(Finder::make).transpose());
}
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("history", |_, me, url: UrlRef| {
me.history.get(url.as_ref()).map(|f| Folder::make(None, f, me)).transpose()
});
}
}

View file

@ -1,36 +0,0 @@
use std::ops::Deref;
use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields};
use yazi_scheduler::Progress;
use yazi_shim::mlua::{SER_OPT, UserDataFieldsExt};
use super::{Lives, PtrCell};
pub(super) struct TaskSnap {
inner: PtrCell<yazi_scheduler::TaskSnap>,
}
impl Deref for TaskSnap {
type Target = yazi_scheduler::TaskSnap;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl TaskSnap {
pub(super) fn make(inner: &yazi_scheduler::TaskSnap) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into() })
}
}
impl UserData for TaskSnap {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("title", |lua, me| lua.create_string(&me.title));
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("cooked", |_, me| Ok(me.prog.cooked()));
fields.add_field_method_get("success", |_, me| Ok(me.prog.success()));
fields.add_field_method_get("failed", |_, me| Ok(me.prog.failed()));
fields.add_field_method_get("percent", |_, me| Ok(me.prog.percent()));
}
}

View file

@ -1,41 +0,0 @@
use std::ops::Deref;
use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields};
use yazi_shim::mlua::{SER_OPT, UserDataFieldsExt};
use super::{Lives, PtrCell};
use crate::lives::{Behavior, TaskSnap};
pub(super) struct Tasks {
inner: PtrCell<yazi_core::tasks::Tasks>,
}
impl Deref for Tasks {
type Target = yazi_core::tasks::Tasks;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Tasks {
pub(super) fn make(inner: &yazi_core::tasks::Tasks) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into() })
}
}
impl UserData for Tasks {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cursor", |_, me| Ok(me.cursor));
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)?;
for snap in &me.snaps {
tbl.raw_push(TaskSnap::make(snap)?)?;
}
Ok(tbl)
});
fields.add_cached_field("summary", |lua, me| lua.to_value_with(&me.summary, SER_OPT));
}
}

View file

@ -1,35 +0,0 @@
use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Lives, PtrCell};
pub(super) struct Which {
inner: PtrCell<yazi_core::which::Which>,
}
impl Deref for Which {
type Target = yazi_core::which::Which;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Which {
pub(super) fn make(inner: &yazi_core::which::Which) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into() })
}
}
impl UserData for Which {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("tx", |_, me| Ok(me.tx.clone().map(yazi_binding::MpscUnboundedTx)));
fields.add_cached_field("cands", |lua, me| {
lua.create_sequence_from(me.inner.cands.iter().cloned())
});
fields.add_field_method_get("times", |_, me| Ok(me.inner.times));
fields.add_field_method_get("active", |_, me| Ok(me.inner.active));
fields.add_field_method_get("silent", |_, me| Ok(me.inner.silent));
}
}

View file

@ -1,37 +0,0 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::ArrowForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Form = ArrowForm;
const NAME: &str = "arrow";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let tab = cx.tab_mut();
let old = tab.current.cursor;
if !tab.current.arrow(form.step) {
succ!();
}
// Retrace
tab.current.retrace();
// Visual selection
if let Some(visual) = tab.mode.visual_mut() {
visual.arrow(form.step, old, tab.current.cursor);
}
act!(mgr:hover, cx)?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
cx.tasks.scheduler.behavior.reset();
succ!(render!());
}
}

View file

@ -1,22 +0,0 @@
use anyhow::Result;
use yazi_core::mgr::CdSource;
use yazi_macro::{act, succ};
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Back;
impl Actor for Back {
type Form = VoidForm;
const NAME: &str = "back";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
if let Some(u) = cx.tab_mut().backstack.shift_backward().cloned() {
act!(mgr:cd, cx, (u, CdSource::Back))?;
}
succ!();
}
}

View file

@ -1,175 +0,0 @@
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,19 +0,0 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::mgr::BulkExitForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct BulkExit;
impl Actor for BulkExit {
type Form = BulkExitForm;
const NAME: &str = "bulk_exit";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.mgr.batcher.decide(form.target, form.accept);
succ!();
}
}

View file

@ -1,308 +0,0 @@
use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path};
use anyhow::{Result, anyhow};
use hashbrown::HashMap;
use scopeguard::defer;
use tokio::io::AsyncWriteExt;
use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRuleArc};
use yazi_dds::Pubsub;
use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::Local}, max_common_root, path::skip_url};
use yazi_macro::{err, succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt};
use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_tty::{TTY, sequence::EraseScreen};
use yazi_vfs::{engine::{self, Demand}, maybe_exists};
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
pub struct BulkRename;
impl Actor for BulkRename {
type Form = VoidForm;
const NAME: &str = "bulk_rename";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let Some(opener) = Self::opener() else {
succ!(NotifyProxy::push_warn("Bulk rename", "No text opener found"));
};
let selected: Vec<_> = cx.tab().selected_or_hovered_urls().cloned().collect();
if selected.is_empty() {
succ!(NotifyProxy::push_warn("Bulk rename", "No files selected"));
}
let root = max_common_root(&selected);
let old: Vec<_> =
selected.iter().enumerate().map(|(i, u)| Tuple::new(i, skip_url(u, root))).collect();
let cwd = cx.cwd().clone();
let batcher = cx.core.mgr.batcher.clone();
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk-rename");
let mut rw = Demand::default().write(true).create_new(true).open(&tmp).await?;
rw.write_all(old.join(Strand::Utf8("\n")).encoded_bytes()).await?;
defer! {
let tmp = tmp.clone();
batcher.drain(&tmp);
tokio::spawn(async move {
Local::regular(&tmp).remove_file().await
});
}
batcher.prime(&tmp);
TasksProxy::process_exec(ShellOpt {
cwd,
cmd: Splatter::new(&[rw.into_file().await?]).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 new: Vec<_> = Local::regular(&tmp)
.read_to_string()
.await?
.lines()
.take(old.len())
.enumerate()
.map(|(i, s)| Tuple::new(i, s))
.collect();
let decision = batcher.drain(&tmp);
Self::r#do(root, old, new, selected, decision).await
});
succ!();
}
}
impl BulkRename {
async fn r#do(
root: usize,
old: Vec<Tuple>,
new: Vec<Tuple>,
selected: Vec<UrlBuf>,
decision: Option<bool>,
) -> Result<()> {
writef!(TTY.writer(), "{EraseScreen}\n")?;
if old.len() != new.len() {
#[rustfmt::skip]
writef!(TTY.writer(), "Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len())?;
TTY.reader().read_exact(&mut [0])?;
return Ok(());
}
let (old, new) = old.into_iter().zip(new).filter(|(o, n)| o != n).unzip();
let todo = Self::prioritized_paths(old, new);
if todo.is_empty() {
return Ok(());
}
if !Self::ask_continue(&todo, decision)? {
return Ok(());
}
let permit = WATCHER.acquire().await.unwrap();
let (mut failed, mut succeeded) = (Vec::new(), HashMap::with_capacity(todo.len()));
for (o, n) in todo {
let (Ok(old), Ok(new)) =
(Self::replace_url(&selected[o.0], root, &o), Self::replace_url(&selected[n.0], root, &n))
else {
failed.push((o, n, anyhow!("Invalid new or old file name")));
continue;
};
if maybe_exists(&new).await && !engine::must_identical(&old, &new).await {
failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = engine::rename(&old, &new).await {
failed.push((o, n, e.into()));
} else if let Ok(f) = engine::file(new).await {
succeeded.insert(old, f);
} else {
failed.push((o, n, anyhow!("Failed to retrieve file info")));
}
}
if !succeeded.is_empty() {
let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url()));
err!(Pubsub::pub_after_bulk_rename(it));
FilesOp::rename(succeeded);
}
drop(permit);
if !failed.is_empty() {
Self::output_failed(failed).await?;
}
Ok(())
}
fn opener() -> Option<OpenerRuleArc> {
YAZI
.open
.match_dummy(Path::new("bulk-rename.txt"), "text/plain")
.and_then(|r| YAZI.opener.block(&r))
}
fn replace_url(url: &UrlBuf, take: usize, rep: &StrandBuf) -> Result<UrlBuf> {
Ok(url.try_replace(take, PathDyn::with(url.kind(), rep)?)?.into_owned())
}
fn ask_continue(todo: &[(Tuple, Tuple)], decision: Option<bool>) -> Result<bool> {
if let Some(decision) = decision {
return Ok(decision);
}
{
let mut w = TTY.lockout();
for (old, new) in todo {
writeln!(w, "{} -> {}", old.display(), new.display())?;
}
write!(w, "Continue to rename? (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<(Tuple, Tuple, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout();
writeln!(stdout, "{EraseScreen}")?;
writeln!(stdout, "Failed to rename:")?;
for (old, new, err) in failed {
writeln!(stdout, "{} -> {}: {err}", old.display(), new.display())?;
}
writeln!(stdout, "\nPress ENTER to exit")?;
stdout.flush()?;
TTY.reader().read_exact(&mut [0])?;
Ok(())
}
fn prioritized_paths(old: Vec<Tuple>, new: Vec<Tuple>) -> Vec<(Tuple, Tuple)> {
let orders: HashMap<_, _> = old.iter().enumerate().map(|(i, t)| (t, i)).collect();
let mut incomes: HashMap<_, _> = old.iter().map(|t| (t, false)).collect();
let mut todos: HashMap<_, _> = old
.iter()
.zip(new)
.map(|(o, n)| {
incomes.get_mut(&n).map(|b| *b = true);
(o, n)
})
.collect();
let mut sorted = Vec::with_capacity(old.len());
while !todos.is_empty() {
// Paths that are non-incomes and don't need to be prioritized in this round
let mut outcomes: Vec<_> = incomes.iter().filter(|&(_, b)| !b).map(|(&t, _)| t).collect();
outcomes.sort_unstable_by(|a, b| orders[b].cmp(&orders[a]));
// If there're no outcomes, it means there are cycles in the renaming
if outcomes.is_empty() {
let mut remain: Vec<_> = todos.into_iter().map(|(o, n)| (o.clone(), n)).collect();
remain.sort_unstable_by(|(a, _), (b, _)| orders[a].cmp(&orders[b]));
sorted.reverse();
sorted.extend(remain);
return sorted;
}
for old in outcomes {
let Some(new) = todos.remove(old) else { unreachable!() };
incomes.remove(&old);
incomes.get_mut(&new).map(|b| *b = false);
sorted.push((old.clone(), new));
}
}
sorted.reverse();
sorted
}
}
// --- Tuple
#[derive(Clone, Debug)]
struct Tuple(usize, StrandBuf);
impl Deref for Tuple {
type Target = StrandBuf;
fn deref(&self) -> &Self::Target { &self.1 }
}
impl PartialEq for Tuple {
fn eq(&self, other: &Self) -> bool { self.1 == other.1 }
}
impl Eq for Tuple {}
impl Hash for Tuple {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.1.hash(state); }
}
impl AsStrand for &Tuple {
fn as_strand(&self) -> Strand<'_> { self.1.as_strand() }
}
impl Tuple {
fn new(index: usize, inner: impl Into<StrandBuf>) -> Self { Self(index, inner.into()) }
}
// --- Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sort() {
fn cmp(input: &[(&str, &str)], expected: &[(&str, &str)]) {
let sorted = BulkRename::prioritized_paths(
input.iter().map(|&(o, _)| Tuple::new(0, o)).collect(),
input.iter().map(|&(_, n)| Tuple::new(0, n)).collect(),
);
let sorted: Vec<_> =
sorted.iter().map(|(o, n)| (o.to_str().unwrap(), n.to_str().unwrap())).collect();
assert_eq!(sorted, expected);
}
#[rustfmt::skip]
cmp(
&[("2", "3"), ("1", "2"), ("3", "4")],
&[("3", "4"), ("2", "3"), ("1", "2")]
);
#[rustfmt::skip]
cmp(
&[("1", "3"), ("2", "3"), ("3", "4")],
&[("3", "4"), ("1", "3"), ("2", "3")]
);
#[rustfmt::skip]
cmp(
&[("2", "1"), ("1", "2")],
&[("2", "1"), ("1", "2")]
);
#[rustfmt::skip]
cmp(
&[("3", "2"), ("2", "1"), ("1", "3"), ("a", "b"), ("b", "c")],
&[("b", "c"), ("a", "b"), ("3", "2"), ("2", "1"), ("1", "3")]
);
#[rustfmt::skip]
cmp(
&[("b", "b_"), ("a", "a_"), ("c", "c_")],
&[("b", "b_"), ("a", "a_"), ("c", "c_")],
);
}
}

View file

@ -1,98 +0,0 @@
use std::{mem, time::Duration};
use anyhow::Result;
use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::YAZI;
use yazi_core::mgr::CdSource;
use yazi_dds::Pubsub;
use yazi_fs::{FilesOp, path::{clean_url, expand_url}};
use yazi_macro::{act, err, input, render, succ};
use yazi_parser::mgr::CdForm;
use yazi_proxy::{CmpProxy, MgrProxy};
use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_vfs::engine;
use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx};
pub struct Cd;
impl Actor for Cd {
type Form = CdForm;
const NAME: &str = "cd";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
act!(mgr:escape_visual, cx)?;
if form.interactive {
return Self::cd_interactive(cx);
} else if form.target == *cx.cwd() {
succ!();
}
// Stash first so it's possible to access the original cwd in hooks
act!(mgr:stash, cx, &form).ok();
// Take parent to history
let tab = cx.tab_mut();
if let Some(t) = tab.parent.take() {
tab.history.insert(t.url.clone(), t);
}
// Current
let rep = tab.history.remove_or(&form.target);
let rep = mem::replace(&mut tab.current, rep);
tab.history.insert(rep.url.clone(), rep);
// Parent
if let Some(parent) = form.target.parent() {
tab.parent = Some(tab.history.remove_or(parent));
}
err!(Pubsub::pub_after_cd(tab.id, tab.cwd()));
act!(mgr:displace, cx)?;
act!(mgr:hidden, cx).ok();
act!(mgr:sort, cx).ok();
act!(mgr:hover, cx)?;
act!(mgr:refresh, cx)?;
act!(app:title, cx).ok();
succ!(render!());
}
}
impl Cd {
fn cd_interactive(cx: &mut Ctx) -> Result<Data> {
let input = input!(cx, YAZI.input.cd(cx.cwd().as_url()))?;
tokio::spawn(async move {
let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50));
pin!(rx);
while let Some(result) = rx.next().await {
match result {
InputEvent::Submit(s) => {
let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return };
let Ok(url) = engine::absolute(&url).await else { return };
let url = clean_url(url);
let Ok(file) = engine::file(&url).await else { return };
if file.is_dir() {
return MgrProxy::cd(&url, CdSource::Cd);
}
if let Some((p, k)) = url.pair2() {
FilesOp::Upserting(p.into(), [(k.into(), file)].into()).emit();
}
MgrProxy::reveal(url);
}
InputEvent::Trigger(before, ticket) => {
CmpProxy::trigger(before, ticket);
}
_ => break,
}
}
});
succ!();
}
}

View file

@ -1,26 +0,0 @@
use anyhow::Result;
use yazi_macro::act;
use yazi_parser::{mgr::CloseForm, spark::SparkKind};
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, Self::Form { opt }: Self::Form) -> Result<Data> {
if cx.tabs().len() > 1 {
act!(mgr:tab_close, cx, cx.tabs().cursor)
} else {
act!(mgr:quit, cx, opt)
}
}
fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> {
cx.source().is_key().then_some(SparkKind::KeyClose)
}
}

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