diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 5d47b605..a33878ad 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -13,6 +13,8 @@ body: - macOS - Windows - Windows WSL + - FreeBSD X11 + - FreeBSD Wayland validations: required: true - type: input @@ -22,31 +24,32 @@ body: placeholder: "ex: kitty v0.32.2" validations: required: true - - type: dropdown - id: tried_main - attributes: - label: Did you try the latest code to see if this problem got fixed? - options: - - Tried, but the problem still - - Not tried, and I'll explain why below - validations: - required: true - type: textarea id: debug attributes: label: "`yazi --debug` output" - description: Please do a `yazi --debug` and paste the output here. + description: Please run `yazi --debug` and paste the debug information here. value: |
- ```sh + ##### ↓↓↓ Paste the output here: ↓↓↓ ##### + ```
validations: required: true + - type: dropdown + id: tried_main + attributes: + label: Did you try the latest nightly build to see if the problem got fixed? + options: + - Yes, and I updated the debug information above (`yazi --debug`) to the nightly that I tried + - No, and I'll explain why below + validations: + required: true - type: textarea id: description attributes: diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ce034802..0e7b1d54 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -39,3 +39,13 @@ jobs: - name: Rustfmt run: cargo +nightly fmt --all -- --check + + stylua: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: JohnnyMorganz/stylua-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: latest + args: --color always --check . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5130261e..29eee19e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,10 @@ jobs: - name: Install gcc-aarch64-linux-gnu if: matrix.target == 'aarch64-unknown-linux-gnu' - run: sudo apt-get update && sudo apt-get install -yq gcc-aarch64-linux-gnu + run: | + sudo apt-get update && sudo apt-get install -yq gcc-aarch64-linux-gnu + echo "JEMALLOC_SYS_WITH_LG_PAGE=16" >> $GITHUB_ENV + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc" >> $GITHUB_ENV - name: Build run: ./scripts/build.sh ${{ matrix.target }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 857897cc..c7296246 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,4 +28,4 @@ jobs: run: cargo build --verbose - name: Test - run: cargo test --verbose + run: cargo test --all --verbose diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 00000000..d8e2f141 --- /dev/null +++ b/.styluaignore @@ -0,0 +1,2 @@ +# this file caused some issues with the build system +yazi-plugin/preset/plugins/mime.lua diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cb99aa35 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,146 @@ +# Contributing to Yazi + +Thank you for your interest in contributing to Yazi! We welcome contributions in the form of bug reports, feature requests, documentation improvements, and code changes. + +This guide will help you understand how to contribute to the project. + +## Table of Contents + +1. [Getting Started](#getting-started) +2. [Project Structure](#project-structure) +3. [Development Setup](#development-setup) +4. [How to Contribute](#how-to-contribute) +5. [Pull Request Process](#pull-request-process) + +## Getting Started + +### Prerequisites + +Before you begin, ensure you have met the following requirements: + +- Rust installed on your machine. You can download it from [rustup.rs](https://rustup.rs). +- Familiarity with Git and GitHub. + +### Fork the Repository + +1. Fork the [Yazi repository](https://github.com/sxyazi/yazi) to your GitHub account. +2. Clone your fork to your local machine: + + ```sh + git clone https://github.com//yazi.git + ``` + +3. Set up the upstream remote: + ```sh + git remote add upstream https://github.com/sxyazi/yazi.git + ``` + +## Project Structure + +A brief overview of the project's structure: + +```sh +. +├── assets/ # Assets like images and fonts +├── nix/ # Nix-related configurations +├── scripts/ # Helper scripts used by CI/CD +├── snap/ # Snapcraft configuration +├── yazi-adapter/ # Yazi image adapter +├── yazi-boot/ # Yazi bootstrapper +├── yazi-cli/ # Yazi command-line interface +├── yazi-config/ # Yazi configuration file parser +├── yazi-core/ # Yazi core logic +├── yazi-dds/ # Yazi data distribution service +├── yazi-fm/ # Yazi file manager +├── yazi-plugin/ # Yazi plugin system +├── yazi-proxy/ # Yazi event proxy +├── yazi-scheduler/ # Yazi task scheduler +├── yazi-shared/ # Yazi shared library +├── .github/ # GitHub-specific files and workflows +├── Cargo.toml # Rust workflow configuration +└── README.md # Project overview +``` + +## Development Setup + +1. Ensure the latest stable Rust is installed: + + ```sh + rustc --version + cargo --version + ``` + +2. Build the project: + + ```sh + cargo build + ``` + +3. Run the tests: + + ```sh + cargo test + ``` + +4. Format the code (requires `rustfmt` nightly): + + ```sh + rustup component add rustfmt --toolchain nightly + rustfmt +nightly **/*.rs + ``` + +## How to Contribute + +### Reporting Bugs + +If you find a bug, please file an issue. + +### Suggesting Features + +If you have a feature request, please file an issue. + +### Improving Documentation + +Yazi's documentation placed at [yazi-rs/yazi-rs.github.io](https://github.com/yazi-rs/yazi-rs.github.io), contributions related to documentation need to be made within this repository. + +### Submitting Code Changes + +1. Create a new branch for your changes: + + ```sh + git checkout -b your-branch-name + ``` + +2. Make your changes. Ensure that your code follows the project's [coding style](https://github.com/sxyazi/yazi/blob/main/rustfmt.toml) and passes all tests. +3. Commit your changes with a descriptive commit message: + + ```sh + git commit -m "feat: an awesome feature" + ``` + +4. Push your changes to your fork: + ```sh + git push origin your-branch-name + ``` + +## Pull Request Process + +1. Ensure your fork is up-to-date with the upstream repository: + + ```sh + git fetch upstream + git checkout main + git merge upstream/main + ``` + +2. Rebase your feature branch onto the main branch: + + ```sh + git checkout your-branch-name + git rebase main + ``` + +3. Create a pull request to the `main` branch of the upstream repository. Follow the pull request template and ensure that: + - 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. diff --git a/Cargo.lock b/Cargo.lock index 64898c5f..e1233811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -72,47 +72,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -120,9 +121,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arc-swap" @@ -141,15 +142,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -168,9 +169,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "better-panic" @@ -218,6 +219,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "bstr" version = "1.9.1" @@ -236,9 +246,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" @@ -269,9 +279,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.95" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" [[package]] name = "cfg-if" @@ -293,9 +303,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -303,30 +313,30 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] name = "clap_complete" -version = "4.5.2" +version = "4.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" +checksum = "fbca90c87c2a04da41e95d1856e8bcd22f159bdbfa147314d2ce5218057b0e58" dependencies = [ "clap", ] [[package]] name = "clap_complete_fig" -version = "4.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b3e65f91fabdd23cac3d57d39d5d938b4daabd070c335c006dccb866a61110" +checksum = "fb4bc503cddc1cd320736fb555d6598309ad07c2ddeaa23891a10ffb759ee612" dependencies = [ "clap", "clap_complete", @@ -334,9 +344,9 @@ dependencies = [ [[package]] name = "clap_complete_nushell" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0e48e026ce7df2040239117d25e4e79714907420c70294a5ce4b6bbe6a7b6" +checksum = "1accf1b463dee0d3ab2be72591dccdab8bef314958340447c882c4c72acfe2a3" dependencies = [ "clap", "clap_complete", @@ -344,21 +354,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "clipboard-win" @@ -377,9 +387,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "compact_str" @@ -396,9 +406,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -423,18 +433,18 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -460,9 +470,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -508,9 +518,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -518,27 +528,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.60", + "strsim", + "syn 2.0.67", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] @@ -583,9 +593,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "encode_unicode" @@ -601,18 +611,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" dependencies = [ "serde", + "typeid", ] [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -678,15 +689,15 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -781,7 +792,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] @@ -826,9 +837,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -847,9 +858,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "globset" @@ -876,20 +887,14 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -922,7 +927,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -970,9 +975,9 @@ dependencies = [ [[package]] name = "imagesize" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "indexmap" @@ -984,12 +989,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "indoc" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" - [[package]] name = "inotify" version = "0.9.6" @@ -1010,6 +1009,12 @@ dependencies = [ "libc", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.12.1" @@ -1019,6 +1024,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1074,9 +1088,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lebe" @@ -1086,9 +1100,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libredox" @@ -1108,15 +1122,15 @@ checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1148,23 +1162,14 @@ dependencies = [ [[package]] name = "luajit-src" -version = "210.5.7+d06beb0" +version = "210.5.8+5790d25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d251fdacdabbf87704cf48ac1f8b1eb23d6e10855c3ee08e5beb25b4be2e9e4" +checksum = "441f18d9ad792e871fc2f7f2cb8902c386f6f56fdbddef3b835b61475e375346" dependencies = [ "cc", "which", ] -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "md-5" version = "0.10.6" @@ -1177,9 +1182,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" @@ -1189,9 +1194,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", @@ -1211,9 +1216,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.9.7" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9bed6bce296397a9d6a86f995dd10a547a4e6949825d45225906bdcbfe7367" +checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7" dependencies = [ "bstr", "erased-serde", @@ -1229,9 +1234,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16a9ba1dd2c6ac971b204262d434c24d65067038598f0638b64e5dca28d52b8" +checksum = "a088ed0723df7567f569ba018c5d48c23c501f3878b190b04144dfa5ebfa8abc" dependencies = [ "cc", "cfg-if", @@ -1242,17 +1247,17 @@ dependencies = [ [[package]] name = "mlua_derive" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaade5f94e5829db58791664ba98f35fea6a3ffebc783becb51dc97c7a21abee" +checksum = "09697a6cec88e7f58a02c7ab5c18c611c6907c8654613df9cc0192658a4fb859" dependencies = [ - "itertools", + "itertools 0.12.1", "once_cell", "proc-macro-error", "proc-macro2", "quote", "regex", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] @@ -1307,9 +1312,9 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1334,19 +1339,44 @@ dependencies = [ ] [[package]] -name = "objc" -version = "0.2.7" +name = "objc-sys" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ - "malloc_buf", + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.5.0", + "block2", + "libc", + "objc2", ] [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -1408,9 +1438,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1418,22 +1448,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.2", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" @@ -1518,9 +1548,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1554,21 +1584,22 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8" +checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" dependencies = [ "bitflags 2.5.0", "cassowary", "compact_str", "crossterm", - "indoc", - "itertools", + "itertools 0.13.0", "lru", "paste", "stability", "strum", + "strum_macros", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] @@ -1601,6 +1632,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" version = "0.4.5" @@ -1614,9 +1654,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -1626,9 +1666,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -1637,21 +1677,21 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" @@ -1668,15 +1708,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -1695,9 +1735,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -1714,20 +1754,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -1736,9 +1776,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -1829,9 +1869,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1848,12 +1888,12 @@ dependencies = [ [[package]] name = "stability" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.67", ] [[package]] @@ -1862,12 +1902,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -1885,15 +1919,15 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] @@ -1903,15 +1937,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.60" +version = "2.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" dependencies = [ "proc-macro2", "quote", @@ -1941,22 +1974,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] @@ -2050,9 +2083,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -2069,13 +2102,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] @@ -2091,9 +2124,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", @@ -2104,9 +2137,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "indexmap", "serde", @@ -2117,18 +2150,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", @@ -2168,7 +2201,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] @@ -2208,20 +2241,27 @@ dependencies = [ [[package]] name = "trash" -version = "4.1.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1a7a9a17d3b004898be42be29a4c18d5a4cf008b5cdf72d69b1945dfcb158a" +checksum = "8d8fbfb70b1fad5c0b788f9b2e1bf4d04e5ac6efa828f1ed9ee462c50ff9cf05" dependencies = [ "chrono", "libc", "log", - "objc", + "objc2", + "objc2-foundation", "once_cell", "scopeguard", - "url", + "urlencoding", "windows", ] +[[package]] +name = "typeid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf" + [[package]] name = "typenum" version = "1.17.0" @@ -2256,16 +2296,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] -name = "unicode-width" -version = "0.1.11" +name = "unicode-truncate" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools 0.12.1", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -2273,16 +2323,22 @@ dependencies = [ ] [[package]] -name = "utf8parse" -version = "0.2.1" +name = "urlencoding" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uzers" -version = "0.11.3" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63" +checksum = "7d85875e16d59b3b1549efce83ff8251a64923b03bef94add0a1862847448de4" dependencies = [ "libc", "log", @@ -2315,7 +2371,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] @@ -2379,7 +2435,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", "wasm-bindgen-shared", ] @@ -2401,7 +2457,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2448,11 +2504,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -2463,11 +2519,12 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.44.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" dependencies = [ - "windows-targets 0.42.2", + "windows-core 0.56.0", + "windows-targets 0.52.5", ] [[package]] @@ -2479,6 +2536,49 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.67", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.67", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2497,21 +2597,6 @@ dependencies = [ "windows-targets 0.52.5", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -2543,12 +2628,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.5", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2561,12 +2640,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2579,12 +2652,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2603,12 +2670,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2621,12 +2682,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2639,12 +2694,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2657,12 +2706,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2677,9 +2720,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] @@ -2691,12 +2734,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "yazi-adaptor" +name = "yazi-adapter" version = "0.2.5" dependencies = [ + "ansi-to-tui", "anyhow", "arc-swap", - "base64 0.22.0", + "base64 0.22.1", "color_quant", "crossterm", "futures", @@ -2704,6 +2748,7 @@ dependencies = [ "imagesize", "kamadak-exif", "ratatui", + "scopeguard", "tokio", "tracing", "yazi-config", @@ -2718,9 +2763,10 @@ dependencies = [ "clap_complete", "clap_complete_fig", "clap_complete_nushell", + "regex", "serde", "vergen", - "yazi-adaptor", + "yazi-adapter", "yazi-config", "yazi-shared", ] @@ -2734,9 +2780,14 @@ dependencies = [ "clap_complete", "clap_complete_fig", "clap_complete_nushell", + "crossterm", + "md-5", "serde_json", "tokio", + "toml_edit", + "vergen", "yazi-dds", + "yazi-shared", ] [[package]] @@ -2745,12 +2796,12 @@ version = "0.2.5" dependencies = [ "anyhow", "arc-swap", + "bitflags 2.5.0", "crossterm", "globset", "indexmap", "ratatui", "serde", - "shell-words", "toml", "validator", "yazi-shared", @@ -2761,10 +2812,9 @@ name = "yazi-core" version = "0.2.5" dependencies = [ "anyhow", - "base64 0.22.0", "bitflags 2.5.0", - "clipboard-win", "crossterm", + "dirs", "futures", "libc", "notify", @@ -2773,12 +2823,13 @@ dependencies = [ "regex", "scopeguard", "serde", + "shell-words", "tokio", "tokio-stream", "tokio-util", "tracing", "unicode-width", - "yazi-adaptor", + "yazi-adapter", "yazi-boot", "yazi-config", "yazi-dds", @@ -2801,6 +2852,7 @@ dependencies = [ "tokio-stream", "tokio-util", "uzers", + "vergen", "yazi-boot", "yazi-shared", ] @@ -2826,7 +2878,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", - "yazi-adaptor", + "yazi-adapter", "yazi-boot", "yazi-config", "yazi-core", @@ -2842,22 +2894,24 @@ version = "0.2.5" dependencies = [ "ansi-to-tui", "anyhow", + "base64 0.22.1", + "clipboard-win", + "crossterm", "futures", "md-5", "mlua", "parking_lot", "ratatui", - "serde", - "serde_json", "shell-escape", "shell-words", "syntect", "tokio", + "tokio-stream", "tokio-util", "tracing", "unicode-width", "uzers", - "yazi-adaptor", + "yazi-adapter", "yazi-boot", "yazi-config", "yazi-dds", @@ -2911,7 +2965,6 @@ dependencies = [ "bitflags 2.5.0", "crossterm", "dirs", - "filetime", "futures", "libc", "parking_lot", @@ -2919,28 +2972,29 @@ dependencies = [ "ratatui", "regex", "serde", + "shell-words", "tokio", - "tracing", + "windows-sys 0.52.0", ] [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.67", ] [[package]] diff --git a/LICENSE-ICONS b/LICENSE-ICONS new file mode 100644 index 00000000..df4391dd --- /dev/null +++ b/LICENSE-ICONS @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 nvim-tree + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 617e74b7..3c6ab241 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,14 @@ Yazi (means "duck") is a terminal file manager written in Rust, based on non-blo - 💪 **Powerful Async Task Scheduling and Management**: Provides real-time progress updates, task cancellation, and internal task priority assignment. - 🖼️ **Built-in Support for Multiple Image Protocols**: Also integrated with Überzug++, 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, and custom preloader; Just some pieces of Lua. +- 🔌 **Concurrent Plugin System**: UI plugins (rewriting most of the UI), functional plugins, custom previewer/preloader/fetcher; Just some pieces of Lua. - 📡 **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 always up to date, or pin them to a specific version. - 🧰 Integration with fd, rg, fzf, zoxide - 💫 Vim-like input/select/which/notify component, auto-completion for cd paths - 🏷️ Multi-Tab Support, Cross-directory selection, Scrollable Preview (for videos, PDFs, archives, directories, code, etc.) - 🔄 Bulk Renaming, Visual Mode, File Chooser -- 🎨 Theme System, Custom Layouts, Trash Bin, CSI u +- 🎨 Theme System, Mouse Support, Trash Bin, Custom Layouts, CSI u - ... and more! https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7 @@ -37,21 +38,21 @@ https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265 ## Image Preview -| Platform | Protocol | Support | -| ----------------- | ----------------------------------------------------------------------------------------------------- | --------------------- | -| kitty | [Kitty unicode placeholders](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) | ✅ Built-in | -| Konsole | [Kitty old protocol](https://github.com/sxyazi/yazi/blob/main/yazi-adaptor/src/kitty_old.rs) | ✅ Built-in | -| iTerm2 | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| WezTerm | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| Mintty (Git Bash) | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| foot | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in | -| Ghostty | [Kitty old protocol](https://github.com/sxyazi/yazi/blob/main/yazi-adaptor/src/kitty_old.rs) | ✅ Built-in | -| Black Box | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in | -| VSCode | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| Tabby | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| Hyper | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | -| X11 / Wayland | Window system protocol | ☑️ Überzug++ required | -| Fallback | [Chafa](https://hpjansson.org/chafa/) | ☑️ Überzug++ required | +| Platform | Protocol | Support | +| ----------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| kitty | [Kitty unicode placeholders](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) | ✅ Built-in | +| Konsole | [Kitty old protocol](https://github.com/sxyazi/yazi/blob/main/yazi-adapter/src/kitty_old.rs) | ✅ Built-in | +| iTerm2 | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | +| WezTerm | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | +| Mintty (Git Bash) | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | +| foot | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in | +| Ghostty | [Kitty old protocol](https://github.com/sxyazi/yazi/blob/main/yazi-adapter/src/kitty_old.rs) | ✅ Built-in | +| Black Box | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in | +| VSCode | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | +| Tabby | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | +| Hyper | [Inline images protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in | +| X11 / Wayland | Window system protocol | ☑️ [Überzug++](https://github.com/jstkdng/ueberzugpp) required | +| Fallback | [ASCII art (Unicode block)](https://en.wikipedia.org/wiki/ASCII_art) | ☑️ [Chafa](https://hpjansson.org/chafa/) required | See https://yazi-rs.github.io/docs/image-preview for details. diff --git a/cspell.json b/cspell.json index e69845bf..cc37ffa4 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"version":"0.2","flagWords":[],"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime"]} \ No newline at end of file +{"flagWords":[],"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking"],"version":"0.2"} \ No newline at end of file diff --git a/flake.lock b/flake.lock index 25404c6f..f0a731ee 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1713805509, - "narHash": "sha256-YgSEan4CcrjivCNO5ZNzhg7/8ViLkZ4CB/GrGBVSudo=", + "lastModified": 1716097317, + "narHash": "sha256-1UMrLtgzielG/Sop6gl6oTSM4pDt7rF9j9VuxhDWDlY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1e1dc66fe68972a76679644a5577828b6a7e8be4", + "rev": "8535fb92661f37ff9f0da3007fbc942f7d134b41", "type": "github" }, "original": { @@ -51,11 +51,11 @@ ] }, "locked": { - "lastModified": 1713924823, - "narHash": "sha256-kOeyS3GFwgnKvzuBMmFqEAX0xwZ7Nj4/5tXuvpZ0d4U=", + "lastModified": 1716085073, + "narHash": "sha256-3+9gI93XxszWA2+9S2xZfws1QArPX/MC6nahOGpcMB4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "8a2edac3ae926a2a6ce60f4595dcc4540fc8cad4", + "rev": "cfc8776011bd83508324115d353222475e1601c0", "type": "github" }, "original": { diff --git a/nix/yazi-unwrapped.nix b/nix/yazi-unwrapped.nix index c212de1f..507cdca5 100644 --- a/nix/yazi-unwrapped.nix +++ b/nix/yazi-unwrapped.nix @@ -34,7 +34,7 @@ # Resize logo for RES in 16 24 32 48 64 128 256; do mkdir -p $out/share/icons/hicolor/"$RES"x"$RES"/apps - convert assets/logo.png -resize "$RES"x"$RES" $out/share/icons/hicolor/"$RES"x"$RES"/apps/yazi.png + magick assets/logo.png -resize "$RES"x"$RES" $out/share/icons/hicolor/"$RES"x"$RES"/apps/yazi.png done mkdir -p $out/share/applications diff --git a/scripts/build.sh b/scripts/build.sh index 73b92c47..b694a4ab 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -3,7 +3,6 @@ set -euo pipefail export ARTIFACT_NAME="yazi-$1" export YAZI_GEN_COMPLETIONS=1 -export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc # Setup Rust toolchain if [[ "$1" == *-musl ]]; then diff --git a/scripts/icons/generate.lua b/scripts/icons/generate.lua new file mode 100644 index 00000000..1997f9cf --- /dev/null +++ b/scripts/icons/generate.lua @@ -0,0 +1,36 @@ +local dark = require("icons-default") +local light = require("icons-light") + +function rearrange(by) + local map = {} + local source = by == "exts" and "icons_by_file_extension" or "icons_by_filename" + for k, v in pairs(dark[source]) do + map[k] = map[k] or {} + map[k].icon = v.icon + map[k].fg_dark = v.color:lower() + end + for k, v in pairs(light[source]) do + map[k].fg_light = v.color:lower() + end + return map +end + +function dump(map) + local list = {} + for k, v in pairs(map) do + list[#list + 1] = { name = k, text = v.icon, fg_dark = v.fg_dark, fg_light = v.fg_light } + end + table.sort(list, function(a, b) return a.name:lower() < b.name:lower() end) + for _, v in ipairs(list) do + -- stylua: ignore + print(string.format('\t{ name = "%s", text = "%s", fg_dark = "%s", fg_light = "%s" },', v.name, v.text, v.fg_dark, v.fg_light)) + end +end + +print("files = [") +dump(rearrange("files")) +print("]") + +print("exts = [") +dump(rearrange("exts")) +print("]") diff --git a/scripts/publish.sh b/scripts/publish.sh index e35ba6d2..84b17ead 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,9 +1,11 @@ cargo publish -p yazi-shared cargo publish -p yazi-config cargo publish -p yazi-proxy -cargo publish -p yazi-adaptor +cargo publish -p yazi-adapter cargo publish -p yazi-boot +cargo publish -p yazi-dds cargo publish -p yazi-scheduler cargo publish -p yazi-plugin cargo publish -p yazi-core cargo publish -p yazi-fm +cargo publish -p yazi-cli diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 00000000..ffaa2031 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,6 @@ +indent_width = 2 +call_parentheses = "NoSingleTable" +collapse_simple_statement = "FunctionOnly" + +[sort_requires] +enabled = true diff --git a/yazi-adaptor/Cargo.toml b/yazi-adapter/Cargo.toml similarity index 66% rename from yazi-adaptor/Cargo.toml rename to yazi-adapter/Cargo.toml index 39e06d07..7b6b21bf 100644 --- a/yazi-adaptor/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "yazi-adaptor" +name = "yazi-adapter" version = "0.2.5" edition = "2021" license = "MIT" authors = [ "sxyazi " ] -description = "Yazi image adaptor" +description = "Yazi image adapter" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" @@ -13,17 +13,19 @@ yazi-config = { path = "../yazi-config", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -anyhow = "1.0.82" +ansi-to-tui = "=3.1.0" +anyhow = "1.0.86" arc-swap = "1.7.1" -base64 = "0.22.0" +base64 = "0.22.1" color_quant = "1.1.0" crossterm = "0.27.0" futures = "0.3.30" -image = "0.24.9" -imagesize = "0.12.0" +image = "=0.24.9" +imagesize = "0.13.0" kamadak-exif = "0.5.5" -ratatui = "=0.26.1" -tokio = { version = "1.37.0", features = [ "full" ] } +ratatui = "0.27.0" +scopeguard = "1.2.0" +tokio = { version = "1.38.0", features = [ "full" ] } # Logging tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } diff --git a/yazi-adaptor/src/adaptor.rs b/yazi-adapter/src/adapter.rs similarity index 57% rename from yazi-adaptor/src/adaptor.rs rename to yazi-adapter/src/adapter.rs index 0cece50a..1a2e49bb 100644 --- a/yazi-adaptor/src/adaptor.rs +++ b/yazi-adapter/src/adapter.rs @@ -3,13 +3,13 @@ use std::{env, fmt::Display, path::Path, sync::Arc}; use anyhow::Result; use ratatui::layout::Rect; use tracing::warn; -use yazi_shared::{env_exists, term::Term}; +use yazi_shared::env_exists; use super::{Iterm2, Kitty, KittyOld}; -use crate::{ueberzug::Ueberzug, Emulator, Sixel, SHOWN, TMUX}; +use crate::{Chafa, Emulator, Sixel, Ueberzug, SHOWN, TMUX}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Adaptor { +pub enum Adapter { Kitty, KittyOld, Iterm2, @@ -21,7 +21,7 @@ pub enum Adaptor { Chafa, } -impl Display for Adaptor { +impl Display for Adapter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Kitty => write!(f, "kitty"), @@ -35,49 +35,44 @@ impl Display for Adaptor { } } -impl Adaptor { - pub async fn image_show(self, path: &Path, rect: Rect) -> Result<(u32, u32)> { +impl Adapter { + pub async fn image_show(self, path: &Path, max: Rect) -> Result { + if max.is_empty() { + return Ok(Rect::default()); + } + match self { - Self::Kitty => Kitty::image_show(path, rect).await, - Self::KittyOld => KittyOld::image_show(path, rect).await, - Self::Iterm2 => Iterm2::image_show(path, rect).await, - Self::Sixel => Sixel::image_show(path, rect).await, - _ => Ueberzug::image_show(path, rect).await, + Self::Kitty => Kitty::image_show(path, max).await, + Self::KittyOld => KittyOld::image_show(path, max).await, + Self::Iterm2 => Iterm2::image_show(path, max).await, + Self::Sixel => Sixel::image_show(path, max).await, + Self::X11 | Self::Wayland => Ueberzug::image_show(path, max).await, + Self::Chafa => Chafa::image_show(path, max).await, } } pub fn image_hide(self) -> Result<()> { - if let Some(rect) = SHOWN.swap(None) { self.image_erase(*rect) } else { Ok(()) } + if let Some(area) = SHOWN.swap(None) { self.image_erase(*area) } else { Ok(()) } } - pub fn image_erase(self, rect: Rect) -> Result<()> { + pub fn image_erase(self, area: Rect) -> Result<()> { match self { - Self::Kitty => Kitty::image_erase(rect), - Self::Iterm2 => Iterm2::image_erase(rect), - Self::KittyOld => KittyOld::image_erase(), - Self::Sixel => Sixel::image_erase(rect), - _ => Ueberzug::image_erase(rect), + Self::Kitty => Kitty::image_erase(area), + Self::Iterm2 => Iterm2::image_erase(area), + Self::KittyOld => KittyOld::image_erase(area), + Self::Sixel => Sixel::image_erase(area), + Self::X11 | Self::Wayland => Ueberzug::image_erase(area), + Self::Chafa => Chafa::image_erase(area), } } #[inline] pub fn shown_load(self) -> Option { SHOWN.load_full().map(|r| *r) } - pub(super) fn start(self) { Ueberzug::start(self); } - #[inline] - pub(super) fn shown_store(rect: Rect, size: (u32, u32)) { - SHOWN.store(Some(Arc::new( - Term::ratio() - .map(|(r1, r2)| Rect { - x: rect.x, - y: rect.y, - width: (size.0 as f64 / r1).ceil() as u16, - height: (size.1 as f64 / r2).ceil() as u16, - }) - .unwrap_or(rect), - ))); - } + pub(super) fn shown_store(area: Rect) { SHOWN.store(Some(Arc::new(area))); } + + pub(super) fn start(self) { Ueberzug::start(self); } #[inline] pub(super) fn needs_ueberzug(self) -> bool { @@ -85,7 +80,7 @@ impl Adaptor { } } -impl Adaptor { +impl Adapter { pub fn matches() -> Self { let mut protocols = Emulator::detect().adapters(); @@ -93,8 +88,7 @@ impl Adaptor { protocols.retain(|p| *p == Self::Iterm2); if env_exists("ZELLIJ_SESSION_NAME") { protocols.retain(|p| *p == Self::Sixel); - } - if *TMUX && protocols.len() > 1 { + } else if *TMUX { protocols.retain(|p| *p != Self::KittyOld); } if let Some(p) = protocols.first() { @@ -104,7 +98,7 @@ impl Adaptor { match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() { "x11" => return Self::X11, "wayland" => return Self::Wayland, - _ => warn!("[Adaptor] Could not identify XDG_SESSION_TYPE"), + _ => warn!("[Adapter] Could not identify XDG_SESSION_TYPE"), } if env_exists("WAYLAND_DISPLAY") { return Self::Wayland; @@ -116,7 +110,7 @@ impl Adaptor { return Self::KittyOld; } - warn!("[Adaptor] Falling back to chafa"); + warn!("[Adapter] Falling back to chafa"); Self::Chafa } } diff --git a/yazi-adapter/src/chafa.rs b/yazi-adapter/src/chafa.rs new file mode 100644 index 00000000..c48a3420 --- /dev/null +++ b/yazi-adapter/src/chafa.rs @@ -0,0 +1,77 @@ +use std::{io::Write, path::Path, process::Stdio}; + +use ansi_to_tui::IntoText; +use anyhow::{bail, Result}; +use crossterm::{cursor::MoveTo, queue}; +use ratatui::layout::Rect; +use tokio::process::Command; + +use crate::{Adapter, Emulator}; + +pub(super) struct Chafa; + +impl Chafa { + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let output = Command::new("chafa") + .args([ + "-f", + "symbols", + "--relative", + "off", + "--polite", + "on", + "--passthrough", + "none", + "--animate", + "off", + "--view-size", + ]) + .arg(format!("{}x{}", max.width, max.height)) + .arg(path) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .output() + .await?; + + if !output.status.success() { + bail!("chafa failed with status: {}", output.status); + } else if output.stdout.is_empty() { + bail!("chafa returned no output"); + } + + let lines: Vec<_> = output.stdout.split(|&b| b == b'\n').collect(); + let Ok(Some(first)) = lines[0].into_text().map(|mut t| t.lines.pop()) else { + bail!("failed to parse chafa output"); + }; + + let area = Rect { + x: max.x, + y: max.y, + width: first.spans.into_iter().map(|s| s.content.chars().count() as u16).sum(), + height: lines.len() as u16, + }; + + Adapter::Chafa.image_hide()?; + Adapter::shown_store(area); + Emulator::move_lock((max.x, max.y), |stderr| { + for (i, line) in lines.into_iter().enumerate() { + stderr.write_all(line)?; + queue!(stderr, MoveTo(max.x, max.y + i as u16 + 1))?; + } + Ok(area) + }) + } + + pub(super) fn image_erase(area: Rect) -> Result<()> { + let s = " ".repeat(area.width as usize); + Emulator::move_lock((0, 0), |stderr| { + for y in area.top()..area.bottom() { + queue!(stderr, MoveTo(area.x, y))?; + write!(stderr, "{s}")?; + } + Ok(()) + }) + } +} diff --git a/yazi-adapter/src/dimension.rs b/yazi-adapter/src/dimension.rs new file mode 100644 index 00000000..dfa0f464 --- /dev/null +++ b/yazi-adapter/src/dimension.rs @@ -0,0 +1,35 @@ +use std::mem; + +use crossterm::terminal::WindowSize; + +pub struct Dimension; + +impl Dimension { + pub fn available() -> WindowSize { + let mut size = WindowSize { rows: 0, columns: 0, width: 0, height: 0 }; + if let Ok(s) = crossterm::terminal::window_size() { + _ = mem::replace(&mut size, s); + } + + if size.rows == 0 || size.columns == 0 { + if let Ok(s) = crossterm::terminal::size() { + size.columns = s.0; + size.rows = s.1; + } + } + + // TODO: Use `CSI 14 t` to get the actual size of the terminal + // if size.width == 0 || size.height == 0 {} + + size + } + + #[inline] + pub fn ratio() -> Option<(f64, f64)> { + let s = Self::available(); + if s.width == 0 || s.height == 0 { + return None; + } + Some((f64::from(s.width) / f64::from(s.columns), f64::from(s.height) / f64::from(s.rows))) + } +} diff --git a/yazi-adapter/src/emulator.rs b/yazi-adapter/src/emulator.rs new file mode 100644 index 00000000..32c04cbd --- /dev/null +++ b/yazi-adapter/src/emulator.rs @@ -0,0 +1,216 @@ +use std::{env, io::{stderr, LineWriter}, time::Duration}; + +use anyhow::{anyhow, bail, Result}; +use crossterm::{cursor::{RestorePosition, SavePosition}, execute, style::Print, terminal::{disable_raw_mode, enable_raw_mode}}; +use scopeguard::defer; +use tokio::{io::{AsyncReadExt, BufReader}, time::timeout}; +use tracing::{error, warn}; +use yazi_shared::env_exists; + +use crate::{Adapter, CLOSE, ESCAPE, START, TMUX}; + +#[derive(Clone, Debug)] +pub enum Emulator { + Unknown(Vec), + Kitty, + Konsole, + Iterm2, + WezTerm, + Foot, + Ghostty, + BlackBox, + VSCode, + Tabby, + Hyper, + Mintty, + Neovim, + Apple, + Urxvt, +} + +impl Emulator { + pub fn adapters(self) -> Vec { + match self { + Self::Unknown(adapters) => adapters, + Self::Kitty => vec![Adapter::Kitty], + Self::Konsole => vec![Adapter::KittyOld], + Self::Iterm2 => vec![Adapter::Iterm2, Adapter::Sixel], + Self::WezTerm => vec![Adapter::Iterm2, Adapter::Sixel], + Self::Foot => vec![Adapter::Sixel], + Self::Ghostty => vec![Adapter::KittyOld], + Self::BlackBox => vec![Adapter::Sixel], + Self::VSCode => vec![Adapter::Iterm2, Adapter::Sixel], + Self::Tabby => vec![Adapter::Iterm2, Adapter::Sixel], + Self::Hyper => vec![Adapter::Iterm2, Adapter::Sixel], + Self::Mintty => vec![Adapter::Iterm2], + Self::Neovim => vec![], + Self::Apple => vec![], + Self::Urxvt => vec![], + } + } +} + +impl Emulator { + pub fn detect() -> Self { + if env_exists("NVIM_LOG_FILE") && env_exists("NVIM") { + return Self::Neovim; + } + + let vars = [ + ("KITTY_WINDOW_ID", Self::Kitty), + ("KONSOLE_VERSION", Self::Konsole), + ("ITERM_SESSION_ID", Self::Iterm2), + ("WEZTERM_EXECUTABLE", Self::WezTerm), + ("GHOSTTY_RESOURCES_DIR", Self::Ghostty), + ("VSCODE_INJECTION", Self::VSCode), + ("TABBY_CONFIG_DIRECTORY", Self::Tabby), + ]; + match vars.into_iter().find(|v| env_exists(v.0)) { + Some(var) => return var.1, + None => warn!("[Adapter] No special environment variables detected"), + } + + let (term, program) = Self::via_env(); + match program.as_str() { + "iTerm.app" => return Self::Iterm2, + "WezTerm" => return Self::WezTerm, + "ghostty" => return Self::Ghostty, + "BlackBox" => return Self::BlackBox, + "vscode" => return Self::VSCode, + "Tabby" => return Self::Tabby, + "Hyper" => return Self::Hyper, + "mintty" => return Self::Mintty, + "Apple_Terminal" => return Self::Apple, + _ => warn!("[Adapter] Unknown TERM_PROGRAM: {program}"), + } + match term.as_str() { + "xterm-kitty" => return Self::Kitty, + "foot" => return Self::Foot, + "foot-extra" => return Self::Foot, + "xterm-ghostty" => return Self::Ghostty, + "rxvt-unicode-256color" => return Self::Urxvt, + _ => warn!("[Adapter] Unknown TERM: {term}"), + } + + Self::via_csi().unwrap_or(Self::Unknown(vec![])) + } + + pub fn via_env() -> (String, String) { + fn tmux_env(name: &str) -> Result { + let output = std::process::Command::new("tmux").args(["show-environment", name]).output()?; + + String::from_utf8(output.stdout)? + .trim() + .strip_prefix(&format!("{name}=")) + .map_or_else(|| Err(anyhow!("")), |s| Ok(s.to_string())) + } + + let mut term = env::var("TERM").unwrap_or_default(); + let mut program = env::var("TERM_PROGRAM").unwrap_or_default(); + + if *TMUX { + term = tmux_env("TERM").unwrap_or(term); + program = tmux_env("TERM_PROGRAM").unwrap_or(program); + } + + (term, program) + } + + pub fn via_csi() -> Result { + defer! { disable_raw_mode().ok(); } + enable_raw_mode()?; + + execute!( + LineWriter::new(stderr()), + SavePosition, + Print(format!( + "{}[>q{}_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA{}\\{}[c{}", + START, ESCAPE, ESCAPE, ESCAPE, CLOSE + )), + RestorePosition + )?; + + let resp = futures::executor::block_on(Self::read_until_da1())?; + let names = [ + ("kitty", Self::Kitty), + ("Konsole", Self::Konsole), + ("iTerm2", Self::Iterm2), + ("WezTerm", Self::WezTerm), + ("foot", Self::Foot), + ("ghostty", Self::Ghostty), + ]; + + for (name, emulator) in names.iter() { + if resp.contains(name) { + return Ok(emulator.clone()); + } + } + + let mut adapters = Vec::with_capacity(2); + if resp.contains("\x1b_Gi=31;OK") { + adapters.push(Adapter::KittyOld); + } + if ["?4;", "?4c", ";4;", ";4c"].iter().any(|s| resp.contains(s)) { + adapters.push(Adapter::Sixel); + } + + Ok(Self::Unknown(adapters)) + } + + pub fn move_lock((x, y): (u16, u16), cb: F) -> Result + where + F: FnOnce(&mut std::io::BufWriter) -> Result, + { + use std::{io::Write, thread, time::Duration}; + + use crossterm::{cursor::{Hide, MoveTo, RestorePosition, SavePosition, Show}, queue}; + + let mut buf = std::io::BufWriter::new(stderr().lock()); + + // I really don't want to add this, + // But tmux and ConPTY sometimes cause the cursor position to get out of sync. + if *TMUX || cfg!(windows) { + execute!(buf, SavePosition, MoveTo(x, y), Show)?; + execute!(buf, MoveTo(x, y), Show)?; + execute!(buf, MoveTo(x, y), Show)?; + thread::sleep(Duration::from_millis(1)); + } else { + queue!(buf, SavePosition, MoveTo(x, y))?; + } + + let result = cb(&mut buf); + if *TMUX || cfg!(windows) { + queue!(buf, Hide, RestorePosition)?; + } else { + queue!(buf, RestorePosition)?; + } + + buf.flush()?; + result + } + + pub async fn read_until_da1() -> Result { + let read = async { + let mut stdin = BufReader::new(tokio::io::stdin()); + let mut buf = String::with_capacity(200); + loop { + let mut c = [0; 1]; + if stdin.read(&mut c).await? == 0 { + bail!("unexpected EOF"); + } + buf.push(c[0] as char); + if c[0] == b'c' && buf.contains("\x1b[?") { + break; + } + } + Ok(buf) + }; + + let timeout = timeout(Duration::from_secs(10), read).await; + if let Err(ref e) = timeout { + error!("read_until_da1: {e:?}"); + } + + timeout? + } +} diff --git a/yazi-adaptor/src/image.rs b/yazi-adapter/src/image.rs similarity index 91% rename from yazi-adaptor/src/image.rs rename to yazi-adapter/src/image.rs index 3fbb3b9f..d5d39ca3 100644 --- a/yazi-adaptor/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -5,7 +5,8 @@ use exif::{In, Tag}; use image::{codecs::jpeg::JpegEncoder, imageops::{self, FilterType}, io::Limits, DynamicImage}; use ratatui::layout::Rect; use yazi_config::{PREVIEW, TASKS}; -use yazi_shared::term::Term; + +use crate::Dimension; pub struct Image; @@ -57,7 +58,7 @@ impl Image { }) .await??; - let (mut w, mut h) = Self::max_size(rect); + let (mut w, mut h) = Self::max_pixel(rect); if (5..=8).contains(&orientation) { (w, h) = (h, w); } @@ -76,8 +77,8 @@ impl Image { .await? } - pub(super) fn max_size(rect: Rect) -> (u32, u32) { - Term::ratio() + pub(super) fn max_pixel(rect: Rect) -> (u32, u32) { + Dimension::ratio() .map(|(r1, r2)| { let (w, h) = ((rect.width as f64 * r1) as u32, (rect.height as f64 * r2) as u32); (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) @@ -85,6 +86,17 @@ impl Image { .unwrap_or((PREVIEW.max_width, PREVIEW.max_height)) } + pub(super) fn pixel_area(size: (u32, u32), rect: Rect) -> Rect { + Dimension::ratio() + .map(|(r1, r2)| Rect { + x: rect.x, + y: rect.y, + width: (size.0 as f64 / r1).ceil() as u16, + height: (size.1 as f64 / r2).ceil() as u16, + }) + .unwrap_or(rect) + } + #[inline] fn filter() -> FilterType { match PREVIEW.image_filter.as_str() { diff --git a/yazi-adapter/src/iterm2.rs b/yazi-adapter/src/iterm2.rs new file mode 100644 index 00000000..d71cbf93 --- /dev/null +++ b/yazi-adapter/src/iterm2.rs @@ -0,0 +1,61 @@ +use std::{io::Write, path::Path}; + +use anyhow::Result; +use base64::{engine::{general_purpose::STANDARD, Config}, Engine}; +use crossterm::{cursor::MoveTo, queue}; +use image::{codecs::jpeg::JpegEncoder, DynamicImage}; +use ratatui::layout::Rect; + +use super::image::Image; +use crate::{adapter::Adapter, Emulator, CLOSE, START}; + +pub(super) struct Iterm2; + +impl Iterm2 { + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; + let area = Image::pixel_area((img.width(), img.height()), max); + let b = Self::encode(img).await?; + + Adapter::Iterm2.image_hide()?; + Adapter::shown_store(area); + Emulator::move_lock((max.x, max.y), |stderr| { + stderr.write_all(&b)?; + Ok(area) + }) + } + + pub(super) fn image_erase(area: Rect) -> Result<()> { + let s = " ".repeat(area.width as usize); + Emulator::move_lock((0, 0), |stderr| { + for y in area.top()..area.bottom() { + queue!(stderr, MoveTo(area.x, y))?; + write!(stderr, "{s}")?; + } + Ok(()) + }) + } + + async fn encode(img: DynamicImage) -> Result> { + tokio::task::spawn_blocking(move || { + let mut jpg = vec![]; + JpegEncoder::new_with_quality(&mut jpg, 75).encode_image(&img)?; + + let len = base64::encoded_len(jpg.len(), STANDARD.config().encode_padding()); + let mut buf = Vec::with_capacity(200 + len.unwrap_or(1 << 16)); + + write!( + buf, + "{}]1337;File=inline=1;size={};width={}px;height={}px;doNotMoveCursor=1:{}\x07{}", + START, + jpg.len(), + img.width(), + img.height(), + STANDARD.encode(&jpg), + CLOSE + )?; + Ok(buf) + }) + .await? + } +} diff --git a/yazi-adaptor/src/kitty.rs b/yazi-adapter/src/kitty.rs similarity index 84% rename from yazi-adaptor/src/kitty.rs rename to yazi-adapter/src/kitty.rs index cdc46cb4..b75bbb07 100644 --- a/yazi-adaptor/src/kitty.rs +++ b/yazi-adapter/src/kitty.rs @@ -3,12 +3,12 @@ use std::{io::Write, path::Path}; use anyhow::Result; use base64::{engine::general_purpose, Engine}; +use crossterm::{cursor::MoveTo, queue}; use image::DynamicImage; use ratatui::layout::Rect; -use yazi_shared::term::Term; use super::image::Image; -use crate::{adaptor::Adaptor, CLOSE, ESCAPE, START}; +use crate::{adapter::Adapter, Emulator, CLOSE, ESCAPE, START}; static DIACRITICS: [char; 297] = [ '\u{0305}', @@ -313,31 +313,31 @@ static DIACRITICS: [char; 297] = [ pub(super) struct Kitty; impl Kitty { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - let img = Image::downscale(path, rect).await?; - let size = (img.width(), img.height()); + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; + let area = Image::pixel_area((img.width(), img.height()), max); let b1 = Self::encode(img).await?; - let b2 = Self::place(&rect)?; + let b2 = Self::place(&area)?; - Adaptor::Kitty.image_hide()?; - Adaptor::shown_store(rect, size); - Term::move_lock((rect.x, rect.y), |stderr| { + Adapter::Kitty.image_hide()?; + Adapter::shown_store(area); + Emulator::move_lock((area.x, area.y), |stderr| { stderr.write_all(&b1)?; stderr.write_all(&b2)?; - Ok(size) + Ok(area) }) } - pub(super) fn image_erase(rect: Rect) -> Result<()> { - let s = " ".repeat(rect.width as usize); - Term::move_lock((0, 0), |stderr| { - for y in rect.top()..rect.bottom() { - Term::move_to(stderr, rect.x, y)?; + pub(super) fn image_erase(area: Rect) -> Result<()> { + let s = " ".repeat(area.width as usize); + Emulator::move_lock((0, 0), |stderr| { + for y in area.top()..area.bottom() { + queue!(stderr, MoveTo(area.x, y))?; write!(stderr, "{s}")?; } - write!(stderr, "{}_Gq=1,a=d,d=A{}\\{}", START, ESCAPE, CLOSE)?; + write!(stderr, "{}_Gq=2,a=d,d=A{}\\{}", START, ESCAPE, CLOSE)?; Ok(()) }) } @@ -351,7 +351,7 @@ impl Kitty { if let Some(first) = it.next() { write!( buf, - "{}_Gq=1,a=T,i=1,C=1,U=1,f={},s={},v={},m={};{}{}\\{}", + "{}_Gq=2,a=T,i=1,C=1,U=1,f={},s={},v={},m={};{}{}\\{}", START, format, size.0, @@ -388,11 +388,11 @@ impl Kitty { .await? } - fn place(rect: &Rect) -> Result> { - let mut buf = Vec::with_capacity(rect.width as usize * rect.height as usize * 3 + 50); - for y in 0..rect.height { - write!(buf, "\x1b[{};{}H\x1b[38;5;1m", rect.y + y + 1, rect.x + 1)?; - for x in 0..rect.width { + fn place(area: &Rect) -> Result> { + let mut buf = Vec::with_capacity(area.width as usize * area.height as usize * 3 + 50); + for y in 0..area.height { + write!(buf, "\x1b[{};{}H\x1b[38;5;1m", area.y + y + 1, area.x + 1)?; + for x in 0..area.width { write!(buf, "\u{10EEEE}")?; write!(buf, "{}", *DIACRITICS.get(y as usize).unwrap_or(&DIACRITICS[0]))?; write!(buf, "{}", *DIACRITICS.get(x as usize).unwrap_or(&DIACRITICS[0]))?; diff --git a/yazi-adaptor/src/kitty_old.rs b/yazi-adapter/src/kitty_old.rs similarity index 73% rename from yazi-adaptor/src/kitty_old.rs rename to yazi-adapter/src/kitty_old.rs index cf0a2df9..0de07533 100644 --- a/yazi-adaptor/src/kitty_old.rs +++ b/yazi-adapter/src/kitty_old.rs @@ -5,31 +5,30 @@ use anyhow::Result; use base64::{engine::general_purpose, Engine}; use image::DynamicImage; use ratatui::layout::Rect; -use yazi_shared::term::Term; use super::image::Image; -use crate::{adaptor::Adaptor, CLOSE, ESCAPE, START}; +use crate::{adapter::Adapter, Emulator, CLOSE, ESCAPE, START}; pub(super) struct KittyOld; impl KittyOld { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - let img = Image::downscale(path, rect).await?; - let size = (img.width(), img.height()); + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; + let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; - Adaptor::KittyOld.image_hide()?; - Adaptor::shown_store(rect, size); - Term::move_lock((rect.x, rect.y), |stderr| { + Adapter::KittyOld.image_hide()?; + Adapter::shown_store(area); + Emulator::move_lock((area.x, area.y), |stderr| { stderr.write_all(&b)?; - Ok(size) + Ok(area) }) } #[inline] - pub(super) fn image_erase() -> Result<()> { + pub(super) fn image_erase(_: Rect) -> Result<()> { let mut stderr = LineWriter::new(stderr()); - write!(stderr, "{}_Gq=1,a=d,d=A{}\\{}", START, ESCAPE, CLOSE)?; + write!(stderr, "{}_Gq=2,a=d,d=A{}\\{}", START, ESCAPE, CLOSE)?; stderr.flush()?; Ok(()) } @@ -43,7 +42,7 @@ impl KittyOld { if let Some(first) = it.next() { write!( buf, - "{}_Gq=1,a=T,z=-1,C=1,f={},s={},v={},m={};{}{}\\{}", + "{}_Gq=2,a=T,z=-1,C=1,f={},s={},v={},m={};{}{}\\{}", START, format, size.0, diff --git a/yazi-adaptor/src/lib.rs b/yazi-adapter/src/lib.rs similarity index 79% rename from yazi-adaptor/src/lib.rs rename to yazi-adapter/src/lib.rs index 700e944e..0dfe8f9c 100644 --- a/yazi-adaptor/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -1,6 +1,8 @@ #![allow(clippy::unit_arg)] -mod adaptor; +mod adapter; +mod chafa; +mod dimension; mod emulator; mod image; mod iterm2; @@ -9,17 +11,20 @@ mod kitty_old; mod sixel; mod ueberzug; -pub use adaptor::*; +pub use adapter::*; +use chafa::*; +pub use dimension::*; pub use emulator::*; use iterm2::*; use kitty::*; use kitty_old::*; use sixel::*; +use ueberzug::*; use yazi_shared::{env_exists, RoCell}; pub use crate::image::*; -pub static ADAPTOR: RoCell = RoCell::new(); +pub static ADAPTOR: RoCell = RoCell::new(); // Tmux support pub static TMUX: RoCell = RoCell::new(); @@ -31,22 +36,22 @@ static CLOSE: RoCell<&'static str> = RoCell::new(); static SHOWN: RoCell> = RoCell::new(); pub fn init() { - TMUX.init(env_exists("TMUX")); + TMUX.init(env_exists("TMUX") && env_exists("TMUX_PANE")); START.init(if *TMUX { "\x1bPtmux;\x1b\x1b" } else { "\x1b" }); CLOSE.init(if *TMUX { "\x1b\\" } else { "" }); ESCAPE.init(if *TMUX { "\x1b\x1b" } else { "\x1b" }); - SHOWN.with(Default::default); - - ADAPTOR.init(Adaptor::matches()); - ADAPTOR.start(); - if *TMUX { _ = std::process::Command::new("tmux") .args(["set", "-p", "allow-passthrough", "on"]) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) - .spawn(); + .status(); } + + SHOWN.with(Default::default); + + ADAPTOR.init(Adapter::matches()); + ADAPTOR.start(); } diff --git a/yazi-adaptor/src/sixel.rs b/yazi-adapter/src/sixel.rs similarity index 74% rename from yazi-adaptor/src/sixel.rs rename to yazi-adapter/src/sixel.rs index eb4b22ab..b8e48fce 100644 --- a/yazi-adaptor/src/sixel.rs +++ b/yazi-adapter/src/sixel.rs @@ -2,34 +2,34 @@ use std::{io::Write, path::Path}; use anyhow::{bail, Result}; use color_quant::NeuQuant; +use crossterm::{cursor::MoveTo, queue}; use image::DynamicImage; use ratatui::layout::Rect; use yazi_config::PREVIEW; -use yazi_shared::term::Term; -use crate::{adaptor::Adaptor, Image, CLOSE, ESCAPE, START}; +use crate::{adapter::Adapter, Emulator, Image, CLOSE, ESCAPE, START}; pub(super) struct Sixel; impl Sixel { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - let img = Image::downscale(path, rect).await?; - let size = (img.width(), img.height()); + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; + let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; - Adaptor::Sixel.image_hide()?; - Adaptor::shown_store(rect, size); - Term::move_lock((rect.x, rect.y), |stderr| { + Adapter::Sixel.image_hide()?; + Adapter::shown_store(area); + Emulator::move_lock((area.x, area.y), |stderr| { stderr.write_all(&b)?; - Ok(size) + Ok(area) }) } - pub(super) fn image_erase(rect: Rect) -> Result<()> { - let s = " ".repeat(rect.width as usize); - Term::move_lock((0, 0), |stderr| { - for y in rect.top()..rect.bottom() { - Term::move_to(stderr, rect.x, y)?; + pub(super) fn image_erase(area: Rect) -> Result<()> { + let s = " ".repeat(area.width as usize); + Emulator::move_lock((0, 0), |stderr| { + for y in area.top()..area.bottom() { + queue!(stderr, MoveTo(area.x, y))?; write!(stderr, "{s}")?; } Ok(()) diff --git a/yazi-adaptor/src/ueberzug.rs b/yazi-adapter/src/ueberzug.rs similarity index 74% rename from yazi-adaptor/src/ueberzug.rs rename to yazi-adapter/src/ueberzug.rs index 1e3a0106..7072f544 100644 --- a/yazi-adaptor/src/ueberzug.rs +++ b/yazi-adapter/src/ueberzug.rs @@ -8,7 +8,7 @@ use tracing::{debug, warn}; use yazi_config::PREVIEW; use yazi_shared::RoCell; -use crate::{Adaptor, Image}; +use crate::{Adapter, Dimension}; #[allow(clippy::type_complexity)] static DEMON: RoCell>>> = RoCell::new(); @@ -16,12 +16,12 @@ static DEMON: RoCell>>> = RoCell: pub(super) struct Ueberzug; impl Ueberzug { - pub(super) fn start(adaptor: Adaptor) { - if !adaptor.needs_ueberzug() { + pub(super) fn start(adapter: Adapter) { + if !adapter.needs_ueberzug() { return DEMON.init(None); } - let mut child = Self::create_demon(adaptor).ok(); + let mut child = Self::create_demon(adapter).ok(); let (tx, mut rx) = mpsc::unbounded_channel(); tokio::spawn(async move { @@ -31,7 +31,7 @@ impl Ueberzug { child = None; } if child.is_none() { - child = Self::create_demon(adaptor).ok(); + child = Self::create_demon(adapter).ok(); } if let Some(c) = &mut child { Self::send_command(c, cmd).await.ok(); @@ -41,25 +41,27 @@ impl Ueberzug { DEMON.init(Some(tx)) } - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - if let Some(tx) = &*DEMON { - tx.send(Some((path.to_path_buf(), rect)))?; - Adaptor::shown_store(rect, (0, 0)); - } else { + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let Some(tx) = &*DEMON else { bail!("uninitialized ueberzugpp"); - } + }; - let path = path.to_owned(); + let p = path.to_owned(); let ImageSize { width: w, height: h } = - tokio::task::spawn_blocking(move || imagesize::size(path)).await??; + tokio::task::spawn_blocking(move || imagesize::size(p)).await??; - let (max_w, max_h) = Image::max_size(rect); - if w <= max_w as usize && h <= max_h as usize { - return Ok((w as u32, h as u32)); - } + let area = Dimension::ratio() + .map(|(r1, r2)| Rect { + x: max.x, + y: max.y, + width: max.width.min((w.min(PREVIEW.max_width as _) as f64 / r1).ceil() as _), + height: max.height.min((h.min(PREVIEW.max_height as _) as f64 / r2).ceil() as _), + }) + .unwrap_or(max); - let ratio = f64::min(max_w as f64 / w as f64, max_h as f64 / h as f64); - Ok(((w as f64 * ratio).round() as u32, (h as f64 * ratio).round() as u32)) + tx.send(Some((path.to_owned(), area)))?; + Adapter::shown_store(area); + Ok(area) } pub(super) fn image_erase(_: Rect) -> Result<()> { @@ -70,13 +72,14 @@ impl Ueberzug { } } - fn create_demon(adaptor: Adaptor) -> Result { + fn create_demon(adapter: Adapter) -> Result { // TODO: demon let result = Command::new("ueberzugpp") - .args(["layer", "-so", &adaptor.to_string()]) + .args(["layer", "-so", &adapter.to_string()]) .env("SPDLOG_LEVEL", if cfg!(debug_assertions) { "debug" } else { "" }) .kill_on_drop(true) .stdin(Stdio::piped()) + .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn(); diff --git a/yazi-adaptor/src/emulator.rs b/yazi-adaptor/src/emulator.rs deleted file mode 100644 index 1da786c3..00000000 --- a/yazi-adaptor/src/emulator.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::{env, io::{stderr, LineWriter}}; - -use anyhow::{anyhow, Result}; -use crossterm::{cursor::{RestorePosition, SavePosition}, execute, style::Print, terminal::{disable_raw_mode, enable_raw_mode}}; -use tracing::warn; -use yazi_shared::{env_exists, term::Term}; - -use crate::{Adaptor, TMUX}; - -#[derive(Clone, Debug)] -pub enum Emulator { - Unknown(Vec), - Kitty, - Konsole, - Iterm2, - WezTerm, - Foot, - Ghostty, - BlackBox, - VSCode, - Tabby, - Hyper, - Mintty, - Neovim, - Apple, -} - -impl Emulator { - pub fn adapters(self) -> Vec { - match self { - Self::Unknown(adapters) => adapters, - Self::Kitty => vec![Adaptor::Kitty], - Self::Konsole => vec![Adaptor::KittyOld, Adaptor::Iterm2, Adaptor::Sixel], - Self::Iterm2 => vec![Adaptor::Iterm2, Adaptor::Sixel], - Self::WezTerm => vec![Adaptor::Iterm2, Adaptor::Sixel], - Self::Foot => vec![Adaptor::Sixel], - Self::Ghostty => vec![Adaptor::KittyOld], - Self::BlackBox => vec![Adaptor::Sixel], - Self::VSCode => vec![Adaptor::Iterm2, Adaptor::Sixel], - Self::Tabby => vec![Adaptor::Iterm2, Adaptor::Sixel], - Self::Hyper => vec![Adaptor::Iterm2, Adaptor::Sixel], - Self::Mintty => vec![Adaptor::Iterm2], - Self::Neovim => vec![], - Self::Apple => vec![], - } - } -} - -impl Emulator { - pub fn detect() -> Self { - if env_exists("NVIM_LOG_FILE") && env_exists("NVIM") { - return Self::Neovim; - } - - let vars = [ - ("KITTY_WINDOW_ID", Self::Kitty), - ("KONSOLE_VERSION", Self::Konsole), - ("ITERM_SESSION_ID", Self::Iterm2), - ("WEZTERM_EXECUTABLE", Self::WezTerm), - ("GHOSTTY_RESOURCES_DIR", Self::Ghostty), - ("VSCODE_INJECTION", Self::VSCode), - ("TABBY_CONFIG_DIRECTORY", Self::Tabby), - ]; - match vars.into_iter().find(|v| env_exists(v.0)) { - Some(var) => return var.1, - None => warn!("[Adaptor] No special environment variables detected"), - } - - let (term, program) = Self::via_env(); - match program.as_str() { - "iTerm.app" => return Self::Iterm2, - "WezTerm" => return Self::WezTerm, - "ghostty" => return Self::Ghostty, - "BlackBox" => return Self::BlackBox, - "vscode" => return Self::VSCode, - "Tabby" => return Self::Tabby, - "Hyper" => return Self::Hyper, - "mintty" => return Self::Mintty, - "Apple_Terminal" => return Self::Apple, - _ => warn!("[Adaptor] Unknown TERM_PROGRAM: {program}"), - } - match term.as_str() { - "xterm-kitty" => return Self::Kitty, - "foot" => return Self::Foot, - "foot-extra" => return Self::Foot, - "xterm-ghostty" => return Self::Ghostty, - _ => warn!("[Adaptor] Unknown TERM: {term}"), - } - - Self::via_csi().unwrap_or(Self::Unknown(vec![])) - } - - pub fn via_env() -> (String, String) { - fn tmux_env(name: &str) -> Result { - let output = std::process::Command::new("tmux").args(["show-environment", name]).output()?; - - String::from_utf8(output.stdout)? - .trim() - .strip_prefix(&format!("{name}=")) - .map_or_else(|| Err(anyhow!("")), |s| Ok(s.to_string())) - } - - let mut term = env::var("TERM").unwrap_or_default(); - let mut program = env::var("TERM_PROGRAM").unwrap_or_default(); - - if *TMUX { - term = tmux_env("TERM").unwrap_or(term); - program = tmux_env("TERM_PROGRAM").unwrap_or(program); - } - - (term, program) - } - - pub fn via_csi() -> Result { - enable_raw_mode()?; - execute!( - LineWriter::new(stderr()), - SavePosition, - Print("\x1b[>q\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c"), - RestorePosition - )?; - - let resp = futures::executor::block_on(Term::read_until_da1())?; - disable_raw_mode().ok(); - - let names = [ - ("kitty", Self::Kitty), - ("Konsole", Self::Konsole), - ("iTerm2", Self::Iterm2), - ("WezTerm", Self::WezTerm), - ("foot", Self::Foot), - ("ghostty", Self::Ghostty), - ]; - - for (name, emulator) in names.iter() { - if resp.contains(name) { - return Ok(emulator.clone()); - } - } - - let mut adapters = Vec::with_capacity(2); - if resp.contains("\x1b_Gi=31;OK") { - adapters.push(Adaptor::KittyOld); - } - if ["?4;", "?4c", ";4;", ";4c"].iter().any(|s| resp.contains(s)) { - adapters.push(Adaptor::Sixel); - } - - Ok(Self::Unknown(adapters)) - } -} diff --git a/yazi-adaptor/src/iterm2.rs b/yazi-adaptor/src/iterm2.rs deleted file mode 100644 index 5efcfce6..00000000 --- a/yazi-adaptor/src/iterm2.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::{io::Write, path::Path}; - -use anyhow::Result; -use base64::{engine::general_purpose, Engine}; -use image::{codecs::jpeg::JpegEncoder, DynamicImage}; -use ratatui::layout::Rect; -use yazi_shared::term::Term; - -use super::image::Image; -use crate::{adaptor::Adaptor, CLOSE, START}; - -pub(super) struct Iterm2; - -impl Iterm2 { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - let img = Image::downscale(path, rect).await?; - let size = (img.width(), img.height()); - let b = Self::encode(img).await?; - - Adaptor::Iterm2.image_hide()?; - Adaptor::shown_store(rect, size); - Term::move_lock((rect.x, rect.y), |stderr| { - stderr.write_all(&b)?; - Ok(size) - }) - } - - pub(super) fn image_erase(rect: Rect) -> Result<()> { - let s = " ".repeat(rect.width as usize); - Term::move_lock((0, 0), |stderr| { - for y in rect.top()..rect.bottom() { - Term::move_to(stderr, rect.x, y)?; - write!(stderr, "{s}")?; - } - Ok(()) - }) - } - - async fn encode(img: DynamicImage) -> Result> { - tokio::task::spawn_blocking(move || { - let size = (img.width(), img.height()); - - let mut jpg = vec![]; - JpegEncoder::new_with_quality(&mut jpg, 75).encode_image(&img)?; - - let mut buf = vec![]; - write!( - buf, - "{}]1337;File=inline=1;size={};width={}px;height={}px;doNotMoveCursor=1:{}\x07{}", - START, - jpg.len(), - size.0, - size.1, - general_purpose::STANDARD.encode(&jpg), - CLOSE - )?; - Ok(buf) - }) - .await? - } -} diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 0a60541a..ebb7864f 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -9,17 +9,18 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -yazi-adaptor = { path = "../yazi-adaptor", version = "0.2.5" } +regex = "1.10.5" +yazi-adapter = { path = "../yazi-adapter", version = "0.2.5" } yazi-config = { path = "../yazi-config", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -clap = { version = "4.5.4", features = [ "derive" ] } -serde = { version = "1.0.198", features = [ "derive" ] } +clap = { version = "4.5.7", features = [ "derive" ] } +serde = { version = "1.0.203", features = [ "derive" ] } [build-dependencies] -clap = { version = "4.5.4", features = [ "derive" ] } -clap_complete = "4.5.2" -clap_complete_nushell = "4.5.1" -clap_complete_fig = "4.5.0" +clap = { version = "4.5.7", features = [ "derive" ] } +clap_complete = "4.5.6" +clap_complete_nushell = "4.5.2" +clap_complete_fig = "4.5.1" vergen = { version = "8.3.1", features = [ "build", "git", "gitcl" ] } diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index a007bf32..9f412674 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -1,6 +1,7 @@ -use std::{collections::HashSet, env, ffi::OsString, fmt::Write, path::{Path, PathBuf}, process}; +use std::{collections::HashSet, env, ffi::{OsStr, OsString}, fmt::Write, path::{Path, PathBuf}, process}; use clap::Parser; +use regex::Regex; use serde::Serialize; use yazi_config::PREVIEW; use yazi_shared::{fs::{current_cwd, expand_path}, Xdg}; @@ -37,6 +38,22 @@ impl Boot { (parent.unwrap().to_owned(), Some(entry.file_name().unwrap().to_owned())) } + fn process_output(name: impl AsRef, arg: impl AsRef) -> String { + match std::process::Command::new(name.as_ref()).arg(arg).output() { + Ok(out) if out.status.success() => { + let line = + String::from_utf8_lossy(&out.stdout).trim().lines().next().unwrap_or_default().to_owned(); + Regex::new(r"\d+\.\d+(\.\d+-\d+|\.\d+|\b)") + .unwrap() + .find(&line) + .map(|m| m.as_str().to_owned()) + .unwrap_or(line) + } + Ok(out) => format!("{:?}, {:?}", out.status, String::from_utf8_lossy(&out.stderr)), + Err(e) => format!("{e}"), + } + } + fn action_version() -> String { format!( "{} ({} {})", @@ -47,26 +64,29 @@ impl Boot { } fn action_debug() -> Result { - use std::{env::consts::{ARCH, FAMILY, OS}, process::Command}; + use std::env::consts::{ARCH, FAMILY, OS}; let mut s = String::new(); writeln!(s, "\nYazi")?; writeln!(s, " Version: {}", Self::action_version())?; - writeln!(s, " OS: {}-{} ({})", OS, ARCH, FAMILY)?; - writeln!(s, " Debug: {}", cfg!(debug_assertions))?; + writeln!(s, " Debug : {}", cfg!(debug_assertions))?; + writeln!(s, " OS : {}-{} ({})", OS, ARCH, FAMILY)?; + + writeln!(s, "\nYa")?; + writeln!(s, " Version: {}", Self::process_output("ya", "--version"))?; writeln!(s, "\nEmulator")?; - writeln!(s, " Emulator.via_env: {:?}", yazi_adaptor::Emulator::via_env())?; - writeln!(s, " Emulator.via_csi: {:?}", yazi_adaptor::Emulator::via_csi())?; - writeln!(s, " Emulator.detect: {:?}", yazi_adaptor::Emulator::detect())?; + writeln!(s, " Emulator.via_env: {:?}", yazi_adapter::Emulator::via_env())?; + writeln!(s, " Emulator.via_csi: {:?}", yazi_adapter::Emulator::via_csi())?; + writeln!(s, " Emulator.detect : {:?}", yazi_adapter::Emulator::detect())?; - writeln!(s, "\nAdaptor")?; - writeln!(s, " Adaptor.matches: {:?}", yazi_adaptor::Adaptor::matches())?; + writeln!(s, "\nAdapter")?; + writeln!(s, " Adapter.matches: {:?}", yazi_adapter::Adapter::matches())?; writeln!(s, "\nDesktop")?; writeln!(s, " XDG_SESSION_TYPE: {:?}", env::var_os("XDG_SESSION_TYPE"))?; - writeln!(s, " WAYLAND_DISPLAY: {:?}", env::var_os("WAYLAND_DISPLAY"))?; - writeln!(s, " DISPLAY: {:?}", env::var_os("DISPLAY"))?; + writeln!(s, " WAYLAND_DISPLAY : {:?}", env::var_os("WAYLAND_DISPLAY"))?; + writeln!(s, " DISPLAY : {:?}", env::var_os("DISPLAY"))?; writeln!(s, "\nSSH")?; writeln!(s, " shared.in_ssh_connection: {:?}", yazi_shared::in_ssh_connection())?; @@ -79,18 +99,11 @@ impl Boot { )?; writeln!(s, "\nVariables")?; - writeln!(s, " SHELL: {:?}", env::var_os("SHELL"))?; - writeln!(s, " EDITOR: {:?}", env::var_os("EDITOR"))?; + writeln!(s, " SHELL : {:?}", env::var_os("SHELL"))?; + writeln!(s, " EDITOR : {:?}", env::var_os("EDITOR"))?; writeln!(s, " ZELLIJ_SESSION_NAME: {:?}", env::var_os("ZELLIJ_SESSION_NAME"))?; - writeln!(s, " YAZI_FILE_ONE: {:?}", env::var_os("YAZI_FILE_ONE"))?; - writeln!(s, " YAZI_CONFIG_HOME: {:?}", env::var_os("YAZI_CONFIG_HOME"))?; - - writeln!(s, "\nfile(1)")?; - writeln!( - s, - " Version: {:?}", - Command::new(env::var_os("YAZI_FILE_ONE").unwrap_or("file".into())).arg("--version").output() - )?; + writeln!(s, " YAZI_FILE_ONE : {:?}", env::var_os("YAZI_FILE_ONE"))?; + writeln!(s, " YAZI_CONFIG_HOME : {:?}", env::var_os("YAZI_CONFIG_HOME"))?; writeln!(s, "\nText Opener")?; writeln!( @@ -98,13 +111,28 @@ impl Boot { " default: {:?}", yazi_config::OPEN.openers("f75a.txt", "text/plain").and_then(|a| a.first().cloned()) )?; - writeln!(s, " block: {:?}", yazi_config::OPEN.block_opener("bulk.txt", "text/plain"))?; + writeln!(s, " block : {:?}", yazi_config::OPEN.block_opener("bulk.txt", "text/plain"))?; writeln!(s, "\ntmux")?; - writeln!(s, " TMUX: {:?}", *yazi_adaptor::TMUX)?; + writeln!(s, " TMUX : {:?}", *yazi_adapter::TMUX)?; + writeln!(s, " Version: {}", Self::process_output("tmux", "-V"))?; - writeln!(s, "\nUeberzug++")?; - writeln!(s, " Version: {:?}", Command::new("ueberzugpp").arg("--version").output())?; + writeln!(s, "\nDependencies")?; + writeln!( + s, + " file : {}", + Self::process_output(env::var_os("YAZI_FILE_ONE").unwrap_or("file".into()), "--version") + )?; + writeln!(s, " ueberzugpp : {}", Self::process_output("ueberzugpp", "--version"))?; + writeln!(s, " ffmpegthumbnailer: {}", Self::process_output("ffmpegthumbnailer", "-v"))?; + writeln!(s, " magick : {}", Self::process_output("magick", "--version"))?; + writeln!(s, " fzf : {}", Self::process_output("fzf", "--version"))?; + writeln!(s, " fd : {}", Self::process_output("fd", "--version"))?; + writeln!(s, " rg : {}", Self::process_output("rg", "--version"))?; + writeln!(s, " chafa : {}", Self::process_output("chafa", "--version"))?; + writeln!(s, " zoxide : {}", Self::process_output("zoxide", "--version"))?; + writeln!(s, " unar : {}", Self::process_output("unar", "--version"))?; + writeln!(s, " jq : {}", Self::process_output("jq", "--version"))?; writeln!(s, "\n\n--------------------------------------------------")?; writeln!( @@ -145,7 +173,7 @@ impl Default for Boot { .map(|s| s.split(',').map(|s| s.to_owned()).collect()) .unwrap_or_default(); - let boot = Self { + Self { cwd, file, @@ -156,11 +184,7 @@ impl Default for Boot { plugin_dir: config_dir.join("plugins"), config_dir, state_dir: Xdg::state_dir(), - }; - - std::fs::create_dir_all(&boot.flavor_dir).ok(); - std::fs::create_dir_all(&boot.plugin_dir).ok(); - boot + } } } diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 6464852f..1b2d04c5 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -9,22 +9,31 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -yazi-dds = { path = "../yazi-dds", version = "0.2.5" } +yazi-dds = { path = "../yazi-dds", version = "0.2.5" } +yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -anyhow = "1.0.82" -clap = { version = "4.5.4", features = [ "derive" ] } -serde_json = "1.0.116" -tokio = { version = "1.37.0", features = [ "full" ] } +anyhow = "1.0.86" +clap = { version = "4.5.7", features = [ "derive" ] } +crossterm = "0.27.0" +md-5 = "0.10.6" +serde_json = "1.0.117" +tokio = { version = "1.38.0", features = [ "full" ] } +toml_edit = "0.22.14" [build-dependencies] -anyhow = "1.0.82" -clap = { version = "4.5.4", features = [ "derive" ] } -clap_complete = "4.5.2" -clap_complete_fig = "4.5.0" -clap_complete_nushell = "4.5.1" -serde_json = "1.0.116" +anyhow = "1.0.86" +clap = { version = "4.5.7", features = [ "derive" ] } +clap_complete = "4.5.6" +clap_complete_fig = "4.5.1" +clap_complete_nushell = "4.5.2" +serde_json = "1.0.117" +vergen = { version = "8.3.1", features = [ "build", "git", "gitcl" ] } [[bin]] name = "ya" path = "src/main.rs" + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/yazi-{ target }{ archive-suffix }" +bin-dir = "yazi-{ target }/{ bin }{ binary-ext }" diff --git a/yazi-cli/build.rs b/yazi-cli/build.rs index f01b5d0f..b9eb8efd 100644 --- a/yazi-cli/build.rs +++ b/yazi-cli/build.rs @@ -5,8 +5,11 @@ use std::{env, error::Error}; use clap::CommandFactory; use clap_complete::{generate_to, Shell}; +use vergen::EmitBuilder; fn main() -> Result<(), Box> { + EmitBuilder::builder().build_date().git_sha(true).emit()?; + if env::var_os("YAZI_GEN_COMPLETIONS").is_none() { return Ok(()); } diff --git a/yazi-cli/src/args.rs b/yazi-cli/src/args.rs index 554f2a1b..e18f6a99 100644 --- a/yazi-cli/src/args.rs +++ b/yazi-cli/src/args.rs @@ -4,29 +4,34 @@ use anyhow::{bail, Result}; use clap::{command, Parser, Subcommand}; #[derive(Parser)] -#[command(name = "ya", version, about, long_about = None)] -#[command(propagate_version = true)] +#[command(name = "Ya", about, long_about = None)] pub(super) struct Args { #[command(subcommand)] pub(super) command: Command, + + /// Print version + #[arg(short = 'V', long)] + pub(super) version: bool, } #[derive(Subcommand)] pub(super) enum Command { /// Publish a message to remote instance(s). Pub(CommandPub), - /// Publish a static message to all remote instances. - PubStatic(CommandPubStatic), + /// Manage packages. + Pack(CommandPack), + /// Subscribe to messages from all remote instances. + Sub(CommandSub), } #[derive(clap::Args)] pub(super) struct CommandPub { - /// The receiver ID. - #[arg(index = 1)] - pub(super) receiver: u64, /// The kind of message. - #[arg(index = 2)] + #[arg(index = 1)] pub(super) kind: String, + /// The receiver ID. + #[arg(index = 2)] + pub(super) receiver: Option, /// Send the message with a string body. #[arg(long)] pub(super) str: Option, @@ -37,34 +42,16 @@ pub(super) struct CommandPub { impl CommandPub { #[allow(dead_code)] - pub(super) fn body(&self) -> Result> { - if let Some(json) = &self.json { - Ok(json.into()) - } else if let Some(str) = &self.str { - Ok(serde_json::to_string(str)?.into()) + pub(super) fn receiver(&self) -> Result { + if let Some(receiver) = self.receiver { + Ok(receiver) + } else if let Some(s) = std::env::var("YAZI_PID").ok().filter(|s| !s.is_empty()) { + Ok(s.parse()?) } else { - bail!("No body provided"); + bail!("No receiver ID provided, also no YAZI_ID environment variable found.") } } -} -#[derive(clap::Args)] -pub(super) struct CommandPubStatic { - /// The severity of the message. - #[arg(index = 1)] - pub(super) severity: u16, - /// The kind of message. - #[arg(index = 2)] - pub(super) kind: String, - /// Send the message with a string body. - #[arg(long)] - pub(super) str: Option, - /// Send the message with a JSON body. - #[arg(long)] - pub(super) json: Option, -} - -impl CommandPubStatic { #[allow(dead_code)] pub(super) fn body(&self) -> Result> { if let Some(json) = &self.json { @@ -72,7 +59,31 @@ impl CommandPubStatic { } else if let Some(str) = &self.str { Ok(serde_json::to_string(str)?.into()) } else { - bail!("No body provided"); + Ok("".into()) } } } + +#[derive(clap::Args)] +#[command(arg_required_else_help = true)] +pub(super) struct CommandPack { + /// Add a package. + #[arg(short = 'a', long)] + pub(super) add: Option, + /// Install all packages. + #[arg(short = 'i', long)] + pub(super) install: bool, + /// List all packages. + #[arg(short = 'l', long)] + pub(super) list: bool, + /// Upgrade all packages. + #[arg(short = 'u', long)] + pub(super) upgrade: bool, +} + +#[derive(clap::Args)] +pub(super) struct CommandSub { + /// The kind of messages to subscribe to, separated by commas if multiple. + #[arg(index = 1)] + pub(super) kinds: String, +} diff --git a/yazi-cli/src/main.rs b/yazi-cli/src/main.rs index b4ab6b03..bfa09567 100644 --- a/yazi-cli/src/main.rs +++ b/yazi-cli/src/main.rs @@ -1,27 +1,51 @@ mod args; +mod package; use args::*; use clap::Parser; #[tokio::main] async fn main() -> anyhow::Result<()> { - let args = Args::parse(); + if std::env::args_os().nth(1).is_some_and(|s| s == "-V" || s == "--version") { + println!( + "Ya {} ({} {})", + env!("CARGO_PKG_VERSION"), + env!("VERGEN_GIT_SHA"), + env!("VERGEN_BUILD_DATE") + ); + return Ok(()); + } - match &args.command { + match Args::parse().command { Command::Pub(cmd) => { yazi_dds::init(); - if let Err(e) = yazi_dds::Client::shot(&cmd.kind, cmd.receiver, None, &cmd.body()?).await { + if let Err(e) = yazi_dds::Client::shot(&cmd.kind, cmd.receiver()?, &cmd.body()?).await { eprintln!("Cannot send message: {e}"); std::process::exit(1); } } - Command::PubStatic(cmd) => { - yazi_dds::init(); - if let Err(e) = yazi_dds::Client::shot(&cmd.kind, 0, Some(cmd.severity), &cmd.body()?).await { - eprintln!("Cannot send message: {e}"); - std::process::exit(1); + Command::Pack(cmd) => { + package::init(); + if cmd.install { + package::Package::install_from_config("plugin", false).await?; + package::Package::install_from_config("flavor", false).await?; + } else if cmd.list { + package::Package::list_from_config("plugin").await?; + package::Package::list_from_config("flavor").await?; + } else if cmd.upgrade { + package::Package::install_from_config("plugin", true).await?; + package::Package::install_from_config("flavor", true).await?; + } else if let Some(repo) = &cmd.add { + package::Package::add_to_config(repo).await?; } } + + Command::Sub(cmd) => { + yazi_dds::init(); + yazi_dds::Client::draw(cmd.kinds.split(',').collect()).await?; + + tokio::signal::ctrl_c().await?; + } } Ok(()) diff --git a/yazi-cli/src/package/add.rs b/yazi-cli/src/package/add.rs new file mode 100644 index 00000000..a4de93ec --- /dev/null +++ b/yazi-cli/src/package/add.rs @@ -0,0 +1,20 @@ +use anyhow::Result; +use yazi_shared::fs::must_exists; + +use super::{Git, Package}; + +impl Package { + pub(super) async fn add(&mut self) -> Result<()> { + self.output("Upgrading package `{name}`")?; + + let path = self.local(); + if !must_exists(&path).await { + Git::clone(&self.remote(), &path).await?; + } else { + Git::pull(&path).await?; + }; + + self.commit = Git::hash(&path).await?; + self.deploy().await + } +} diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs new file mode 100644 index 00000000..dfa8b7e0 --- /dev/null +++ b/yazi-cli/src/package/deploy.rs @@ -0,0 +1,50 @@ +use anyhow::{bail, Context, Result}; +use tokio::fs; +use yazi_shared::{fs::{maybe_exists, must_exists}, Xdg}; + +use super::Package; + +const TRACKER: &str = "DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY"; + +impl Package { + pub(super) async fn deploy(&mut self) -> Result<()> { + let Some(name) = self.name().map(ToOwned::to_owned) else { bail!("Invalid package url") }; + let from = self.local().join(&self.child); + + self.output("Deploying package `{name}`")?; + self.is_flavor = maybe_exists(&from.join("flavor.toml")).await; + let to = if self.is_flavor { + Xdg::config_dir().join(format!("flavors/{name}")) + } else { + Xdg::config_dir().join(format!("plugins/{name}")) + }; + + let tracker = to.join(TRACKER); + if maybe_exists(&to).await && !must_exists(&tracker).await { + bail!( + "A user package with the same name `{name}` already exists. +For safety, please manually delete it from your plugin/flavor directory and re-run the command." + ); + } + + fs::create_dir_all(&to).await?; + fs::write(tracker, []).await?; + + let files = if self.is_flavor { + &["flavor.toml", "tmtheme.xml", "README.md", "preview.png", "LICENSE", "LICENSE-tmtheme"][..] + } else { + &["init.lua", "README.md", "LICENSE"][..] + }; + + for file in files { + let (from, to) = (from.join(file), to.join(file)); + + fs::copy(&from, &to) + .await + .with_context(|| format!("Failed to copy `{}` to `{}`", from.display(), to.display()))?; + } + + println!("Done!"); + Ok(()) + } +} diff --git a/yazi-cli/src/package/git.rs b/yazi-cli/src/package/git.rs new file mode 100644 index 00000000..d02a3d45 --- /dev/null +++ b/yazi-cli/src/package/git.rs @@ -0,0 +1,55 @@ +use std::path::Path; + +use anyhow::{bail, Context, Result}; +use tokio::process::Command; +use yazi_shared::strip_trailing_newline; + +pub(super) struct Git; + +impl Git { + pub(super) async fn clone(url: &str, path: &Path) -> Result<()> { + Self::exec(|c| c.args(["clone", url]).arg(path)).await + } + + pub(super) async fn fetch(path: &Path) -> Result<()> { + Self::exec(|c| c.current_dir(path).arg("fetch")).await + } + + pub(super) async fn checkout(path: &Path, commit: &str) -> Result<()> { + Self::exec(|c| c.current_dir(path).args(["checkout", commit])).await + } + + pub(super) async fn pull(path: &Path) -> Result<()> { + Self::fetch(path).await?; + Self::checkout(path, "origin/HEAD").await?; + Ok(()) + } + + pub(super) async fn hash(path: &Path) -> Result { + let output = Command::new("git") + .current_dir(path) + .args(["rev-parse", "--short", "HEAD"]) + .output() + .await + .context("Failed to get current commit hash")?; + + if !output.status.success() { + bail!("Getting commit hash failed: {}", output.status); + } + + Ok(strip_trailing_newline( + String::from_utf8(output.stdout).context("Failed to parse commit hash")?, + )) + } + + async fn exec(f: impl FnOnce(&mut Command) -> &mut Command) -> Result<()> { + let status = + f(&mut Command::new("git")).status().await.context("Failed to execute `git` command")?; + + if !status.success() { + bail!("`git` command failed: {status}"); + } + + Ok(()) + } +} diff --git a/yazi-cli/src/package/install.rs b/yazi-cli/src/package/install.rs new file mode 100644 index 00000000..a770fc79 --- /dev/null +++ b/yazi-cli/src/package/install.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use yazi_shared::fs::must_exists; + +use super::{Git, Package}; + +impl Package { + pub(super) async fn install(&mut self) -> Result<()> { + self.output("Installing package `{name}`")?; + + let path = self.local(); + if !must_exists(&path).await { + Git::clone(&self.remote(), &path).await?; + } else { + Git::fetch(&path).await?; + }; + + if self.commit.is_empty() { + self.commit = Git::hash(&path).await?; + } else { + Git::checkout(&path, self.commit.trim_start_matches('=')).await?; + } + + self.deploy().await + } +} diff --git a/yazi-cli/src/package/mod.rs b/yazi-cli/src/package/mod.rs new file mode 100644 index 00000000..2e7ec2a1 --- /dev/null +++ b/yazi-cli/src/package/mod.rs @@ -0,0 +1,17 @@ +#![allow(clippy::module_inception)] + +mod add; +mod deploy; +mod git; +mod install; +mod package; +mod parser; +mod upgrade; + +use git::*; +pub(super) use package::*; + +pub(super) fn init() { + let root = yazi_shared::Xdg::state_dir().join("packages"); + std::fs::create_dir_all(root).expect("Failed to create packages directory"); +} diff --git a/yazi-cli/src/package/package.rs b/yazi-cli/src/package/package.rs new file mode 100644 index 00000000..bee1db87 --- /dev/null +++ b/yazi-cli/src/package/package.rs @@ -0,0 +1,79 @@ +use std::{borrow::Cow, io::BufWriter, path::PathBuf}; + +use anyhow::Result; +use md5::{Digest, Md5}; +use yazi_shared::Xdg; + +pub(crate) struct Package { + pub(crate) repo: String, + pub(crate) child: String, + pub(crate) commit: String, + pub(super) is_flavor: bool, +} + +impl Package { + pub(super) fn new(url: &str, commit: Option<&str>) -> Self { + let mut parts = url.splitn(2, '#'); + + let mut repo = parts.next().unwrap_or_default().to_owned(); + let child = if let Some(s) = parts.next() { + format!("{s}.yazi") + } else { + repo.push_str(".yazi"); + String::new() + }; + + Self { repo, child, commit: commit.unwrap_or_default().to_owned(), is_flavor: false } + } + + #[inline] + pub(super) fn use_(&self) -> Cow { + if self.child.is_empty() { + self.repo.trim_end_matches(".yazi").into() + } else { + format!("{}#{}", self.repo, self.child.trim_end_matches(".yazi")).into() + } + } + + #[inline] + pub(super) fn name(&self) -> Option<&str> { + let s = if self.child.is_empty() { + self.repo.split('/').last().filter(|s| !s.is_empty()) + } else { + Some(self.child.as_str()) + }; + + s.filter(|s| s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.'))) + } + + #[inline] + pub(super) fn local(&self) -> PathBuf { + Xdg::state_dir() + .join("packages") + .join(format!("{:x}", Md5::new_with_prefix(self.remote()).finalize())) + } + + #[inline] + pub(super) fn remote(&self) -> String { + // Support more Git hosting services in the future + format!("https://github.com/{}.git", self.repo) + } + + pub(super) fn output(&self, s: &str) -> Result<()> { + use crossterm::style::{Attribute, Print, SetAttributes}; + + crossterm::execute!( + BufWriter::new(std::io::stdout()), + Print("\n"), + SetAttributes(Attribute::Reverse.into()), + SetAttributes(Attribute::Bold.into()), + Print(" "), + Print(s.replacen("{name}", self.name().unwrap_or_default(), 1)), + Print(" "), + SetAttributes(Attribute::NoBold.into()), + SetAttributes(Attribute::NoReverse.into()), + Print("\n\n"), + )?; + Ok(()) + } +} diff --git a/yazi-cli/src/package/parser.rs b/yazi-cli/src/package/parser.rs new file mode 100644 index 00000000..15b80b54 --- /dev/null +++ b/yazi-cli/src/package/parser.rs @@ -0,0 +1,135 @@ +use anyhow::{bail, Context, Result}; +use tokio::fs; +use toml_edit::{Array, DocumentMut, InlineTable, Item, Value}; +use yazi_shared::Xdg; + +use super::Package; + +impl Package { + pub(crate) async fn add_to_config(use_: &str) -> Result<()> { + let mut package = Self::new(use_, None); + let Some(name) = package.name() else { bail!("Invalid package `use`") }; + + let path = Xdg::config_dir().join("package.toml"); + let mut doc = Self::ensure_config(&fs::read_to_string(&path).await.unwrap_or_default())?; + + Self::ensure_unique(&doc, name)?; + package.add().await?; + + let mut table = InlineTable::new(); + table.insert("use", package.use_().as_ref().into()); + if !package.commit.is_empty() { + table.insert("commit", package.commit.into()); + } + + if package.is_flavor { + doc["flavor"]["deps"].as_array_mut().unwrap().push(table); + } else { + doc["plugin"]["deps"].as_array_mut().unwrap().push(table); + } + + fs::write(path, doc.to_string()).await?; + Ok(()) + } + + pub(crate) async fn install_from_config(section: &str, upgrade: bool) -> Result<()> { + let path = Xdg::config_dir().join("package.toml"); + let Ok(s) = fs::read_to_string(&path).await else { + return Ok(()); + }; + + let mut doc = s.parse::().context("Failed to parse package.toml")?; + let Some(deps) = doc.get_mut(section).and_then(|d| d.get_mut("deps")) else { + return Ok(()); + }; + + let deps = deps.as_array_mut().context("`deps` must be an array")?; + for dep in deps.iter_mut() { + let dep = dep.as_inline_table_mut().context("Dependency must be an inline table")?; + let use_ = dep.get("use").and_then(|d| d.as_str()).context("Missing `use` field")?; + let commit = dep.get("commit").and_then(|d| d.as_str()); + + let mut package = Package::new(use_, commit); + if upgrade { + package.upgrade().await?; + } else { + package.install().await?; + } + + if package.commit.is_empty() { + dep.remove("commit"); + } else { + dep.insert("commit", package.commit.into()); + } + } + + fs::write(path, doc.to_string()).await.context("Failed to write package.toml") + } + + pub(crate) async fn list_from_config(section: &str) -> Result<()> { + let path = Xdg::config_dir().join("package.toml"); + let Ok(s) = fs::read_to_string(&path).await else { + return Ok(()); + }; + + let doc = s.parse::().context("Failed to parse package.toml")?; + let Some(deps) = doc.get(section).and_then(|d| d.get("deps")) else { + return Ok(()); + }; + + let deps = deps.as_array().context("`deps` must be an array")?; + println!("{section}s:"); + + for dep in deps { + if let Some(Value::String(use_)) = dep.as_inline_table().and_then(|t| t.get("use")) { + println!("\t{}", use_.value()); + } + } + Ok(()) + } + + fn ensure_config(s: &str) -> Result { + let mut doc = s.parse::().context("Failed to parse package.toml")?; + + doc + .entry("plugin") + .or_insert(toml_edit::table()) + .as_table_mut() + .context("Failed to get `plugin` table")? + .entry("deps") + .or_insert(Item::Value(Array::new().into())) + .as_array() + .context("Failed to get `deps` array")?; + + doc + .entry("flavor") + .or_insert(toml_edit::table()) + .as_table_mut() + .context("Failed to get `flavor` table")? + .entry("deps") + .or_insert(Item::Value(Array::new().into())) + .as_array() + .context("Failed to get `deps` array")?; + + Ok(doc) + } + + fn ensure_unique(doc: &DocumentMut, name: &str) -> Result<()> { + #[inline] + fn same(v: &Value, name: &str) -> bool { + v.as_inline_table() + .and_then(|t| t.get("use")) + .and_then(|v| v.as_str()) + .is_some_and(|s| Package::new(s, None).name() == Some(name)) + } + + if doc["plugin"]["deps"].as_array().unwrap().into_iter().any(|v| same(v, name)) { + bail!("Plugin `{name}` already exists in package.toml"); + } + if doc["flavor"]["deps"].as_array().unwrap().into_iter().any(|v| same(v, name)) { + bail!("Flavor `{name}` already exists in package.toml"); + } + + Ok(()) + } +} diff --git a/yazi-cli/src/package/upgrade.rs b/yazi-cli/src/package/upgrade.rs new file mode 100644 index 00000000..24c9df12 --- /dev/null +++ b/yazi-cli/src/package/upgrade.rs @@ -0,0 +1,9 @@ +use anyhow::Result; + +use super::Package; + +impl Package { + pub(super) async fn upgrade(&mut self) -> Result<()> { + if self.commit.starts_with('=') { Ok(()) } else { self.add().await } + } +} diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index 2ca80cff..8df9f74d 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -12,13 +12,13 @@ repository = "https://github.com/sxyazi/yazi" yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -anyhow = "1.0.82" +anyhow = "1.0.86" arc-swap = "1.7.1" +bitflags = "2.5.0" crossterm = "0.27.0" globset = "0.4.14" indexmap = "2.2.6" -ratatui = "=0.26.1" -serde = { version = "1.0.198", features = [ "derive" ] } -shell-words = "1.1.0" -toml = { version = "0.8.12", features = [ "preserve_order" ] } +ratatui = "0.27.0" +serde = { version = "1.0.203", features = [ "derive" ] } +toml = { version = "0.8.14", features = [ "preserve_order" ] } validator = { version = "0.18.1", features = [ "derive" ] } diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index 0ab02b91..daa6b442 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -5,305 +5,305 @@ [manager] keymap = [ - { on = [ "" ], run = "escape", desc = "Exit visual mode, clear selected, or cancel search" }, - { on = [ "" ], run = "escape", desc = "Exit visual mode, clear selected, or cancel search" }, - { on = [ "q" ], run = "quit", desc = "Exit the process" }, - { on = [ "Q" ], run = "quit --no-cwd-file", desc = "Exit the process without writing cwd-file" }, - { on = [ "" ], run = "close", desc = "Close the current tab, or quit if it is last tab" }, - { on = [ "" ], run = "suspend", desc = "Suspend the process" }, + { on = "", run = "escape", desc = "Exit visual mode, clear selected, or cancel search" }, + { on = "", run = "escape", desc = "Exit visual mode, clear selected, or cancel search" }, + { on = "q", run = "quit", desc = "Exit the process" }, + { on = "Q", run = "quit --no-cwd-file", desc = "Exit the process without writing cwd-file" }, + { on = "", run = "close", desc = "Close the current tab, or quit if it is last tab" }, + { on = "", run = "suspend", desc = "Suspend the process" }, # Navigation - { on = [ "k" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "j" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "k", run = "arrow -1", desc = "Move cursor up" }, + { on = "j", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "K" ], run = "arrow -5", desc = "Move cursor up 5 lines" }, - { on = [ "J" ], run = "arrow 5", desc = "Move cursor down 5 lines" }, + { on = "K", run = "arrow -5", desc = "Move cursor up 5 lines" }, + { on = "J", run = "arrow 5", desc = "Move cursor down 5 lines" }, - { on = [ "" ], run = "arrow -5", desc = "Move cursor up 5 lines" }, - { on = [ "" ], run = "arrow 5", desc = "Move cursor down 5 lines" }, + { on = "", run = "arrow -5", desc = "Move cursor up 5 lines" }, + { on = "", run = "arrow 5", desc = "Move cursor down 5 lines" }, - { on = [ "" ], run = "arrow -50%", desc = "Move cursor up half page" }, - { on = [ "" ], run = "arrow 50%", desc = "Move cursor down half page" }, - { on = [ "" ], run = "arrow -100%", desc = "Move cursor up one page" }, - { on = [ "" ], run = "arrow 100%", desc = "Move cursor down one page" }, + { on = "", run = "arrow -50%", desc = "Move cursor up half page" }, + { on = "", run = "arrow 50%", desc = "Move cursor down half page" }, + { on = "", run = "arrow -100%", desc = "Move cursor up one page" }, + { on = "", run = "arrow 100%", desc = "Move cursor down one page" }, - { on = [ "" ], run = "arrow -50%", desc = "Move cursor up half page" }, - { on = [ "" ], run = "arrow 50%", desc = "Move cursor down half page" }, - { on = [ "" ], run = "arrow -100%", desc = "Move cursor up one page" }, - { on = [ "" ], run = "arrow 100%", desc = "Move cursor down one page" }, + { on = "", run = "arrow -50%", desc = "Move cursor up half page" }, + { on = "", run = "arrow 50%", desc = "Move cursor down half page" }, + { on = "", run = "arrow -100%", desc = "Move cursor up one page" }, + { on = "", run = "arrow 100%", desc = "Move cursor down one page" }, - { on = [ "h" ], run = "leave", desc = "Go back to the parent directory" }, - { on = [ "l" ], run = "enter", desc = "Enter the child directory" }, + { on = "h", run = "leave", desc = "Go back to the parent directory" }, + { on = "l", run = "enter", desc = "Enter the child directory" }, - { on = [ "H" ], run = "back", desc = "Go back to the previous directory" }, - { on = [ "L" ], run = "forward", desc = "Go forward to the next directory" }, + { on = "H", run = "back", desc = "Go back to the previous directory" }, + { on = "L", run = "forward", desc = "Go forward to the next directory" }, - { on = [ "" ], run = "seek -5", desc = "Seek up 5 units in the preview" }, - { on = [ "" ], run = "seek 5", desc = "Seek down 5 units in the preview" }, - { on = [ "" ], run = "seek -5", desc = "Seek up 5 units in the preview" }, - { on = [ "" ], run = "seek 5", desc = "Seek down 5 units in the preview" }, + { on = "", run = "seek -5", desc = "Seek up 5 units in the preview" }, + { on = "", run = "seek 5", desc = "Seek down 5 units in the preview" }, + { on = "", run = "seek -5", desc = "Seek up 5 units in the preview" }, + { on = "", run = "seek 5", desc = "Seek down 5 units in the preview" }, - { on = [ "" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "" ], run = "arrow 1", desc = "Move cursor down" }, - { on = [ "" ], run = "leave", desc = "Go back to the parent directory" }, - { on = [ "" ], run = "enter", desc = "Enter the child directory" }, + { on = "", run = "arrow -1", desc = "Move cursor up" }, + { on = "", run = "arrow 1", desc = "Move cursor down" }, + { on = "", run = "leave", desc = "Go back to the parent directory" }, + { on = "", run = "enter", desc = "Enter the child directory" }, { on = [ "g", "g" ], run = "arrow -99999999", desc = "Move cursor to the top" }, - { on = [ "G" ], run = "arrow 99999999", desc = "Move cursor to the bottom" }, + { on = "G", run = "arrow 99999999", desc = "Move cursor to the bottom" }, # Selection - { on = [ "" ], run = [ "select --state=none", "arrow 1" ], desc = "Toggle the current selection state" }, - { on = [ "v" ], run = "visual_mode", desc = "Enter visual mode (selection mode)" }, - { on = [ "V" ], run = "visual_mode --unset", desc = "Enter visual mode (unset mode)" }, - { on = [ "" ], run = "select_all --state=true", desc = "Select all files" }, - { on = [ "" ], run = "select_all --state=none", desc = "Inverse selection of all files" }, + { on = "", run = [ "select --state=none", "arrow 1" ], desc = "Toggle the current selection state" }, + { on = "v", run = "visual_mode", desc = "Enter visual mode (selection mode)" }, + { on = "V", run = "visual_mode --unset", desc = "Enter visual mode (unset mode)" }, + { on = "", run = "select_all --state=true", desc = "Select all files" }, + { on = "", run = "select_all --state=none", desc = "Inverse selection of all files" }, # Operation - { on = [ "o" ], run = "open", desc = "Open the selected files" }, - { on = [ "O" ], run = "open --interactive", desc = "Open the selected files interactively" }, - { on = [ "" ], run = "open", desc = "Open the selected files" }, - { on = [ "" ], run = "open --interactive", desc = "Open the selected files interactively" }, - { on = [ "y" ], run = "yank", desc = "Copy the selected files" }, - { on = [ "Y" ], run = "unyank", desc = "Cancel the yank status of files" }, - { on = [ "x" ], run = "yank --cut", desc = "Cut the selected files" }, - { on = [ "X" ], run = "unyank", desc = "Cancel the yank status of files" }, - { on = [ "p" ], run = "paste", desc = "Paste the files" }, - { on = [ "P" ], run = "paste --force", desc = "Paste the files (overwrite if the destination exists)" }, - { on = [ "-" ], run = "link", desc = "Symlink the absolute path of files" }, - { on = [ "_" ], run = "link --relative", desc = "Symlink the relative path of files" }, - { on = [ "d" ], run = "remove", desc = "Move the files to the trash" }, - { on = [ "D" ], run = "remove --permanently", desc = "Permanently delete the files" }, - { on = [ "a" ], run = "create", desc = "Create a file or directory (ends with / for directories)" }, - { on = [ "r" ], run = "rename --cursor=before_ext", desc = "Rename a file or directory" }, - { on = [ ";" ], run = "shell", desc = "Run a shell command" }, - { on = [ ":" ], run = "shell --block", desc = "Run a shell command (block the UI until the command finishes)" }, - { on = [ "." ], run = "hidden toggle", desc = "Toggle the visibility of hidden files" }, - { on = [ "s" ], run = "search fd", desc = "Search files by name using fd" }, - { on = [ "S" ], run = "search rg", desc = "Search files by content using ripgrep" }, - { on = [ "" ], run = "search none", desc = "Cancel the ongoing search" }, - { on = [ "z" ], run = "plugin zoxide", desc = "Jump to a directory using zoxide" }, - { on = [ "Z" ], run = "plugin fzf", desc = "Jump to a directory, or reveal a file using fzf" }, + { on = "o", run = "open", desc = "Open selected files" }, + { on = "O", run = "open --interactive", desc = "Open selected files interactively" }, + { on = "", run = "open", desc = "Open selected files" }, + { on = "", run = "open --interactive", desc = "Open selected files interactively" }, + { on = "y", run = "yank", desc = "Copy selected files" }, + { on = "x", run = "yank --cut", desc = "Cut the selected files" }, + { on = "Y", run = "unyank", desc = "Cancel the yank status" }, + { on = "X", run = "unyank", desc = "Cancel the yank status" }, + { on = "p", run = "paste", desc = "Paste yanked files" }, + { on = "P", run = "paste --force", desc = "Paste yanked files (overwrite if the destination exists)" }, + { on = "-", run = "link", desc = "Symlink the absolute path of yanked files" }, + { on = "_", run = "link --relative", desc = "Symlink the relative path of yanked files" }, + { on = "d", run = "remove", desc = "Trash selected files" }, + { on = "D", run = "remove --permanently", desc = "Permanently delete selected files" }, + { on = "a", run = "create", desc = "Create a file (ends with / for directories)" }, + { on = "r", run = "rename --cursor=before_ext", desc = "Rename selected file(s)" }, + { on = ";", run = "shell --interactive", desc = "Run a shell command" }, + { on = ":", run = "shell --block --interactive", desc = "Run a shell command (block until finishes)" }, + { on = ".", run = "hidden toggle", desc = "Toggle the visibility of hidden files" }, + { on = "s", run = "search fd", desc = "Search files by name using fd" }, + { on = "S", run = "search rg", desc = "Search files by content using ripgrep" }, + { on = "", run = "search none", desc = "Cancel the ongoing search" }, + { on = "z", run = "plugin zoxide", desc = "Jump to a directory using zoxide" }, + { on = "Z", run = "plugin fzf", desc = "Jump to a directory or reveal a file using fzf" }, # Linemode { on = [ "m", "s" ], run = "linemode size", desc = "Set linemode to size" }, { on = [ "m", "p" ], run = "linemode permissions", desc = "Set linemode to permissions" }, { on = [ "m", "m" ], run = "linemode mtime", desc = "Set linemode to mtime" }, + { on = [ "m", "o" ], run = "linemode owner", desc = "Set linemode to owner" }, { on = [ "m", "n" ], run = "linemode none", desc = "Set linemode to none" }, # Copy - { on = [ "c", "c" ], run = "copy path", desc = "Copy the absolute path" }, - { on = [ "c", "d" ], run = "copy dirname", desc = "Copy the path of the parent directory" }, - { on = [ "c", "f" ], run = "copy filename", desc = "Copy the name of the file" }, - { on = [ "c", "n" ], run = "copy name_without_ext", desc = "Copy the name of the file without the extension" }, + { on = [ "c", "c" ], run = "copy path", desc = "Copy the file path" }, + { on = [ "c", "d" ], run = "copy dirname", desc = "Copy the directory path" }, + { on = [ "c", "f" ], run = "copy filename", desc = "Copy the filename" }, + { on = [ "c", "n" ], run = "copy name_without_ext", desc = "Copy the filename without extension" }, # Filter - { on = [ "f" ], run = "filter --smart", desc = "Filter the files" }, + { on = "f", run = "filter --smart", desc = "Filter files" }, # Find - { on = [ "/" ], run = "find --smart", desc = "Find next file" }, - { on = [ "?" ], run = "find --previous --smart", desc = "Find previous file" }, - { on = [ "n" ], run = "find_arrow", desc = "Go to next found file" }, - { on = [ "N" ], run = "find_arrow --previous", desc = "Go to previous found file" }, + { on = "/", run = "find --smart", desc = "Find next file" }, + { on = "?", run = "find --previous --smart", desc = "Find previous file" }, + { on = "n", run = "find_arrow", desc = "Go to the next found" }, + { on = "N", run = "find_arrow --previous", desc = "Go to the previous found" }, # Sorting - { on = [ ",", "m" ], run = "sort modified --dir-first", desc = "Sort by modified time" }, - { on = [ ",", "M" ], run = "sort modified --reverse --dir-first", desc = "Sort by modified time (reverse)" }, - { on = [ ",", "c" ], run = "sort created --dir-first", desc = "Sort by created time" }, - { on = [ ",", "C" ], run = "sort created --reverse --dir-first", desc = "Sort by created time (reverse)" }, - { on = [ ",", "e" ], run = "sort extension --dir-first", desc = "Sort by extension" }, - { on = [ ",", "E" ], run = "sort extension --reverse --dir-first", desc = "Sort by extension (reverse)" }, - { on = [ ",", "a" ], run = "sort alphabetical --dir-first", desc = "Sort alphabetically" }, - { on = [ ",", "A" ], run = "sort alphabetical --reverse --dir-first", desc = "Sort alphabetically (reverse)" }, - { on = [ ",", "n" ], run = "sort natural --dir-first", desc = "Sort naturally" }, - { on = [ ",", "N" ], run = "sort natural --reverse --dir-first", desc = "Sort naturally (reverse)" }, - { on = [ ",", "s" ], run = "sort size --dir-first", desc = "Sort by size" }, - { on = [ ",", "S" ], run = "sort size --reverse --dir-first", desc = "Sort by size (reverse)" }, + { on = [ ",", "m" ], run = "sort modified --reverse=no", desc = "Sort by modified time" }, + { on = [ ",", "M" ], run = "sort modified --reverse", desc = "Sort by modified time (reverse)" }, + { on = [ ",", "c" ], run = "sort created --reverse=no", desc = "Sort by created time" }, + { on = [ ",", "C" ], run = "sort created --reverse", desc = "Sort by created time (reverse)" }, + { on = [ ",", "e" ], run = "sort extension --reverse=no", desc = "Sort by extension" }, + { on = [ ",", "E" ], run = "sort extension --reverse", desc = "Sort by extension (reverse)" }, + { on = [ ",", "a" ], run = "sort alphabetical --reverse=no", desc = "Sort alphabetically" }, + { on = [ ",", "A" ], run = "sort alphabetical --reverse", desc = "Sort alphabetically (reverse)" }, + { on = [ ",", "n" ], run = "sort natural --reverse=no", desc = "Sort naturally" }, + { on = [ ",", "N" ], run = "sort natural --reverse", desc = "Sort naturally (reverse)" }, + { on = [ ",", "s" ], run = "sort size --reverse=no", desc = "Sort by size" }, + { on = [ ",", "S" ], run = "sort size --reverse", desc = "Sort by size (reverse)" }, # Tabs - { on = [ "t" ], run = "tab_create --current", desc = "Create a new tab using the current path" }, + { on = "t", run = "tab_create --current", desc = "Create a new tab with CWD" }, - { on = [ "1" ], run = "tab_switch 0", desc = "Switch to the first tab" }, - { on = [ "2" ], run = "tab_switch 1", desc = "Switch to the second tab" }, - { on = [ "3" ], run = "tab_switch 2", desc = "Switch to the third tab" }, - { on = [ "4" ], run = "tab_switch 3", desc = "Switch to the fourth tab" }, - { on = [ "5" ], run = "tab_switch 4", desc = "Switch to the fifth tab" }, - { on = [ "6" ], run = "tab_switch 5", desc = "Switch to the sixth tab" }, - { on = [ "7" ], run = "tab_switch 6", desc = "Switch to the seventh tab" }, - { on = [ "8" ], run = "tab_switch 7", desc = "Switch to the eighth tab" }, - { on = [ "9" ], run = "tab_switch 8", desc = "Switch to the ninth tab" }, + { on = "1", run = "tab_switch 0", desc = "Switch to the first tab" }, + { on = "2", run = "tab_switch 1", desc = "Switch to the second tab" }, + { on = "3", run = "tab_switch 2", desc = "Switch to the third tab" }, + { on = "4", run = "tab_switch 3", desc = "Switch to the fourth tab" }, + { on = "5", run = "tab_switch 4", desc = "Switch to the fifth tab" }, + { on = "6", run = "tab_switch 5", desc = "Switch to the sixth tab" }, + { on = "7", run = "tab_switch 6", desc = "Switch to the seventh tab" }, + { on = "8", run = "tab_switch 7", desc = "Switch to the eighth tab" }, + { on = "9", run = "tab_switch 8", desc = "Switch to the ninth tab" }, - { on = [ "[" ], run = "tab_switch -1 --relative", desc = "Switch to the previous tab" }, - { on = [ "]" ], run = "tab_switch 1 --relative", desc = "Switch to the next tab" }, + { on = "[", run = "tab_switch -1 --relative", desc = "Switch to the previous tab" }, + { on = "]", run = "tab_switch 1 --relative", desc = "Switch to the next tab" }, - { on = [ "{" ], run = "tab_swap -1", desc = "Swap the current tab with the previous tab" }, - { on = [ "}" ], run = "tab_swap 1", desc = "Swap the current tab with the next tab" }, + { on = "{", run = "tab_swap -1", desc = "Swap current tab with previous tab" }, + { on = "}", run = "tab_swap 1", desc = "Swap current tab with next tab" }, # Tasks - { on = [ "w" ], run = "tasks_show", desc = "Show the tasks manager" }, + { on = "w", run = "tasks_show", desc = "Show task manager" }, # Goto { on = [ "g", "h" ], run = "cd ~", desc = "Go to the home directory" }, { on = [ "g", "c" ], run = "cd ~/.config", desc = "Go to the config directory" }, { on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go to the downloads directory" }, - { on = [ "g", "t" ], run = "cd /tmp", desc = "Go to the temporary directory" }, { on = [ "g", "" ], run = "cd --interactive", desc = "Go to a directory interactively" }, # Help - { on = [ "~" ], run = "help", desc = "Open help" }, + { on = "~", run = "help", desc = "Open help" }, ] [tasks] keymap = [ - { on = [ "" ], run = "close", desc = "Hide the task manager" }, - { on = [ "" ], run = "close", desc = "Hide the task manager" }, - { on = [ "" ], run = "close", desc = "Hide the task manager" }, - { on = [ "w" ], run = "close", desc = "Hide the task manager" }, + { on = "", run = "close", desc = "Close task manager" }, + { on = "", run = "close", desc = "Close task manager" }, + { on = "", run = "close", desc = "Close task manager" }, + { on = "w", run = "close", desc = "Close task manager" }, - { on = [ "k" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "j" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "k", run = "arrow -1", desc = "Move cursor up" }, + { on = "j", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "", run = "arrow -1", desc = "Move cursor up" }, + { on = "", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "" ], run = "inspect", desc = "Inspect the task" }, - { on = [ "x" ], run = "cancel", desc = "Cancel the task" }, + { on = "", run = "inspect", desc = "Inspect the task" }, + { on = "x", run = "cancel", desc = "Cancel the task" }, - { on = [ "~" ], run = "help", desc = "Open help" } + { on = "~", run = "help", desc = "Open help" } ] [select] keymap = [ - { on = [ "" ], run = "close", desc = "Cancel selection" }, - { on = [ "" ], run = "close", desc = "Cancel selection" }, - { on = [ "" ], run = "close", desc = "Cancel selection" }, - { on = [ "" ], run = "close --submit", desc = "Submit the selection" }, + { on = "", run = "close", desc = "Cancel selection" }, + { on = "", run = "close", desc = "Cancel selection" }, + { on = "", run = "close", desc = "Cancel selection" }, + { on = "", run = "close --submit", desc = "Submit the selection" }, - { on = [ "k" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "j" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "k", run = "arrow -1", desc = "Move cursor up" }, + { on = "j", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "K" ], run = "arrow -5", desc = "Move cursor up 5 lines" }, - { on = [ "J" ], run = "arrow 5", desc = "Move cursor down 5 lines" }, + { on = "K", run = "arrow -5", desc = "Move cursor up 5 lines" }, + { on = "J", run = "arrow 5", desc = "Move cursor down 5 lines" }, - { on = [ "" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "", run = "arrow -1", desc = "Move cursor up" }, + { on = "", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "" ], run = "arrow -5", desc = "Move cursor up 5 lines" }, - { on = [ "" ], run = "arrow 5", desc = "Move cursor down 5 lines" }, + { on = "", run = "arrow -5", desc = "Move cursor up 5 lines" }, + { on = "", run = "arrow 5", desc = "Move cursor down 5 lines" }, - { on = [ "~" ], run = "help", desc = "Open help" } + { on = "~", run = "help", desc = "Open help" } ] [input] keymap = [ - { on = [ "" ], run = "close", desc = "Cancel input" }, - { on = [ "" ], run = "close --submit", desc = "Submit the input" }, - { on = [ "" ], run = "escape", desc = "Go back the normal mode, or cancel input" }, - { on = [ "" ], run = "escape", desc = "Go back the normal mode, or cancel input" }, + { on = "", run = "close", desc = "Cancel input" }, + { on = "", run = "close --submit", desc = "Submit the input" }, + { on = "", run = "escape", desc = "Go back the normal mode, or cancel input" }, + { on = "", run = "escape", desc = "Go back the normal mode, or cancel input" }, # Mode - { on = [ "i" ], run = "insert", desc = "Enter insert mode" }, - { on = [ "a" ], run = "insert --append", desc = "Enter append mode" }, - { on = [ "I" ], run = [ "move -999", "insert" ], desc = "Move to the BOL, and enter insert mode" }, - { on = [ "A" ], run = [ "move 999", "insert --append" ], desc = "Move to the EOL, and enter append mode" }, - { on = [ "v" ], run = "visual", desc = "Enter visual mode" }, - { on = [ "V" ], run = [ "move -999", "visual", "move 999" ], desc = "Enter visual mode and select all" }, + { on = "i", run = "insert", desc = "Enter insert mode" }, + { on = "a", run = "insert --append", desc = "Enter append mode" }, + { on = "I", run = [ "move -999", "insert" ], desc = "Move to the BOL, and enter insert mode" }, + { on = "A", run = [ "move 999", "insert --append" ], desc = "Move to the EOL, and enter append mode" }, + { on = "v", run = "visual", desc = "Enter visual mode" }, + { on = "V", run = [ "move -999", "visual", "move 999" ], desc = "Enter visual mode and select all" }, # Character-wise movement - { on = [ "h" ], run = "move -1", desc = "Move back a character" }, - { on = [ "l" ], run = "move 1", desc = "Move forward a character" }, - { on = [ "" ], run = "move -1", desc = "Move back a character" }, - { on = [ "" ], run = "move 1", desc = "Move forward a character" }, - { on = [ "" ], run = "move -1", desc = "Move back a character" }, - { on = [ "" ], run = "move 1", desc = "Move forward a character" }, + { on = "h", run = "move -1", desc = "Move back a character" }, + { on = "l", run = "move 1", desc = "Move forward a character" }, + { on = "", run = "move -1", desc = "Move back a character" }, + { on = "", run = "move 1", desc = "Move forward a character" }, + { on = "", run = "move -1", desc = "Move back a character" }, + { on = "", run = "move 1", desc = "Move forward a character" }, # Word-wise movement - { on = [ "b" ], run = "backward", desc = "Move back to the start of the current or previous word" }, - { on = [ "w" ], run = "forward", desc = "Move forward to the start of the next word" }, - { on = [ "e" ], run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, - { on = [ "" ], run = "backward", desc = "Move back to the start of the current or previous word" }, - { on = [ "" ], run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, + { on = "b", run = "backward", desc = "Move back to the start of the current or previous word" }, + { on = "w", run = "forward", desc = "Move forward to the start of the next word" }, + { on = "e", run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, + { on = "", run = "backward", desc = "Move back to the start of the current or previous word" }, + { on = "", run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, # Line-wise movement - { on = [ "0" ], run = "move -999", desc = "Move to the BOL" }, - { on = [ "$" ], run = "move 999", desc = "Move to the EOL" }, - { on = [ "" ], run = "move -999", desc = "Move to the BOL" }, - { on = [ "" ], run = "move 999", desc = "Move to the EOL" }, - { on = [ "" ], run = "move -999", desc = "Move to the BOL" }, - { on = [ "" ], run = "move 999", desc = "Move to the EOL" }, + { on = "0", run = "move -999", desc = "Move to the BOL" }, + { on = "$", run = "move 999", desc = "Move to the EOL" }, + { on = "", run = "move -999", desc = "Move to the BOL" }, + { on = "", run = "move 999", desc = "Move to the EOL" }, + { on = "", run = "move -999", desc = "Move to the BOL" }, + { on = "", run = "move 999", desc = "Move to the EOL" }, # Delete - { on = [ "" ], run = "backspace", desc = "Delete the character before the cursor" }, - { on = [ "" ], run = "backspace --under", desc = "Delete the character under the cursor" }, - { on = [ "" ], run = "backspace", desc = "Delete the character before the cursor" }, - { on = [ "" ], run = "backspace --under", desc = "Delete the character under the cursor" }, + { on = "", run = "backspace", desc = "Delete the character before the cursor" }, + { on = "", run = "backspace --under", desc = "Delete the character under the cursor" }, + { on = "", run = "backspace", desc = "Delete the character before the cursor" }, + { on = "", run = "backspace --under", desc = "Delete the character under the cursor" }, # Kill - { on = [ "" ], run = "kill bol", desc = "Kill backwards to the BOL" }, - { on = [ "" ], run = "kill eol", desc = "Kill forwards to the EOL" }, - { on = [ "" ], run = "kill backward", desc = "Kill backwards to the start of the current word" }, - { on = [ "" ], run = "kill forward", desc = "Kill forwards to the end of the current word" }, + { on = "", run = "kill bol", desc = "Kill backwards to the BOL" }, + { on = "", run = "kill eol", desc = "Kill forwards to the EOL" }, + { on = "", run = "kill backward", desc = "Kill backwards to the start of the current word" }, + { on = "", run = "kill forward", desc = "Kill forwards to the end of the current word" }, # Cut/Yank/Paste - { on = [ "d" ], run = "delete --cut", desc = "Cut the selected characters" }, - { on = [ "D" ], run = [ "delete --cut", "move 999" ], desc = "Cut until the EOL" }, - { on = [ "c" ], run = "delete --cut --insert", desc = "Cut the selected characters, and enter insert mode" }, - { on = [ "C" ], run = [ "delete --cut --insert", "move 999" ], desc = "Cut until the EOL, and enter insert mode" }, - { on = [ "x" ], run = [ "delete --cut", "move 1 --in-operating" ], desc = "Cut the current character" }, - { on = [ "y" ], run = "yank", desc = "Copy the selected characters" }, - { on = [ "p" ], run = "paste", desc = "Paste the copied characters after the cursor" }, - { on = [ "P" ], run = "paste --before", desc = "Paste the copied characters before the cursor" }, + { on = "d", run = "delete --cut", desc = "Cut the selected characters" }, + { on = "D", run = [ "delete --cut", "move 999" ], desc = "Cut until the EOL" }, + { on = "c", run = "delete --cut --insert", desc = "Cut the selected characters, and enter insert mode" }, + { on = "C", run = [ "delete --cut --insert", "move 999" ], desc = "Cut until the EOL, and enter insert mode" }, + { on = "x", run = [ "delete --cut", "move 1 --in-operating" ], desc = "Cut the current character" }, + { on = "y", run = "yank", desc = "Copy the selected characters" }, + { on = "p", run = "paste", desc = "Paste the copied characters after the cursor" }, + { on = "P", run = "paste --before", desc = "Paste the copied characters before the cursor" }, # Undo/Redo - { on = [ "u" ], run = "undo", desc = "Undo the last operation" }, - { on = [ "" ], run = "redo", desc = "Redo the last operation" }, + { on = "u", run = "undo", desc = "Undo the last operation" }, + { on = "", run = "redo", desc = "Redo the last operation" }, # Help - { on = [ "~" ], run = "help", desc = "Open help" } + { on = "~", run = "help", desc = "Open help" } ] [completion] keymap = [ - { on = [ "" ], run = "close", desc = "Cancel completion" }, - { on = [ "" ], run = "close --submit", desc = "Submit the completion" }, - { on = [ "" ], run = [ "close --submit", "close_input --submit" ], desc = "Submit the completion and input" }, + { on = "", run = "close", desc = "Cancel completion" }, + { on = "", run = "close --submit", desc = "Submit the completion" }, + { on = "", run = [ "close --submit", "close_input --submit" ], desc = "Submit the completion and input" }, - { on = [ "" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "", run = "arrow -1", desc = "Move cursor up" }, + { on = "", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "", run = "arrow -1", desc = "Move cursor up" }, + { on = "", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "", run = "arrow -1", desc = "Move cursor up" }, + { on = "", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "~" ], run = "help", desc = "Open help" } + { on = "~", run = "help", desc = "Open help" } ] [help] keymap = [ - { on = [ "" ], run = "escape", desc = "Clear the filter, or hide the help" }, - { on = [ "" ], run = "escape", desc = "Clear the filter, or hide the help" }, - { on = [ "q" ], run = "close", desc = "Exit the process" }, - { on = [ "" ], run = "close", desc = "Hide the help" }, + { on = "", run = "escape", desc = "Clear the filter, or hide the help" }, + { on = "", run = "escape", desc = "Clear the filter, or hide the help" }, + { on = "q", run = "close", desc = "Exit the process" }, + { on = "", run = "close", desc = "Hide the help" }, # Navigation - { on = [ "k" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "j" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "k", run = "arrow -1", desc = "Move cursor up" }, + { on = "j", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "K" ], run = "arrow -5", desc = "Move cursor up 5 lines" }, - { on = [ "J" ], run = "arrow 5", desc = "Move cursor down 5 lines" }, + { on = "K", run = "arrow -5", desc = "Move cursor up 5 lines" }, + { on = "J", run = "arrow 5", desc = "Move cursor down 5 lines" }, - { on = [ "" ], run = "arrow -1", desc = "Move cursor up" }, - { on = [ "" ], run = "arrow 1", desc = "Move cursor down" }, + { on = "", run = "arrow -1", desc = "Move cursor up" }, + { on = "", run = "arrow 1", desc = "Move cursor down" }, - { on = [ "" ], run = "arrow -5", desc = "Move cursor up 5 lines" }, - { on = [ "" ], run = "arrow 5", desc = "Move cursor down 5 lines" }, + { on = "", run = "arrow -5", desc = "Move cursor up 5 lines" }, + { on = "", run = "arrow 5", desc = "Move cursor down 5 lines" }, # Filtering - { on = [ "/" ], run = "filter", desc = "Apply a filter for the help items" }, + { on = "/", run = "filter", desc = "Apply a filter for the help items" }, ] diff --git a/yazi-config/preset/theme.toml b/yazi-config/preset/theme.toml index b1eb01c7..5a0b8624 100644 --- a/yazi-config/preset/theme.toml +++ b/yazi-config/preset/theme.toml @@ -170,17 +170,24 @@ icon_error = "" rules = [ # Images - { mime = "image/*", fg = "cyan" }, + { mime = "image/*", fg = "yellow" }, # Media - { mime = "{audio,video}/*", fg = "yellow" }, + { mime = "{audio,video}/*", fg = "magenta" }, # Archives - { mime = "application/*zip", fg = "magenta" }, - { mime = "application/x-{tar,bzip*,7z-compressed,xz,rar}", fg = "magenta" }, + { mime = "application/{,g}zip", fg = "red" }, + { mime = "application/x-{tar,bzip*,7z-compressed,xz,rar}", fg = "red" }, # Documents - { mime = "application/{pdf,doc,rtf,vnd.*}", fg = "green" }, + { mime = "application/{pdf,doc,rtf,vnd.*}", fg = "cyan" }, + + # Empty files + # { mime = "inode/x-empty", fg = "red" }, + + # Special files + { name = "*", is = "orphan", bg = "red" }, + { name = "*", is = "exec" , fg = "green" }, # Fallback # { name = "*", fg = "white" }, @@ -189,155 +196,586 @@ rules = [ [icon] -rules = [ - # Programming - { name = "*.c" , text = "", fg = "#599eff" }, - { name = "*.cpp" , text = "", fg = "#519aba" }, - { name = "*.class", text = "", fg = "#cc3e44" }, - { name = "*.cs" , text = "󰌛", fg = "#596706" }, - { name = "*.css" , text = "", fg = "#42a5f5" }, - { name = "*.elm" , text = "", fg = "#4391d2" }, - { name = "*.fish" , text = "", fg = "#4d5a5e" }, - { name = "*.go" , text = "", fg = "#519aba" }, - { name = "*.h" , text = "", fg = "#a074c4" }, - { name = "*.hpp" , text = "", fg = "#a074c4" }, - { name = "*.html" , text = "", fg = "#e44d26" }, - { name = "*.jar" , text = "", fg = "#cc3e44" }, - { name = "*.java" , text = "", fg = "#cc3e44" }, - { name = "*.js" , text = "", fg = "#F1F134" }, - { name = "*.jsx" , text = "", fg = "#20c2e3" }, - { name = "*.lua" , text = "", fg = "#51a0cf" }, - { name = "*.nix" , text = "", fg = "#7ebae4" }, - { name = "*.nu" , text = ">", fg = "#3aa675" }, - { name = "*.php" , text = "", fg = "#a074c4" }, - { name = "*.py" , text = "", fg = "#ffbc03" }, - { name = "*.rb" , text = "", fg = "#701516" }, - { name = "*.rs" , text = "", fg = "#dea584" }, - { name = "*.sbt" , text = "", fg = "#4d5a5e" }, - { name = "*.scala", text = "", fg = "#cc463e" }, - { name = "*.scss" , text = "", fg = "#f55385" }, - { name = "*.sh" , text = "", fg = "#4d5a5e" }, - { name = "*.swift", text = "", fg = "#e37933" }, - { name = "*.ts" , text = "", fg = "#519aba" }, - { name = "*.tsx" , text = "", fg = "#1354bf" }, - { name = "*.vim" , text = "", fg = "#019833" }, - { name = "*.vue" , text = "󰡄", fg = "#8dc149" }, +globs = [] +dirs = [ + { name = ".config", text = "" }, + { name = ".git", text = "" }, + { name = "Desktop", text = "" }, + { name = "Development", text = "" }, + { name = "Documents", text = "" }, + { name = "Downloads", text = "" }, + { name = "Library", text = "" }, + { name = "Movies", text = "" }, + { name = "Music", text = "" }, + { name = "Pictures", text = "" }, + { name = "Public", text = "" }, + { name = "Videos", text = "" }, +] +files = [ + { name = ".babelrc", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = ".bash_profile", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = ".bashrc", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = ".dockerignore", text = "󰡨", fg_dark = "#458ee6", fg_light = "#2e5f99" }, + { name = ".ds_store", text = "", fg_dark = "#41535b", fg_light = "#41535b" }, + { name = ".editorconfig", text = "", fg_dark = "#fff2f2", fg_light = "#333030" }, + { name = ".env", text = "", fg_dark = "#faf743", fg_light = "#32310d" }, + { name = ".eslintignore", text = "", fg_dark = "#4b32c3", fg_light = "#4b32c3" }, + { name = ".eslintrc", text = "", fg_dark = "#4b32c3", fg_light = "#4b32c3" }, + { name = ".gitattributes", text = "", fg_dark = "#f54d27", fg_light = "#b83a1d" }, + { name = ".gitconfig", text = "", fg_dark = "#f54d27", fg_light = "#b83a1d" }, + { name = ".gitignore", text = "", fg_dark = "#f54d27", fg_light = "#b83a1d" }, + { name = ".gitlab-ci.yml", text = "", fg_dark = "#e24329", fg_light = "#aa321f" }, + { name = ".gitmodules", text = "", fg_dark = "#f54d27", fg_light = "#b83a1d" }, + { name = ".gtkrc-2.0", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = ".gvimrc", text = "", fg_dark = "#019833", fg_light = "#017226" }, + { name = ".luaurc", text = "", fg_dark = "#00a2ff", fg_light = "#007abf" }, + { name = ".mailmap", text = "󰊢", fg_dark = "#41535b", fg_light = "#41535b" }, + { name = ".npmignore", text = "", fg_dark = "#e8274b", fg_light = "#ae1d38" }, + { name = ".npmrc", text = "", fg_dark = "#e8274b", fg_light = "#ae1d38" }, + { name = ".prettierrc", text = "", fg_dark = "#4285f4", fg_light = "#3264b7" }, + { name = ".settings.json", text = "", fg_dark = "#854cc7", fg_light = "#643995" }, + { name = ".SRCINFO", text = "󰣇", fg_dark = "#0f94d2", fg_light = "#0b6f9e" }, + { name = ".vimrc", text = "", fg_dark = "#019833", fg_light = "#017226" }, + { name = ".Xauthority", text = "", fg_dark = "#e54d18", fg_light = "#ac3a12" }, + { name = ".xinitrc", text = "", fg_dark = "#e54d18", fg_light = "#ac3a12" }, + { name = ".Xresources", text = "", fg_dark = "#e54d18", fg_light = "#ac3a12" }, + { name = ".xsession", text = "", fg_dark = "#e54d18", fg_light = "#ac3a12" }, + { name = ".zprofile", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = ".zshenv", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = ".zshrc", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "_gvimrc", text = "", fg_dark = "#019833", fg_light = "#017226" }, + { name = "_vimrc", text = "", fg_dark = "#019833", fg_light = "#017226" }, + { name = "avif", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "brewfile", text = "", fg_dark = "#701516", fg_light = "#701516" }, + { name = "bspwmrc", text = "", fg_dark = "#2f2f2f", fg_light = "#2f2f2f" }, + { name = "build", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "build.gradle", text = "", fg_dark = "#005f87", fg_light = "#005f87" }, + { name = "build.zig.zon", text = "", fg_dark = "#f69a1b", fg_light = "#7b4d0e" }, + { name = "cantorrc", text = "", fg_dark = "#1c99f3", fg_light = "#1573b6" }, + { name = "checkhealth", text = "󰓙", fg_dark = "#75b4fb", fg_light = "#3a5a7e" }, + { name = "cmakelists.txt", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "commit_editmsg", text = "", fg_dark = "#f54d27", fg_light = "#b83a1d" }, + { name = "compose.yaml", text = "󰡨", fg_dark = "#458ee6", fg_light = "#2e5f99" }, + { name = "compose.yml", text = "󰡨", fg_dark = "#458ee6", fg_light = "#2e5f99" }, + { name = "config", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "containerfile", text = "󰡨", fg_dark = "#458ee6", fg_light = "#2e5f99" }, + { name = "copying", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "copying.lesser", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "docker-compose.yaml", text = "󰡨", fg_dark = "#458ee6", fg_light = "#2e5f99" }, + { name = "docker-compose.yml", text = "󰡨", fg_dark = "#458ee6", fg_light = "#2e5f99" }, + { name = "dockerfile", text = "󰡨", fg_dark = "#458ee6", fg_light = "#2e5f99" }, + { name = "ext_typoscript_setup.txt", text = "", fg_dark = "#ff8700", fg_light = "#aa5a00" }, + { name = "favicon.ico", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "fp-info-cache", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "fp-lib-table", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "FreeCAD.conf", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "gemfile$", text = "", fg_dark = "#701516", fg_light = "#701516" }, + { name = "gnumakefile", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "gradle-wrapper.properties", text = "", fg_dark = "#005f87", fg_light = "#005f87" }, + { name = "gradle.properties", text = "", fg_dark = "#005f87", fg_light = "#005f87" }, + { name = "gradlew", text = "", fg_dark = "#005f87", fg_light = "#005f87" }, + { name = "groovy", text = "", fg_dark = "#4a687c", fg_light = "#384e5d" }, + { name = "gruntfile.babel.js", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "gruntfile.coffee", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "gruntfile.js", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "gruntfile.ts", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "gtkrc", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "gulpfile.babel.js", text = "", fg_dark = "#cc3e44", fg_light = "#992e33" }, + { name = "gulpfile.coffee", text = "", fg_dark = "#cc3e44", fg_light = "#992e33" }, + { name = "gulpfile.js", text = "", fg_dark = "#cc3e44", fg_light = "#992e33" }, + { name = "gulpfile.ts", text = "", fg_dark = "#cc3e44", fg_light = "#992e33" }, + { name = "hyprland.conf", text = "", fg_dark = "#00aaae", fg_light = "#008082" }, + { name = "i3blocks.conf", text = "", fg_dark = "#e8ebee", fg_light = "#2e2f30" }, + { name = "i3status.conf", text = "", fg_dark = "#e8ebee", fg_light = "#2e2f30" }, + { name = "kalgebrarc", text = "", fg_dark = "#1c99f3", fg_light = "#1573b6" }, + { name = "kdeglobals", text = "", fg_dark = "#1c99f3", fg_light = "#1573b6" }, + { name = "kdenlive-layoutsrc", text = "", fg_dark = "#83b8f2", fg_light = "#425c79" }, + { name = "kdenliverc", text = "", fg_dark = "#83b8f2", fg_light = "#425c79" }, + { name = "kritadisplayrc", text = "", fg_dark = "#f245fb", fg_light = "#a12ea7" }, + { name = "kritarc", text = "", fg_dark = "#f245fb", fg_light = "#a12ea7" }, + { name = "license", text = "", fg_dark = "#d0bf41", fg_light = "#686020" }, + { name = "lxde-rc.xml", text = "", fg_dark = "#909090", fg_light = "#606060" }, + { name = "lxqt.conf", text = "", fg_dark = "#0192d3", fg_light = "#016e9e" }, + { name = "makefile", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "mix.lock", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "mpv.conf", text = "", fg_dark = "#3b1342", fg_light = "#3b1342" }, + { name = "node_modules", text = "", fg_dark = "#e8274b", fg_light = "#ae1d38" }, + { name = "package-lock.json", text = "", fg_dark = "#7a0d21", fg_light = "#7a0d21" }, + { name = "package.json", text = "", fg_dark = "#e8274b", fg_light = "#ae1d38" }, + { name = "PKGBUILD", text = "", fg_dark = "#0f94d2", fg_light = "#0b6f9e" }, + { name = "platformio.ini", text = "", fg_dark = "#f6822b", fg_light = "#a4571d" }, + { name = "pom.xml", text = "", fg_dark = "#7a0d21", fg_light = "#7a0d21" }, + { name = "procfile", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "PrusaSlicer.ini", text = "", fg_dark = "#ec6b23", fg_light = "#9d4717" }, + { name = "PrusaSlicerGcodeViewer.ini", text = "", fg_dark = "#ec6b23", fg_light = "#9d4717" }, + { name = "py.typed", text = "", fg_dark = "#ffbc03", fg_light = "#805e02" }, + { name = "QtProject.conf", text = "", fg_dark = "#40cd52", fg_light = "#2b8937" }, + { name = "R", text = "󰟔", fg_dark = "#2266ba", fg_light = "#1a4c8c" }, + { name = "r", text = "󰟔", fg_dark = "#2266ba", fg_light = "#1a4c8c" }, + { name = "rakefile", text = "", fg_dark = "#701516", fg_light = "#701516" }, + { name = "rmd", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "settings.gradle", text = "", fg_dark = "#005f87", fg_light = "#005f87" }, + { name = "svelte.config.js", text = "", fg_dark = "#ff3e00", fg_light = "#bf2e00" }, + { name = "sxhkdrc", text = "", fg_dark = "#2f2f2f", fg_light = "#2f2f2f" }, + { name = "sym-lib-table", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "tailwind.config.js", text = "󱏿", fg_dark = "#20c2e3", fg_light = "#158197" }, + { name = "tailwind.config.mjs", text = "󱏿", fg_dark = "#20c2e3", fg_light = "#158197" }, + { name = "tailwind.config.ts", text = "󱏿", fg_dark = "#20c2e3", fg_light = "#158197" }, + { name = "tmux.conf", text = "", fg_dark = "#14ba19", fg_light = "#0f8c13" }, + { name = "tmux.conf.local", text = "", fg_dark = "#14ba19", fg_light = "#0f8c13" }, + { name = "tsconfig.json", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "unlicense", text = "", fg_dark = "#d0bf41", fg_light = "#686020" }, + { name = "vagrantfile$", text = "", fg_dark = "#1563ff", fg_light = "#104abf" }, + { name = "vlcrc", text = "󰕼", fg_dark = "#ee7a00", fg_light = "#9f5100" }, + { name = "webpack", text = "󰜫", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "weston.ini", text = "", fg_dark = "#ffbb01", fg_light = "#805e00" }, + { name = "workspace", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "xmobarrc", text = "", fg_dark = "#fd4d5d", fg_light = "#a9333e" }, + { name = "xmobarrc.hs", text = "", fg_dark = "#fd4d5d", fg_light = "#a9333e" }, + { name = "xmonad.hs", text = "", fg_dark = "#fd4d5d", fg_light = "#a9333e" }, + { name = "xorg.conf", text = "", fg_dark = "#e54d18", fg_light = "#ac3a12" }, + { name = "xsettingsd.conf", text = "", fg_dark = "#e54d18", fg_light = "#ac3a12" }, +] +exts = [ + { name = "3gp", text = "", fg_dark = "#fd971f", fg_light = "#7e4c10" }, + { name = "3mf", text = "󰆧", fg_dark = "#888888", fg_light = "#5b5b5b" }, + { name = "7z", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "a", text = "", fg_dark = "#dcddd6", fg_light = "#494a47" }, + { name = "aac", text = "", fg_dark = "#00afff", fg_light = "#0075aa" }, + { name = "ai", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "aif", text = "", fg_dark = "#00afff", fg_light = "#0075aa" }, + { name = "aiff", text = "", fg_dark = "#00afff", fg_light = "#0075aa" }, + { name = "android", text = "", fg_dark = "#34a853", fg_light = "#277e3e" }, + { name = "ape", text = "", fg_dark = "#00afff", fg_light = "#0075aa" }, + { name = "apk", text = "", fg_dark = "#34a853", fg_light = "#277e3e" }, + { name = "app", text = "", fg_dark = "#9f0500", fg_light = "#9f0500" }, + { name = "applescript", text = "", fg_dark = "#6d8085", fg_light = "#526064" }, + { name = "asc", text = "󰦝", fg_dark = "#576d7f", fg_light = "#41525f" }, + { name = "ass", text = "󰨖", fg_dark = "#ffb713", fg_light = "#805c0a" }, + { name = "astro", text = "", fg_dark = "#e23f67", fg_light = "#aa2f4d" }, + { name = "awk", text = "", fg_dark = "#4d5a5e", fg_light = "#3a4446" }, + { name = "azcli", text = "", fg_dark = "#0078d4", fg_light = "#005a9f" }, + { name = "bak", text = "󰁯", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "bash", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "bat", text = "", fg_dark = "#c1f12e", fg_light = "#40500f" }, + { name = "bazel", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "bib", text = "󱉟", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "bicep", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "bicepparam", text = "", fg_dark = "#9f74b3", fg_light = "#6a4d77" }, + { name = "bin", text = "", fg_dark = "#9f0500", fg_light = "#9f0500" }, + { name = "blade.php", text = "", fg_dark = "#f05340", fg_light = "#a0372b" }, + { name = "blend", text = "󰂫", fg_dark = "#ea7600", fg_light = "#9c4f00" }, + { name = "blp", text = "󰺾", fg_dark = "#5796e2", fg_light = "#3a6497" }, + { name = "bmp", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "brep", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "bz", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "bz2", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "bz3", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "bzl", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "c", text = "", fg_dark = "#599eff", fg_light = "#3b69aa" }, + { name = "c++", text = "", fg_dark = "#f34b7d", fg_light = "#a23253" }, + { name = "cache", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "cast", text = "", fg_dark = "#fd971f", fg_light = "#7e4c10" }, + { name = "cbl", text = "⚙", fg_dark = "#005ca5", fg_light = "#005ca5" }, + { name = "cc", text = "", fg_dark = "#f34b7d", fg_light = "#a23253" }, + { name = "ccm", text = "", fg_dark = "#f34b7d", fg_light = "#a23253" }, + { name = "cfg", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "cjs", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "clj", text = "", fg_dark = "#8dc149", fg_light = "#466024" }, + { name = "cljc", text = "", fg_dark = "#8dc149", fg_light = "#466024" }, + { name = "cljd", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "cljs", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "cmake", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "cob", text = "⚙", fg_dark = "#005ca5", fg_light = "#005ca5" }, + { name = "cobol", text = "⚙", fg_dark = "#005ca5", fg_light = "#005ca5" }, + { name = "coffee", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "conf", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "config.ru", text = "", fg_dark = "#701516", fg_light = "#701516" }, + { name = "cp", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "cpp", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "cppm", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "cpy", text = "⚙", fg_dark = "#005ca5", fg_light = "#005ca5" }, + { name = "cr", text = "", fg_dark = "#c8c8c8", fg_light = "#434343" }, + { name = "crdownload", text = "", fg_dark = "#44cda8", fg_light = "#226654" }, + { name = "cs", text = "󰌛", fg_dark = "#596706", fg_light = "#434d04" }, + { name = "csh", text = "", fg_dark = "#4d5a5e", fg_light = "#3a4446" }, + { name = "cshtml", text = "󱦗", fg_dark = "#512bd4", fg_light = "#512bd4" }, + { name = "cson", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "csproj", text = "󰪮", fg_dark = "#512bd4", fg_light = "#512bd4" }, + { name = "css", text = "", fg_dark = "#42a5f5", fg_light = "#2c6ea3" }, + { name = "csv", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "cts", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "cu", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "cue", text = "󰲹", fg_dark = "#ed95ae", fg_light = "#764a57" }, + { name = "cuh", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "cxx", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "cxxm", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "d", text = "", fg_dark = "#427819", fg_light = "#325a13" }, + { name = "d.ts", text = "", fg_dark = "#d59855", fg_light = "#6a4c2a" }, + { name = "dart", text = "", fg_dark = "#03589c", fg_light = "#03589c" }, + { name = "db", text = "", fg_dark = "#dad8d8", fg_light = "#494848" }, + { name = "dconf", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "desktop", text = "", fg_dark = "#563d7c", fg_light = "#563d7c" }, + { name = "diff", text = "", fg_dark = "#41535b", fg_light = "#41535b" }, + { name = "dll", text = "", fg_dark = "#4d2c0b", fg_light = "#4d2c0b" }, + { name = "doc", text = "󰈬", fg_dark = "#185abd", fg_light = "#185abd" }, + { name = "Dockerfile", text = "󰡨", fg_dark = "#458ee6", fg_light = "#2e5f99" }, + { name = "docx", text = "󰈬", fg_dark = "#185abd", fg_light = "#185abd" }, + { name = "dot", text = "󱁉", fg_dark = "#30638e", fg_light = "#244a6a" }, + { name = "download", text = "", fg_dark = "#44cda8", fg_light = "#226654" }, + { name = "drl", text = "", fg_dark = "#ffafaf", fg_light = "#553a3a" }, + { name = "dropbox", text = "", fg_dark = "#0061fe", fg_light = "#0049be" }, + { name = "dump", text = "", fg_dark = "#dad8d8", fg_light = "#494848" }, + { name = "dwg", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "dxf", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "ebook", text = "", fg_dark = "#eab16d", fg_light = "#755836" }, + { name = "edn", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "eex", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "ejs", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "el", text = "", fg_dark = "#8172be", fg_light = "#61568e" }, + { name = "elc", text = "", fg_dark = "#8172be", fg_light = "#61568e" }, + { name = "elf", text = "", fg_dark = "#9f0500", fg_light = "#9f0500" }, + { name = "elm", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "eln", text = "", fg_dark = "#8172be", fg_light = "#61568e" }, + { name = "env", text = "", fg_dark = "#faf743", fg_light = "#32310d" }, + { name = "eot", text = "", fg_dark = "#ececec", fg_light = "#2f2f2f" }, + { name = "epp", text = "", fg_dark = "#ffa61a", fg_light = "#80530d" }, + { name = "epub", text = "", fg_dark = "#eab16d", fg_light = "#755836" }, + { name = "erb", text = "", fg_dark = "#701516", fg_light = "#701516" }, + { name = "erl", text = "", fg_dark = "#b83998", fg_light = "#8a2b72" }, + { name = "ex", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "exe", text = "", fg_dark = "#9f0500", fg_light = "#9f0500" }, + { name = "exs", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "f#", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "f3d", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "f90", text = "󱈚", fg_dark = "#734f96", fg_light = "#563b70" }, + { name = "fbx", text = "󰆧", fg_dark = "#888888", fg_light = "#5b5b5b" }, + { name = "fcbak", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "fcmacro", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "fcmat", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "fcparam", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "fcscript", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "fcstd", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "fcstd1", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "fctb", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "fctl", text = "", fg_dark = "#cb0d0d", fg_light = "#cb0d0d" }, + { name = "fdmdownload", text = "", fg_dark = "#44cda8", fg_light = "#226654" }, + { name = "fish", text = "", fg_dark = "#4d5a5e", fg_light = "#3a4446" }, + { name = "flac", text = "", fg_dark = "#0075aa", fg_light = "#005880" }, + { name = "flc", text = "", fg_dark = "#ececec", fg_light = "#2f2f2f" }, + { name = "flf", text = "", fg_dark = "#ececec", fg_light = "#2f2f2f" }, + { name = "fnl", text = "", fg_dark = "#fff3d7", fg_light = "#33312b" }, + { name = "fs", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "fsi", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "fsscript", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "fsx", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "gcode", text = "󰐫", fg_dark = "#1471ad", fg_light = "#0f5582" }, + { name = "gd", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "gemspec", text = "", fg_dark = "#701516", fg_light = "#701516" }, + { name = "gif", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "git", text = "", fg_dark = "#f14c28", fg_light = "#b5391e" }, + { name = "glb", text = "", fg_dark = "#ffb13b", fg_light = "#80581e" }, + { name = "gnumakefile", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "go", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "godot", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "gql", text = "", fg_dark = "#e535ab", fg_light = "#ac2880" }, + { name = "graphql", text = "", fg_dark = "#e535ab", fg_light = "#ac2880" }, + { name = "gresource", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "gv", text = "󱁉", fg_dark = "#30638e", fg_light = "#244a6a" }, + { name = "gz", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "h", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "haml", text = "", fg_dark = "#eaeae1", fg_light = "#2f2f2d" }, + { name = "hbs", text = "", fg_dark = "#f0772b", fg_light = "#a04f1d" }, + { name = "heex", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "hex", text = "", fg_dark = "#2e63ff", fg_light = "#224abf" }, + { name = "hh", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "hpp", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "hrl", text = "", fg_dark = "#b83998", fg_light = "#8a2b72" }, + { name = "hs", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "htm", text = "", fg_dark = "#e34c26", fg_light = "#aa391c" }, + { name = "html", text = "", fg_dark = "#e44d26", fg_light = "#ab3a1c" }, + { name = "huff", text = "󰡘", fg_dark = "#4242c7", fg_light = "#4242c7" }, + { name = "hurl", text = "", fg_dark = "#ff0288", fg_light = "#bf0266" }, + { name = "hx", text = "", fg_dark = "#ea8220", fg_light = "#9c5715" }, + { name = "hxx", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "ical", text = "", fg_dark = "#2b2e83", fg_light = "#2b2e83" }, + { name = "icalendar", text = "", fg_dark = "#2b2e83", fg_light = "#2b2e83" }, + { name = "ico", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "ics", text = "", fg_dark = "#2b2e83", fg_light = "#2b2e83" }, + { name = "ifb", text = "", fg_dark = "#2b2e83", fg_light = "#2b2e83" }, + { name = "ifc", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "ige", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "iges", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "igs", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "image", text = "", fg_dark = "#d0bec8", fg_light = "#453f43" }, + { name = "img", text = "", fg_dark = "#d0bec8", fg_light = "#453f43" }, + { name = "import", text = "", fg_dark = "#ececec", fg_light = "#2f2f2f" }, + { name = "info", text = "", fg_dark = "#ffffcd", fg_light = "#333329" }, + { name = "ini", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "ino", text = "", fg_dark = "#56b6c2", fg_light = "#397981" }, + { name = "ipynb", text = "", fg_dark = "#51a0cf", fg_light = "#366b8a" }, + { name = "iso", text = "", fg_dark = "#d0bec8", fg_light = "#453f43" }, + { name = "ixx", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "java", text = "", fg_dark = "#cc3e44", fg_light = "#992e33" }, + { name = "jl", text = "", fg_dark = "#a270ba", fg_light = "#6c4b7c" }, + { name = "jpeg", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "jpg", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "js", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "json", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "json5", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "jsonc", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "jsx", text = "", fg_dark = "#20c2e3", fg_light = "#158197" }, + { name = "jwmrc", text = "", fg_dark = "#0078cd", fg_light = "#005a9a" }, + { name = "jxl", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "kbx", text = "󰯄", fg_dark = "#737672", fg_light = "#565856" }, + { name = "kdb", text = "", fg_dark = "#529b34", fg_light = "#3e7427" }, + { name = "kdbx", text = "", fg_dark = "#529b34", fg_light = "#3e7427" }, + { name = "kdenlive", text = "", fg_dark = "#83b8f2", fg_light = "#425c79" }, + { name = "kdenlivetitle", text = "", fg_dark = "#83b8f2", fg_light = "#425c79" }, + { name = "kicad_dru", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "kicad_mod", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "kicad_pcb", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "kicad_prl", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "kicad_pro", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "kicad_sch", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "kicad_sym", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "kicad_wks", text = "", fg_dark = "#ffffff", fg_light = "#333333" }, + { name = "ko", text = "", fg_dark = "#dcddd6", fg_light = "#494a47" }, + { name = "kpp", text = "", fg_dark = "#f245fb", fg_light = "#a12ea7" }, + { name = "kra", text = "", fg_dark = "#f245fb", fg_light = "#a12ea7" }, + { name = "krz", text = "", fg_dark = "#f245fb", fg_light = "#a12ea7" }, + { name = "ksh", text = "", fg_dark = "#4d5a5e", fg_light = "#3a4446" }, + { name = "kt", text = "", fg_dark = "#7f52ff", fg_light = "#5f3ebf" }, + { name = "kts", text = "", fg_dark = "#7f52ff", fg_light = "#5f3ebf" }, + { name = "lck", text = "", fg_dark = "#bbbbbb", fg_light = "#5e5e5e" }, + { name = "leex", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "less", text = "", fg_dark = "#563d7c", fg_light = "#563d7c" }, + { name = "lff", text = "", fg_dark = "#ececec", fg_light = "#2f2f2f" }, + { name = "lhs", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "lib", text = "", fg_dark = "#4d2c0b", fg_light = "#4d2c0b" }, + { name = "license", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "liquid", text = "", fg_dark = "#95bf47", fg_light = "#4a6024" }, + { name = "lock", text = "", fg_dark = "#bbbbbb", fg_light = "#5e5e5e" }, + { name = "log", text = "󰌱", fg_dark = "#dddddd", fg_light = "#4a4a4a" }, + { name = "lrc", text = "󰨖", fg_dark = "#ffb713", fg_light = "#805c0a" }, + { name = "lua", text = "", fg_dark = "#51a0cf", fg_light = "#366b8a" }, + { name = "luac", text = "", fg_dark = "#51a0cf", fg_light = "#366b8a" }, + { name = "luau", text = "", fg_dark = "#00a2ff", fg_light = "#007abf" }, + { name = "m", text = "", fg_dark = "#599eff", fg_light = "#3b69aa" }, + { name = "m3u", text = "󰲹", fg_dark = "#ed95ae", fg_light = "#764a57" }, + { name = "m3u8", text = "󰲹", fg_dark = "#ed95ae", fg_light = "#764a57" }, + { name = "m4a", text = "", fg_dark = "#00afff", fg_light = "#0075aa" }, + { name = "m4v", text = "", fg_dark = "#fd971f", fg_light = "#7e4c10" }, + { name = "magnet", text = "", fg_dark = "#a51b16", fg_light = "#a51b16" }, + { name = "makefile", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "markdown", text = "", fg_dark = "#dddddd", fg_light = "#4a4a4a" }, + { name = "material", text = "󰔉", fg_dark = "#b83998", fg_light = "#8a2b72" }, + { name = "md", text = "", fg_dark = "#dddddd", fg_light = "#4a4a4a" }, + { name = "md5", text = "󰕥", fg_dark = "#8c86af", fg_light = "#5d5975" }, + { name = "mdx", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "mint", text = "󰌪", fg_dark = "#87c095", fg_light = "#44604a" }, + { name = "mjs", text = "", fg_dark = "#f1e05a", fg_light = "#504b1e" }, + { name = "mk", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "mkv", text = "", fg_dark = "#fd971f", fg_light = "#7e4c10" }, + { name = "ml", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "mli", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "mm", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "mo", text = "∞", fg_dark = "#9772fb", fg_light = "#654ca7" }, + { name = "mobi", text = "", fg_dark = "#eab16d", fg_light = "#755836" }, + { name = "mov", text = "", fg_dark = "#fd971f", fg_light = "#7e4c10" }, + { name = "mp3", text = "", fg_dark = "#00afff", fg_light = "#0075aa" }, + { name = "mp4", text = "", fg_dark = "#fd971f", fg_light = "#7e4c10" }, + { name = "mpp", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "msf", text = "", fg_dark = "#137be1", fg_light = "#0e5ca9" }, + { name = "mts", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "mustache", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "nfo", text = "", fg_dark = "#ffffcd", fg_light = "#333329" }, + { name = "nim", text = "", fg_dark = "#f3d400", fg_light = "#514700" }, + { name = "nix", text = "", fg_dark = "#7ebae4", fg_light = "#3f5d72" }, + { name = "nswag", text = "", fg_dark = "#85ea2d", fg_light = "#427516" }, + { name = "nu", text = ">", fg_dark = "#3aa675", fg_light = "#276f4e" }, + { name = "o", text = "", fg_dark = "#9f0500", fg_light = "#9f0500" }, + { name = "obj", text = "󰆧", fg_dark = "#888888", fg_light = "#5b5b5b" }, + { name = "ogg", text = "", fg_dark = "#0075aa", fg_light = "#005880" }, + { name = "opus", text = "", fg_dark = "#0075aa", fg_light = "#005880" }, + { name = "org", text = "", fg_dark = "#77aa99", fg_light = "#4f7166" }, + { name = "otf", text = "", fg_dark = "#ececec", fg_light = "#2f2f2f" }, + { name = "out", text = "", fg_dark = "#9f0500", fg_light = "#9f0500" }, + { name = "part", text = "", fg_dark = "#44cda8", fg_light = "#226654" }, + { name = "patch", text = "", fg_dark = "#41535b", fg_light = "#41535b" }, + { name = "pck", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "pcm", text = "", fg_dark = "#0075aa", fg_light = "#005880" }, + { name = "pdf", text = "", fg_dark = "#b30b00", fg_light = "#b30b00" }, + { name = "php", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "pl", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "pls", text = "󰲹", fg_dark = "#ed95ae", fg_light = "#764a57" }, + { name = "ply", text = "󰆧", fg_dark = "#888888", fg_light = "#5b5b5b" }, + { name = "pm", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "png", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "po", text = "", fg_dark = "#2596be", fg_light = "#1c708e" }, + { name = "pot", text = "", fg_dark = "#2596be", fg_light = "#1c708e" }, + { name = "pp", text = "", fg_dark = "#ffa61a", fg_light = "#80530d" }, + { name = "ppt", text = "󰈧", fg_dark = "#cb4a32", fg_light = "#983826" }, + { name = "prisma", text = "", fg_dark = "#5a67d8", fg_light = "#444da2" }, + { name = "pro", text = "", fg_dark = "#e4b854", fg_light = "#725c2a" }, + { name = "ps1", text = "󰨊", fg_dark = "#4273ca", fg_light = "#325698" }, + { name = "psb", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "psd", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "psd1", text = "󰨊", fg_dark = "#6975c4", fg_light = "#4f5893" }, + { name = "psm1", text = "󰨊", fg_dark = "#6975c4", fg_light = "#4f5893" }, + { name = "pub", text = "󰷖", fg_dark = "#e3c58e", fg_light = "#4c422f" }, + { name = "pxd", text = "", fg_dark = "#5aa7e4", fg_light = "#3c6f98" }, + { name = "pxi", text = "", fg_dark = "#5aa7e4", fg_light = "#3c6f98" }, + { name = "py", text = "", fg_dark = "#ffbc03", fg_light = "#805e02" }, + { name = "pyc", text = "", fg_dark = "#ffe291", fg_light = "#332d1d" }, + { name = "pyd", text = "", fg_dark = "#ffe291", fg_light = "#332d1d" }, + { name = "pyi", text = "", fg_dark = "#ffbc03", fg_light = "#805e02" }, + { name = "pyo", text = "", fg_dark = "#ffe291", fg_light = "#332d1d" }, + { name = "pyx", text = "", fg_dark = "#5aa7e4", fg_light = "#3c6f98" }, + { name = "qm", text = "", fg_dark = "#2596be", fg_light = "#1c708e" }, + { name = "qml", text = "", fg_dark = "#40cd52", fg_light = "#2b8937" }, + { name = "qrc", text = "", fg_dark = "#40cd52", fg_light = "#2b8937" }, + { name = "qss", text = "", fg_dark = "#40cd52", fg_light = "#2b8937" }, + { name = "query", text = "", fg_dark = "#90a850", fg_light = "#607035" }, + { name = "r", text = "󰟔", fg_dark = "#2266ba", fg_light = "#1a4c8c" }, + { name = "rake", text = "", fg_dark = "#701516", fg_light = "#701516" }, + { name = "rar", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "razor", text = "󱦘", fg_dark = "#512bd4", fg_light = "#512bd4" }, + { name = "rb", text = "", fg_dark = "#701516", fg_light = "#701516" }, + { name = "res", text = "", fg_dark = "#cc3e44", fg_light = "#992e33" }, + { name = "resi", text = "", fg_dark = "#f55385", fg_light = "#a33759" }, + { name = "rlib", text = "", fg_dark = "#dea584", fg_light = "#6f5242" }, + { name = "rmd", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "rproj", text = "󰗆", fg_dark = "#358a5b", fg_light = "#286844" }, + { name = "rs", text = "", fg_dark = "#dea584", fg_light = "#6f5242" }, + { name = "rss", text = "", fg_dark = "#fb9d3b", fg_light = "#7e4e1e" }, + { name = "sass", text = "", fg_dark = "#f55385", fg_light = "#a33759" }, + { name = "sbt", text = "", fg_dark = "#cc3e44", fg_light = "#992e33" }, + { name = "sc", text = "", fg_dark = "#cc3e44", fg_light = "#992e33" }, + { name = "scad", text = "", fg_dark = "#f9d72c", fg_light = "#53480f" }, + { name = "scala", text = "", fg_dark = "#cc3e44", fg_light = "#992e33" }, + { name = "scm", text = "󰘧", fg_dark = "#eeeeee", fg_light = "#303030" }, + { name = "scss", text = "", fg_dark = "#f55385", fg_light = "#a33759" }, + { name = "sh", text = "", fg_dark = "#4d5a5e", fg_light = "#3a4446" }, + { name = "sha1", text = "󰕥", fg_dark = "#8c86af", fg_light = "#5d5975" }, + { name = "sha224", text = "󰕥", fg_dark = "#8c86af", fg_light = "#5d5975" }, + { name = "sha256", text = "󰕥", fg_dark = "#8c86af", fg_light = "#5d5975" }, + { name = "sha384", text = "󰕥", fg_dark = "#8c86af", fg_light = "#5d5975" }, + { name = "sha512", text = "󰕥", fg_dark = "#8c86af", fg_light = "#5d5975" }, + { name = "sig", text = "λ", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "signature", text = "λ", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "skp", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "sldasm", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "sldprt", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "slim", text = "", fg_dark = "#e34c26", fg_light = "#aa391c" }, + { name = "sln", text = "", fg_dark = "#854cc7", fg_light = "#643995" }, + { name = "slvs", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "sml", text = "λ", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "so", text = "", fg_dark = "#dcddd6", fg_light = "#494a47" }, + { name = "sol", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "spec.js", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "spec.jsx", text = "", fg_dark = "#20c2e3", fg_light = "#158197" }, + { name = "spec.ts", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "spec.tsx", text = "", fg_dark = "#1354bf", fg_light = "#1354bf" }, + { name = "sql", text = "", fg_dark = "#dad8d8", fg_light = "#494848" }, + { name = "sqlite", text = "", fg_dark = "#dad8d8", fg_light = "#494848" }, + { name = "sqlite3", text = "", fg_dark = "#dad8d8", fg_light = "#494848" }, + { name = "srt", text = "󰨖", fg_dark = "#ffb713", fg_light = "#805c0a" }, + { name = "ssa", text = "󰨖", fg_dark = "#ffb713", fg_light = "#805c0a" }, + { name = "ste", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "step", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "stl", text = "󰆧", fg_dark = "#888888", fg_light = "#5b5b5b" }, + { name = "stp", text = "󰻫", fg_dark = "#839463", fg_light = "#576342" }, + { name = "strings", text = "", fg_dark = "#2596be", fg_light = "#1c708e" }, + { name = "styl", text = "", fg_dark = "#8dc149", fg_light = "#466024" }, + { name = "sub", text = "󰨖", fg_dark = "#ffb713", fg_light = "#805c0a" }, + { name = "sublime", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "suo", text = "", fg_dark = "#854cc7", fg_light = "#643995" }, + { name = "sv", text = "󰍛", fg_dark = "#019833", fg_light = "#017226" }, + { name = "svelte", text = "", fg_dark = "#ff3e00", fg_light = "#bf2e00" }, + { name = "svg", text = "󰜡", fg_dark = "#ffb13b", fg_light = "#80581e" }, + { name = "svh", text = "󰍛", fg_dark = "#019833", fg_light = "#017226" }, + { name = "swift", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "t", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "tbc", text = "󰛓", fg_dark = "#1e5cb3", fg_light = "#1e5cb3" }, + { name = "tcl", text = "󰛓", fg_dark = "#1e5cb3", fg_light = "#1e5cb3" }, + { name = "templ", text = "", fg_dark = "#dbbd30", fg_light = "#6e5e18" }, + { name = "terminal", text = "", fg_dark = "#31b53e", fg_light = "#217929" }, + { name = "test.js", text = "", fg_dark = "#cbcb41", fg_light = "#666620" }, + { name = "test.jsx", text = "", fg_dark = "#20c2e3", fg_light = "#158197" }, + { name = "test.ts", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "test.tsx", text = "", fg_dark = "#1354bf", fg_light = "#1354bf" }, + { name = "tex", text = "", fg_dark = "#3d6117", fg_light = "#3d6117" }, + { name = "tf", text = "", fg_dark = "#5f43e9", fg_light = "#4732af" }, + { name = "tfvars", text = "", fg_dark = "#5f43e9", fg_light = "#4732af" }, + { name = "tgz", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "tmux", text = "", fg_dark = "#14ba19", fg_light = "#0f8c13" }, + { name = "toml", text = "", fg_dark = "#9c4221", fg_light = "#753219" }, + { name = "torrent", text = "", fg_dark = "#44cda8", fg_light = "#226654" }, + { name = "tres", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "ts", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "tscn", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "tsconfig", text = "", fg_dark = "#ff8700", fg_light = "#aa5a00" }, + { name = "tsx", text = "", fg_dark = "#1354bf", fg_light = "#1354bf" }, + { name = "ttf", text = "", fg_dark = "#ececec", fg_light = "#2f2f2f" }, + { name = "twig", text = "", fg_dark = "#8dc149", fg_light = "#466024" }, + { name = "txt", text = "󰈙", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "txz", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "typoscript", text = "", fg_dark = "#ff8700", fg_light = "#aa5a00" }, + { name = "ui", text = "", fg_dark = "#0c306e", fg_light = "#0c306e" }, + { name = "v", text = "󰍛", fg_dark = "#019833", fg_light = "#017226" }, + { name = "vala", text = "", fg_dark = "#7239b3", fg_light = "#562b86" }, + { name = "vh", text = "󰍛", fg_dark = "#019833", fg_light = "#017226" }, + { name = "vhd", text = "󰍛", fg_dark = "#019833", fg_light = "#017226" }, + { name = "vhdl", text = "󰍛", fg_dark = "#019833", fg_light = "#017226" }, + { name = "vim", text = "", fg_dark = "#019833", fg_light = "#017226" }, + { name = "vsh", text = "", fg_dark = "#5d87bf", fg_light = "#3e5a7f" }, + { name = "vsix", text = "", fg_dark = "#854cc7", fg_light = "#643995" }, + { name = "vue", text = "", fg_dark = "#8dc149", fg_light = "#466024" }, + { name = "wasm", text = "", fg_dark = "#5c4cdb", fg_light = "#4539a4" }, + { name = "wav", text = "", fg_dark = "#00afff", fg_light = "#0075aa" }, + { name = "webm", text = "", fg_dark = "#fd971f", fg_light = "#7e4c10" }, + { name = "webmanifest", text = "", fg_dark = "#f1e05a", fg_light = "#504b1e" }, + { name = "webp", text = "", fg_dark = "#a074c4", fg_light = "#6b4d83" }, + { name = "webpack", text = "󰜫", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "wma", text = "", fg_dark = "#00afff", fg_light = "#0075aa" }, + { name = "woff", text = "", fg_dark = "#ececec", fg_light = "#2f2f2f" }, + { name = "woff2", text = "", fg_dark = "#ececec", fg_light = "#2f2f2f" }, + { name = "wrl", text = "󰆧", fg_dark = "#888888", fg_light = "#5b5b5b" }, + { name = "wrz", text = "󰆧", fg_dark = "#888888", fg_light = "#5b5b5b" }, + { name = "x", text = "", fg_dark = "#599eff", fg_light = "#3b69aa" }, + { name = "xaml", text = "󰙳", fg_dark = "#512bd4", fg_light = "#512bd4" }, + { name = "xcf", text = "", fg_dark = "#635b46", fg_light = "#4a4434" }, + { name = "xcplayground", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "xcstrings", text = "", fg_dark = "#2596be", fg_light = "#1c708e" }, + { name = "xls", text = "󰈛", fg_dark = "#207245", fg_light = "#207245" }, + { name = "xlsx", text = "󰈛", fg_dark = "#207245", fg_light = "#207245" }, + { name = "xm", text = "", fg_dark = "#519aba", fg_light = "#36677c" }, + { name = "xml", text = "󰗀", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "xpi", text = "", fg_dark = "#ff1b01", fg_light = "#bf1401" }, + { name = "xul", text = "", fg_dark = "#e37933", fg_light = "#975122" }, + { name = "xz", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "yaml", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "yml", text = "", fg_dark = "#6d8086", fg_light = "#526064" }, + { name = "zig", text = "", fg_dark = "#f69a1b", fg_light = "#7b4d0e" }, + { name = "zip", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, + { name = "zsh", text = "", fg_dark = "#89e051", fg_light = "#447028" }, + { name = "zst", text = "", fg_dark = "#eca517", fg_light = "#76520c" }, +] +conds = [ + # Special files + { if = "orphan", text = "" }, + { if = "link" , text = "" }, + { if = "block" , text = "" }, + { if = "char" , text = "" }, + { if = "fifo" , text = "" }, + { if = "sock" , text = "" }, + { if = "sticky", text = "" }, - # Text - { name = "*.conf", text = "", fg = "#6d8086" }, - { name = "*.ini" , text = "", fg = "#6d8086" }, - { name = "*.json", text = "", fg = "#cbcb41" }, - { name = "*.kdl" , text = "", fg = "#6d8086" }, - { name = "*.md" , text = "", fg = "#ffffff" }, - { name = "*.toml", text = "", fg = "#ffffff" }, - { name = "*.txt" , text = "", fg = "#89e051" }, - { name = "*.yaml", text = "", fg = "#6d8086" }, - { name = "*.yml" , text = "", fg = "#6d8086" }, - - # Archives - { name = "*.7z" , text = "" }, - { name = "*.bz2", text = "" }, - { name = "*.gz" , text = "" }, - { name = "*.rar", text = "" }, - { name = "*.tar", text = "" }, - { name = "*.xz" , text = "" }, - { name = "*.zip", text = "" }, - - # Images - { name = "*.HEIC", text = "", fg = "#a074c4" }, - { name = "*.avif", text = "", fg = "#a074c4" }, - { name = "*.bmp" , text = "", fg = "#a074c4" }, - { name = "*.gif" , text = "", fg = "#a074c4" }, - { name = "*.ico" , text = "", fg = "#cbcb41" }, - { name = "*.jpeg", text = "", fg = "#a074c4" }, - { name = "*.jpg" , text = "", fg = "#a074c4" }, - { name = "*.png" , text = "", fg = "#a074c4" }, - { name = "*.svg" , text = "", fg = "#FFB13B" }, - { name = "*.webp", text = "", fg = "#a074c4" }, - - # Movies - { name = "*.avi" , text = "", fg = "#FD971F" }, - { name = "*.mkv" , text = "", fg = "#FD971F" }, - { name = "*.mov" , text = "", fg = "#FD971F" }, - { name = "*.mp4" , text = "", fg = "#FD971F" }, - { name = "*.webm", text = "", fg = "#FD971F" }, - - # Audio - { name = "*.aac" , text = "", fg = "#66D8EF" }, - { name = "*.flac", text = "", fg = "#66D8EF" }, - { name = "*.m4a" , text = "", fg = "#66D8EF" }, - { name = "*.mp3" , text = "", fg = "#66D8EF" }, - { name = "*.ogg" , text = "", fg = "#66D8EF" }, - { name = "*.wav" , text = "", fg = "#66D8EF" }, - - # Documents - { name = "*.csv" , text = "", fg = "#89e051" }, - { name = "*.doc" , text = "", fg = "#185abd" }, - { name = "*.doct", text = "", fg = "#185abd" }, - { name = "*.docx", text = "", fg = "#185abd" }, - { name = "*.dot" , text = "", fg = "#185abd" }, - { name = "*.ods" , text = "", fg = "#207245" }, - { name = "*.ots" , text = "", fg = "#207245" }, - { name = "*.pdf" , text = "", fg = "#b30b00" }, - { name = "*.pom" , text = "", fg = "#cc3e44" }, - { name = "*.pot" , text = "", fg = "#cb4a32" }, - { name = "*.potx", text = "", fg = "#cb4a32" }, - { name = "*.ppm" , text = "", fg = "#a074c4" }, - { name = "*.ppmx", text = "", fg = "#cb4a32" }, - { name = "*.pps" , text = "", fg = "#cb4a32" }, - { name = "*.ppsx", text = "", fg = "#cb4a32" }, - { name = "*.ppt" , text = "", fg = "#cb4a32" }, - { name = "*.pptx", text = "", fg = "#cb4a32" }, - { name = "*.xlc" , text = "", fg = "#207245" }, - { name = "*.xlm" , text = "", fg = "#207245" }, - { name = "*.xls" , text = "", fg = "#207245" }, - { name = "*.xlsm", text = "", fg = "#207245" }, - { name = "*.xlsx", text = "", fg = "#207245" }, - { name = "*.xlt" , text = "", fg = "#207245" }, - - # Lockfiles - { name = "*.lock", text = "", fg = "#bbbbbb" }, - - # Misc - { name = "*.bin", text = "", fg = "#9F0500" }, - { name = "*.exe", text = "", fg = "#9F0500" }, - { name = "*.pkg", text = "", fg = "#9F0500" }, - - # Dotfiles - { name = ".DS_Store" , text = "", fg = "#41535b" }, - { name = ".bashprofile" , text = "", fg = "#89e051" }, - { name = ".bashrc" , text = "", fg = "#89e051" }, - { name = ".gitattributes", text = "", fg = "#41535b" }, - { name = ".gitignore" , text = "", fg = "#41535b" }, - { name = ".gitmodules" , text = "", fg = "#41535b" }, - { name = ".vimrc" , text = "", fg = "#019833" }, - { name = ".zprofile" , text = "", fg = "#89e051" }, - { name = ".zshenv" , text = "", fg = "#89e051" }, - { name = ".zshrc" , text = "", fg = "#89e051" }, - - # Named files - { name = "COPYING" , text = "󰿃", fg = "#cbcb41" }, - { name = "Containerfile", text = "󰡨", fg = "#458ee6" }, - { name = "Dockerfile" , text = "󰡨", fg = "#458ee6" }, - { name = "LICENSE" , text = "󰿃", fg = "#d0bf41" }, - - # Directories - { name = ".config/" , text = "" }, - { name = ".git/" , text = "" }, - { name = "Desktop/" , text = "" }, - { name = "Development/", text = "" }, - { name = "Documents/" , text = "" }, - { name = "Downloads/" , text = "" }, - { name = "Library/" , text = "" }, - { name = "Movies/" , text = "" }, - { name = "Music/" , text = "" }, - { name = "Pictures/" , text = "" }, - { name = "Public/" , text = "" }, - { name = "Videos/" , text = "" }, - - # Default - { name = "*" , text = "" }, - { name = "*/", text = "" }, + # Fallback + { if = "dir", text = "" }, + { if = "exec", text = "" }, + { if = "!dir", text = "" }, ] # : }}} diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index f3dac472..c36417e2 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -6,12 +6,14 @@ ratio = [ 1, 4, 3 ] sort_by = "alphabetical" sort_sensitive = false -sort_reverse = false +sort_reverse = false sort_dir_first = true +sort_translit = false linemode = "none" show_hidden = false show_symlink = true scrolloff = 5 +mouse_events = [ "click", "scroll" ] [preview] tab_size = 2 @@ -27,16 +29,16 @@ ueberzug_offset = [ 0, 0, 0, 0 ] [opener] edit = [ { run = '${EDITOR:=vi} "$@"', desc = "$EDITOR", block = true, for = "unix" }, - { run = 'code "%*"', orphan = true, desc = "code", for = "windows" }, - { run = 'code -w "%*"', block = true, desc = "code (block)", for = "windows" }, + { run = 'code %*', orphan = true, desc = "code", for = "windows" }, + { run = 'code -w %*', block = true, desc = "code (block)", for = "windows" }, ] open = [ - { run = 'xdg-open "$@"', desc = "Open", for = "linux" }, + { run = 'xdg-open "$1"', desc = "Open", for = "linux" }, { run = 'open "$@"', desc = "Open", for = "macos" }, { run = 'start "" "%1"', orphan = true, desc = "Open", for = "windows" }, ] reveal = [ - { run = 'xdg-open "$(dirname "$0")"', desc = "Reveal", for = "linux" }, + { run = 'xdg-open "$(dirname "$1")"', desc = "Reveal", for = "linux" }, { run = 'open -R "$1"', desc = "Reveal", for = "macos" }, { run = 'explorer /select, "%1"', orphan = true, desc = "Reveal", for = "windows" }, { run = '''exiftool "$1"; echo "Press enter to exit"; read _''', block = true, desc = "Show EXIF", for = "unix" }, @@ -46,8 +48,8 @@ extract = [ { run = 'unar "%1"', desc = "Extract here", for = "windows" }, ] play = [ - { run = 'mpv "$@"', orphan = true, for = "unix" }, - { run = 'mpv "%1"', orphan = true, for = "windows" }, + { run = 'mpv --force-window "$@"', orphan = true, for = "unix" }, + { run = 'mpv --force-window "%1"', orphan = true, for = "windows" }, { run = '''mediainfo "$1"; echo "Press enter to exit"; read _''', block = true, desc = "Show media info", for = "unix" }, ] @@ -60,13 +62,13 @@ rules = [ { mime = "{audio,video}/*", use = [ "play", "reveal" ] }, { mime = "inode/x-empty", use = [ "edit", "reveal" ] }, - { mime = "application/*zip", use = [ "extract", "reveal" ] }, + { mime = "application/{,g}zip", use = [ "extract", "reveal" ] }, { mime = "application/x-{tar,bzip*,7z-compressed,xz,rar}", use = [ "extract", "reveal" ] }, - { mime = "application/json", use = [ "edit", "reveal" ] }, - { mime = "*/javascript", use = [ "edit", "reveal" ] }, + { mime = "application/{json,x-ndjson}", use = [ "edit", "reveal" ] }, + { mime = "*/javascript", use = [ "edit", "reveal" ] }, - { mime = "*", use = [ "open", "reveal" ] }, + { name = "*", use = [ "open", "reveal" ] }, ] [tasks] @@ -79,14 +81,21 @@ suppress_preload = false [plugin] +fetchers = [ + # Mimetype + { id = "mime", name = "*", run = "mime", if = "!mime", prio = "high" }, +] preloaders = [ - { name = "*", cond = "!mime", run = "mime", multi = true, prio = "high" }, # Image + { mime = "image/{avif,heic,jxl,svg+xml}", run = "magick" }, { mime = "image/*", run = "image" }, # Video { mime = "video/*", run = "video" }, # PDF { mime = "application/pdf", run = "pdf" }, + # Font + { mime = "font/*", run = "font" }, + { mime = "application/vnd.ms-opentype", run = "font" }, ] previewers = [ { name = "*/", run = "folder", sync = true }, @@ -94,22 +103,27 @@ previewers = [ { mime = "text/*", run = "code" }, { mime = "*/{xml,javascript,x-wine-extension-ini}", run = "code" }, # JSON - { mime = "application/json", run = "json" }, + { mime = "application/{json,x-ndjson}", run = "json" }, # Image - { mime = "image/vnd.djvu", run = "noop" }, - { mime = "image/*", run = "image" }, + { mime = "image/{avif,heic,jxl,svg+xml}", run = "magick" }, + { mime = "image/*", run = "image" }, # Video { mime = "video/*", run = "video" }, # PDF { mime = "application/pdf", run = "pdf" }, # Archive - { mime = "application/*zip", run = "archive" }, - { mime = "application/x-{tar,bzip*,7z-compressed,xz,rar}", run = "archive" }, + { mime = "application/{,g}zip", run = "archive" }, + { mime = "application/x-{tar,bzip*,7z-compressed,xz,rar,iso9660-image}", run = "archive" }, + # Font + { mime = "font/*", run = "font" }, + { mime = "application/vnd.ms-opentype", run = "font" }, # Fallback { name = "*", run = "file" }, ] [input] +cursor_blink = true + # cd cd_title = "Change directory:" cd_origin = "top-center" @@ -171,11 +185,10 @@ open_origin = "hovered" open_offset = [ 0, 1, 50, 7 ] [which] -sort_by = "none" +sort_by = "none" sort_sensitive = false -sort_reverse = false +sort_reverse = false +sort_translit = false [log] enabled = false - -[headsup] diff --git a/yazi-config/src/headsup/headsup.rs b/yazi-config/src/headsup/headsup.rs deleted file mode 100644 index 062725fa..00000000 --- a/yazi-config/src/headsup/headsup.rs +++ /dev/null @@ -1,34 +0,0 @@ -use serde::{Deserialize, Deserializer}; - -use crate::MERGED_YAZI; - -#[derive(Debug)] -pub struct Headsup { - // TODO: remove this once Yazi 0.3 is released -- - pub disable_exec_warn: bool, -} - -impl Default for Headsup { - fn default() -> Self { toml::from_str(&MERGED_YAZI).unwrap() } -} - -impl<'de> Deserialize<'de> for Headsup { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct Outer { - headsup: Shadow, - } - #[derive(Deserialize)] - struct Shadow { - #[serde(default)] - disable_exec_warn: bool, - } - - let outer = Outer::deserialize(deserializer)?; - - Ok(Self { disable_exec_warn: outer.headsup.disable_exec_warn }) - } -} diff --git a/yazi-config/src/headsup/mod.rs b/yazi-config/src/headsup/mod.rs deleted file mode 100644 index 82e85f0a..00000000 --- a/yazi-config/src/headsup/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod headsup; - -pub use headsup::*; diff --git a/yazi-config/src/keymap/control.rs b/yazi-config/src/keymap/control.rs index adbfb626..85c6e906 100644 --- a/yazi-config/src/keymap/control.rs +++ b/yazi-config/src/keymap/control.rs @@ -1,14 +1,15 @@ -use std::{borrow::Cow, collections::VecDeque, sync::atomic::Ordering}; +use std::{borrow::Cow, collections::VecDeque}; -use serde::{Deserialize, Deserializer}; +use serde::Deserialize; use yazi_shared::event::Cmd; use super::Key; -use crate::DEPRECATED_EXEC; -#[derive(Debug, Default)] +#[derive(Debug, Default, Deserialize)] pub struct Control { + #[serde(deserialize_with = "super::deserialize_on")] pub on: Vec, + #[serde(deserialize_with = "super::deserialize_run")] pub run: Vec, pub desc: Option, } @@ -24,7 +25,7 @@ impl Control { #[inline] pub fn run(&self) -> String { - self.run.iter().map(|e| e.to_string()).collect::>().join("; ") + self.run.iter().map(|c| c.to_string()).collect::>().join("; ") } #[inline] @@ -40,33 +41,3 @@ impl Control { || self.on().to_lowercase().contains(&s) } } - -// TODO: remove this once Yazi 0.3 is released -impl<'de> Deserialize<'de> for Control { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - pub struct Shadow { - pub on: Vec, - pub run: Option, - pub exec: Option, - pub desc: Option, - } - - let shadow = Shadow::deserialize(deserializer)?; - - #[derive(Deserialize)] - struct VecCmd(#[serde(deserialize_with = "super::run_deserialize")] Vec); - - if shadow.exec.is_some() { - DEPRECATED_EXEC.store(true, Ordering::Relaxed); - } - let Some(run) = shadow.run.or(shadow.exec) else { - return Err(serde::de::Error::custom("missing field `run` within `[keymap]`")); - }; - - Ok(Self { on: shadow.on, run: run.0, desc: shadow.desc }) - } -} diff --git a/yazi-config/src/keymap/deserializers.rs b/yazi-config/src/keymap/deserializers.rs new file mode 100644 index 00000000..dea0a226 --- /dev/null +++ b/yazi-config/src/keymap/deserializers.rs @@ -0,0 +1,83 @@ +use std::{fmt, str::FromStr}; + +use anyhow::Result; +use serde::{de::{self, Visitor}, Deserializer}; +use yazi_shared::event::Cmd; + +use crate::keymap::Key; + +pub(super) fn deserialize_on<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct OnVisitor; + + impl<'de> Visitor<'de> for OnVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a `on` string or array of strings within keymap.toml") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut cmds = vec![]; + while let Some(value) = &seq.next_element::()? { + cmds.push(Key::from_str(value).map_err(de::Error::custom)?); + } + if cmds.is_empty() { + return Err(de::Error::custom("`on` within keymap.toml cannot be empty")); + } + Ok(cmds) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(vec![Key::from_str(value).map_err(de::Error::custom)?]) + } + } + + deserializer.deserialize_any(OnVisitor) +} + +pub(super) fn deserialize_run<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct RunVisitor; + + impl<'de> Visitor<'de> for RunVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a `run` string or array of strings within keymap.toml") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut cmds = vec![]; + while let Some(value) = &seq.next_element::()? { + cmds.push(Cmd::from_str(value).map_err(de::Error::custom)?); + } + if cmds.is_empty() { + return Err(de::Error::custom("`run` within keymap.toml cannot be empty")); + } + Ok(cmds) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(vec![Cmd::from_str(value).map_err(de::Error::custom)?]) + } + } + + deserializer.deserialize_any(RunVisitor) +} diff --git a/yazi-config/src/keymap/key.rs b/yazi-config/src/keymap/key.rs index 0de1ba7d..93998705 100644 --- a/yazi-config/src/keymap/key.rs +++ b/yazi-config/src/keymap/key.rs @@ -2,34 +2,30 @@ use std::{fmt::{Display, Write}, str::FromStr}; use anyhow::bail; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use serde::Deserialize; -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)] -#[serde(try_from = "String")] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Key { - pub code: KeyCode, - pub shift: bool, - pub ctrl: bool, - pub alt: bool, + pub code: KeyCode, + pub shift: bool, + pub ctrl: bool, + pub alt: bool, + pub super_: bool, } impl Key { #[inline] pub fn plain(&self) -> Option { match self.code { - KeyCode::Char(c) if !self.ctrl && !self.alt => Some(c), + KeyCode::Char(c) if !self.ctrl && !self.alt && !self.super_ => Some(c), _ => None, } } - - #[inline] - pub fn is_enter(&self) -> bool { - matches!(self, Key { code: KeyCode::Enter, shift: false, ctrl: false, alt: false }) - } } impl Default for Key { - fn default() -> Self { Self { code: KeyCode::Null, shift: false, ctrl: false, alt: false } } + fn default() -> Self { + Self { code: KeyCode::Null, shift: false, ctrl: false, alt: false, super_: false } + } } impl From for Key { @@ -56,6 +52,7 @@ impl From for Key { shift, ctrl: value.modifiers.contains(KeyModifiers::CONTROL), alt: value.modifiers.contains(KeyModifiers::ALT), + super_: value.modifiers.contains(KeyModifiers::SUPER), } } } @@ -77,45 +74,46 @@ impl FromStr for Key { } let mut it = s[1..s.len() - 1].split_inclusive('-').peekable(); - while let Some(x) = it.next() { - match x { - "S-" => key.shift = true, - "C-" => key.ctrl = true, - "A-" => key.alt = true, + while let Some(next) = it.next() { + match next.to_ascii_lowercase().as_str() { + "s-" => key.shift = true, + "c-" => key.ctrl = true, + "a-" => key.alt = true, + "d-" => key.super_ = true, - "Space" => key.code = KeyCode::Char(' '), - "Backspace" => key.code = KeyCode::Backspace, - "Enter" => key.code = KeyCode::Enter, - "Left" => key.code = KeyCode::Left, - "Right" => key.code = KeyCode::Right, - "Up" => key.code = KeyCode::Up, - "Down" => key.code = KeyCode::Down, - "Home" => key.code = KeyCode::Home, - "End" => key.code = KeyCode::End, - "PageUp" => key.code = KeyCode::PageUp, - "PageDown" => key.code = KeyCode::PageDown, - "Tab" => key.code = KeyCode::Tab, - "BackTab" => key.code = KeyCode::BackTab, - "Delete" => key.code = KeyCode::Delete, - "Insert" => key.code = KeyCode::Insert, - "F1" => key.code = KeyCode::F(1), - "F2" => key.code = KeyCode::F(2), - "F3" => key.code = KeyCode::F(3), - "F4" => key.code = KeyCode::F(4), - "F5" => key.code = KeyCode::F(5), - "F6" => key.code = KeyCode::F(6), - "F7" => key.code = KeyCode::F(7), - "F8" => key.code = KeyCode::F(8), - "F9" => key.code = KeyCode::F(9), - "F10" => key.code = KeyCode::F(10), - "F11" => key.code = KeyCode::F(11), - "F12" => key.code = KeyCode::F(12), - "Esc" => key.code = KeyCode::Esc, + "space" => key.code = KeyCode::Char(' '), + "backspace" => key.code = KeyCode::Backspace, + "enter" => key.code = KeyCode::Enter, + "left" => key.code = KeyCode::Left, + "right" => key.code = KeyCode::Right, + "up" => key.code = KeyCode::Up, + "down" => key.code = KeyCode::Down, + "home" => key.code = KeyCode::Home, + "end" => key.code = KeyCode::End, + "pageup" => key.code = KeyCode::PageUp, + "pagedown" => key.code = KeyCode::PageDown, + "tab" => key.code = KeyCode::Tab, + "backtab" => key.code = KeyCode::BackTab, + "delete" => key.code = KeyCode::Delete, + "insert" => key.code = KeyCode::Insert, + "f1" => key.code = KeyCode::F(1), + "f2" => key.code = KeyCode::F(2), + "f3" => key.code = KeyCode::F(3), + "f4" => key.code = KeyCode::F(4), + "f5" => key.code = KeyCode::F(5), + "f6" => key.code = KeyCode::F(6), + "f7" => key.code = KeyCode::F(7), + "f8" => key.code = KeyCode::F(8), + "f9" => key.code = KeyCode::F(9), + "f10" => key.code = KeyCode::F(10), + "f11" => key.code = KeyCode::F(11), + "f12" => key.code = KeyCode::F(12), + "esc" => key.code = KeyCode::Esc, - c if it.peek().is_none() => { - key.code = KeyCode::Char(c.chars().next().unwrap()); - } - k => bail!("unknown key: {k}"), + _ => match next { + s if it.peek().is_none() => key.code = KeyCode::Char(s.chars().next().unwrap()), + s => bail!("unknown key: {s}"), + }, } } @@ -126,12 +124,6 @@ impl FromStr for Key { } } -impl TryFrom for Key { - type Error = anyhow::Error; - - fn try_from(s: String) -> Result { Self::from_str(&s) } -} - impl Display for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(c) = self.plain() { @@ -140,6 +132,9 @@ impl Display for Key { } write!(f, "<")?; + if self.super_ { + write!(f, "D-")?; + } if self.ctrl { write!(f, "C-")?; } diff --git a/yazi-config/src/keymap/keymap.rs b/yazi-config/src/keymap/keymap.rs index c5f755b9..f5542281 100644 --- a/yazi-config/src/keymap/keymap.rs +++ b/yazi-config/src/keymap/keymap.rs @@ -1,8 +1,10 @@ +use std::str::FromStr; + use serde::{Deserialize, Deserializer}; use yazi_shared::Layer; use super::Control; -use crate::{Preset, MERGED_KEYMAP}; +use crate::Preset; #[derive(Debug)] pub struct Keymap { @@ -14,6 +16,28 @@ pub struct Keymap { pub completion: Vec, } +impl Keymap { + #[inline] + pub fn get(&self, layer: Layer) -> &Vec { + match layer { + Layer::App => unreachable!(), + Layer::Manager => &self.manager, + Layer::Tasks => &self.tasks, + Layer::Select => &self.select, + Layer::Input => &self.input, + Layer::Help => &self.help, + Layer::Completion => &self.completion, + Layer::Which => unreachable!(), + } + } +} + +impl FromStr for Keymap { + type Err = toml::de::Error; + + fn from_str(s: &str) -> Result { toml::from_str(s) } +} + impl<'de> Deserialize<'de> for Keymap { fn deserialize(deserializer: D) -> Result where @@ -62,23 +86,3 @@ impl<'de> Deserialize<'de> for Keymap { }) } } - -impl Default for Keymap { - fn default() -> Self { toml::from_str(&MERGED_KEYMAP).unwrap() } -} - -impl Keymap { - #[inline] - pub fn get(&self, layer: Layer) -> &Vec { - match layer { - Layer::App => unreachable!(), - Layer::Manager => &self.manager, - Layer::Tasks => &self.tasks, - Layer::Select => &self.select, - Layer::Input => &self.input, - Layer::Help => &self.help, - Layer::Completion => &self.completion, - Layer::Which => unreachable!(), - } - } -} diff --git a/yazi-config/src/keymap/mod.rs b/yazi-config/src/keymap/mod.rs index 7de322c3..6c330dd3 100644 --- a/yazi-config/src/keymap/mod.rs +++ b/yazi-config/src/keymap/mod.rs @@ -1,12 +1,11 @@ mod control; mod cow; +mod deserializers; mod key; mod keymap; -mod run; pub use control::*; pub use cow::*; +use deserializers::*; pub use key::*; pub use keymap::*; -#[allow(unused_imports)] -pub use run::*; diff --git a/yazi-config/src/keymap/run.rs b/yazi-config/src/keymap/run.rs deleted file mode 100644 index 65307a9c..00000000 --- a/yazi-config/src/keymap/run.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::{fmt, mem}; - -use anyhow::{bail, Result}; -use serde::{de::{self, Visitor}, Deserializer}; -use yazi_shared::event::{Cmd, Data}; - -pub(super) fn run_deserialize<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct RunVisitor; - - #[allow(clippy::explicit_counter_loop)] - fn parse(s: &str) -> Result { - let mut args = shell_words::split(s)?; - let mut cmd = Cmd { name: mem::take(&mut args[0]), ..Default::default() }; - - let mut i = 0usize; - for arg in args.into_iter().skip(1) { - let Some(arg) = arg.strip_prefix("--") else { - cmd.args.insert(i.to_string(), Data::String(arg)); - i += 1; - continue; - }; - - let mut parts = arg.splitn(2, '='); - let Some(key) = parts.next().map(|s| s.to_owned()) else { - bail!("invalid argument: {arg}"); - }; - - if let Some(val) = parts.next() { - cmd.args.insert(key, Data::String(val.to_owned())); - } else { - cmd.args.insert(key, Data::Boolean(true)); - } - } - Ok(cmd) - } - - impl<'de> Visitor<'de> for RunVisitor { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a `run` string or array of strings within keymap.toml") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: de::SeqAccess<'de>, - { - let mut cmds = vec![]; - while let Some(value) = &seq.next_element::()? { - cmds.push(parse(value).map_err(de::Error::custom)?); - } - if cmds.is_empty() { - return Err(de::Error::custom("`run` within keymap.toml cannot be empty")); - } - Ok(cmds) - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - Ok(vec![parse(value).map_err(de::Error::custom)?]) - } - } - - deserializer.deserialize_any(RunVisitor) -} diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 5b6b0798..60204f59 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -1,8 +1,9 @@ #![allow(clippy::module_inception)] +use std::str::FromStr; + use yazi_shared::{RoCell, Xdg}; -pub mod headsup; pub mod keymap; mod layout; mod log; @@ -16,7 +17,6 @@ pub mod preview; mod priority; mod tasks; pub mod theme; -mod validation; pub mod which; pub use layout::*; @@ -24,17 +24,8 @@ pub(crate) use pattern::*; pub(crate) use preset::*; pub use priority::*; -// TODO: remove this once Yazi 0.3 is released -- -pub static DEPRECATED_EXEC: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); - -static MERGED_YAZI: RoCell = RoCell::new(); -static MERGED_KEYMAP: RoCell = RoCell::new(); -static MERGED_THEME: RoCell = RoCell::new(); - pub static LAYOUT: RoCell> = RoCell::new(); -pub static HEADSUP: RoCell = RoCell::new(); pub static KEYMAP: RoCell = RoCell::new(); pub static LOG: RoCell = RoCell::new(); pub static MANAGER: RoCell = RoCell::new(); @@ -49,37 +40,37 @@ pub static WHICH: RoCell = RoCell::new(); pub fn init() -> anyhow::Result<()> { let config_dir = Xdg::config_dir(); - MERGED_YAZI.init(Preset::yazi(&config_dir)?); - MERGED_KEYMAP.init(Preset::keymap(&config_dir)?); - MERGED_THEME.init(Preset::theme(&config_dir)?); + let yazi_toml = &Preset::yazi(&config_dir)?; + let keymap_toml = &Preset::keymap(&config_dir)?; + let theme_toml = &Preset::theme(&config_dir)?; LAYOUT.with(Default::default); - HEADSUP.with(Default::default); - KEYMAP.with(Default::default); - LOG.with(Default::default); - MANAGER.with(Default::default); - OPEN.with(Default::default); - PLUGIN.with(Default::default); - PREVIEW.with(Default::default); - TASKS.with(Default::default); - THEME.with(Default::default); - INPUT.with(Default::default); - SELECT.with(Default::default); - WHICH.with(Default::default); + KEYMAP.init(<_>::from_str(keymap_toml)?); + LOG.init(<_>::from_str(yazi_toml)?); + MANAGER.init(<_>::from_str(yazi_toml)?); + OPEN.init(<_>::from_str(yazi_toml)?); + PLUGIN.init(<_>::from_str(yazi_toml)?); + PREVIEW.init(<_>::from_str(yazi_toml)?); + TASKS.init(<_>::from_str(yazi_toml)?); + THEME.init(<_>::from_str(theme_toml)?); + INPUT.init(<_>::from_str(yazi_toml)?); + SELECT.init(<_>::from_str(yazi_toml)?); + WHICH.init(<_>::from_str(yazi_toml)?); - // TODO: remove this once Yazi 0.3 is released -- - if !HEADSUP.disable_exec_warn && DEPRECATED_EXEC.load(std::sync::atomic::Ordering::Relaxed) { - eprintln!( - r#" -WARNING: `exec` will be deprecated in the next major version v0.3 and replaced by `run`. + // TODO: Remove in v0.3.2 + for c in &KEYMAP.manager { + for r in &c.run { + if r.name == "shell" && !r.bool("confirm") && !r.bool("interactive") { + eprintln!( + r#"WARNING: In Yazi v0.3, the behavior of the interactive `shell` (i.e., shell templates) must be explicitly specified with `--interactive`. -Please replace all `exec = ...` with `run = ...`, in your `yazi.toml` and `keymap.toml`. - ---- -Add `disable_exec_warn = true` to your `yazi.toml` under `[headsup]` to suppress this warning. -"# - ); +Please replace e.g. `shell` with `shell --interactive`, `shell "my-template"` with `shell "my-template" --interactive`, in your keymap.toml"# + ); + return Ok(()); + } + } } + Ok(()) } diff --git a/yazi-config/src/log/log.rs b/yazi-config/src/log/log.rs index 74433fdf..c92cf604 100644 --- a/yazi-config/src/log/log.rs +++ b/yazi-config/src/log/log.rs @@ -1,14 +1,16 @@ -use serde::{Deserialize, Deserializer}; +use std::str::FromStr; -use crate::MERGED_YAZI; +use serde::{Deserialize, Deserializer}; #[derive(Debug)] pub struct Log { pub enabled: bool, } -impl Default for Log { - fn default() -> Self { toml::from_str(&MERGED_YAZI).unwrap() } +impl FromStr for Log { + type Err = toml::de::Error; + + fn from_str(s: &str) -> Result { toml::from_str(s) } } impl<'de> Deserialize<'de> for Log { diff --git a/yazi-config/src/manager/manager.rs b/yazi-config/src/manager/manager.rs index 27f4acaa..cd980ac2 100644 --- a/yazi-config/src/manager/manager.rs +++ b/yazi-config/src/manager/manager.rs @@ -1,8 +1,9 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; use validator::Validate; -use super::{ManagerRatio, SortBy}; -use crate::{validation::check_validation, MERGED_YAZI}; +use super::{ManagerRatio, MouseEvents, SortBy}; #[derive(Debug, Deserialize, Serialize, Validate)] pub struct Manager { @@ -13,6 +14,7 @@ pub struct Manager { pub sort_sensitive: bool, pub sort_reverse: bool, pub sort_dir_first: bool, + pub sort_translit: bool, // Display #[validate(length(min = 1, max = 20, message = "must be between 1 and 20 characters"))] @@ -20,18 +22,21 @@ pub struct Manager { pub show_hidden: bool, pub show_symlink: bool, pub scrolloff: u8, + pub mouse_events: MouseEvents, } -impl Default for Manager { - fn default() -> Self { +impl FromStr for Manager { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { #[derive(Deserialize)] struct Outer { manager: Manager, } - let manager = toml::from_str::(&MERGED_YAZI).unwrap().manager; + let manager = toml::from_str::(s)?.manager; + manager.validate()?; - check_validation(manager.validate()); - manager + Ok(manager) } } diff --git a/yazi-config/src/manager/mod.rs b/yazi-config/src/manager/mod.rs index 2ddd6599..f8b9b13c 100644 --- a/yazi-config/src/manager/mod.rs +++ b/yazi-config/src/manager/mod.rs @@ -1,7 +1,9 @@ mod manager; +mod mouse; mod ratio; mod sorting; pub use manager::*; +pub use mouse::*; pub use ratio::*; pub use sorting::*; diff --git a/yazi-config/src/manager/mouse.rs b/yazi-config/src/manager/mouse.rs new file mode 100644 index 00000000..6507db33 --- /dev/null +++ b/yazi-config/src/manager/mouse.rs @@ -0,0 +1,62 @@ +use anyhow::{bail, Result}; +use bitflags::bitflags; +use crossterm::event::MouseEventKind; +use serde::{Deserialize, Serialize}; + +bitflags! { + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] + #[serde(try_from = "Vec", into = "Vec")] + pub struct MouseEvents: u8 { + const CLICK = 0b00001; + const SCROLL = 0b00010; + const TOUCH = 0b00100; + const MOVE = 0b01000; + const DRAG = 0b10000; + } +} + +impl MouseEvents { + pub const fn draggable(self) -> bool { self.contains(Self::DRAG) } +} + +impl TryFrom> for MouseEvents { + type Error = anyhow::Error; + + fn try_from(value: Vec) -> Result { + value.into_iter().try_fold(Self::empty(), |aac, s| { + Ok(match s.as_str() { + "click" => aac | Self::CLICK, + "scroll" => aac | Self::SCROLL, + "touch" => aac | Self::TOUCH, + "move" => aac | Self::MOVE, + "drag" => aac | Self::DRAG, + _ => bail!("Invalid mouse event: {s}"), + }) + }) + } +} + +impl From for Vec { + fn from(value: MouseEvents) -> Self { + let events = [ + (MouseEvents::CLICK, "click"), + (MouseEvents::SCROLL, "scroll"), + (MouseEvents::TOUCH, "touch"), + (MouseEvents::MOVE, "move"), + (MouseEvents::DRAG, "drag"), + ]; + events.into_iter().filter(|v| value.contains(v.0)).map(|v| v.1.to_owned()).collect() + } +} + +impl From for MouseEvents { + fn from(value: crossterm::event::MouseEventKind) -> Self { + match value { + MouseEventKind::Down(_) | MouseEventKind::Up(_) => Self::CLICK, + MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => Self::SCROLL, + MouseEventKind::ScrollLeft | MouseEventKind::ScrollRight => Self::TOUCH, + MouseEventKind::Moved => Self::MOVE, + MouseEventKind::Drag(_) => Self::DRAG, + } + } +} diff --git a/yazi-config/src/open/open.rs b/yazi-config/src/open/open.rs index 610bdc62..a83fbe92 100644 --- a/yazi-config/src/open/open.rs +++ b/yazi-config/src/open/open.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, path::Path}; +use std::{collections::HashMap, path::Path, str::FromStr}; use indexmap::IndexSet; use serde::{Deserialize, Deserializer}; use yazi_shared::MIME_DIR; use super::Opener; -use crate::{open::OpenRule, Preset, MERGED_YAZI}; +use crate::{open::OpenRule, Preset}; #[derive(Debug)] pub struct Open { @@ -13,20 +13,16 @@ pub struct Open { openers: HashMap>, } -impl Default for Open { - fn default() -> Self { toml::from_str(&MERGED_YAZI).unwrap() } -} - impl Open { pub fn openers(&self, path: P, mime: M) -> Option> where P: AsRef, M: AsRef, { - let is_folder = mime.as_ref() == MIME_DIR; + let is_dir = mime.as_ref() == MIME_DIR; self.rules.iter().find_map(|rule| { if rule.mime.as_ref().is_some_and(|p| p.match_mime(&mime)) - || rule.name.as_ref().is_some_and(|p| p.match_path(&path, is_folder)) + || rule.name.as_ref().is_some_and(|p| p.match_path(&path, is_dir)) { let openers = rule .use_ @@ -58,6 +54,12 @@ impl Open { } } +impl FromStr for Open { + type Err = toml::de::Error; + + fn from_str(s: &str) -> Result { toml::from_str(s) } +} + impl<'de> Deserialize<'de> for Open { fn deserialize(deserializer: D) -> Result where @@ -78,6 +80,13 @@ impl<'de> Deserialize<'de> for Open { } let mut outer = Outer::deserialize(deserializer)?; + + if outer.open.append_rules.iter().any(|r| r.any_file()) { + outer.open.rules.retain(|r| !r.any_file()); + } + if outer.open.append_rules.iter().any(|r| r.any_dir()) { + outer.open.rules.retain(|r| !r.any_dir()); + } Preset::mix(&mut outer.open.rules, outer.open.prepend_rules, outer.open.append_rules); let openers = outer diff --git a/yazi-config/src/open/opener.rs b/yazi-config/src/open/opener.rs index 40409c7b..9d298eb8 100644 --- a/yazi-config/src/open/opener.rs +++ b/yazi-config/src/open/opener.rs @@ -1,9 +1,5 @@ -use std::sync::atomic::Ordering; - use serde::{Deserialize, Deserializer}; -use crate::DEPRECATED_EXEC; - #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Opener { pub run: String, @@ -36,9 +32,7 @@ impl<'de> Deserialize<'de> for Opener { { #[derive(Deserialize)] pub struct Shadow { - run: Option, - // TODO: remove this once Yazi 0.3 is released -- - exec: Option, + run: String, #[serde(default)] block: bool, #[serde(default)] @@ -50,13 +44,7 @@ impl<'de> Deserialize<'de> for Opener { let shadow = Shadow::deserialize(deserializer)?; - // TODO: remove this once Yazi 0.3 is released -- - if shadow.exec.is_some() { - DEPRECATED_EXEC.store(true, Ordering::Relaxed); - } - let run = shadow.run.or(shadow.exec).unwrap_or_default(); - // TODO: -- remove this once Yazi 0.3 is released - + let run = shadow.run; if run.is_empty() { return Err(serde::de::Error::custom("`run` cannot be empty")); } diff --git a/yazi-config/src/open/rule.rs b/yazi-config/src/open/rule.rs index 08333c92..d69371d2 100644 --- a/yazi-config/src/open/rule.rs +++ b/yazi-config/src/open/rule.rs @@ -13,6 +13,14 @@ pub(super) struct OpenRule { pub(super) use_: Vec, } +impl OpenRule { + #[inline] + pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } + + #[inline] + pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } +} + impl OpenRule { fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 74096b1d..d92c6ee6 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -6,25 +6,27 @@ use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(try_from = "String")] pub struct Pattern { - inner: globset::GlobMatcher, - is_star: bool, - is_folder: bool, + inner: globset::GlobMatcher, + is_dir: bool, + is_star: bool, } impl Pattern { #[inline] - pub fn match_mime(&self, str: impl AsRef) -> bool { self.inner.is_match(str.as_ref()) } - - #[inline] - pub fn match_path(&self, path: impl AsRef, is_folder: bool) -> bool { - is_folder == self.is_folder && (self.is_star || self.inner.is_match(path)) + pub fn match_mime(&self, str: impl AsRef) -> bool { + self.is_star || self.inner.is_match(str.as_ref()) } #[inline] - pub fn any_file(&self) -> bool { self.is_star && !self.is_folder } + pub fn match_path(&self, path: impl AsRef, is_dir: bool) -> bool { + is_dir == self.is_dir && (self.is_star || self.inner.is_match(path)) + } #[inline] - pub fn any_dir(&self) -> bool { self.is_star && self.is_folder } + pub fn any_file(&self) -> bool { self.is_star && !self.is_dir } + + #[inline] + pub fn any_dir(&self) -> bool { self.is_star && self.is_dir } } impl TryFrom<&str> for Pattern { @@ -36,13 +38,13 @@ impl TryFrom<&str> for Pattern { let inner = GlobBuilder::new(b) .case_insensitive(a.len() == s.len()) - .literal_separator(b.contains('/')) + .literal_separator(false) .backslash_escape(false) - .empty_alternates(false) + .empty_alternates(true) .build()? .compile_matcher(); - Ok(Self { inner, is_star: b == "*", is_folder: b.len() < a.len() }) + Ok(Self { inner, is_dir: b.len() < a.len(), is_star: b == "*" }) } } diff --git a/yazi-config/src/plugin/fetcher.rs b/yazi-config/src/plugin/fetcher.rs new file mode 100644 index 00000000..c08a710b --- /dev/null +++ b/yazi-config/src/plugin/fetcher.rs @@ -0,0 +1,32 @@ +use serde::Deserialize; +use yazi_shared::{event::Cmd, Condition}; + +use crate::{Pattern, Priority}; + +#[derive(Debug, Deserialize)] +pub struct Fetcher { + #[serde(skip)] + pub idx: u8, + + pub id: String, + #[serde(rename = "if")] + pub if_: Option, + pub name: Option, + pub mime: Option, + pub run: Cmd, + #[serde(default)] + pub prio: Priority, +} + +#[derive(Debug, Clone)] +pub struct FetcherProps { + pub id: u8, + pub name: String, + pub prio: Priority, +} + +impl From<&Fetcher> for FetcherProps { + fn from(fetcher: &Fetcher) -> Self { + Self { id: fetcher.idx, name: fetcher.run.name.to_owned(), prio: fetcher.prio } + } +} diff --git a/yazi-config/src/plugin/mod.rs b/yazi-config/src/plugin/mod.rs index 6147d1b8..5594f01f 100644 --- a/yazi-config/src/plugin/mod.rs +++ b/yazi-config/src/plugin/mod.rs @@ -1,12 +1,11 @@ +mod fetcher; mod plugin; -mod props; -mod rule; -mod run; +mod preloader; +mod previewer; +pub use fetcher::*; pub use plugin::*; -pub use props::*; -pub use rule::*; -#[allow(unused_imports)] -pub use run::*; +pub use preloader::*; +pub use previewer::*; -pub const MAX_PRELOADERS: u8 = 32; +pub const MAX_PREWORKERS: u8 = 32; diff --git a/yazi-config/src/plugin/plugin.rs b/yazi-config/src/plugin/plugin.rs index 0d226037..9e51fa0e 100644 --- a/yazi-config/src/plugin/plugin.rs +++ b/yazi-config/src/plugin/plugin.rs @@ -1,19 +1,68 @@ -use std::path::Path; +use std::{path::Path, str::FromStr}; use serde::Deserialize; use yazi_shared::MIME_DIR; -use super::PluginRule; -use crate::{plugin::MAX_PRELOADERS, Preset, MERGED_YAZI}; +use super::{Fetcher, Preloader, Previewer}; +use crate::{plugin::MAX_PREWORKERS, Preset}; #[derive(Deserialize)] pub struct Plugin { - pub preloaders: Vec, - pub previewers: Vec, + pub fetchers: Vec, + pub preloaders: Vec, + pub previewers: Vec, } -impl Default for Plugin { - fn default() -> Self { +impl Plugin { + pub fn fetchers( + &self, + path: &Path, + mime: Option<&str>, + f: impl Fn(&str) -> bool + Copy, + ) -> Vec<&Fetcher> { + let is_dir = mime == Some(MIME_DIR); + self + .fetchers + .iter() + .filter(|&p| { + p.if_.as_ref().and_then(|c| c.eval(f)) != Some(false) + && (p.mime.as_ref().zip(mime).map_or(false, |(p, m)| p.match_mime(m)) + || p.name.as_ref().is_some_and(|p| p.match_path(path, is_dir))) + }) + .collect() + } + + pub fn preloaders(&self, path: &Path, mime: Option<&str>) -> Vec<&Preloader> { + let is_dir = mime == Some(MIME_DIR); + let mut preloaders = Vec::with_capacity(1); + + for p in &self.preloaders { + if !p.mime.as_ref().zip(mime).map_or(false, |(p, m)| p.match_mime(m)) + && !p.name.as_ref().is_some_and(|p| p.match_path(path, is_dir)) + { + continue; + } + + preloaders.push(p); + if !p.next { + break; + } + } + preloaders + } + + pub fn previewer(&self, path: &Path, mime: &str) -> Option<&Previewer> { + let is_dir = mime == MIME_DIR; + self.previewers.iter().find(|&p| { + p.mime.as_ref().is_some_and(|p| p.match_mime(mime)) + || p.name.as_ref().is_some_and(|p| p.match_path(path, is_dir)) + }) + } +} +impl FromStr for Plugin { + type Err = toml::de::Error; + + fn from_str(s: &str) -> Result { #[derive(Deserialize)] struct Outer { plugin: Shadow, @@ -21,20 +70,26 @@ impl Default for Plugin { #[derive(Deserialize)] struct Shadow { - preloaders: Vec, + fetchers: Vec, #[serde(default)] - prepend_preloaders: Vec, + prepend_fetchers: Vec, #[serde(default)] - append_preloaders: Vec, + append_fetchers: Vec, - previewers: Vec, + preloaders: Vec, #[serde(default)] - prepend_previewers: Vec, + prepend_preloaders: Vec, #[serde(default)] - append_previewers: Vec, + append_preloaders: Vec, + + previewers: Vec, + #[serde(default)] + prepend_previewers: Vec, + #[serde(default)] + append_previewers: Vec, } - let mut shadow = toml::from_str::(&MERGED_YAZI).unwrap().plugin; + let mut shadow = toml::from_str::(s)?.plugin; if shadow.append_previewers.iter().any(|r| r.any_file()) { shadow.previewers.retain(|r| !r.any_file()); } @@ -42,48 +97,25 @@ impl Default for Plugin { shadow.previewers.retain(|r| !r.any_dir()); } + Preset::mix(&mut shadow.fetchers, shadow.prepend_fetchers, shadow.append_fetchers); Preset::mix(&mut shadow.preloaders, shadow.prepend_preloaders, shadow.append_preloaders); Preset::mix(&mut shadow.previewers, shadow.prepend_previewers, shadow.append_previewers); - if shadow.preloaders.len() > MAX_PRELOADERS as usize { - panic!("Too many preloaders"); + if shadow.fetchers.len() + shadow.preloaders.len() > MAX_PREWORKERS as usize { + panic!("Fetchers and preloaders exceed the limit of {MAX_PREWORKERS}"); } - for (i, preloader) in shadow.preloaders.iter_mut().enumerate() { - if preloader.sync { - panic!("Preloaders cannot be synchronous"); - } - preloader.id = i as u8; + for (i, p) in shadow.fetchers.iter_mut().enumerate() { + p.idx = i as u8; + } + for (i, p) in shadow.preloaders.iter_mut().enumerate() { + p.idx = shadow.fetchers.len() as u8 + i as u8; } - Self { preloaders: shadow.preloaders, previewers: shadow.previewers } - } -} - -impl Plugin { - pub fn preloaders( - &self, - path: &Path, - mime: Option<&str>, - f: impl Fn(&str) -> bool + Copy, - ) -> Vec<&PluginRule> { - let is_folder = mime == Some(MIME_DIR); - self - .preloaders - .iter() - .filter(|&rule| { - rule.cond.as_ref().and_then(|c| c.eval(f)) != Some(false) - && (rule.mime.as_ref().zip(mime).map_or(false, |(p, m)| p.match_mime(m)) - || rule.name.as_ref().is_some_and(|p| p.match_path(path, is_folder))) - }) - .collect() - } - - pub fn previewer(&self, path: &Path, mime: &str) -> Option<&PluginRule> { - let is_folder = mime == MIME_DIR; - self.previewers.iter().find(|&rule| { - rule.mime.as_ref().is_some_and(|p| p.match_mime(mime)) - || rule.name.as_ref().is_some_and(|p| p.match_path(path, is_folder)) + Ok(Self { + fetchers: shadow.fetchers, + preloaders: shadow.preloaders, + previewers: shadow.previewers, }) } } diff --git a/yazi-config/src/plugin/preloader.rs b/yazi-config/src/plugin/preloader.rs new file mode 100644 index 00000000..03a4e513 --- /dev/null +++ b/yazi-config/src/plugin/preloader.rs @@ -0,0 +1,31 @@ +use serde::Deserialize; +use yazi_shared::event::Cmd; + +use crate::{Pattern, Priority}; + +#[derive(Debug, Deserialize)] +pub struct Preloader { + #[serde(skip)] + pub idx: u8, + + pub name: Option, + pub mime: Option, + pub run: Cmd, + #[serde(default)] + pub next: bool, + #[serde(default)] + pub prio: Priority, +} + +#[derive(Debug, Clone)] +pub struct PreloaderProps { + pub id: u8, + pub name: String, + pub prio: Priority, +} + +impl From<&Preloader> for PreloaderProps { + fn from(preloader: &Preloader) -> Self { + Self { id: preloader.idx, name: preloader.run.name.to_owned(), prio: preloader.prio } + } +} diff --git a/yazi-config/src/plugin/previewer.rs b/yazi-config/src/plugin/previewer.rs new file mode 100644 index 00000000..a960201f --- /dev/null +++ b/yazi-config/src/plugin/previewer.rs @@ -0,0 +1,21 @@ +use serde::Deserialize; +use yazi_shared::event::Cmd; + +use crate::Pattern; + +#[derive(Debug, Deserialize)] +pub struct Previewer { + pub name: Option, + pub mime: Option, + pub run: Cmd, + #[serde(default)] + pub sync: bool, +} + +impl Previewer { + #[inline] + pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } + + #[inline] + pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } +} diff --git a/yazi-config/src/plugin/props.rs b/yazi-config/src/plugin/props.rs deleted file mode 100644 index 410bb73f..00000000 --- a/yazi-config/src/plugin/props.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::PluginRule; -use crate::Priority; - -#[derive(Debug, Clone)] -pub struct PluginProps { - pub id: u8, - pub name: String, - pub multi: bool, - pub prio: Priority, -} - -impl From<&PluginRule> for PluginProps { - fn from(rule: &PluginRule) -> Self { - Self { id: rule.id, name: rule.cmd.name.to_owned(), multi: rule.multi, prio: rule.prio } - } -} diff --git a/yazi-config/src/plugin/rule.rs b/yazi-config/src/plugin/rule.rs deleted file mode 100644 index 57c223f5..00000000 --- a/yazi-config/src/plugin/rule.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::sync::atomic::Ordering; - -use serde::{Deserialize, Deserializer}; -use yazi_shared::{event::Cmd, Condition}; - -use crate::{Pattern, Priority, DEPRECATED_EXEC}; - -#[derive(Debug)] -pub struct PluginRule { - pub id: u8, - pub cond: Option, - pub name: Option, - pub mime: Option, - pub cmd: Cmd, - pub sync: bool, - pub multi: bool, - pub prio: Priority, -} - -impl PluginRule { - #[inline] - pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } - - #[inline] - pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } -} - -// TODO: remove this once Yazi 0.3 is released -impl<'de> Deserialize<'de> for PluginRule { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - pub struct Shadow { - #[serde(default)] - pub id: u8, - pub cond: Option, - pub name: Option, - pub mime: Option, - pub run: Option, - pub exec: Option, - #[serde(default)] - pub sync: bool, - #[serde(default)] - pub multi: bool, - #[serde(default)] - pub prio: Priority, - } - - let shadow = Shadow::deserialize(deserializer)?; - - #[derive(Deserialize)] - struct WrappedCmd(#[serde(deserialize_with = "super::run_deserialize")] Cmd); - - if shadow.exec.is_some() { - DEPRECATED_EXEC.store(true, Ordering::Relaxed); - } - let Some(run) = shadow.run.or(shadow.exec) else { - return Err(serde::de::Error::custom("missing field `run` within `[plugin]`")); - }; - - Ok(Self { - id: shadow.id, - cond: shadow.cond, - name: shadow.name, - mime: shadow.mime, - cmd: run.0, - sync: shadow.sync, - multi: shadow.multi, - prio: shadow.prio, - }) - } -} diff --git a/yazi-config/src/plugin/run.rs b/yazi-config/src/plugin/run.rs deleted file mode 100644 index 1ddcb561..00000000 --- a/yazi-config/src/plugin/run.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::fmt; - -use anyhow::Result; -use serde::{de::{self, Visitor}, Deserializer}; -use yazi_shared::event::Cmd; - -pub(super) fn run_deserialize<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - struct RunVisitor; - - impl<'de> Visitor<'de> for RunVisitor { - type Value = Cmd; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a `run` string or array of strings") - } - - fn visit_seq(self, _: A) -> Result - where - A: de::SeqAccess<'de>, - { - Err(de::Error::custom("`run` within [plugin] must be a string")) - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - if value.is_empty() { - return Err(de::Error::custom("`run` within [plugin] cannot be empty")); - } - Ok(Cmd { name: value.to_owned(), ..Default::default() }) - } - } - - deserializer.deserialize_any(RunVisitor) -} diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index 4b1f9d52..9ec27cdc 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -1,10 +1,13 @@ +use std::str::FromStr; + use serde::Deserialize; use super::{Offset, Origin}; -use crate::MERGED_YAZI; #[derive(Deserialize)] pub struct Input { + pub cursor_blink: bool, + // cd pub cd_title: String, pub cd_origin: Origin, @@ -61,18 +64,19 @@ pub struct Input { pub quit_offset: Offset, } -impl Default for Input { - fn default() -> Self { +impl Input { + pub const fn border(&self) -> u16 { 2 } +} + +impl FromStr for Input { + type Err = toml::de::Error; + + fn from_str(s: &str) -> Result { #[derive(Deserialize)] struct Outer { input: Input, } - toml::from_str::(&MERGED_YAZI).unwrap().input + Ok(toml::from_str::(s)?.input) } } - -impl Input { - #[inline] - pub const fn border(&self) -> u16 { 2 } -} diff --git a/yazi-config/src/popup/position.rs b/yazi-config/src/popup/position.rs index a67e44da..d8974947 100644 --- a/yazi-config/src/popup/position.rs +++ b/yazi-config/src/popup/position.rs @@ -1,6 +1,5 @@ use crossterm::terminal::WindowSize; use ratatui::layout::Rect; -use yazi_shared::term::Term; use super::{Offset, Origin}; @@ -14,10 +13,9 @@ impl Position { #[inline] pub fn new(origin: Origin, offset: Offset) -> Self { Self { origin, offset } } - pub fn rect(&self) -> Rect { + pub fn rect(&self, WindowSize { columns, rows, .. }: WindowSize) -> Rect { use Origin::*; let Offset { x, y, width, height } = self.offset; - let WindowSize { columns, rows, .. } = Term::size(); let max_x = columns.saturating_sub(width); let new_x = match self.origin { @@ -45,9 +43,8 @@ impl Position { } } - pub fn sticky(base: Rect, offset: Offset) -> Rect { + pub fn sticky(WindowSize { columns, rows, .. }: WindowSize, base: Rect, offset: Offset) -> Rect { let Offset { x, y, width, height } = offset; - let WindowSize { columns, rows, .. } = Term::size(); let above = base.y.saturating_add(base.height).saturating_add(height).saturating_add_signed(y) > rows; diff --git a/yazi-config/src/popup/select.rs b/yazi-config/src/popup/select.rs index 05d24a19..df584df8 100644 --- a/yazi-config/src/popup/select.rs +++ b/yazi-config/src/popup/select.rs @@ -1,7 +1,8 @@ +use std::str::FromStr; + use serde::Deserialize; use super::{Offset, Origin}; -use crate::MERGED_YAZI; #[derive(Deserialize)] pub struct Select { @@ -11,18 +12,19 @@ pub struct Select { pub open_offset: Offset, } -impl Default for Select { - fn default() -> Self { +impl Select { + pub const fn border(&self) -> u16 { 2 } +} + +impl FromStr for Select { + type Err = toml::de::Error; + + fn from_str(s: &str) -> Result { #[derive(Deserialize)] struct Outer { select: Select, } - toml::from_str::(&MERGED_YAZI).unwrap().select + Ok(toml::from_str::(s)?.select) } } - -impl Select { - #[inline] - pub const fn border(&self) -> u16 { 2 } -} diff --git a/yazi-config/src/preset.rs b/yazi-config/src/preset.rs index 317c8fd4..67c59ee3 100644 --- a/yazi-config/src/preset.rs +++ b/yazi-config/src/preset.rs @@ -1,4 +1,4 @@ -use std::{mem, path::{Path, PathBuf}}; +use std::{borrow::Cow, mem, path::{Path, PathBuf}}; use anyhow::{anyhow, Context, Result}; use toml::{Table, Value}; @@ -8,17 +8,17 @@ use crate::theme::Flavor; pub(crate) struct Preset; impl Preset { - pub(crate) fn yazi(p: &Path) -> Result { + pub(crate) fn yazi(p: &Path) -> Result> { Self::merge_path(p.join("yazi.toml"), include_str!("../preset/yazi.toml")) } - pub(crate) fn keymap(p: &Path) -> Result { + pub(crate) fn keymap(p: &Path) -> Result> { Self::merge_path(p.join("keymap.toml"), include_str!("../preset/keymap.toml")) } - pub(crate) fn theme(p: &Path) -> Result { + pub(crate) fn theme(p: &Path) -> Result> { let Ok(user) = std::fs::read_to_string(p.join("theme.toml")) else { - return Ok(include_str!("../preset/theme.toml").to_owned()); + return Ok(include_str!("../preset/theme.toml").into()); }; let Some(use_) = Flavor::parse_use(&user) else { return Self::merge_str(&user, include_str!("../preset/theme.toml")); @@ -37,18 +37,18 @@ impl Preset { } #[inline] - pub(crate) fn merge_str(user: &str, base: &str) -> Result { + pub(crate) fn merge_str(user: &str, base: &str) -> Result> { let mut t = user.parse()?; Self::merge(&mut t, base.parse()?, 2); - Ok(t.to_string()) + Ok(t.to_string().into()) } #[inline] - fn merge_path(user: PathBuf, base: &str) -> Result { + fn merge_path(user: PathBuf, base: &str) -> Result> { let s = std::fs::read_to_string(&user).unwrap_or_default(); if s.is_empty() { - return Ok(base.to_string()); + return Ok(base.into()); } Self::merge_str(&s, base).with_context(|| anyhow!("Loading {user:?}")) diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index 690ccb58..5f38a109 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -1,10 +1,11 @@ -use std::{path::PathBuf, time::{self, SystemTime}}; +use std::{path::PathBuf, str::FromStr, time::{self, SystemTime}}; +use anyhow::Context; use serde::{Deserialize, Serialize}; use validator::Validate; use yazi_shared::fs::expand_path; -use crate::{validation::check_validation, Xdg, MERGED_YAZI}; +use crate::Xdg; #[derive(Debug, Serialize)] pub struct Preview { @@ -22,8 +23,18 @@ pub struct Preview { pub ueberzug_offset: (f32, f32, f32, f32), } -impl Default for Preview { - fn default() -> Self { +impl Preview { + #[inline] + pub fn tmpfile(&self, prefix: &str) -> PathBuf { + let nanos = SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_nanos(); + self.cache_dir.join(format!("{prefix}-{}", nanos / 1000)) + } +} + +impl FromStr for Preview { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { #[derive(Deserialize)] struct Outer { preview: Shadow, @@ -46,17 +57,14 @@ impl Default for Preview { ueberzug_offset: (f32, f32, f32, f32), } - let preview = toml::from_str::(&MERGED_YAZI).unwrap().preview; - check_validation(preview.validate()); + let preview = toml::from_str::(s)?.preview; + preview.validate()?; let cache_dir = preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path); + std::fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?; - if !cache_dir.is_dir() { - std::fs::create_dir(&cache_dir).expect("Failed to create cache directory"); - } - - Preview { + Ok(Preview { tab_size: preview.tab_size, max_width: preview.max_width, max_height: preview.max_height, @@ -69,14 +77,6 @@ impl Default for Preview { ueberzug_scale: preview.ueberzug_scale, ueberzug_offset: preview.ueberzug_offset, - } - } -} - -impl Preview { - #[inline] - pub fn tmpfile(&self, prefix: &str) -> PathBuf { - let nanos = SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_nanos(); - self.cache_dir.join(format!("{prefix}-{}", nanos / 1000)) + }) } } diff --git a/yazi-config/src/tasks/tasks.rs b/yazi-config/src/tasks/tasks.rs index 21900dfe..e922d359 100644 --- a/yazi-config/src/tasks/tasks.rs +++ b/yazi-config/src/tasks/tasks.rs @@ -1,8 +1,8 @@ +use std::str::FromStr; + use serde::Deserialize; use validator::Validate; -use crate::{validation::check_validation, MERGED_YAZI}; - #[derive(Debug, Deserialize, Validate)] pub struct Tasks { #[validate(range(min = 1, message = "Cannot be less than 1"))] @@ -18,16 +18,18 @@ pub struct Tasks { pub suppress_preload: bool, } -impl Default for Tasks { - fn default() -> Self { +impl FromStr for Tasks { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { #[derive(Deserialize)] struct Outer { tasks: Tasks, } - let tasks = toml::from_str::(&MERGED_YAZI).unwrap().tasks; - check_validation(tasks.validate()); + let tasks = toml::from_str::(s)?.tasks; + tasks.validate()?; - tasks + Ok(tasks) } } diff --git a/yazi-config/src/theme/icon.rs b/yazi-config/src/theme/icon.rs deleted file mode 100644 index 05f20552..00000000 --- a/yazi-config/src/theme/icon.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::ops::Deref; - -use serde::{Deserialize, Deserializer}; -use yazi_shared::{fs::File, theme::{Color, StyleShadow}}; - -use crate::{preset::Preset, theme::Is, Pattern}; - -pub struct Icon { - is: Is, - name: Pattern, - inner: yazi_shared::theme::Icon, -} - -impl Deref for Icon { - type Target = yazi_shared::theme::Icon; - - fn deref(&self) -> &Self::Target { &self.inner } -} - -impl Icon { - pub fn matches(&self, file: &File) -> bool { - if !self.is.check(&file.cha) { - return false; - } - - self.name.match_path(&file.url, file.is_dir()) - } -} - -impl Icon { - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct IconOuter { - rules: Vec, - #[serde(default)] - prepend_rules: Vec, - #[serde(default)] - append_rules: Vec, - } - #[derive(Deserialize)] - struct IconRule { - #[serde(default)] - is: Is, - name: Pattern, - text: String, - - fg: Option, - } - - let mut outer = IconOuter::deserialize(deserializer)?; - if outer.append_rules.iter().any(|r| r.name.any_file()) { - outer.rules.retain(|r| !r.name.any_file()); - } - if outer.append_rules.iter().any(|r| r.name.any_dir()) { - outer.rules.retain(|r| !r.name.any_dir()); - } - - Preset::mix(&mut outer.rules, outer.prepend_rules, outer.append_rules); - - Ok( - outer - .rules - .into_iter() - .map(|r| Icon { - is: r.is, - name: r.name, - inner: yazi_shared::theme::Icon { - text: r.text, - style: StyleShadow { fg: r.fg, ..Default::default() }.into(), - }, - }) - .collect(), - ) - } -} diff --git a/yazi-config/src/theme/icons.rs b/yazi-config/src/theme/icons.rs new file mode 100644 index 00000000..04c1e81b --- /dev/null +++ b/yazi-config/src/theme/icons.rs @@ -0,0 +1,169 @@ +use std::collections::HashMap; + +use anyhow::Result; +use serde::{Deserialize, Deserializer}; +use yazi_shared::{fs::File, theme::{Color, Icon, Style}, Condition}; + +use crate::{Pattern, Preset}; + +pub struct Icons { + globs: Vec<(Pattern, Icon)>, + dirs: HashMap, + files: HashMap, + exts: HashMap, + conds: Vec<(Condition, Icon)>, +} + +impl Icons { + pub fn matches(&self, file: &File) -> Option<&Icon> { + if let Some(i) = self.match_by_glob(file) { + return Some(i); + } + + if let Some(i) = self.match_by_name(file) { + return Some(i); + } + + let f = |s: &str| match s { + "dir" => file.is_dir(), + "hidden" => file.is_hidden(), + "link" => file.is_link(), + "orphan" => file.is_orphan(), + "block" => file.is_block(), + "char" => file.is_char(), + "fifo" => file.is_fifo(), + "sock" => file.is_sock(), + "exec" => file.is_exec(), + "sticky" => file.is_sticky(), + _ => false, + }; + self.conds.iter().find(|(c, _)| c.eval(f) == Some(true)).map(|(_, i)| i) + } + + #[inline] + fn match_by_glob(&self, file: &File) -> Option<&Icon> { + self.globs.iter().find(|(p, _)| p.match_path(&file.url, file.is_dir())).map(|(_, i)| i) + } + + #[inline] + fn match_by_name(&self, file: &File) -> Option<&Icon> { + let name = file.name()?.to_str()?; + if file.is_dir() { + self.dirs.get(name).or_else(|| self.dirs.get(&name.to_ascii_lowercase())) + } else { + self + .files + .get(name) + .or_else(|| self.files.get(&name.to_ascii_lowercase())) + .or_else(|| self.exts.get(file.url.extension()?.to_str()?)) + } + } +} + +impl<'de> Deserialize<'de> for Icons { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + pub struct Shadow { + globs: Vec, + #[serde(default)] + prepend_globs: Vec, + #[serde(default)] + append_globs: Vec, + + dirs: Vec, + #[serde(default)] + prepend_dirs: Vec, + #[serde(default)] + append_dirs: Vec, + + files: Vec, + #[serde(default)] + prepend_files: Vec, + #[serde(default)] + append_files: Vec, + + exts: Vec, + #[serde(default)] + prepend_exts: Vec, + #[serde(default)] + append_exts: Vec, + + conds: Vec, + #[serde(default)] + prepend_conds: Vec, + #[serde(default)] + append_conds: Vec, + } + #[derive(Deserialize)] + pub struct ShadowPat { + name: Pattern, + text: String, + fg_dark: Option, + #[allow(dead_code)] + fg_light: Option, + } + #[derive(Deserialize)] + pub struct ShadowStr { + name: String, + text: String, + fg_dark: Option, + #[allow(dead_code)] + fg_light: Option, + } + #[derive(Deserialize)] + pub struct ShadowCond { + #[serde(rename = "if")] + if_: Condition, + text: String, + fg_dark: Option, + #[allow(dead_code)] + fg_light: Option, + } + + let mut shadow = Shadow::deserialize(deserializer)?; + Preset::mix(&mut shadow.globs, shadow.prepend_globs, shadow.append_globs); + Preset::mix(&mut shadow.dirs, shadow.prepend_dirs, shadow.append_dirs); + Preset::mix(&mut shadow.files, shadow.prepend_files, shadow.append_files); + Preset::mix(&mut shadow.exts, shadow.prepend_exts, shadow.append_exts); + Preset::mix(&mut shadow.conds, shadow.prepend_conds, shadow.append_conds); + + let globs = shadow + .globs + .into_iter() + .map(|v| { + (v.name, Icon { text: v.text, style: Style { fg: v.fg_dark, ..Default::default() } }) + }) + .collect(); + + let conds = shadow + .conds + .into_iter() + .map(|v| { + (v.if_, Icon { text: v.text, style: Style { fg: v.fg_dark, ..Default::default() } }) + }) + .collect(); + + fn as_map(v: Vec) -> HashMap { + let mut map = HashMap::with_capacity(v.len()); + for item in v { + map.entry(item.name).or_insert(Icon { + text: item.text, + style: Style { fg: item.fg_dark, ..Default::default() }, + }); + } + map.shrink_to_fit(); + map + } + + Ok(Self { + globs, + dirs: as_map(shadow.dirs), + files: as_map(shadow.files), + exts: as_map(shadow.exts), + conds, + }) + } +} diff --git a/yazi-config/src/theme/is.rs b/yazi-config/src/theme/is.rs index 994d620b..89a9ad7f 100644 --- a/yazi-config/src/theme/is.rs +++ b/yazi-config/src/theme/is.rs @@ -48,13 +48,13 @@ impl Is { pub fn check(&self, cha: &Cha) -> bool { match self { Self::None => true, - Self::Block => cha.is_block_device(), - Self::Char => cha.is_char_device(), + Self::Block => cha.is_block(), + Self::Char => cha.is_char(), Self::Exec => cha.is_exec(), Self::Fifo => cha.is_fifo(), Self::Link => cha.is_link(), Self::Orphan => cha.is_orphan(), - Self::Sock => cha.is_socket(), + Self::Sock => cha.is_sock(), Self::Sticky => cha.is_sticky(), } } diff --git a/yazi-config/src/theme/mod.rs b/yazi-config/src/theme/mod.rs index 28051c97..c914fef5 100644 --- a/yazi-config/src/theme/mod.rs +++ b/yazi-config/src/theme/mod.rs @@ -1,11 +1,11 @@ mod filetype; mod flavor; -mod icon; +mod icons; mod is; mod theme; pub use filetype::*; pub use flavor::*; -pub use icon::*; +pub use icons::*; pub use is::*; pub use theme::*; diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index 00508ded..da757143 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -1,11 +1,10 @@ -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; use serde::{Deserialize, Serialize}; use validator::Validate; use yazi_shared::{fs::expand_path, theme::Style, Xdg}; -use super::{Filetype, Flavor, Icon}; -use crate::{validation::check_validation, MERGED_THEME}; +use super::{Filetype, Flavor, Icons}; #[derive(Deserialize, Serialize)] pub struct Theme { @@ -23,16 +22,17 @@ pub struct Theme { // File-specific styles #[serde(rename = "filetype", deserialize_with = "Filetype::deserialize", skip_serializing)] pub filetypes: Vec, - #[serde(rename = "icon", deserialize_with = "Icon::deserialize", skip_serializing)] - pub icons: Vec, + #[serde(rename = "icon", skip_serializing)] + pub icons: Icons, } -impl Default for Theme { - fn default() -> Self { - let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap(); +impl FromStr for Theme { + type Err = anyhow::Error; - check_validation(theme.manager.validate()); - check_validation(theme.which.validate()); + fn from_str(s: &str) -> Result { + let mut theme: Self = toml::from_str(s)?; + theme.manager.validate()?; + theme.which.validate()?; if theme.flavor.use_.is_empty() { theme.manager.syntect_theme = expand_path(&theme.manager.syntect_theme); @@ -41,7 +41,7 @@ impl Default for Theme { Xdg::config_dir().join(format!("flavors/{}.yazi/tmtheme.xml", theme.flavor.use_)); } - theme + Ok(theme) } } diff --git a/yazi-config/src/validation.rs b/yazi-config/src/validation.rs deleted file mode 100644 index b71e150a..00000000 --- a/yazi-config/src/validation.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::{borrow::Cow, process}; - -use validator::{ValidationErrors, ValidationErrorsKind}; - -pub fn check_validation(res: Result<(), ValidationErrors>) { - let Err(errors) = res else { return }; - - for (field, kind) in errors.into_errors() { - match kind { - ValidationErrorsKind::Struct(errors) => check_validation(Err(*errors)), - ValidationErrorsKind::List(errors) => { - for (i, errors) in errors { - eprint!("Config `{field}[{i}]` format error: "); - check_validation(Err(*errors)); - eprintln!(); - } - } - ValidationErrorsKind::Field(error) => { - for e in error { - eprintln!( - "Config `{field}` format error: {}\n", - e.message.unwrap_or(Cow::Borrowed("unknown error")) - ); - } - } - } - } - process::exit(1); -} diff --git a/yazi-config/src/which/which.rs b/yazi-config/src/which/which.rs index b8e3305c..205a2bc4 100644 --- a/yazi-config/src/which/which.rs +++ b/yazi-config/src/which/which.rs @@ -1,8 +1,9 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; use validator::Validate; use super::SortBy; -use crate::MERGED_YAZI; #[derive(Debug, Deserialize, Serialize, Validate)] pub struct Which { @@ -10,15 +11,18 @@ pub struct Which { pub sort_by: SortBy, pub sort_sensitive: bool, pub sort_reverse: bool, + pub sort_translit: bool, } -impl Default for Which { - fn default() -> Self { +impl FromStr for Which { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { #[derive(Deserialize)] struct Outer { which: Which, } - toml::from_str::(&MERGED_YAZI).unwrap().which + Ok(toml::from_str::(s)?.which) } } diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index d7dc8b00..f35bdc6a 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -9,7 +9,7 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -yazi-adaptor = { path = "../yazi-adaptor", version = "0.2.5" } +yazi-adapter = { path = "../yazi-adapter", version = "0.2.5" } yazi-boot = { path = "../yazi-boot", version = "0.2.5" } yazi-config = { path = "../yazi-config", version = "0.2.5" } yazi-dds = { path = "../yazi-dds", version = "0.2.5" } @@ -19,27 +19,25 @@ yazi-scheduler = { path = "../yazi-scheduler", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -anyhow = "1.0.82" -base64 = "0.22.0" +anyhow = "1.0.86" bitflags = "2.5.0" crossterm = "0.27.0" +dirs = "5.0.1" futures = "0.3.30" notify = { version = "6.1.1", default-features = false, features = [ "macos_fsevent" ] } -parking_lot = "0.12.1" -ratatui = "=0.26.1" -regex = "1.10.4" +parking_lot = "0.12.3" +ratatui = "0.27.0" +regex = "1.10.5" scopeguard = "1.2.0" -serde = "1.0.198" -tokio = { version = "1.37.0", features = [ "full" ] } +serde = "1.0.203" +shell-words = "1.1.0" +tokio = { version = "1.38.0", features = [ "full" ] } tokio-stream = "0.1.15" -tokio-util = "0.7.10" -unicode-width = "0.1.11" +tokio-util = "0.7.11" +unicode-width = "0.1.13" # Logging tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } [target."cfg(unix)".dependencies] -libc = "0.2.153" - -[target."cfg(windows)".dependencies] -clipboard-win = "5.3.1" +libc = "0.2.155" diff --git a/yazi-core/src/completion/commands/close.rs b/yazi-core/src/completion/commands/close.rs index d331978b..447e9ef7 100644 --- a/yazi-core/src/completion/commands/close.rs +++ b/yazi-core/src/completion/commands/close.rs @@ -10,6 +10,9 @@ pub struct Opt { impl From for Opt { fn from(c: Cmd) -> Self { Self { submit: c.bool("submit") } } } +impl From for Opt { + fn from(submit: bool) -> Self { Self { submit } } +} impl Completion { pub fn close(&mut self, opt: impl Into) { diff --git a/yazi-core/src/completion/commands/trigger.rs b/yazi-core/src/completion/commands/trigger.rs index 535fc3c7..788d5555 100644 --- a/yazi-core/src/completion/commands/trigger.rs +++ b/yazi-core/src/completion/commands/trigger.rs @@ -1,4 +1,4 @@ -use std::{mem, path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR}}; +use std::{borrow::Cow, mem, path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR}}; use tokio::fs; use yazi_shared::{emit, event::{Cmd, Data}, render, Layer}; @@ -33,7 +33,9 @@ impl Completion { } self.ticket = opt.ticket; - let (parent, child) = Self::split_path(&opt.word); + let Some((parent, child)) = Self::split_path(&opt.word) else { + return self.close(false); + }; if self.caches.contains_key(&parent) { return self.show( @@ -72,12 +74,24 @@ impl Completion { render!(mem::replace(&mut self.visible, false)); } - #[inline] - fn split_path(s: &str) -> (String, String) { - match s.rsplit_once(SEPARATOR) { - Some((p, c)) => (format!("{p}{}", MAIN_SEPARATOR), c.to_owned()), - None => (".".to_owned(), s.to_owned()), + fn split_path(s: &str) -> Option<(String, String)> { + if s == "~" { + return None; // We don't autocomplete a `~`, but `~/` } + + let s = if let Some(rest) = s.strip_prefix("~") { + Cow::Owned(format!( + "{}{rest}", + dirs::home_dir().unwrap_or_default().to_string_lossy().trim_end_matches(SEPARATOR), + )) + } else { + Cow::Borrowed(s) + }; + + Some(match s.rsplit_once(SEPARATOR) { + Some((p, c)) => (format!("{p}{}", MAIN_SEPARATOR), c.to_owned()), + None => (".".to_owned(), s.into_owned()), + }) } } @@ -85,28 +99,32 @@ impl Completion { mod tests { use super::*; + fn compare(s: &str, parent: &str, child: &str) -> bool { + matches!(Completion::split_path(s), Some((p, c)) if p == parent && c == child) + } + #[cfg(unix)] #[test] fn test_split() { - assert_eq!(Completion::split_path(""), (".".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path(" "), (".".to_owned(), " ".to_owned())); - assert_eq!(Completion::split_path("/"), ("/".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("//"), ("//".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("/foo"), ("/".to_owned(), "foo".to_owned())); - assert_eq!(Completion::split_path("/foo/"), ("/foo/".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("/foo/bar"), ("/foo/".to_owned(), "bar".to_owned())); + assert!(compare("", ".", "")); + assert!(compare(" ", ".", " ")); + assert!(compare("/", "/", "")); + assert!(compare("//", "//", "")); + assert!(compare("/foo", "/", "foo")); + assert!(compare("/foo/", "/foo/", "")); + assert!(compare("/foo/bar", "/foo/", "bar")); } #[cfg(windows)] #[test] fn test_split() { - assert_eq!(Completion::split_path("foo"), (".".to_owned(), "foo".to_owned())); - assert_eq!(Completion::split_path("foo\\"), ("foo\\".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("foo\\bar"), ("foo\\".to_owned(), "bar".to_owned())); - assert_eq!(Completion::split_path("foo\\bar\\"), ("foo\\bar\\".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("C:\\"), ("C:\\".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("C:\\foo"), ("C:\\".to_owned(), "foo".to_owned())); - assert_eq!(Completion::split_path("C:\\foo\\"), ("C:\\foo\\".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("C:\\foo\\bar"), ("C:\\foo\\".to_owned(), "bar".to_owned())); + assert!(compare("foo", ".", "foo")); + assert!(compare("foo\\", "foo\\", "")); + assert!(compare("foo\\bar", "foo\\", "bar")); + assert!(compare("foo\\bar\\", "foo\\bar\\", "")); + assert!(compare("C:\\", "C:\\", "")); + assert!(compare("C:\\foo", "C:\\", "foo")); + assert!(compare("C:\\foo\\", "C:\\foo\\", "")); + assert!(compare("C:\\foo\\bar", "C:\\foo\\", "bar")); } } diff --git a/yazi-core/src/folder/files.rs b/yazi-core/src/folder/files.rs index 9558fa6c..f1297a7b 100644 --- a/yazi-core/src/folder/files.rs +++ b/yazi-core/src/folder/files.rs @@ -2,7 +2,7 @@ use std::{collections::{HashMap, HashSet}, fs::Metadata, mem, ops::Deref, sync:: use tokio::{fs::{self, DirEntry}, select, sync::mpsc::{self, UnboundedReceiver}}; use yazi_config::{manager::SortBy, MANAGER}; -use yazi_shared::fs::{accessible, File, FilesOp, Url, FILES_TICKET}; +use yazi_shared::fs::{maybe_exists, File, FilesOp, Url, FILES_TICKET}; use super::{FilesSorter, Filter}; @@ -101,7 +101,7 @@ impl Files { Ok(m) if mtime == m.modified().ok() => {} Ok(m) => return Some(m), Err(e) => { - if accessible(url).await { + if maybe_exists(url).await { FilesOp::IOErr(url.clone(), e.kind()).emit(); } else if let Some(p) = url.parent_url() { FilesOp::Deleting(p, vec![url.clone()]).emit(); diff --git a/yazi-core/src/folder/filter.rs b/yazi-core/src/folder/filter.rs index 8d7ca45e..290b0067 100644 --- a/yazi-core/src/folder/filter.rs +++ b/yazi-core/src/folder/filter.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsStr, ops::Range}; +use std::{ffi::OsStr, fmt::Display, ops::Range}; use anyhow::Result; use regex::bytes::{Regex, RegexBuilder}; @@ -9,10 +9,6 @@ pub struct Filter { regex: Regex, } -impl PartialEq for Filter { - fn eq(&self, other: &Self) -> bool { self.raw == other.raw } -} - impl Filter { pub fn new(s: &str, case: FilterCase) -> Result { let regex = match case { @@ -35,6 +31,14 @@ impl Filter { } } +impl PartialEq for Filter { + fn eq(&self, other: &Self) -> bool { self.raw == other.raw } +} + +impl Display for Filter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.raw) } +} + #[derive(Default, PartialEq, Eq)] pub enum FilterCase { Smart, diff --git a/yazi-core/src/folder/sorter.rs b/yazi-core/src/folder/sorter.rs index 5130ac68..eb7079b7 100644 --- a/yazi-core/src/folder/sorter.rs +++ b/yazi-core/src/folder/sorter.rs @@ -1,7 +1,7 @@ use std::{cmp::Ordering, collections::HashMap, mem}; use yazi_config::manager::SortBy; -use yazi_shared::{fs::{File, Url}, natsort}; +use yazi_shared::{fs::{File, Url}, natsort, Transliterator}; #[derive(Clone, Copy, Default, PartialEq)] pub struct FilesSorter { @@ -9,6 +9,7 @@ pub struct FilesSorter { pub sensitive: bool, pub reverse: bool, pub dir_first: bool, + pub translit: bool, } impl FilesSorter { @@ -76,7 +77,16 @@ impl FilesSorter { return promote; } - let ordering = natsort(entities[a], entities[b], !self.sensitive); + let ordering = if !self.translit { + natsort(entities[a], entities[b], !self.sensitive) + } else { + natsort( + entities[a].transliterate().as_bytes(), + entities[b].transliterate().as_bytes(), + !self.sensitive, + ) + }; + if self.reverse { ordering.reverse() } else { ordering } }); diff --git a/yazi-core/src/help/help.rs b/yazi-core/src/help/help.rs index f2898945..cde803c4 100644 --- a/yazi-core/src/help/help.rs +++ b/yazi-core/src/help/help.rs @@ -1,7 +1,8 @@ use crossterm::event::KeyCode; use unicode_width::UnicodeWidthStr; +use yazi_adapter::Dimension; use yazi_config::{keymap::{Control, Key}, KEYMAP}; -use yazi_shared::{render, render_and, term::Term, Layer}; +use yazi_shared::{render, render_and, Layer}; use super::HELP_MARGIN; use crate::input::Input; @@ -22,7 +23,7 @@ pub struct Help { impl Help { #[inline] - pub fn limit() -> usize { Term::size().rows.saturating_sub(HELP_MARGIN) as usize } + pub fn limit() -> usize { Dimension::available().rows.saturating_sub(HELP_MARGIN) as usize } pub fn toggle(&mut self, layer: Layer) { self.visible = !self.visible; @@ -43,15 +44,15 @@ impl Help { }; match key { - Key { code: KeyCode::Esc, shift: false, ctrl: false, alt: false } => { + Key { code: KeyCode::Esc, shift: false, ctrl: false, alt: false, super_: false } => { self.in_filter = None; render!(); } - Key { code: KeyCode::Enter, shift: false, ctrl: false, alt: false } => { + Key { code: KeyCode::Enter, shift: false, ctrl: false, alt: false, super_: false } => { self.in_filter = None; return render_and!(true); // Don't do the `filter_apply` below, since we already have the filtered results. } - Key { code: KeyCode::Backspace, shift: false, ctrl: false, alt: false } => { + Key { code: KeyCode::Backspace, shift: false, ctrl: false, alt: false, super_: false } => { input.backspace(false); } _ => { @@ -106,7 +107,7 @@ impl Help { return None; } if let Some(kw) = self.keyword() { - return Some((kw.width() as u16, Term::size().rows)); + return Some((kw.width() as u16, Dimension::available().rows)); } None } diff --git a/yazi-core/src/input/commands/kill.rs b/yazi-core/src/input/commands/kill.rs index 754f04de..d1cbeb07 100644 --- a/yazi-core/src/input/commands/kill.rs +++ b/yazi-core/src/input/commands/kill.rs @@ -70,6 +70,7 @@ impl Input { let snap = self.snap_mut(); match opt.kind.as_str() { + "all" => self.kill_range(..), "bol" => { let end = snap.idx(snap.cursor).unwrap_or(snap.len()); self.kill_range(..end) diff --git a/yazi-core/src/input/commands/paste.rs b/yazi-core/src/input/commands/paste.rs index 4cd88758..2c313ea4 100644 --- a/yazi-core/src/input/commands/paste.rs +++ b/yazi-core/src/input/commands/paste.rs @@ -1,6 +1,7 @@ +use yazi_plugin::CLIPBOARD; use yazi_shared::{event::Cmd, render}; -use crate::{input::{op::InputOp, Input}, CLIPBOARD}; +use crate::input::{op::InputOp, Input}; pub struct Opt { before: bool, diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 99e9d27d..3fea5764 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -3,10 +3,10 @@ use std::ops::Range; use tokio::sync::mpsc::UnboundedSender; use unicode_width::UnicodeWidthStr; use yazi_config::{popup::Position, INPUT}; +use yazi_plugin::CLIPBOARD; use yazi_shared::{render, InputError}; use super::{mode::InputMode, op::InputOp, InputSnap, InputSnaps}; -use crate::CLIPBOARD; #[derive(Default)] pub struct Input { @@ -87,16 +87,17 @@ impl Input { } pub(super) fn flush_value(&mut self) { + let Some(tx) = &self.callback else { return }; self.ticket = self.ticket.wrapping_add(1); if self.realtime { let value = self.snap().value.clone(); - self.callback.as_ref().unwrap().send(Err(InputError::Typed(value))).ok(); + tx.send(Err(InputError::Typed(value))).ok(); } if self.completion { let before = self.partition()[0].to_owned(); - self.callback.as_ref().unwrap().send(Err(InputError::Completed(before, self.ticket))).ok(); + tx.send(Err(InputError::Completed(before, self.ticket))).ok(); } } } diff --git a/yazi-core/src/input/snaps.rs b/yazi-core/src/input/snaps.rs index 14b8b444..b1960bcf 100644 --- a/yazi-core/src/input/snaps.rs +++ b/yazi-core/src/input/snaps.rs @@ -19,6 +19,10 @@ impl InputSnaps { } pub(super) fn tag(&mut self, limit: usize) -> bool { + if self.versions.len() <= self.idx { + return false; + } + // Sync *current* cursor position to the *last* version: // Save offset/cursor/ect. of the *current* as the last version, // while keeping the *last* value unchanged. @@ -49,7 +53,7 @@ impl InputSnaps { } pub(super) fn redo(&mut self) -> bool { - if self.idx + 1 == self.versions.len() { + if self.idx + 1 >= self.versions.len() { return false; } diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 43ab4f08..9850ca81 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -6,7 +6,6 @@ clippy::unit_arg )] -mod clipboard; pub mod completion; pub mod folder; pub mod help; @@ -19,12 +18,9 @@ pub mod tab; pub mod tasks; pub mod which; -pub use clipboard::*; pub use step::*; pub fn init() { - CLIPBOARD.with(Default::default); - manager::WATCHED.with(Default::default); manager::LINKED.with(Default::default); } diff --git a/yazi-core/src/manager/commands/bulk_rename.rs b/yazi-core/src/manager/commands/bulk_rename.rs index e3b65244..e10b48df 100644 --- a/yazi-core/src/manager/commands/bulk_rename.rs +++ b/yazi-core/src/manager/commands/bulk_rename.rs @@ -4,8 +4,9 @@ use anyhow::{anyhow, Result}; use scopeguard::defer; use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; use yazi_config::{OPEN, PREVIEW}; +use yazi_dds::Pubsub; use yazi_proxy::{AppProxy, TasksProxy, HIDER, WATCHER}; -use yazi_shared::{fs::{accessible, max_common_root, File, FilesOp, Url}, term::Term}; +use yazi_shared::{fs::{max_common_root, maybe_exists, paths_to_same_file, File, FilesOp, Url}, terminal_clear}; use crate::manager::Manager; @@ -51,7 +52,7 @@ impl Manager { old: Vec, new: Vec, ) -> Result<()> { - Term::clear(&mut stderr())?; + terminal_clear(&mut stderr())?; if old.len() != new.len() { eprintln!("Number of old and new differ, press ENTER to exit"); stdin().read_exact(&mut [0]).await?; @@ -83,7 +84,7 @@ impl Manager { for (o, n) in todo { let (old, new) = (root.join(&o), root.join(&n)); - if accessible(&new).await { + if maybe_exists(&new).await && !paths_to_same_file(&old, &new).await { failed.push((o, n, anyhow!("Destination already exists"))); } else if let Err(e) = fs::rename(&old, &new).await { failed.push((o, n, e.into())); @@ -95,6 +96,7 @@ impl Manager { } if !succeeded.is_empty() { + Pubsub::pub_from_bulk(succeeded.iter().map(|(u, f)| (u, &f.url)).collect()); FilesOp::Upserting(cwd, succeeded).emit(); } drop(permit); @@ -106,7 +108,7 @@ impl Manager { } async fn output_failed(failed: Vec<(PathBuf, PathBuf, anyhow::Error)>) -> Result<()> { - Term::clear(&mut stderr())?; + terminal_clear(&mut stderr())?; { let mut stderr = BufWriter::new(stderr().lock()); diff --git a/yazi-core/src/manager/commands/create.rs b/yazi-core/src/manager/commands/create.rs index 6e41386a..351b40b5 100644 --- a/yazi-core/src/manager/commands/create.rs +++ b/yazi-core/src/manager/commands/create.rs @@ -1,9 +1,10 @@ -use std::path::PathBuf; +use std::collections::HashMap; +use anyhow::Result; use tokio::fs; use yazi_config::popup::InputCfg; -use yazi_proxy::{InputProxy, ManagerProxy}; -use yazi_shared::{event::Cmd, fs::{accessible, File, FilesOp, Url}}; +use yazi_proxy::{InputProxy, TabProxy, WATCHER}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, symlink_realpath, File, FilesOp, Url}}; use crate::manager::Manager; @@ -24,29 +25,42 @@ impl Manager { let Some(Ok(name)) = result.recv().await else { return Ok(()); }; + if name.is_empty() { + return Ok(()); + } - let path = cwd.join(&name); - if !opt.force && accessible(&path).await { + let new = cwd.join(&name); + if !opt.force && maybe_exists(&new).await { match InputProxy::show(InputCfg::overwrite()).recv().await { Some(Ok(c)) if c == "y" || c == "Y" => (), _ => return Ok(()), } } - if name.ends_with('/') || name.ends_with('\\') { - fs::create_dir_all(&path).await?; - } else { - fs::create_dir_all(&path.parent().unwrap()).await.ok(); - fs::File::create(&path).await?; - } - - let child = - Url::from(path.components().take(cwd.components().count() + 1).collect::()); - if let Ok(f) = File::from(child.clone()).await { - FilesOp::Creating(cwd, vec![f]).emit(); - ManagerProxy::hover(Some(child)); - } - Ok::<(), anyhow::Error>(()) + Self::create_do(new, name.ends_with('/') || name.ends_with('\\')).await }); } + + async fn create_do(new: Url, dir: bool) -> Result<()> { + let Some(parent) = new.parent_url() else { return Ok(()) }; + let _permit = WATCHER.acquire().await.unwrap(); + + if dir { + fs::create_dir_all(&new).await?; + } else if let Ok(real) = symlink_realpath(&new).await { + ok_or_not_found(fs::remove_file(&new).await)?; + FilesOp::Deleting(parent.clone(), vec![Url::from(real)]).emit(); + fs::File::create(&new).await?; + } else { + fs::create_dir_all(&parent).await.ok(); + ok_or_not_found(fs::remove_file(&new).await)?; + fs::File::create(&new).await?; + } + + if let Ok(f) = File::from(new.clone()).await { + FilesOp::Upserting(parent, HashMap::from_iter([(f.url(), f)])).emit(); + TabProxy::reveal(&new) + } + Ok(()) + } } diff --git a/yazi-core/src/manager/commands/hardlink.rs b/yazi-core/src/manager/commands/hardlink.rs new file mode 100644 index 00000000..c215deeb --- /dev/null +++ b/yazi-core/src/manager/commands/hardlink.rs @@ -0,0 +1,23 @@ +use yazi_shared::event::Cmd; + +use crate::{manager::Manager, tasks::Tasks}; + +pub struct Opt { + force: bool, + follow: bool, +} + +impl From for Opt { + fn from(c: Cmd) -> Self { Self { force: c.bool("force"), follow: c.bool("follow") } } +} + +impl Manager { + pub fn hardlink(&mut self, opt: impl Into, tasks: &Tasks) { + if self.yanked.cut { + return; + } + + let opt = opt.into() as Opt; + tasks.file_hardlink(&self.yanked, self.cwd(), opt.force, opt.follow); + } +} diff --git a/yazi-core/src/manager/commands/hover.rs b/yazi-core/src/manager/commands/hover.rs index de934d69..78b9c738 100644 --- a/yazi-core/src/manager/commands/hover.rs +++ b/yazi-core/src/manager/commands/hover.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use yazi_dds::Pubsub; use yazi_shared::{event::{Cmd, Data}, fs::Url, render}; use crate::manager::Manager; @@ -42,5 +43,8 @@ impl Manager { } } self.watcher.watch(to_watch); + + // Publish through DDS + Pubsub::pub_from_hover(self.active().idx, self.hovered().map(|h| &h.url)); } } diff --git a/yazi-core/src/manager/commands/mod.rs b/yazi-core/src/manager/commands/mod.rs index 9827366f..d690ce0c 100644 --- a/yazi-core/src/manager/commands/mod.rs +++ b/yazi-core/src/manager/commands/mod.rs @@ -1,6 +1,7 @@ mod bulk_rename; mod close; mod create; +mod hardlink; mod hover; mod link; mod open; diff --git a/yazi-core/src/manager/commands/open.rs b/yazi-core/src/manager/commands/open.rs index 4e3d6b3e..1b36edc1 100644 --- a/yazi-core/src/manager/commands/open.rs +++ b/yazi-core/src/manager/commands/open.rs @@ -63,8 +63,8 @@ impl Manager { } done.extend(files.iter().map(|f| (f.url(), String::new()))); - if let Err(e) = isolate::preload("mime", files, true).await { - error!("preload in open failed: {e}"); + if let Err(e) = isolate::fetch("mime", files).await { + error!("Fetch `mime` failed in opening: {e}"); } ManagerProxy::open_do(OpenDoOpt { hovered, targets: done, interactive: opt.interactive }); diff --git a/yazi-core/src/manager/commands/quit.rs b/yazi-core/src/manager/commands/quit.rs index 04d5be98..d2691f2f 100644 --- a/yazi-core/src/manager/commands/quit.rs +++ b/yazi-core/src/manager/commands/quit.rs @@ -1,3 +1,6 @@ +use std::time::Duration; + +use tokio::{select, time}; use yazi_config::popup::InputCfg; use yazi_proxy::InputProxy; use yazi_shared::{emit, event::{Cmd, EventQuit}}; @@ -19,14 +22,36 @@ impl Manager { pub fn quit(&self, opt: impl Into, tasks: &Tasks) { let opt = EventQuit { no_cwd_file: opt.into().no_cwd_file, ..Default::default() }; - let tasks = tasks.len(); - if tasks == 0 { + let ongoing = tasks.ongoing().clone(); + let left = ongoing.lock().len(); + + if left == 0 { emit!(Quit(opt)); return; } tokio::spawn(async move { - let mut result = InputProxy::show(InputCfg::quit(tasks)); + let mut i = 0; + let mut result = InputProxy::show(InputCfg::quit(left)); + loop { + select! { + _ = time::sleep(Duration::from_millis(100)) => { + i += 1; + if i > 30 { break } + else if ongoing.lock().len() == 0 { + emit!(Quit(opt)); + return; + } + } + choice = result.recv() => { + if matches!(choice, Some(Ok(s)) if s == "y" || s == "Y") { + emit!(Quit(opt)); + } + return; + } + } + } + if let Some(Ok(choice)) = result.recv().await { if choice == "y" || choice == "Y" { emit!(Quit(opt)); diff --git a/yazi-core/src/manager/commands/refresh.rs b/yazi-core/src/manager/commands/refresh.rs index f9a1f88c..8076c53e 100644 --- a/yazi-core/src/manager/commands/refresh.rs +++ b/yazi-core/src/manager/commands/refresh.rs @@ -1,4 +1,4 @@ -use std::env; +use std::{env, path::MAIN_SEPARATOR}; use crossterm::{execute, terminal::SetTitle}; use yazi_shared::event::Cmd; @@ -7,13 +7,9 @@ use crate::{manager::Manager, tasks::Tasks}; impl Manager { fn title(&self) -> String { - #[cfg(unix)] - let home = env::var_os("HOME").unwrap_or_default(); - #[cfg(windows)] - let home = env::var_os("USERPROFILE").unwrap_or_default(); - + let home = dirs::home_dir().unwrap_or_default(); if let Some(p) = self.cwd().strip_prefix(home) { - format!("Yazi: ~/{}", p.display()) + format!("Yazi: ~{}{}", MAIN_SEPARATOR, p.display()) } else { format!("Yazi: {}", self.cwd().display()) } @@ -35,6 +31,6 @@ impl Manager { self.hover(None); self.update_paged((), tasks); - tasks.preload_sorted(&self.current().files); + tasks.prework_sorted(&self.current().files); } } diff --git a/yazi-core/src/manager/commands/remove.rs b/yazi-core/src/manager/commands/remove.rs index 8be5b536..e67c135d 100644 --- a/yazi-core/src/manager/commands/remove.rs +++ b/yazi-core/src/manager/commands/remove.rs @@ -7,6 +7,7 @@ use crate::{manager::Manager, tasks::Tasks}; pub struct Opt { force: bool, permanently: bool, + hovered: bool, targets: Vec, } @@ -15,6 +16,7 @@ impl From for Opt { Self { force: c.bool("force"), permanently: c.bool("permanently"), + hovered: c.bool("hovered"), targets: c.take_any("targets").unwrap_or_default(), } } @@ -25,9 +27,16 @@ impl Manager { if !self.active_mut().try_escape_visual() { return; } + let Some(hovered) = self.hovered().map(|h| &h.url) else { + return; + }; let mut opt = opt.into() as Opt; - opt.targets = self.selected_or_hovered(false).cloned().collect(); + opt.targets = if opt.hovered { + vec![hovered.clone()] + } else { + self.selected_or_hovered(false).cloned().collect() + }; if opt.force { return self.remove_do(opt, tasks); diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index d4d7d491..adebe7d4 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -4,23 +4,25 @@ use anyhow::Result; use tokio::fs; use yazi_config::popup::InputCfg; use yazi_dds::Pubsub; -use yazi_proxy::{InputProxy, ManagerProxy, WATCHER}; -use yazi_shared::{event::Cmd, fs::{accessible, File, FilesOp, Url}}; +use yazi_proxy::{InputProxy, TabProxy, WATCHER}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, paths_to_same_file, symlink_realpath, File, FilesOp, Url}}; use crate::manager::Manager; pub struct Opt { - force: bool, - empty: String, - cursor: String, + hovered: bool, + force: bool, + empty: String, + cursor: String, } impl From for Opt { fn from(mut c: Cmd) -> Self { Self { - force: c.bool("force"), - empty: c.take_str("empty").unwrap_or_default(), - cursor: c.take_str("cursor").unwrap_or_default(), + hovered: c.bool("hovered"), + force: c.bool("force"), + empty: c.take_str("empty").unwrap_or_default(), + cursor: c.take_str("cursor").unwrap_or_default(), } } } @@ -29,15 +31,16 @@ impl Manager { pub fn rename(&mut self, opt: impl Into) { if !self.active_mut().try_escape_visual() { return; - } else if !self.active().selected.is_empty() { - return self.bulk_rename(); } - let Some(hovered) = self.hovered().map(|h| h.url()) else { return; }; let opt = opt.into() as Opt; + if !opt.hovered && !self.active().selected.is_empty() { + return self.bulk_rename(); + } + let name = Self::empty_url_part(&hovered, &opt.empty); let cursor = match opt.cursor.as_str() { "start" => Some(0), @@ -62,7 +65,7 @@ impl Manager { } let new = hovered.parent().unwrap().join(name); - if opt.force || !accessible(&new).await { + if opt.force || !maybe_exists(&new).await || paths_to_same_file(&hovered, &new).await { Self::rename_do(tab, hovered, Url::from(new)).await.ok(); return; } @@ -77,19 +80,23 @@ impl Manager { } async fn rename_do(tab: usize, old: Url, new: Url) -> Result<()> { + let Some(p_old) = old.parent_url() else { return Ok(()) }; + let Some(p_new) = new.parent_url() else { return Ok(()) }; let _permit = WATCHER.acquire().await.unwrap(); + let overwritten = symlink_realpath(&new).await; fs::rename(&old, &new).await?; - if old.parent() != new.parent() { - return Ok(()); - } - let file = File::from(new.clone()).await?; + if let Ok(p) = overwritten { + ok_or_not_found(fs::rename(&p, &new).await)?; + FilesOp::Deleting(p_new.clone(), vec![Url::from(p)]).emit(); + } Pubsub::pub_from_rename(tab, &old, &new); - FilesOp::Deleting(file.parent().unwrap(), vec![new.clone()]).emit(); - FilesOp::Upserting(file.parent().unwrap(), HashMap::from_iter([(old, file)])).emit(); - Ok(ManagerProxy::hover(Some(new))) + let file = File::from(new.clone()).await?; + FilesOp::Deleting(p_old, vec![old]).emit(); + FilesOp::Upserting(p_new, HashMap::from_iter([(new.clone(), file)])).emit(); + Ok(TabProxy::reveal(&new)) } fn empty_url_part(url: &Url, by: &str) -> String { diff --git a/yazi-core/src/manager/commands/seek.rs b/yazi-core/src/manager/commands/seek.rs index a80c57c2..5beb7a17 100644 --- a/yazi-core/src/manager/commands/seek.rs +++ b/yazi-core/src/manager/commands/seek.rs @@ -32,6 +32,6 @@ impl Manager { }; let opt = opt.into() as Opt; - isolate::seek_sync(&previewer.cmd, hovered.clone(), opt.units); + isolate::seek_sync(&previewer.run, hovered.clone(), opt.units); } } diff --git a/yazi-core/src/manager/commands/update_files.rs b/yazi-core/src/manager/commands/update_files.rs index e393a7b1..10a73c8a 100644 --- a/yazi-core/src/manager/commands/update_files.rs +++ b/yazi-core/src/manager/commands/update_files.rs @@ -88,7 +88,7 @@ impl Manager { ManagerProxy::hover(None); // Re-hover in next loop ManagerProxy::update_paged(); // Update for paged files in next loop if calc { - tasks.preload_sorted(&tab.current.files); + tasks.prework_sorted(&tab.current.files); } } @@ -111,9 +111,10 @@ impl Manager { |(p, pp)| matches!(*op, FilesOp::Deleting(ref parent, ref urls) if *parent == pp && urls.contains(p)), ); - if let Some(f) = tab.history.get_mut(op.url()) { - let hovered = f.hovered().filter(|_| f.tracing).map(|h| h.url()); - _ = f.update(op.into_owned()) && f.repos(hovered); + let folder = tab.history.entry(op.url().clone()).or_insert_with(|| Folder::from(op.url())); + let hovered = folder.hovered().filter(|_| folder.tracing).map(|h| h.url()); + if folder.update(op.into_owned()) { + folder.repos(hovered); } if leave { diff --git a/yazi-core/src/manager/commands/update_mimetype.rs b/yazi-core/src/manager/commands/update_mimetype.rs index e9ca16ca..15f24e0b 100644 --- a/yazi-core/src/manager/commands/update_mimetype.rs +++ b/yazi-core/src/manager/commands/update_mimetype.rs @@ -52,7 +52,7 @@ impl Manager { self.mimetype.extend(updates); self.peek(false); - tasks.preload_affected(&affected, &self.mimetype); + tasks.prework_affected(&affected, &self.mimetype); render!(); } } diff --git a/yazi-core/src/manager/commands/update_paged.rs b/yazi-core/src/manager/commands/update_paged.rs index d9d963b4..6c28791b 100644 --- a/yazi-core/src/manager/commands/update_paged.rs +++ b/yazi-core/src/manager/commands/update_paged.rs @@ -32,6 +32,7 @@ impl Manager { } let targets = self.current().paginate(opt.page.unwrap_or(self.current().page)); + tasks.fetch_paged(targets, &self.mimetype); tasks.preload_paged(targets, &self.mimetype); } } diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index 8d4ffa08..53449f3a 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -1,14 +1,14 @@ use std::{collections::{HashMap, HashSet}, time::{Duration, SystemTime}}; use anyhow::Result; -use notify::{event::{MetadataKind, ModifyKind}, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _Watcher}; +use notify::{RecommendedWatcher, RecursiveMode, Watcher as _Watcher}; use parking_lot::RwLock; use tokio::{fs, pin, sync::{mpsc::{self, UnboundedReceiver}, watch}}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tracing::error; use yazi_plugin::isolate; use yazi_proxy::WATCHER; -use yazi_shared::{fs::{File, FilesOp, Url}, RoCell}; +use yazi_shared::{fs::{symlink_realname, File, FilesOp, Url}, RoCell}; use super::Linked; use crate::folder::{Files, Folder}; @@ -28,24 +28,6 @@ impl Watcher { let watcher = RecommendedWatcher::new( move |res: Result| { let Ok(event) = res else { return }; - - match event.kind { - EventKind::Create(_) => {} - EventKind::Modify(kind) => match kind { - ModifyKind::Data(_) => {} - ModifyKind::Metadata(md) => match md { - MetadataKind::WriteTime => {} - MetadataKind::Permissions => {} - MetadataKind::Ownership => {} - _ => return, - }, - ModifyKind::Name(_) => {} - _ => return, - }, - EventKind::Remove(_) => {} - _ => return, - } - for path in event.paths { out_tx.send(Url::from(path)).ok(); } @@ -53,8 +35,8 @@ impl Watcher { Default::default(), ); - tokio::spawn(Self::on_in(in_rx, watcher.unwrap())); - tokio::spawn(Self::on_out(out_rx)); + tokio::spawn(Self::fan_in(in_rx, watcher.unwrap())); + tokio::spawn(Self::fan_out(out_rx)); Self { tx: in_tx } } @@ -83,7 +65,7 @@ impl Watcher { }); } - async fn on_in(mut rx: watch::Receiver>, mut watcher: RecommendedWatcher) { + async fn fan_in(mut rx: watch::Receiver>, mut watcher: RecommendedWatcher) { loop { let (mut to_unwatch, mut to_watch): (HashSet<_>, HashSet<_>) = { let (new, old) = (&*rx.borrow_and_update(), &*WATCHED.read()); @@ -109,34 +91,46 @@ impl Watcher { } } - async fn on_out(rx: UnboundedReceiver) { + async fn fan_out(rx: UnboundedReceiver) { // TODO: revert this once a new notification is implemented let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(50)); pin!(rx); - while let Some(urls) = rx.next().await { + while let Some(chunk) = rx.next().await { + let urls: HashSet<_> = chunk.into_iter().collect(); + let mut cached: HashMap<_, _> = HashMap::new(); + let _permit = WATCHER.acquire().await.unwrap(); let mut reload = Vec::with_capacity(urls.len()); - for u in urls.into_iter().collect::>() { - let Some(parent) = u.parent_url() else { continue }; + for url in urls { + let Some(name) = url.file_name() else { continue }; + let Some(parent) = url.parent_url() else { continue }; - let Ok(file) = File::from(u.clone()).await else { - FilesOp::Deleting(parent, vec![u]).emit(); + let Ok(file) = File::from(url.clone()).await else { + FilesOp::Deleting(parent, vec![url]).emit(); continue; }; + let eq = (!file.is_link() && fs::canonicalize(&url).await.is_ok_and(|p| p == *url)) + || symlink_realname(&url, &mut cached).await.is_ok_and(|s| s == name); + + if !eq { + FilesOp::Deleting(parent, vec![url]).emit(); + continue; + } + if !file.is_dir() { reload.push(file.clone()); } - FilesOp::Upserting(parent, HashMap::from_iter([(u, file)])).emit(); + FilesOp::Upserting(parent, HashMap::from_iter([(url, file)])).emit(); } if reload.is_empty() { continue; } - if let Err(e) = isolate::preload("mime", reload, true).await { - error!("preload in watcher failed: {e}"); + if let Err(e) = isolate::fetch("mime", reload).await { + error!("Fetch `mime` failed in watcher: {e}"); } } } diff --git a/yazi-core/src/notify/message.rs b/yazi-core/src/notify/message.rs index 6610290c..6e611ca6 100644 --- a/yazi-core/src/notify/message.rs +++ b/yazi-core/src/notify/message.rs @@ -19,7 +19,9 @@ pub struct Message { impl From for Message { fn from(opt: NotifyOpt) -> Self { let title = opt.title.lines().next().unwrap_or_default(); - let max_width = opt.content.lines().map(|s| s.width()).max().unwrap_or(0).max(title.width()); + let title_width = title.width() + (opt.level.icon().width() + /* Space */ 1); + + let max_width = opt.content.lines().map(|s| s.width()).max().unwrap_or(0).max(title_width); Self { title: title.to_owned(), diff --git a/yazi-core/src/tab/commands/arrow.rs b/yazi-core/src/tab/commands/arrow.rs index 1e8a4f7e..49ec378c 100644 --- a/yazi-core/src/tab/commands/arrow.rs +++ b/yazi-core/src/tab/commands/arrow.rs @@ -1,4 +1,3 @@ -use yazi_dds::Pubsub; use yazi_proxy::ManagerProxy; use yazi_shared::{event::{Cmd, Data}, render}; @@ -44,7 +43,6 @@ impl Tab { } } - Pubsub::pub_from_hover(self.idx, self.current.hovered().map(|h| &h.url)); ManagerProxy::hover(None); render!(); } diff --git a/yazi-core/src/tab/commands/copy.rs b/yazi-core/src/tab/commands/copy.rs index 22344720..6c8e0bb4 100644 --- a/yazi-core/src/tab/commands/copy.rs +++ b/yazi-core/src/tab/commands/copy.rs @@ -1,8 +1,9 @@ use std::ffi::{OsStr, OsString}; +use yazi_plugin::CLIPBOARD; use yazi_shared::event::Cmd; -use crate::{tab::Tab, CLIPBOARD}; +use crate::tab::Tab; pub struct Opt { type_: String, diff --git a/yazi-core/src/tab/commands/escape.rs b/yazi-core/src/tab/commands/escape.rs index 23bbc0d2..b05b5fb3 100644 --- a/yazi-core/src/tab/commands/escape.rs +++ b/yazi-core/src/tab/commands/escape.rs @@ -8,8 +8,8 @@ bitflags! { pub struct Opt: u8 { const FIND = 0b00001; const VISUAL = 0b00010; - const SELECT = 0b00100; - const FILTER = 0b01000; + const FILTER = 0b00100; + const SELECT = 0b01000; const SEARCH = 0b10000; } } @@ -21,8 +21,8 @@ impl From for Opt { ("all", true) => Self::all(), ("find", true) => acc | Self::FIND, ("visual", true) => acc | Self::VISUAL, - ("select", true) => acc | Self::SELECT, ("filter", true) => acc | Self::FILTER, + ("select", true) => acc | Self::SELECT, ("search", true) => acc | Self::SEARCH, _ => acc, } @@ -36,8 +36,8 @@ impl Tab { if opt.is_empty() { _ = self.escape_find() || self.escape_visual() - || self.escape_select() || self.escape_filter() + || self.escape_select() || self.escape_search(); return; } @@ -48,12 +48,12 @@ impl Tab { if opt.contains(Opt::VISUAL) { self.escape_visual(); } - if opt.contains(Opt::SELECT) { - self.escape_select(); - } if opt.contains(Opt::FILTER) { self.escape_filter(); } + if opt.contains(Opt::SELECT) { + self.escape_select(); + } if opt.contains(Opt::SEARCH) { self.escape_search(); } @@ -70,6 +70,15 @@ impl Tab { true } + pub fn escape_filter(&mut self) -> bool { + if self.current.files.filter().is_none() { + return false; + } + + self.filter_do(super::filter::Opt::default()); + render_and!(true) + } + pub fn escape_select(&mut self) -> bool { if self.selected.is_empty() { return false; @@ -82,15 +91,6 @@ impl Tab { render_and!(true) } - pub fn escape_filter(&mut self) -> bool { - if self.current.files.filter().is_none() { - return false; - } - - self.filter_do(super::filter::Opt::default()); - render_and!(true) - } - pub fn escape_search(&mut self) -> bool { if !self.current.cwd.is_search() { return false; diff --git a/yazi-core/src/tab/commands/jump.rs b/yazi-core/src/tab/commands/jump.rs deleted file mode 100644 index 810abbbf..00000000 --- a/yazi-core/src/tab/commands/jump.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::time::Duration; - -use yazi_proxy::{options::{NotifyLevel, NotifyOpt}, AppProxy}; -use yazi_shared::{emit, event::Cmd, Layer}; - -use crate::tab::Tab; - -pub struct Opt { - type_: OptType, -} - -#[derive(PartialEq, Eq)] -pub enum OptType { - None, - Fzf, - Zoxide, -} - -impl From for Opt { - fn from(mut c: Cmd) -> Self { - Self { - type_: match c.take_first_str().as_deref() { - Some("fzf") => OptType::Fzf, - Some("zoxide") => OptType::Zoxide, - _ => OptType::None, - }, - } - } -} - -impl Tab { - // TODO: Remove this once Yazi v0.2.7 is released - pub fn jump(&self, opt: impl Into) { - AppProxy::notify(NotifyOpt { - title: "Jump".to_owned(), - content: r#"The `jump` command has been deprecated in Yazi v0.2.5. -Please replace `jump fzf` with `plugin fzf`, and `jump zoxide` with `plugin zoxide`, in your `keymap.toml`. - -See https://github.com/sxyazi/yazi/issues/865 for more details."#.to_owned(), - level: NotifyLevel::Warn, - timeout: Duration::from_secs(15), - }); - - let opt = opt.into() as Opt; - if opt.type_ == OptType::None { - return; - } - - if opt.type_ == OptType::Fzf { - emit!(Call(Cmd::args("plugin", vec!["fzf".to_owned()]), Layer::App)); - } else { - emit!(Call(Cmd::args("plugin", vec!["zoxide".to_owned()]), Layer::App)); - } - } -} diff --git a/yazi-core/src/tab/commands/mod.rs b/yazi-core/src/tab/commands/mod.rs index 8f0c6467..87f2fde6 100644 --- a/yazi-core/src/tab/commands/mod.rs +++ b/yazi-core/src/tab/commands/mod.rs @@ -8,7 +8,6 @@ mod filter; mod find; mod forward; mod hidden; -mod jump; mod leave; mod linemode; mod preview; diff --git a/yazi-core/src/tab/commands/search.rs b/yazi-core/src/tab/commands/search.rs index d4119e90..700f5423 100644 --- a/yazi-core/src/tab/commands/search.rs +++ b/yazi-core/src/tab/commands/search.rs @@ -5,7 +5,7 @@ use tokio::pin; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use yazi_config::popup::InputCfg; use yazi_plugin::external; -use yazi_proxy::{InputProxy, ManagerProxy, TabProxy}; +use yazi_proxy::{AppProxy, InputProxy, ManagerProxy, TabProxy}; use yazi_shared::{event::Cmd, fs::FilesOp, render}; use crate::tab::Tab; @@ -39,15 +39,26 @@ impl Display for OptType { pub struct Opt { pub type_: OptType, + pub args: Vec, } -impl From for Opt { - fn from(mut c: Cmd) -> Self { Self { type_: c.take_first_str().unwrap_or_default().into() } } +impl TryFrom for Opt { + type Error = (); + + fn try_from(mut c: Cmd) -> Result { + Ok(Self { + type_: c.take_first_str().unwrap_or_default().into(), + args: shell_words::split(c.str("args").unwrap_or_default()).map_err(|_| ())?, + }) + } } impl Tab { - pub fn search(&mut self, opt: impl Into) { - let opt = opt.into() as Opt; + pub fn search(&mut self, opt: impl TryInto) { + let Ok(opt) = opt.try_into() else { + return AppProxy::notify_error("Invalid `search` option", "Failed to parse search option"); + }; + if opt.type_ == OptType::None { return self.search_stop(); } @@ -65,9 +76,9 @@ impl Tab { cwd = cwd.into_search(subject.clone()); let rx = if opt.type_ == OptType::Rg { - external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject }) + external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject, args: opt.args }) } else { - external::fd(external::FdOpt { cwd: cwd.clone(), hidden, glob: false, subject }) + external::fd(external::FdOpt { cwd: cwd.clone(), hidden, subject, args: opt.args }) }?; let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(300)); diff --git a/yazi-core/src/tab/commands/shell.rs b/yazi-core/src/tab/commands/shell.rs index 4effe5db..422267be 100644 --- a/yazi-core/src/tab/commands/shell.rs +++ b/yazi-core/src/tab/commands/shell.rs @@ -1,25 +1,27 @@ use std::borrow::Cow; use yazi_config::{open::Opener, popup::InputCfg}; -use yazi_proxy::{InputProxy, TasksProxy}; +use yazi_proxy::{AppProxy, InputProxy, TasksProxy}; use yazi_shared::event::Cmd; use crate::tab::Tab; pub struct Opt { - run: String, - block: bool, - orphan: bool, - confirm: bool, + run: String, + block: bool, + orphan: bool, + confirm: bool, + interactive: bool, } impl From for Opt { fn from(mut c: Cmd) -> Self { Self { - run: c.take_first_str().unwrap_or_default(), - block: c.bool("block"), - orphan: c.bool("orphan"), - confirm: c.bool("confirm"), + run: c.take_first_str().unwrap_or_default(), + block: c.bool("block"), + orphan: c.bool("orphan"), + confirm: c.bool("confirm"), + interactive: c.bool("interactive"), } } } @@ -31,6 +33,24 @@ impl Tab { } let mut opt = opt.into() as Opt; + + // TODO: Remove in v0.3.2 + if !opt.interactive && !opt.confirm { + AppProxy::notify_error( + "`shell` command", + r#"In Yazi v0.3, the behavior of the interactive `shell` (i.e., shell templates) must be explicitly specified with `--interactive`. + +Please replace e.g. `shell` with `shell --interactive`, `shell "my-template"` with `shell "my-template" --interactive`, in your keymap.toml"#, + ); + return; + } else if opt.interactive && opt.confirm { + AppProxy::notify_error( + "`shell` command", + "The `shell` command cannot specify both `--confirm` and `--interactive` at the same time.", + ); + return; + } + let selected = self.hovered_and_selected(true).cloned().collect(); tokio::spawn(async move { diff --git a/yazi-core/src/tab/commands/sort.rs b/yazi-core/src/tab/commands/sort.rs index 8b8db845..1325eaea 100644 --- a/yazi-core/src/tab/commands/sort.rs +++ b/yazi-core/src/tab/commands/sort.rs @@ -8,16 +8,19 @@ use crate::{tab::Tab, tasks::Tasks}; impl Tab { pub fn sort(&mut self, mut c: Cmd, tasks: &Tasks) { + let conf = &mut self.conf; if let Some(by) = c.take_first_str() { - self.conf.sort_by = SortBy::from_str(&by).unwrap_or_default(); + conf.sort_by = SortBy::from_str(&by).unwrap_or_default(); } - self.conf.sort_sensitive = c.bool("sensitive"); - self.conf.sort_reverse = c.bool("reverse"); - self.conf.sort_dir_first = c.bool("dir-first"); + + conf.sort_reverse = c.maybe_bool("reverse").unwrap_or(conf.sort_reverse); + conf.sort_dir_first = c.maybe_bool("dir-first").unwrap_or(conf.sort_dir_first); + conf.sort_sensitive = c.maybe_bool("sensitive").unwrap_or(conf.sort_sensitive); + conf.sort_translit = c.maybe_bool("translit").unwrap_or(conf.sort_translit); self.apply_files_attrs(); ManagerProxy::update_paged(); - tasks.preload_sorted(&self.current.files); + tasks.prework_sorted(&self.current.files); } } diff --git a/yazi-core/src/tab/config.rs b/yazi-core/src/tab/config.rs index 9ac497e2..edce7fb7 100644 --- a/yazi-core/src/tab/config.rs +++ b/yazi-core/src/tab/config.rs @@ -9,6 +9,7 @@ pub struct Config { pub sort_sensitive: bool, pub sort_reverse: bool, pub sort_dir_first: bool, + pub sort_translit: bool, // Display pub linemode: String, @@ -23,6 +24,7 @@ impl Default for Config { sort_sensitive: MANAGER.sort_sensitive, sort_reverse: MANAGER.sort_reverse, sort_dir_first: MANAGER.sort_dir_first, + sort_translit: MANAGER.sort_translit, // Display linemode: MANAGER.linemode.to_owned(), @@ -45,6 +47,7 @@ impl Config { sensitive: self.sort_sensitive, reverse: self.sort_reverse, dir_first: self.sort_dir_first, + translit: self.sort_translit, } } } diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index 0b7b560e..8f1741e7 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -3,7 +3,7 @@ use std::time::{Duration, SystemTime}; use tokio::{pin, task::JoinHandle}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tokio_util::sync::CancellationToken; -use yazi_adaptor::ADAPTOR; +use yazi_adapter::ADAPTOR; use yazi_config::PLUGIN; use yazi_plugin::{external::Highlighter, isolate, utils::PreviewLock}; use yazi_shared::{fs::{Cha, File, FilesOp, Url}, MIME_DIR}; @@ -32,9 +32,9 @@ impl Preview { self.abort(); if previewer.sync { - isolate::peek_sync(&previewer.cmd, file, self.skip); + isolate::peek_sync(&previewer.run, file, self.skip); } else { - self.previewer_ct = Some(isolate::peek(&previewer.cmd, file, self.skip)); + self.previewer_ct = Some(isolate::peek(&previewer.run, file, self.skip)); } } diff --git a/yazi-core/src/tasks/commands/cancel.rs b/yazi-core/src/tasks/commands/cancel.rs index e8c9aca6..5b4fb30a 100644 --- a/yazi-core/src/tasks/commands/cancel.rs +++ b/yazi-core/src/tasks/commands/cancel.rs @@ -4,7 +4,7 @@ use crate::tasks::Tasks; impl Tasks { pub fn cancel(&mut self, _: Cmd) { - let id = self.scheduler.ongoing.lock().get_id(self.cursor); + let id = self.ongoing().lock().get_id(self.cursor); if id.map(|id| self.scheduler.cancel(id)) != Some(true) { return; } diff --git a/yazi-core/src/tasks/commands/inspect.rs b/yazi-core/src/tasks/commands/inspect.rs index b17f3cce..4828b913 100644 --- a/yazi-core/src/tasks/commands/inspect.rs +++ b/yazi-core/src/tasks/commands/inspect.rs @@ -4,23 +4,23 @@ use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use scopeguard::defer; use tokio::{io::{stdin, AsyncReadExt}, select, sync::mpsc, time}; use yazi_proxy::{AppProxy, HIDER}; -use yazi_shared::{event::Cmd, term::Term}; +use yazi_shared::{event::Cmd, terminal_clear}; use crate::tasks::Tasks; impl Tasks { pub fn inspect(&self, _: Cmd) { - let Some(id) = self.scheduler.ongoing.lock().get_id(self.cursor) else { + let ongoing = self.ongoing().clone(); + let Some(id) = ongoing.lock().get_id(self.cursor) else { return; }; - let scheduler = self.scheduler.clone(); tokio::spawn(async move { let _permit = HIDER.acquire().await.unwrap(); let (tx, mut rx) = mpsc::unbounded_channel(); let mut buffered = { - let mut ongoing = scheduler.ongoing.lock(); + let mut ongoing = ongoing.lock(); let Some(task) = ongoing.get_mut(id) else { return }; task.logger = Some(tx); @@ -30,7 +30,7 @@ impl Tasks { defer!(AppProxy::resume()); AppProxy::stop().await; - Term::clear(&mut stderr()).ok(); + terminal_clear(&mut stderr()).ok(); BufWriter::new(stderr().lock()).write_all(mem::take(&mut buffered).as_bytes()).ok(); defer! { disable_raw_mode().ok(); } @@ -46,7 +46,7 @@ impl Tasks { stderr.write_all(b"\r\n").ok(); } _ = time::sleep(time::Duration::from_millis(500)) => { - if scheduler.ongoing.lock().get(id).is_none() { + if ongoing.lock().get(id).is_none() { stderr().write_all(b"Task finished, press `q` to quit\r\n").ok(); break; } @@ -60,7 +60,7 @@ impl Tasks { } } - if let Some(task) = scheduler.ongoing.lock().get_mut(id) { + if let Some(task) = ongoing.lock().get_mut(id) { task.logger = None; } while answer != b'q' { diff --git a/yazi-core/src/tasks/file.rs b/yazi-core/src/tasks/file.rs index 7e2e2fe3..c09068d5 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -39,6 +39,17 @@ impl Tasks { } } + pub fn file_hardlink(&self, src: &HashSet, dest: &Url, force: bool, follow: bool) { + for u in src { + let to = dest.join(u.file_name().unwrap()); + if force && *u == to { + debug!("file_hardlink: same file, skipping {:?}", to); + } else { + self.scheduler.file_hardlink(u.clone(), to, force, follow); + } + } + } + pub fn file_remove(&self, targets: Vec, permanently: bool) { for u in targets { if permanently { diff --git a/yazi-core/src/tasks/preload.rs b/yazi-core/src/tasks/preload.rs index 8f1915df..f2ce3d5b 100644 --- a/yazi-core/src/tasks/preload.rs +++ b/yazi-core/src/tasks/preload.rs @@ -1,17 +1,15 @@ -use std::{collections::HashMap, mem}; +use std::collections::HashMap; -use yazi_config::{manager::SortBy, plugin::{PluginRule, MAX_PRELOADERS}, PLUGIN}; +use yazi_config::{manager::SortBy, plugin::MAX_PREWORKERS, PLUGIN}; use yazi_shared::{fs::{File, Url}, MIME_DIR}; use super::Tasks; use crate::folder::Files; impl Tasks { - pub fn preload_paged(&self, paged: &[File], mimetype: &HashMap) { - let mut single_tasks = Vec::with_capacity(paged.len()); - let mut multi_tasks: [Vec<_>; MAX_PRELOADERS as usize] = Default::default(); - - let loaded = self.scheduler.preload.rule_loaded.read(); + pub fn fetch_paged(&self, paged: &[File], mimetype: &HashMap) { + let mut loaded = self.scheduler.prework.loaded.lock(); + let mut tasks: [Vec<_>; MAX_PREWORKERS as usize] = Default::default(); for f in paged { let mime = if f.is_dir() { Some(MIME_DIR) } else { mimetype.get(&f.url).map(|s| &**s) }; let factors = |s: &str| match s { @@ -19,61 +17,58 @@ impl Tasks { _ => false, }; - for rule in PLUGIN.preloaders(&f.url, mime, factors) { - if loaded.get(&f.url).is_some_and(|x| x & (1 << rule.id) != 0) { - continue; - } - if rule.multi { - multi_tasks[rule.id as usize].push(f); - } else { - single_tasks.push((rule, f)); + for p in PLUGIN.fetchers(&f.url, mime, factors) { + match loaded.get_mut(&f.url) { + Some(n) if *n & (1 << p.idx) != 0 => continue, + Some(n) => *n |= 1 << p.idx, + None => _ = loaded.insert(f.url.clone(), 1 << p.idx), } + tasks[p.idx as usize].push(f.clone()); } } drop(loaded); - let mut loaded = self.scheduler.preload.rule_loaded.write(); - - let mut go = |rule: &PluginRule, targets: Vec<&File>| { - for &f in &targets { - if let Some(n) = loaded.get_mut(&f.url) { - *n |= 1 << rule.id; - } else { - loaded.insert(f.url.clone(), 1 << rule.id); - } + for (i, tasks) in tasks.into_iter().enumerate() { + if !tasks.is_empty() { + self.scheduler.fetch_paged(&PLUGIN.fetchers[i], tasks); } - self.scheduler.preload_paged(rule, targets); - }; - - #[allow(clippy::needless_range_loop)] - for i in 0..PLUGIN.preloaders.len() { - if !multi_tasks[i].is_empty() { - go(&PLUGIN.preloaders[i], mem::take(&mut multi_tasks[i])); - } - } - for (rule, target) in single_tasks { - go(rule, vec![target]); } } - pub fn preload_affected(&self, affected: &[File], mimetype: &HashMap) { + pub fn preload_paged(&self, paged: &[File], mimetype: &HashMap) { + let mut loaded = self.scheduler.prework.loaded.lock(); + for f in paged { + let mime = if f.is_dir() { Some(MIME_DIR) } else { mimetype.get(&f.url).map(|s| &**s) }; + for p in PLUGIN.preloaders(&f.url, mime) { + match loaded.get_mut(&f.url) { + Some(n) if *n & (1 << p.idx) != 0 => continue, + Some(n) => *n |= 1 << p.idx, + None => _ = loaded.insert(f.url.clone(), 1 << p.idx), + } + self.scheduler.preload_paged(p, f); + } + } + } + + pub fn prework_affected(&self, affected: &[File], mimetype: &HashMap) { { - let mut loaded = self.scheduler.preload.rule_loaded.write(); + let mut loaded = self.scheduler.prework.loaded.lock(); for f in affected { loaded.remove(&f.url); } } + self.fetch_paged(affected, mimetype); self.preload_paged(affected, mimetype); } - pub fn preload_sorted(&self, targets: &Files) { + pub fn prework_sorted(&self, targets: &Files) { if targets.sorter().by != SortBy::Size { return; } let targets: Vec<_> = { - let loading = self.scheduler.preload.size_loading.read(); + let loading = self.scheduler.prework.size_loading.read(); targets .iter() .filter(|f| f.is_dir() && !targets.sizes.contains_key(&f.url) && !loading.contains(&f.url)) @@ -84,11 +79,11 @@ impl Tasks { return; } - let mut loading = self.scheduler.preload.size_loading.write(); + let mut loading = self.scheduler.prework.size_loading.write(); for &target in &targets { loading.insert(target.clone()); } - self.scheduler.preload_size(targets); + self.scheduler.prework_size(targets); } } diff --git a/yazi-core/src/tasks/tasks.rs b/yazi-core/src/tasks/tasks.rs index 1f137867..e62fd0dd 100644 --- a/yazi-core/src/tasks/tasks.rs +++ b/yazi-core/src/tasks/tasks.rs @@ -1,8 +1,10 @@ use std::{sync::Arc, time::Duration}; +use parking_lot::Mutex; use tokio::{task::JoinHandle, time::sleep}; -use yazi_scheduler::{Scheduler, TaskSummary}; -use yazi_shared::{emit, event::Cmd, term::Term, Layer}; +use yazi_adapter::Dimension; +use yazi_scheduler::{Ongoing, Scheduler, TaskSummary}; +use yazi_shared::{emit, event::Cmd, Layer}; use super::{TasksProgress, TASKS_BORDER, TASKS_PADDING, TASKS_PERCENT}; @@ -52,14 +54,14 @@ impl Tasks { #[inline] pub fn limit() -> usize { - (Term::size().rows * TASKS_PERCENT / 100).saturating_sub(TASKS_BORDER + TASKS_PADDING) as usize + (Dimension::available().rows * TASKS_PERCENT / 100).saturating_sub(TASKS_BORDER + TASKS_PADDING) + as usize } pub fn paginate(&self) -> Vec { - let ongoing = self.scheduler.ongoing.lock(); - ongoing.values().take(Self::limit()).map(Into::into).collect() + self.ongoing().lock().values().take(Self::limit()).map(Into::into).collect() } #[inline] - pub fn len(&self) -> usize { self.scheduler.ongoing.lock().len() } + pub fn ongoing(&self) -> &Arc> { &self.scheduler.ongoing } } diff --git a/yazi-core/src/which/commands/show.rs b/yazi-core/src/which/commands/show.rs index 9a5ae428..39f69c47 100644 --- a/yazi-core/src/which/commands/show.rs +++ b/yazi-core/src/which/commands/show.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{collections::HashSet, str::FromStr}; use yazi_config::{keymap::{Control, Key}, KEYMAP}; use yazi_shared::{event::Cmd, render, Layer}; @@ -43,12 +43,15 @@ impl Which { } pub fn show_with(&mut self, key: &Key, layer: Layer) { + let mut seen = HashSet::new(); + self.layer = layer; self.times = 1; self.cands = KEYMAP .get(layer) .iter() .filter(|c| c.on.len() > 1 && &c.on[0] == key) + .filter(|&c| seen.insert(&c.on)) .map(|c| c.into()) .collect(); diff --git a/yazi-core/src/which/sorter.rs b/yazi-core/src/which/sorter.rs index 571201be..d2dc8dfb 100644 --- a/yazi-core/src/which/sorter.rs +++ b/yazi-core/src/which/sorter.rs @@ -1,13 +1,14 @@ use std::{borrow::Cow, mem}; use yazi_config::{keymap::ControlCow, which::SortBy, WHICH}; -use yazi_shared::natsort; +use yazi_shared::{natsort, Transliterator}; #[derive(Clone, Copy, PartialEq)] pub struct WhichSorter { pub by: SortBy, pub sensitive: bool, pub reverse: bool, + pub translit: bool, } impl Default for WhichSorter { @@ -16,6 +17,7 @@ impl Default for WhichSorter { by: WHICH.sort_by, sensitive: WHICH.sort_sensitive, reverse: WHICH.sort_reverse, + translit: WHICH.sort_translit, } } } @@ -38,7 +40,16 @@ impl WhichSorter { } indices.sort_unstable_by(|&a, &b| { - let ordering = natsort(entities[a].as_bytes(), entities[b].as_bytes(), !self.sensitive); + let ordering = if !self.translit { + natsort(entities[a].as_bytes(), entities[b].as_bytes(), !self.sensitive) + } else { + natsort( + entities[a].as_bytes().transliterate().as_bytes(), + entities[b].as_bytes().transliterate().as_bytes(), + !self.sensitive, + ) + }; + if self.reverse { ordering.reverse() } else { ordering } }); diff --git a/yazi-dds/Cargo.toml b/yazi-dds/Cargo.toml index e7aa134b..d364f54d 100644 --- a/yazi-dds/Cargo.toml +++ b/yazi-dds/Cargo.toml @@ -17,14 +17,17 @@ yazi-boot = { path = "../yazi-boot", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -anyhow = "1.0.82" -mlua = { version = "0.9.7", features = [ "lua54" ] } -parking_lot = "0.12.1" -serde = { version = "1.0.198", features = [ "derive" ] } -serde_json = "1.0.116" -tokio = { version = "1.37.0", features = [ "full" ] } +anyhow = "1.0.86" +mlua = { version = "0.9.9", features = [ "lua54" ] } +parking_lot = "0.12.3" +serde = { version = "1.0.203", features = [ "derive" ] } +serde_json = "1.0.117" +tokio = { version = "1.38.0", features = [ "full" ] } tokio-stream = "0.1.15" -tokio-util = "0.7.10" +tokio-util = "0.7.11" + +[build-dependencies] +vergen = { version = "8.3.1", features = [ "build", "git", "gitcl" ] } [target."cfg(unix)".dependencies] -uzers = "0.11.3" +uzers = "0.12.0" diff --git a/yazi-dds/build.rs b/yazi-dds/build.rs new file mode 100644 index 00000000..aef969b9 --- /dev/null +++ b/yazi-dds/build.rs @@ -0,0 +1,9 @@ +use std::error::Error; + +use vergen::EmitBuilder; + +fn main() -> Result<(), Box> { + EmitBuilder::builder().git_sha(true).emit()?; + + Ok(()) +} diff --git a/yazi-dds/src/body/body.rs b/yazi-dds/src/body/body.rs index 718ff4b3..ff123996 100644 --- a/yazi-dds/src/body/body.rs +++ b/yazi-dds/src/body/body.rs @@ -32,7 +32,7 @@ impl Body<'static> { "hover" => Self::Hover(serde_json::from_str(body)?), "rename" => Self::Rename(serde_json::from_str(body)?), "bulk" => Self::Bulk(serde_json::from_str(body)?), - "yank" => Self::Yank(serde_json::from_str(body)?), + "@yank" => Self::Yank(serde_json::from_str(body)?), "move" => Self::Move(serde_json::from_str(body)?), "trash" => Self::Trash(serde_json::from_str(body)?), "delete" => Self::Delete(serde_json::from_str(body)?), @@ -45,36 +45,29 @@ impl Body<'static> { BodyCustom::from_lua(kind, value) } - pub fn tab(kind: &str, body: &str) -> usize { - match kind { - "cd" | "hover" | "bulk" | "rename" => {} - _ => return 0, - } - - match Self::from_str(kind, body) { - Ok(Self::Cd(b)) => b.tab, - Ok(Self::Hover(b)) => b.tab, - Ok(Self::Bulk(b)) => b.tab, - Ok(Self::Rename(b)) => b.tab, - _ => 0, - } - } - pub fn validate(kind: &str) -> Result<()> { if matches!( kind, "hi" - | "hey" | "bye" - | "cd" | "hover" + | "hey" + | "bye" + | "cd" + | "hover" | "rename" - | "bulk" | "yank" - | "move" | "trash" + | "bulk" + | "@yank" + | "move" + | "trash" | "delete" ) { bail!("Cannot construct system event"); } - if !kind.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') { + let mut it = kind.bytes().peekable(); + if it.peek() == Some(&b'@') { + it.next(); // Skip `@` as it's a prefix for static messages + } + if !it.all(|b| b.is_ascii_alphanumeric() || b == b'-') { bail!("Kind must be alphanumeric with dashes"); } @@ -93,7 +86,7 @@ impl<'a> Body<'a> { Self::Hover(_) => "hover", Self::Rename(_) => "rename", Self::Bulk(_) => "bulk", - Self::Yank(_) => "yank", + Self::Yank(_) => "@yank", Self::Move(_) => "move", Self::Trash(_) => "trash", Self::Delete(_) => "delete", @@ -108,11 +101,6 @@ impl<'a> Body<'a> { #[inline] pub fn with_sender(self, sender: u64) -> Payload<'a> { Payload::new(self).with_sender(sender) } - - #[inline] - pub fn with_severity(self, severity: u16) -> Payload<'a> { - Payload::new(self).with_severity(severity) - } } impl IntoLua<'_> for Body<'static> { diff --git a/yazi-dds/src/body/bulk.rs b/yazi-dds/src/body/bulk.rs index 4e35f1cc..47c8840a 100644 --- a/yazi-dds/src/body/bulk.rs +++ b/yazi-dds/src/body/bulk.rs @@ -8,21 +8,27 @@ use super::Body; #[derive(Debug, Serialize, Deserialize)] pub struct BodyBulk<'a> { - pub tab: usize, - pub changes: Cow<'a, HashMap>, + pub changes: HashMap, Cow<'a, Url>>, } impl<'a> BodyBulk<'a> { #[inline] - pub fn borrowed(tab: usize, changes: &'a HashMap) -> Body<'a> { - Self { tab, changes: Cow::Borrowed(changes) }.into() + pub fn borrowed(changes: &HashMap<&'a Url, &'a Url>) -> Body<'a> { + let iter = changes.iter().map(|(&from, &to)| (Cow::Borrowed(from), Cow::Borrowed(to))); + + Self { changes: iter.collect() }.into() } } impl BodyBulk<'static> { #[inline] - pub fn owned(tab: usize, changes: &HashMap) -> Body<'static> { - Self { tab, changes: Cow::Owned(changes.clone()) }.into() + pub fn owned(changes: &HashMap<&Url, &Url>) -> Body<'static> { + let changes = changes + .iter() + .map(|(&from, &to)| (Cow::Owned(from.clone()), Cow::Owned(to.clone()))) + .collect(); + + Self { changes }.into() } } @@ -32,27 +38,22 @@ impl<'a> From> for Body<'a> { impl IntoLua<'_> for BodyBulk<'static> { fn into_lua(self, lua: &Lua) -> mlua::Result { - BodyBulkIter { tab: self.tab, inner: self.changes.into_owned().into_iter() }.into_lua(lua) + BodyBulkIter { inner: self.changes.into_iter() }.into_lua(lua) } } // --- Iterator pub struct BodyBulkIter { - pub tab: usize, - pub inner: hash_map::IntoIter, + pub inner: hash_map::IntoIter, Cow<'static, Url>>, } impl UserData for BodyBulkIter { - fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("tab", |_, me| Ok(me.tab)); - } - fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.inner.len())); methods.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| { let iter = lua.create_function(|lua, mut me: UserDataRefMut| { - if let Some((from, to)) = me.inner.next() { + if let Some((Cow::Owned(from), Cow::Owned(to))) = me.inner.next() { (lua.create_any_userdata(from)?, lua.create_any_userdata(to)?).into_lua_multi(lua) } else { ().into_lua_multi(lua) diff --git a/yazi-dds/src/body/bye.rs b/yazi-dds/src/body/bye.rs index 898af34b..256bd1d9 100644 --- a/yazi-dds/src/body/bye.rs +++ b/yazi-dds/src/body/bye.rs @@ -4,11 +4,11 @@ use serde::{Deserialize, Serialize}; use super::Body; #[derive(Debug, Serialize, Deserialize)] -pub struct BodyBye {} +pub struct BodyBye; impl BodyBye { #[inline] - pub fn borrowed() -> Body<'static> { Self {}.into() } + pub fn owned() -> Body<'static> { Self.into() } } impl<'a> From for Body<'a> { diff --git a/yazi-dds/src/body/hey.rs b/yazi-dds/src/body/hey.rs index 49e6bb6e..58d8cf93 100644 --- a/yazi-dds/src/body/hey.rs +++ b/yazi-dds/src/body/hey.rs @@ -3,12 +3,20 @@ use std::collections::HashMap; use mlua::{ExternalResult, IntoLua, Lua, Value}; use serde::{Deserialize, Serialize}; -use super::Body; +use super::{Body, BodyHi}; use crate::Peer; #[derive(Debug, Serialize, Deserialize)] pub struct BodyHey { - pub peers: HashMap, + pub peers: HashMap, + pub version: String, +} + +impl BodyHey { + #[inline] + pub fn owned(peers: HashMap) -> Body<'static> { + Self { peers, version: BodyHi::version() }.into() + } } impl From for Body<'_> { diff --git a/yazi-dds/src/body/hi.rs b/yazi-dds/src/body/hi.rs index b536b55d..c80e6f11 100644 --- a/yazi-dds/src/body/hi.rs +++ b/yazi-dds/src/body/hi.rs @@ -5,16 +5,26 @@ use serde::{Deserialize, Serialize}; use super::Body; +/// The client handshake #[derive(Debug, Serialize, Deserialize)] pub struct BodyHi<'a> { - pub abilities: HashSet>, + /// Specifies the kinds of events that the client can handle + pub abilities: HashSet>, + pub version: String, } impl<'a> BodyHi<'a> { #[inline] - pub fn borrowed(abilities: HashSet<&'a String>) -> Body<'a> { - Self { abilities: abilities.into_iter().map(Cow::Borrowed).collect() }.into() + pub fn borrowed(abilities: HashSet<&'a str>) -> Body<'a> { + Self { + abilities: abilities.into_iter().map(Cow::Borrowed).collect(), + version: Self::version(), + } + .into() } + + #[inline] + pub fn version() -> String { format!("{} {}", env!("CARGO_PKG_VERSION"), env!("VERGEN_GIT_SHA")) } } impl<'a> From> for Body<'a> { diff --git a/yazi-dds/src/client.rs b/yazi-dds/src/client.rs index de3d0493..0c29cd1c 100644 --- a/yazi-dds/src/client.rs +++ b/yazi-dds/src/client.rs @@ -1,6 +1,6 @@ use std::{collections::{HashMap, HashSet}, mem, str::FromStr}; -use anyhow::Result; +use anyhow::{bail, Context, Result}; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use tokio::{io::AsyncWriteExt, select, sync::mpsc, task::JoinHandle, time}; @@ -27,6 +27,7 @@ pub struct Peer { } impl Client { + /// Connect to an existing server or start a new one. pub(super) fn serve() { let mut rx = QUEUE_RX.drop(); while rx.try_recv().is_ok() {} @@ -52,7 +53,7 @@ impl Client { if line.is_empty() { continue; } else if line.starts_with("hey,") { - Self::handle_hey(line); + Self::handle_hey(&line); } else { Payload::from_str(&line).map(|p| p.emit()).ok(); } @@ -62,14 +63,14 @@ impl Client { }); } - pub async fn shot(kind: &str, receiver: u64, severity: Option, body: &str) -> Result<()> { + /// Connect to an existing server to send a single message. + pub async fn shot(kind: &str, receiver: u64, body: &str) -> Result<()> { Body::validate(kind)?; - let sender = severity.map(Into::into).unwrap_or(*ID); let payload = format!( - "{}\n{kind},{receiver},{sender},{body}\n{}\n", + "{}\n{kind},{receiver},{ID},{body}\n{}\n", Payload::new(BodyHi::borrowed(Default::default())), - Payload::new(BodyBye::borrowed()) + Payload::new(BodyBye::owned()) ); let (mut lines, mut writer) = Stream::connect().await?; @@ -77,15 +78,63 @@ impl Client { writer.flush().await?; drop(writer); - while let Ok(Some(s)) = lines.next_line().await { - if matches!(s.split(',').next(), Some(kind) if kind == "bye") { - break; + let mut version = None; + while let Ok(Some(line)) = lines.next_line().await { + match line.split(',').next() { + Some("hey") if version.is_none() => { + if let Ok(Body::Hey(hey)) = Payload::from_str(&line).map(|p| p.body) { + version = Some(hey.version); + } + } + Some("bye") => break, + _ => {} } } + if version != Some(BodyHi::version()) { + bail!( + "Incompatible version (Ya {}, Yazi {})", + BodyHi::version(), + version.as_deref().unwrap_or("Unknown") + ); + } Ok(()) } + /// Connect to an existing server and listen in on the messages that are being + /// sent by other yazi instances: + /// - If no server is running, fail right away; + /// - If a server is closed, attempt to reconnect forever. + pub async fn draw(kinds: HashSet<&str>) -> Result<()> { + async fn make(kinds: &HashSet<&str>) -> Result { + let (lines, mut writer) = Stream::connect().await?; + let hi = Payload::new(BodyHi::borrowed(kinds.clone())); + writer.write_all(format!("{hi}\n").as_bytes()).await?; + writer.flush().await?; + Ok(lines) + } + + let mut lines = make(&kinds).await.context("No running Yazi instance found")?; + loop { + match lines.next_line().await? { + Some(s) => { + let kind = s.split(',').next(); + if matches!(kind, Some(kind) if kinds.contains(kind)) { + println!("{s}"); + } + } + None => loop { + if let Ok(new) = make(&kinds).await { + lines = new; + break; + } else { + time::sleep(time::Duration::from_secs(1)).await; + } + }, + } + } + } + #[inline] pub(super) fn push<'a>(payload: impl Into>) { QUEUE_TX.send(format!("{}\n", payload.into())).ok(); @@ -122,8 +171,8 @@ impl Client { Self::connect(server).await } - fn handle_hey(s: String) { - if let Ok(Body::Hey(mut hey)) = Payload::from_str(&s).map(|p| p.body) { + fn handle_hey(s: &str) { + if let Ok(Body::Hey(mut hey)) = Payload::from_str(s).map(|p| p.body) { hey.peers.retain(|&id, _| id != *ID); *PEERS.write() = hey.peers; } diff --git a/yazi-dds/src/lib.rs b/yazi-dds/src/lib.rs index b837cdd6..6a50bcde 100644 --- a/yazi-dds/src/lib.rs +++ b/yazi-dds/src/lib.rs @@ -42,6 +42,9 @@ pub fn init() { USERS_CACHE.with(Default::default); // Env + if let Some(s) = std::env::var("YAZI_ID").ok().filter(|s| !s.is_empty()) { + std::env::set_var("YAZI_PID", s); + } std::env::set_var("YAZI_ID", ID.to_string()); std::env::set_var( "YAZI_LEVEL", diff --git a/yazi-dds/src/payload.rs b/yazi-dds/src/payload.rs index bdd9621a..3b385506 100644 --- a/yazi-dds/src/payload.rs +++ b/yazi-dds/src/payload.rs @@ -41,11 +41,6 @@ impl<'a> Payload<'a> { self.sender = sender; self } - - pub(super) fn with_severity(mut self, severity: u16) -> Self { - self.sender = severity as u64; - self - } } impl Payload<'static> { diff --git a/yazi-dds/src/pubsub.rs b/yazi-dds/src/pubsub.rs index 698a9ea4..c016889c 100644 --- a/yazi-dds/src/pubsub.rs +++ b/yazi-dds/src/pubsub.rs @@ -5,7 +5,7 @@ use parking_lot::RwLock; use yazi_boot::BOOT; use yazi_shared::{fs::Url, RoCell}; -use crate::{body::{Body, BodyCd, BodyDelete, BodyHi, BodyHover, BodyMove, BodyMoveItem, BodyRename, BodyTrash, BodyYank}, Client, ID, PEERS}; +use crate::{body::{Body, BodyBulk, BodyCd, BodyDelete, BodyHi, BodyHover, BodyMove, BodyMoveItem, BodyRename, BodyTrash, BodyYank}, Client, ID, PEERS}; pub static LOCAL: RoCell>>>> = RoCell::new(); @@ -80,15 +80,9 @@ impl Pubsub { } } - pub fn pub_static(severity: u16, body: Body) { - if Self::own_static_ability(body.kind()) { - Client::push(body.with_severity(severity)); - } - } - pub fn pub_from_hi() -> bool { let abilities = REMOTE.read().keys().cloned().collect(); - let abilities = BOOT.remote_events.union(&abilities).collect(); + let abilities = BOOT.remote_events.union(&abilities).map(|s| s.as_str()).collect(); Client::push(BodyHi::borrowed(abilities)); true @@ -98,8 +92,8 @@ impl Pubsub { if LOCAL.read().contains_key("cd") { Self::pub_(BodyCd::dummy(tab)); } - if Self::own_static_ability("cd") { - Client::push(BodyCd::borrowed(tab, url).with_severity(100)); + if PEERS.read().values().any(|p| p.able("cd")) { + Client::push(BodyCd::borrowed(tab, url)); } if BOOT.local_events.contains("cd") { BodyCd::borrowed(tab, url).with_receiver(*ID).flush(); @@ -110,8 +104,8 @@ impl Pubsub { if LOCAL.read().contains_key("hover") { Self::pub_(BodyHover::dummy(tab)); } - if Self::own_static_ability("hover") { - Client::push(BodyHover::borrowed(tab, url).with_severity(200)); + if PEERS.read().values().any(|p| p.able("hover")) { + Client::push(BodyHover::borrowed(tab, url)); } if BOOT.local_events.contains("hover") { BodyHover::borrowed(tab, url).with_receiver(*ID).flush(); @@ -130,14 +124,26 @@ impl Pubsub { } } + pub fn pub_from_bulk(changes: HashMap<&Url, &Url>) { + if LOCAL.read().contains_key("bulk") { + Self::pub_(BodyBulk::owned(&changes)); + } + if PEERS.read().values().any(|p| p.able("bulk")) { + Client::push(BodyBulk::borrowed(&changes)); + } + if BOOT.local_events.contains("bulk") { + BodyBulk::borrowed(&changes).with_receiver(*ID).flush(); + } + } + pub fn pub_from_yank(cut: bool, urls: &HashSet) { - if LOCAL.read().contains_key("yank") { + if LOCAL.read().contains_key("@yank") { Self::pub_(BodyYank::dummy()); } - if Self::own_static_ability("yank") { - Client::push(BodyYank::borrowed(cut, urls).with_severity(300)); + if Self::own_static_ability("@yank") { + Client::push(BodyYank::borrowed(cut, urls)); } - if BOOT.local_events.contains("yank") { + if BOOT.local_events.contains("@yank") { BodyYank::borrowed(cut, urls).with_receiver(*ID).flush(); } } diff --git a/yazi-dds/src/server.rs b/yazi-dds/src/server.rs index b3e51b12..8085fe51 100644 --- a/yazi-dds/src/server.rs +++ b/yazi-dds/src/server.rs @@ -2,10 +2,10 @@ use std::{collections::HashMap, str::FromStr, time::Duration}; use anyhow::Result; use parking_lot::RwLock; -use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, select, sync::mpsc, task::JoinHandle, time}; +use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, select, sync::mpsc::{self, UnboundedReceiver}, task::JoinHandle, time}; use yazi_shared::RoCell; -use crate::{body::{Body, BodyBye, BodyHey}, Client, Payload, Peer, Stream, STATE}; +use crate::{body::{Body, BodyBye, BodyHey}, Client, ClientWriter, Payload, Peer, Stream, STATE}; pub(super) static CLIENTS: RoCell>> = RoCell::new(); @@ -44,7 +44,7 @@ impl Server { let Some(id) = id else { continue }; if line.starts_with("bye,") { - writer.write_all(BodyBye::borrowed().with_receiver(id).with_sender(0).to_string().as_bytes()).await.ok(); + Self::handle_bye(id, rx, writer).await; break; } @@ -66,9 +66,9 @@ impl Server { continue; } - if receiver == 0 && sender <= u16::MAX as u64 { + if receiver == 0 && kind.starts_with('@') { let Some(body) = parts.next() else { continue }; - if !STATE.set(kind, sender as u16, body) { continue } + if !STATE.set(kind, sender, body) { continue } } line.push('\n'); @@ -77,7 +77,11 @@ impl Server { else => break } } - Self::handle_bye(id); + + let mut clients = CLIENTS.write(); + if id.and_then(|id| clients.remove(&id)).is_some() { + Self::handle_hey(&clients); + } }); } })) @@ -87,10 +91,6 @@ impl Server { let Ok(payload) = Payload::from_str(&s) else { return }; let Body::Hi(hi) = payload.body else { return }; - if payload.sender <= u16::MAX as u64 { - return; // The kind of static messages cannot be "hi" - } - if id.is_none() { if let Some(ref state) = *STATE.read() { state.values().for_each(|s| _ = tx.send(s.clone())); @@ -111,18 +111,24 @@ impl Server { fn handle_hey(clients: &HashMap) { let payload = format!( "{}\n", - Payload::new( - BodyHey { peers: clients.values().map(|c| (c.id, Peer::new(&c.abilities))).collect() } - .into() - ) + Payload::new(BodyHey::owned( + clients.values().map(|c| (c.id, Peer::new(&c.abilities))).collect() + )) ); clients.values().for_each(|c| _ = c.tx.send(payload.clone())); } - fn handle_bye(id: Option) { - let mut clients = CLIENTS.write(); - if id.and_then(|id| clients.remove(&id)).is_some() { - Self::handle_hey(&clients); + async fn handle_bye(id: u64, mut rx: UnboundedReceiver, mut writer: ClientWriter) { + while let Ok(payload) = rx.try_recv() { + if writer.write_all(payload.as_bytes()).await.is_err() { + break; + } } + + _ = writer + .write_all(BodyBye::owned().with_receiver(id).with_sender(0).to_string().as_bytes()) + .await; + + writer.flush().await.ok(); } } diff --git a/yazi-dds/src/state.rs b/yazi-dds/src/state.rs index 9beb8992..ef0a4857 100644 --- a/yazi-dds/src/state.rs +++ b/yazi-dds/src/state.rs @@ -6,7 +6,7 @@ use tokio::{fs::{self, File, OpenOptions}, io::{AsyncBufReadExt, AsyncWriteExt, use yazi_boot::BOOT; use yazi_shared::{timestamp_us, RoCell}; -use crate::{body::Body, CLIENTS}; +use crate::CLIENTS; pub static STATE: RoCell = RoCell::new(); @@ -23,23 +23,22 @@ impl Deref for State { } impl State { - pub fn set(&self, kind: &str, severity: u16, body: &str) -> bool { + pub fn set(&self, kind: &str, sender: u64, body: &str) -> bool { let Some(inner) = &mut *self.inner.write() else { return false }; - let key = format!("{}_{severity}_{kind}", Body::tab(kind, body)); if body == "null" { return inner - .remove(&key) + .remove(kind) .map(|_| self.last.store(timestamp_us(), Ordering::Relaxed)) .is_some(); } - let value = format!("{kind},0,{severity},{body}\n"); - if inner.get(&key).is_some_and(|s| *s == value) { + let value = format!("{kind},0,{sender},{body}\n"); + if inner.get(kind).is_some_and(|s| *s == value) { return false; } - inner.insert(key, value); + inner.insert(kind.to_owned(), value); self.last.store(timestamp_us(), Ordering::Relaxed); true } @@ -86,9 +85,7 @@ impl State { let mut parts = line.splitn(4, ','); let Some(kind) = parts.next() else { continue }; let Some(_) = parts.next() else { continue }; - let Some(severity) = parts.next().and_then(|s| s.parse::().ok()) else { continue }; - let Some(body) = parts.next() else { continue }; - inner.insert(format!("{}_{severity}_{kind}", Body::tab(kind, body)), mem::take(&mut line)); + inner.insert(kind.to_owned(), mem::take(&mut line)); } let clients = CLIENTS.read(); diff --git a/yazi-dds/src/stream.rs b/yazi-dds/src/stream.rs index 3ae7b9c1..2262c66c 100644 --- a/yazi-dds/src/stream.rs +++ b/yazi-dds/src/stream.rs @@ -49,11 +49,12 @@ impl Stream { #[cfg(unix)] fn socket_file() -> std::path::PathBuf { + use std::env::temp_dir; + use uzers::Users; - use yazi_shared::Xdg; use crate::USERS_CACHE; - Xdg::cache_dir().join(format!(".dds-{}.sock", USERS_CACHE.get_current_uid())) + temp_dir().join(format!(".yazi_dds-{}.sock", USERS_CACHE.get_current_uid())) } } diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 6f7112fc..f243a0ba 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -13,7 +13,7 @@ default = [ "vendored-lua" ] vendored-lua = [ "mlua/vendored" ] [dependencies] -yazi-adaptor = { path = "../yazi-adaptor", version = "0.2.5" } +yazi-adapter = { path = "../yazi-adapter", version = "0.2.5" } yazi-boot = { path = "../yazi-boot", version = "0.2.5" } yazi-config = { path = "../yazi-config", version = "0.2.5" } yazi-core = { path = "../yazi-core", version = "0.2.5" } @@ -23,17 +23,17 @@ yazi-proxy = { path = "../yazi-proxy", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -anyhow = "1.0.82" +anyhow = "1.0.86" better-panic = "0.3.0" crossterm = { version = "0.27.0", features = [ "event-stream" ] } fdlimit = "0.3.0" futures = "0.3.30" -mlua = { version = "0.9.7", features = [ "lua54" ] } -ratatui = "=0.26.1" +mlua = { version = "0.9.9", features = [ "lua54" ] } +ratatui = "0.27.0" scopeguard = "1.2.0" syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } -tokio = { version = "1.37.0", features = [ "full" ] } -tokio-util = "0.7.10" +tokio = { version = "1.38.0", features = [ "full" ] } +tokio-util = "0.7.11" # Logging tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } @@ -41,7 +41,7 @@ tracing-appender = "0.2.3" tracing-subscriber = "0.3.18" [target."cfg(unix)".dependencies] -libc = "0.2.153" +libc = "0.2.155" signal-hook-tokio = { version = "0.3.1", features = [ "futures-v0_3" ] } [target.'cfg(all(not(target_os = "macos"), not(target_os = "windows")))'.dependencies] @@ -50,3 +50,7 @@ tikv-jemallocator = "0.5.4" [[bin]] name = "yazi" path = "src/main.rs" + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/yazi-{ target }{ archive-suffix }" +bin-dir = "yazi-{ target }/{ bin }{ binary-ext }" diff --git a/yazi-fm/src/app/app.rs b/yazi-fm/src/app/app.rs index f69da7e3..f28912bc 100644 --- a/yazi-fm/src/app/app.rs +++ b/yazi-fm/src/app/app.rs @@ -4,9 +4,9 @@ use anyhow::Result; use crossterm::event::KeyEvent; use yazi_config::keymap::Key; use yazi_core::input::InputMode; -use yazi_shared::{emit, event::{Cmd, Event, NEED_RENDER}, term::Term, Layer}; +use yazi_shared::{emit, event::{Cmd, Event, NEED_RENDER}, Layer}; -use crate::{lives::Lives, Ctx, Executor, Router, Signals}; +use crate::{lives::Lives, Ctx, Executor, Router, Signals, Term}; pub(crate) struct App { pub(crate) cx: Ctx, @@ -56,6 +56,7 @@ impl App { Event::Seq(cmds, layer) => self.dispatch_seq(cmds, layer), Event::Render => self.dispatch_render(), Event::Key(key) => self.dispatch_key(key), + Event::Mouse(mouse) => self.mouse(mouse), Event::Resize => self.resize(()), Event::Paste(str) => self.dispatch_paste(str), Event::Quit(opt) => self.quit(opt), diff --git a/yazi-fm/src/app/commands/mod.rs b/yazi-fm/src/app/commands/mod.rs index 72d279a4..4cc55b54 100644 --- a/yazi-fm/src/app/commands/mod.rs +++ b/yazi-fm/src/app/commands/mod.rs @@ -1,4 +1,5 @@ mod accept_payload; +mod mouse; mod notify; mod plugin; mod quit; diff --git a/yazi-fm/src/app/commands/mouse.rs b/yazi-fm/src/app/commands/mouse.rs new file mode 100644 index 00000000..5e786efc --- /dev/null +++ b/yazi-fm/src/app/commands/mouse.rs @@ -0,0 +1,65 @@ +use crossterm::event::{MouseEvent, MouseEventKind}; +use mlua::Table; +use ratatui::layout::{Position, Rect}; +use tracing::error; +use yazi_config::{LAYOUT, MANAGER}; +use yazi_plugin::{bindings::Cast, LUA}; + +use crate::{app::App, components, lives::Lives}; + +pub struct Opt { + event: MouseEvent, +} + +impl From for Opt { + fn from(event: MouseEvent) -> Self { Self { event } } +} + +impl App { + pub(crate) fn mouse(&mut self, opt: impl Into) { + let event = (opt.into() as Opt).event; + + let layout = LAYOUT.load(); + let position = Position { x: event.column, y: event.row }; + + if matches!(event.kind, MouseEventKind::Moved | MouseEventKind::Drag(_)) { + self.mouse_do(crate::Root::mouse, event, None); + return; + } + + if layout.current.contains(position) { + self.mouse_do(components::Current::mouse, event, Some(layout.current)); + } else if layout.preview.contains(position) { + self.mouse_do(components::Preview::mouse, event, Some(layout.preview)); + } else if layout.parent.contains(position) { + self.mouse_do(components::Parent::mouse, event, Some(layout.parent)); + } else if layout.header.contains(position) { + self.mouse_do(components::Header::mouse, event, Some(layout.header)); + } else if layout.status.contains(position) { + self.mouse_do(components::Status::mouse, event, Some(layout.status)); + } + } + + fn mouse_do( + &self, + f: impl FnOnce(MouseEvent) -> mlua::Result<()>, + mut event: MouseEvent, + rect: Option, + ) { + if matches!(event.kind, MouseEventKind::Down(_) if MANAGER.mouse_events.draggable()) { + let evt = yazi_plugin::bindings::MouseEvent::cast(&LUA, event); + if let (Ok(evt), Ok(root)) = (evt, LUA.globals().raw_get::<_, Table>("Root")) { + root.raw_set("drag_start", evt).ok(); + } + } + + if let Some(rect) = rect { + event.row -= rect.y; + event.column -= rect.x; + } + + if let Err(e) = Lives::scope(&self.cx, move |_| f(event)) { + error!("{:?}", e); + } + } +} diff --git a/yazi-fm/src/app/commands/quit.rs b/yazi-fm/src/app/commands/quit.rs index f7b7ec3f..f83bc264 100644 --- a/yazi-fm/src/app/commands/quit.rs +++ b/yazi-fm/src/app/commands/quit.rs @@ -1,9 +1,9 @@ use std::ffi::OsString; use yazi_boot::ARGS; -use yazi_shared::{event::EventQuit, term::Term}; +use yazi_shared::event::EventQuit; -use crate::app::App; +use crate::{app::App, Term}; impl App { pub(crate) fn quit(&mut self, opt: EventQuit) -> ! { diff --git a/yazi-fm/src/app/commands/render.rs b/yazi-fm/src/app/commands/render.rs index e62f50b4..5ed72eb0 100644 --- a/yazi-fm/src/app/commands/render.rs +++ b/yazi-fm/src/app/commands/render.rs @@ -1,6 +1,8 @@ use std::{io::{stderr, BufWriter}, sync::atomic::Ordering}; +use crossterm::{execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; use ratatui::{backend::{Backend, CrosstermBackend}, buffer::Buffer, CompletedFrame}; +use scopeguard::defer; use yazi_plugin::elements::COLLISION; use crate::{app::App, lives::Lives, root::Root}; @@ -11,6 +13,9 @@ impl App { return; }; + queue!(stderr(), BeginSynchronizedUpdate).ok(); + defer! { execute!(stderr(), EndSynchronizedUpdate).ok(); } + let collision = COLLISION.swap(false, Ordering::Relaxed); let frame = term .draw(|f| { diff --git a/yazi-fm/src/app/commands/resume.rs b/yazi-fm/src/app/commands/resume.rs index fe6ebf68..4f9ca1a2 100644 --- a/yazi-fm/src/app/commands/resume.rs +++ b/yazi-fm/src/app/commands/resume.rs @@ -1,6 +1,6 @@ -use yazi_shared::{event::Cmd, term::Term}; +use yazi_shared::event::Cmd; -use crate::app::App; +use crate::{app::App, Term}; impl App { pub(crate) fn resume(&mut self, _: Cmd) { diff --git a/yazi-fm/src/app/commands/update_notify.rs b/yazi-fm/src/app/commands/update_notify.rs index 4da856c9..4729ead2 100644 --- a/yazi-fm/src/app/commands/update_notify.rs +++ b/yazi-fm/src/app/commands/update_notify.rs @@ -1,12 +1,13 @@ use crossterm::terminal::WindowSize; use ratatui::layout::Rect; -use yazi_shared::{event::Cmd, term::Term}; +use yazi_adapter::Dimension; +use yazi_shared::event::Cmd; use crate::{app::App, notify}; impl App { pub(crate) fn update_notify(&mut self, cmd: Cmd) { - let WindowSize { rows, columns, .. } = Term::size(); + let WindowSize { rows, columns, .. } = Dimension::available(); let area = notify::Layout::available(Rect { x: 0, y: 0, width: columns, height: rows }); diff --git a/yazi-fm/src/completion/completion.rs b/yazi-fm/src/completion/completion.rs index 7dee9d63..75d2b6bb 100644 --- a/yazi-fm/src/completion/completion.rs +++ b/yazi-fm/src/completion/completion.rs @@ -1,6 +1,7 @@ use std::path::MAIN_SEPARATOR; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, List, ListItem, Widget}}; +use yazi_adapter::Dimension; use yazi_config::{popup::{Offset, Position}, THEME}; use crate::Ctx; @@ -40,7 +41,7 @@ impl<'a> Widget for Completion<'a> { .collect(); let input_area = self.cx.area(&self.cx.input.position); - let mut area = Position::sticky(input_area, Offset { + let mut area = Position::sticky(Dimension::available(), input_area, Offset { x: 1, y: 0, width: input_area.width.saturating_sub(2), diff --git a/yazi-fm/src/components/current.rs b/yazi-fm/src/components/current.rs new file mode 100644 index 00000000..a487047c --- /dev/null +++ b/yazi-fm/src/components/current.rs @@ -0,0 +1,23 @@ +use crossterm::event::MouseEventKind; +use mlua::{Table, TableExt}; +use yazi_plugin::{bindings::{Cast, MouseEvent}, LUA}; + +pub(crate) struct Current; + +impl Current { + pub(crate) fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> { + let evt = MouseEvent::cast(&LUA, event)?; + let comp: Table = LUA.globals().raw_get("Current")?; + + match event.kind { + MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?, + MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?, + MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?, + MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?, + MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?, + MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?, + _ => (), + } + Ok(()) + } +} diff --git a/yazi-fm/src/components/header.rs b/yazi-fm/src/components/header.rs index 12dcca5d..67ab8216 100644 --- a/yazi-fm/src/components/header.rs +++ b/yazi-fm/src/components/header.rs @@ -1,7 +1,8 @@ +use crossterm::event::MouseEventKind; use mlua::{Table, TableExt}; use ratatui::{buffer::Buffer, widgets::Widget}; use tracing::error; -use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA}; +use yazi_plugin::{bindings::{Cast, MouseEvent}, elements::{render_widgets, Rect}, LUA}; pub(crate) struct Header; @@ -18,3 +19,21 @@ impl Widget for Header { } } } + +impl Header { + pub(crate) fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> { + let evt = MouseEvent::cast(&LUA, event)?; + let comp: Table = LUA.globals().raw_get("Header")?; + + match event.kind { + MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?, + MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?, + MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?, + MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?, + MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?, + MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?, + _ => (), + } + Ok(()) + } +} diff --git a/yazi-fm/src/components/mod.rs b/yazi-fm/src/components/mod.rs index 3549f1df..1898af85 100644 --- a/yazi-fm/src/components/mod.rs +++ b/yazi-fm/src/components/mod.rs @@ -1,13 +1,17 @@ #![allow(clippy::module_inception)] +mod current; mod header; mod manager; +mod parent; mod preview; mod progress; mod status; +pub(super) use current::*; pub(super) use header::*; pub(super) use manager::*; +pub(super) use parent::*; pub(super) use preview::*; pub(super) use progress::*; pub(super) use status::*; diff --git a/yazi-fm/src/components/parent.rs b/yazi-fm/src/components/parent.rs new file mode 100644 index 00000000..86adf246 --- /dev/null +++ b/yazi-fm/src/components/parent.rs @@ -0,0 +1,23 @@ +use crossterm::event::MouseEventKind; +use mlua::{Table, TableExt}; +use yazi_plugin::{bindings::{Cast, MouseEvent}, LUA}; + +pub(crate) struct Parent; + +impl Parent { + pub(crate) fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> { + let evt = MouseEvent::cast(&LUA, event)?; + let comp: Table = LUA.globals().raw_get("Parent")?; + + match event.kind { + MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?, + MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?, + MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?, + MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?, + MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?, + MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?, + _ => (), + } + Ok(()) + } +} diff --git a/yazi-fm/src/components/preview.rs b/yazi-fm/src/components/preview.rs index 897a9f13..d6ae62d0 100644 --- a/yazi-fm/src/components/preview.rs +++ b/yazi-fm/src/components/preview.rs @@ -1,4 +1,7 @@ +use crossterm::event::MouseEventKind; +use mlua::{Table, TableExt}; use ratatui::{buffer::Buffer, widgets::Widget}; +use yazi_plugin::{bindings::{Cast, MouseEvent}, LUA}; use crate::Ctx; @@ -9,6 +12,22 @@ pub(crate) struct Preview<'a> { impl<'a> Preview<'a> { #[inline] pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } + + pub(crate) fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> { + let evt = MouseEvent::cast(&LUA, event)?; + let comp: Table = LUA.globals().raw_get("Preview")?; + + match event.kind { + MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?, + MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?, + MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?, + MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?, + MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?, + MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?, + _ => (), + } + Ok(()) + } } impl Widget for Preview<'_> { diff --git a/yazi-fm/src/components/status.rs b/yazi-fm/src/components/status.rs index dc0e3e0a..b9a1fe5e 100644 --- a/yazi-fm/src/components/status.rs +++ b/yazi-fm/src/components/status.rs @@ -1,7 +1,8 @@ +use crossterm::event::MouseEventKind; use mlua::{Table, TableExt}; use ratatui::widgets::Widget; use tracing::error; -use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA}; +use yazi_plugin::{bindings::{Cast, MouseEvent}, elements::{render_widgets, Rect}, LUA}; pub(crate) struct Status; @@ -18,3 +19,21 @@ impl Widget for Status { } } } + +impl Status { + pub(crate) fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> { + let evt = MouseEvent::cast(&LUA, event)?; + let comp: Table = LUA.globals().raw_get("Status")?; + + match event.kind { + MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?, + MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?, + MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?, + MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?, + MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?, + MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?, + _ => (), + } + Ok(()) + } +} diff --git a/yazi-fm/src/context.rs b/yazi-fm/src/context.rs index 3c12b7d5..0fec1dd8 100644 --- a/yazi-fm/src/context.rs +++ b/yazi-fm/src/context.rs @@ -1,4 +1,5 @@ use ratatui::layout::Rect; +use yazi_adapter::Dimension; use yazi_config::popup::{Origin, Position}; use yazi_core::{completion::Completion, help::Help, input::Input, manager::Manager, notify::Notify, select::Select, tasks::Tasks, which::Which}; @@ -28,16 +29,17 @@ impl Ctx { } pub fn area(&self, position: &Position) -> Rect { + let ws = Dimension::available(); if position.origin != Origin::Hovered { - return position.rect(); + return position.rect(ws); } if let Some(r) = self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url)) { - Position::sticky(r, position.offset) + Position::sticky(ws, r, position.offset) } else { - Position::new(Origin::TopCenter, position.offset).rect() + Position::new(Origin::TopCenter, position.offset).rect(ws) } } diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 2da363c9..dcc9f89b 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -99,6 +99,7 @@ impl<'a> Executor<'a> { on!(MANAGER, unyank); on!(MANAGER, paste, &self.app.cx.tasks); on!(MANAGER, link, &self.app.cx.tasks); + on!(MANAGER, hardlink, &self.app.cx.tasks); on!(MANAGER, remove, &self.app.cx.tasks); on!(MANAGER, remove_do, &self.app.cx.tasks); on!(MANAGER, create); @@ -108,7 +109,6 @@ impl<'a> Executor<'a> { on!(ACTIVE, hidden); on!(ACTIVE, linemode); on!(ACTIVE, search); - on!(ACTIVE, jump); // Filter on!(ACTIVE, filter); @@ -160,7 +160,6 @@ impl<'a> Executor<'a> { on!(open_with); on!(process_exec); - #[allow(clippy::single_match)] match cmd.name.as_str() { // Help "help" => self.app.cx.help.toggle(Layer::Tasks), @@ -183,7 +182,6 @@ impl<'a> Executor<'a> { on!(close); on!(arrow); - #[allow(clippy::single_match)] match cmd.name.as_str() { // Help "help" => self.app.cx.help.toggle(Layer::Select), @@ -234,7 +232,6 @@ impl<'a> Executor<'a> { on!(undo); on!(redo); - #[allow(clippy::single_match)] match cmd.name.as_str() { // Help "help" => self.app.cx.help.toggle(Layer::Input), @@ -263,7 +260,6 @@ impl<'a> Executor<'a> { on!(arrow); on!(filter); - #[allow(clippy::single_match)] match cmd.name.as_str() { "close" => self.app.cx.help.toggle(Layer::Help), // Plugin @@ -286,7 +282,6 @@ impl<'a> Executor<'a> { on!(close); on!(arrow); - #[allow(clippy::single_match)] match cmd.name.as_str() { "close_input" => self.app.cx.input.close(cmd), // Help diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index a0863347..f4ad39cf 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -6,9 +6,8 @@ use syntect::easy::HighlightLines; use yazi_config::THEME; use yazi_core::input::InputMode; use yazi_plugin::external::Highlighter; -use yazi_shared::term::Term; -use crate::Ctx; +use crate::{Ctx, Term}; pub(crate) struct Input<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/lives/config.rs b/yazi-fm/src/lives/config.rs index ccdb5490..aa2f598b 100644 --- a/yazi-fm/src/lives/config.rs +++ b/yazi-fm/src/lives/config.rs @@ -26,6 +26,7 @@ impl Config { reg.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive)); reg.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse)); reg.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); + reg.add_field_method_get("sort_translit", |_, me| Ok(me.sort_translit)); reg.add_field_method_get("linemode", |_, me| Ok(me.linemode.to_owned())); reg.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden)); diff --git a/yazi-fm/src/lives/file.rs b/yazi-fm/src/lives/file.rs index 8037fd22..bb8e28c5 100644 --- a/yazi-fm/src/lives/file.rs +++ b/yazi-fm/src/lives/file.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use mlua::{AnyUserData, IntoLua, Lua, UserDataFields, UserDataMethods}; use yazi_config::THEME; -use yazi_plugin::{bindings::{Cast, Cha, Icon, Range}, elements::Style, url::Url}; +use yazi_plugin::{bindings::Range, elements::Style}; use yazi_shared::MIME_DIR; use super::{CtxRef, SCOPE}; @@ -19,6 +19,10 @@ impl Deref for File { fn deref(&self) -> &Self::Target { &self.folder().files[self.idx] } } +impl AsRef for File { + fn as_ref(&self) -> &yazi_shared::fs::File { self } +} + impl File { #[inline] pub(super) fn make( @@ -31,16 +35,9 @@ impl File { pub(super) fn register(lua: &Lua) -> mlua::Result<()> { lua.register_userdata_type::(|reg| { - reg.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); - reg.add_field_method_get("url", |lua, me| Url::cast(lua, me.url.clone())); - reg.add_field_method_get("cha", |lua, me| Cha::cast(lua, me.cha)); - reg.add_field_method_get("link_to", |lua, me| { - me.link_to.as_ref().cloned().map(|u| Url::cast(lua, u)).transpose() - }); + yazi_plugin::file::File::register_with(reg); - reg.add_field_method_get("name", |lua, me| { - me.url.file_name().map(|n| lua.create_string(n.as_encoded_bytes())).transpose() - }); + reg.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); reg.add_method("size", |_, me, ()| { Ok(if me.is_dir() { me.folder().files.sizes.get(&me.url).copied() } else { Some(me.len) }) }); @@ -57,19 +54,6 @@ impl File { p.next_back(); Some(lua.create_string(p.as_path().as_os_str().as_encoded_bytes())).transpose() }); - reg.add_method("icon", |lua, me, ()| { - use yazi_shared::theme::IconCache; - - match me.icon.get() { - IconCache::Missing => { - let matched = THEME.icons.iter().find(|&i| i.matches(me)); - me.icon.set(matched.map_or(IconCache::Undefined, |i| IconCache::Icon(i))); - matched.map(|i| Icon::cast(lua, i)).transpose() - } - IconCache::Undefined => Ok(None), - IconCache::Icon(cached) => Some(Icon::cast(lua, cached)).transpose(), - } - }); reg.add_method("style", |lua, me, ()| { let cx = lua.named_registry_value::("cx")?; let mime = diff --git a/yazi-fm/src/lives/filter.rs b/yazi-fm/src/lives/filter.rs index 686465fa..79c9119c 100644 --- a/yazi-fm/src/lives/filter.rs +++ b/yazi-fm/src/lives/filter.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use mlua::{AnyUserData, Lua}; +use mlua::{AnyUserData, Lua, MetaMethod, UserDataMethods}; use super::SCOPE; @@ -21,8 +21,8 @@ impl Filter { } pub(super) fn register(lua: &Lua) -> mlua::Result<()> { - lua.register_userdata_type::(|_| {})?; - - Ok(()) + lua.register_userdata_type::(|reg| { + reg.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.to_string())); + }) } } diff --git a/yazi-fm/src/lives/finder.rs b/yazi-fm/src/lives/finder.rs new file mode 100644 index 00000000..1907064d --- /dev/null +++ b/yazi-fm/src/lives/finder.rs @@ -0,0 +1,26 @@ +use std::ops::Deref; + +use mlua::{AnyUserData, Lua}; + +use super::SCOPE; + +pub(super) struct Finder { + inner: *const yazi_core::tab::Finder, +} + +impl Deref for Finder { + type Target = yazi_core::tab::Finder; + + fn deref(&self) -> &Self::Target { unsafe { &*self.inner } } +} + +impl Finder { + #[inline] + pub(super) fn make(inner: &yazi_core::tab::Finder) -> mlua::Result> { + SCOPE.create_any_userdata(Self { inner }) + } + + pub(super) fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|_| {}) + } +} diff --git a/yazi-fm/src/lives/lives.rs b/yazi-fm/src/lives/lives.rs index 7fd60203..7380fc47 100644 --- a/yazi-fm/src/lives/lives.rs +++ b/yazi-fm/src/lives/lives.rs @@ -19,6 +19,7 @@ impl Lives { super::File::register(&LUA)?; super::Files::register(&LUA)?; super::Filter::register(&LUA)?; + super::Finder::register(&LUA)?; super::Folder::register(&LUA)?; super::Mode::register(&LUA)?; super::Preview::register(&LUA)?; diff --git a/yazi-fm/src/lives/mod.rs b/yazi-fm/src/lives/mod.rs index edbf3b9d..c8ccc771 100644 --- a/yazi-fm/src/lives/mod.rs +++ b/yazi-fm/src/lives/mod.rs @@ -4,6 +4,7 @@ mod config; mod file; mod files; mod filter; +mod finder; mod folder; mod iter; mod lives; @@ -19,6 +20,7 @@ use config::*; use file::*; use files::*; use filter::*; +use finder::*; use folder::*; use iter::*; pub(super) use lives::*; diff --git a/yazi-fm/src/lives/tab.rs b/yazi-fm/src/lives/tab.rs index 6451082d..6f64c371 100644 --- a/yazi-fm/src/lives/tab.rs +++ b/yazi-fm/src/lives/tab.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use mlua::{AnyUserData, Lua, UserDataFields, UserDataMethods}; -use super::{Config, Folder, Mode, Preview, Selected, SCOPE}; +use super::{Config, Finder, Folder, Mode, Preview, Selected, SCOPE}; pub(super) struct Tab { inner: *const yazi_core::tab::Tab, @@ -44,6 +44,7 @@ impl Tab { reg.add_field_method_get("selected", |_, me| Selected::make(&me.selected)); reg.add_field_method_get("preview", |_, me| Preview::make(me)); + reg.add_field_method_get("finder", |_, me| me.finder.as_ref().map(Finder::make).transpose()); })?; Ok(()) diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index def019fb..93d67543 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -21,6 +21,7 @@ mod router; mod select; mod signals; mod tasks; +mod term; mod which; use context::*; @@ -31,6 +32,7 @@ use panic::*; use root::*; use router::*; use signals::*; +use term::*; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -43,7 +45,7 @@ async fn main() -> anyhow::Result<()> { yazi_config::init()?; - yazi_adaptor::init(); + yazi_adapter::init(); yazi_boot::init(); diff --git a/yazi-fm/src/notify/layout.rs b/yazi-fm/src/notify/layout.rs index 068011ec..ee4cbfad 100644 --- a/yazi-fm/src/notify/layout.rs +++ b/yazi-fm/src/notify/layout.rs @@ -1,7 +1,5 @@ use ratatui::{buffer::Buffer, layout::{self, Constraint, Offset, Rect}, widgets::{Block, BorderType, Paragraph, Widget, Wrap}}; -use yazi_config::THEME; use yazi_core::notify::Message; -use yazi_proxy::options::NotifyLevel; use crate::Ctx; @@ -49,12 +47,6 @@ impl<'a> Widget for Layout<'a> { let tile = Self::tile(available, ¬ify.messages[..limit]); for (i, m) in notify.messages.iter().enumerate().take(limit) { - let (icon, style) = match m.level { - NotifyLevel::Info => (&THEME.notify.icon_info, THEME.notify.title_info), - NotifyLevel::Warn => (&THEME.notify.icon_warn, THEME.notify.title_warn), - NotifyLevel::Error => (&THEME.notify.icon_error, THEME.notify.title_error), - }; - let mut rect = tile[i].offset(Offset { x: (100 - m.percent) as i32 * tile[i].width as i32 / 100, y: 0 }); rect.width -= rect.x - tile[i].x; @@ -65,9 +57,9 @@ impl<'a> Widget for Layout<'a> { .block( Block::bordered() .border_type(BorderType::Rounded) - .title(format!("{icon} {}", m.title)) - .title_style(style) - .border_style(style), + .title(format!("{} {}", m.level.icon(), m.title)) + .title_style(*m.level.style()) + .border_style(*m.level.style()), ) .render(rect, buf); } diff --git a/yazi-fm/src/panic.rs b/yazi-fm/src/panic.rs index f0773886..ca8cedfe 100644 --- a/yazi-fm/src/panic.rs +++ b/yazi-fm/src/panic.rs @@ -1,4 +1,4 @@ -use yazi_shared::term::Term; +use crate::Term; pub(super) struct Panic; diff --git a/yazi-fm/src/root.rs b/yazi-fm/src/root.rs index f5dfdda0..3ffeaaf8 100644 --- a/yazi-fm/src/root.rs +++ b/yazi-fm/src/root.rs @@ -1,4 +1,7 @@ +use crossterm::event::MouseEventKind; +use mlua::{Table, TableExt}; use ratatui::{buffer::Buffer, layout::{Constraint, Layout, Rect}, widgets::Widget}; +use yazi_plugin::{bindings::{Cast, MouseEvent}, LUA}; use super::{completion, input, select, tasks, which}; use crate::{components, help, Ctx}; @@ -47,3 +50,17 @@ impl<'a> Widget for Root<'a> { } } } + +impl Root<'_> { + pub(super) fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> { + let evt = MouseEvent::cast(&LUA, event)?; + let comp: Table = LUA.globals().raw_get("Root")?; + + match event.kind { + MouseEventKind::Moved => comp.call_method("move", evt)?, + MouseEventKind::Drag(_) => comp.call_method("drag", evt)?, + _ => (), + } + Ok(()) + } +} diff --git a/yazi-fm/src/signals.rs b/yazi-fm/src/signals.rs index 8d1e7193..bd86a984 100644 --- a/yazi-fm/src/signals.rs +++ b/yazi-fm/src/signals.rs @@ -3,6 +3,7 @@ use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEvent, KeyEventK use futures::StreamExt; use tokio::{select, task::JoinHandle}; use tokio_util::sync::CancellationToken; +use yazi_config::MANAGER; use yazi_shared::event::Event; pub(super) struct Signals { @@ -50,7 +51,7 @@ impl Signals { Ok(tokio::spawn(async move { while let Some(signal) = signals.next().await { match signal { - SIGHUP | SIGTERM | SIGQUIT | SIGINT => { + SIGHUP | SIGTERM | SIGQUIT => { Event::Quit(Default::default()).emit(); } SIGCONT if HIDER.try_acquire().is_ok() => AppProxy::resume(), @@ -71,6 +72,11 @@ impl Signals { Some(Ok(event)) = reader.next() => { match event { CrosstermEvent::Key(key @ KeyEvent { kind: KeyEventKind::Press, .. }) => Event::Key(key).emit(), + CrosstermEvent::Mouse(mouse) => { + if MANAGER.mouse_events.contains(mouse.kind.into()) { + Event::Mouse(mouse).emit(); + } + }, CrosstermEvent::Paste(str) => Event::Paste(str).emit(), CrosstermEvent::Resize(..) => Event::Resize.emit(), _ => {}, diff --git a/yazi-shared/src/term/term.rs b/yazi-fm/src/term.rs similarity index 54% rename from yazi-shared/src/term/term.rs rename to yazi-fm/src/term.rs index 3be23370..f2950cdd 100644 --- a/yazi-shared/src/term/term.rs +++ b/yazi-fm/src/term.rs @@ -1,19 +1,21 @@ -use std::{io::{self, stderr, BufWriter, Stderr, Write}, mem, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, Ordering}}; +use std::{io::{self, stderr, BufWriter, Stderr}, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, Ordering}}; use anyhow::Result; -use crossterm::{cursor::{RestorePosition, SavePosition}, event::{DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, SetTitle, WindowSize}}; +use crossterm::{cursor::{RestorePosition, SavePosition}, event::{DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle}}; use ratatui::{backend::CrosstermBackend, buffer::Buffer, layout::Rect, CompletedFrame, Frame, Terminal}; +use yazi_adapter::Emulator; +use yazi_config::INPUT; static CSI_U: AtomicBool = AtomicBool::new(false); -pub struct Term { +pub(super) struct Term { inner: Terminal>>, last_area: Rect, last_buffer: Buffer, } impl Term { - pub fn start() -> Result { + pub(super) fn start() -> Result { let mut term = Self { inner: Terminal::new(CrosstermBackend::new(BufWriter::new(stderr())))?, last_area: Default::default(), @@ -25,13 +27,13 @@ impl Term { BufWriter::new(stderr()), EnterAlternateScreen, EnableBracketedPaste, - EnableFocusChange, + mouse::SetMouse(true), SavePosition, Print("\x1b[?u\x1b[c"), RestorePosition )?; - let resp = futures::executor::block_on(Self::read_until_da1()); + let resp = futures::executor::block_on(Emulator::read_until_da1()); if resp.is_ok_and(|s| s.contains("\x1b[?0u")) { queue!( stderr(), @@ -56,7 +58,7 @@ impl Term { execute!( stderr(), - DisableFocusChange, + mouse::SetMouse(false), DisableBracketedPaste, LeaveAlternateScreen, crossterm::cursor::SetCursorStyle::DefaultUserShape @@ -66,7 +68,7 @@ impl Term { Ok(disable_raw_mode()?) } - pub fn goodbye(f: impl FnOnce() -> bool) -> ! { + pub(super) fn goodbye(f: impl FnOnce() -> bool) -> ! { if CSI_U.swap(false, Ordering::Relaxed) { execute!(stderr(), PopKeyboardEnhancementFlags).ok(); } @@ -74,7 +76,7 @@ impl Term { execute!( stderr(), SetTitle(""), - DisableFocusChange, + mouse::SetMouse(false), DisableBracketedPaste, LeaveAlternateScreen, crossterm::cursor::SetCursorStyle::DefaultUserShape, @@ -87,7 +89,7 @@ impl Term { std::process::exit(f() as i32); } - pub fn draw(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result { + pub(super) fn draw(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result { let last = self.inner.draw(f)?; self.last_area = last.area; @@ -95,7 +97,7 @@ impl Term { Ok(last) } - pub fn draw_partial(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result { + pub(super) fn draw_partial(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result { self.inner.draw(|frame| { let buffer = frame.buffer_mut(); for y in self.last_area.top()..self.last_area.bottom() { @@ -111,43 +113,28 @@ impl Term { } #[inline] - pub fn can_partial(&mut self) -> bool { + pub(super) fn can_partial(&mut self) -> bool { self.inner.autoresize().is_ok() && self.last_area == self.inner.get_frame().size() } - pub fn size() -> WindowSize { - let mut size = WindowSize { rows: 0, columns: 0, width: 0, height: 0 }; - if let Ok(s) = crossterm::terminal::window_size() { - _ = mem::replace(&mut size, s); - } - - if size.rows == 0 || size.columns == 0 { - if let Ok(s) = crossterm::terminal::size() { - size.columns = s.0; - size.rows = s.1; - } - } - - // TODO: Use `CSI 14 t` to get the actual size of the terminal - // if size.width == 0 || size.height == 0 {} - - size + #[inline] + pub(super) fn set_cursor_block() -> Result<()> { + use crossterm::cursor::SetCursorStyle; + Ok(if INPUT.cursor_blink { + queue!(stderr(), SetCursorStyle::BlinkingBlock)? + } else { + queue!(stderr(), SetCursorStyle::SteadyBlock)? + }) } #[inline] - pub fn ratio() -> Option<(f64, f64)> { - let s = Self::size(); - if s.width == 0 || s.height == 0 { - return None; - } - Some((f64::from(s.width) / f64::from(s.columns), f64::from(s.height) / f64::from(s.rows))) - } - - #[inline] - pub fn clear(w: &mut impl Write) -> Result<()> { - queue!(w, Clear(ClearType::All))?; - writeln!(w)?; - Ok(w.flush()?) + pub(super) fn set_cursor_bar() -> Result<()> { + use crossterm::cursor::SetCursorStyle; + Ok(if INPUT.cursor_blink { + queue!(stderr(), SetCursorStyle::BlinkingBar)? + } else { + queue!(stderr(), SetCursorStyle::SteadyBar)? + }) } } @@ -164,3 +151,43 @@ impl Deref for Term { impl DerefMut for Term { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } + +// --- Mouse support +mod mouse { + use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; + use yazi_config::MANAGER; + + pub struct SetMouse(pub bool); + + impl crossterm::Command for SetMouse { + fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { + if MANAGER.mouse_events.is_empty() { + Ok(()) + } else if self.0 { + EnableMouseCapture.write_ansi(f) + } else { + DisableMouseCapture.write_ansi(f) + } + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + if MANAGER.mouse_events.is_empty() { + Ok(()) + } else if self.0 { + EnableMouseCapture.execute_winapi() + } else { + DisableMouseCapture.execute_winapi() + } + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + if self.0 { + EnableMouseCapture.is_ansi_code_supported() + } else { + DisableMouseCapture.is_ansi_code_supported() + } + } + } +} diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 55033878..f8d96693 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -13,7 +13,7 @@ default = [ "vendored-lua" ] vendored-lua = [ "mlua/vendored" ] [dependencies] -yazi-adaptor = { path = "../yazi-adaptor", version = "0.2.5" } +yazi-adapter = { path = "../yazi-adapter", version = "0.2.5" } yazi-boot = { path = "../yazi-boot", version = "0.2.5" } yazi-config = { path = "../yazi-config", version = "0.2.5" } yazi-dds = { path = "../yazi-dds", version = "0.2.5" } @@ -21,25 +21,29 @@ yazi-proxy = { path = "../yazi-proxy", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -ansi-to-tui = "3.1.0" -anyhow = "1.0.82" +ansi-to-tui = "=3.1.0" +anyhow = "1.0.86" +base64 = "0.22.1" +crossterm = "0.27.0" futures = "0.3.30" md-5 = "0.10.6" -mlua = { version = "0.9.7", features = [ "lua54", "serialize", "macros", "async" ] } -parking_lot = "0.12.1" -ratatui = "=0.26.1" -serde = "1.0.198" -serde_json = "1.0.116" +mlua = { version = "0.9.9", features = [ "lua54", "serialize", "macros", "async" ] } +parking_lot = "0.12.3" +ratatui = "0.27.0" shell-escape = "0.1.5" shell-words = "1.1.0" syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } -tokio = { version = "1.37.0", features = [ "full" ] } -tokio-util = "0.7.10" -unicode-width = "0.1.11" +tokio = { version = "1.38.0", features = [ "full" ] } +tokio-stream = "0.1.15" +tokio-util = "0.7.11" +unicode-width = "0.1.13" yazi-prebuild = "0.1.2" # Logging tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } [target."cfg(unix)".dependencies] -uzers = "0.11.3" +uzers = "0.12.0" + +[target."cfg(windows)".dependencies] +clipboard-win = "5.3.1" diff --git a/yazi-plugin/preset/components/current.lua b/yazi-plugin/preset/components/current.lua index 5916ef1b..c79bcb26 100644 --- a/yazi-plugin/preset/components/current.lua +++ b/yazi-plugin/preset/components/current.lua @@ -42,3 +42,23 @@ function Current:render(area) Folder:markers(area, markers), } end + +function Current:click(event, up) + if up or event.is_middle then + return + end + + local f = Folder:by_kind(Folder.CURRENT) + if event.y > #f.window or not f.hovered then + return + end + + ya.manager_emit("arrow", { event.y + f.offset - f.hovered.idx }) + if event.is_right then + ya.manager_emit("open", {}) + end +end + +function Current:scroll(event, step) ya.manager_emit("arrow", { step }) end + +function Current:touch(event, step) end diff --git a/yazi-plugin/preset/components/file.lua b/yazi-plugin/preset/components/file.lua index d0b4a6a4..1e7cabd2 100644 --- a/yazi-plugin/preset/components/file.lua +++ b/yazi-plugin/preset/components/file.lua @@ -24,12 +24,12 @@ function File:highlights(file) end local spans, last = {}, 0 - for _, r in ipairs(highlights) do - if r[1] > last then - spans[#spans + 1] = ui.Span(name:sub(last + 1, r[1])) + for _, h in ipairs(highlights) do + if h[1] > last then + spans[#spans + 1] = ui.Span(name:sub(last + 1, h[1])) end - spans[#spans + 1] = ui.Span(name:sub(r[1] + 1, r[2])):style(THEME.manager.find_keyword) - last = r[2] + spans[#spans + 1] = ui.Span(name:sub(h[1] + 1, h[2])):style(THEME.manager.find_keyword) + last = h[2] end if last < #name then spans[#spans + 1] = ui.Span(name:sub(last + 1)) diff --git a/yazi-plugin/preset/components/folder.lua b/yazi-plugin/preset/components/folder.lua index aaaa7df5..b172d419 100644 --- a/yazi-plugin/preset/components/folder.lua +++ b/yazi-plugin/preset/components/folder.lua @@ -21,6 +21,9 @@ function Folder:linemode(area, files) spans[#spans + 1] = ui.Span(time and os.date("%y-%m-%d %H:%M", time // 1) or "") elseif mode == "permissions" then spans[#spans + 1] = ui.Span(f.cha:permissions() or "") + elseif mode == "owner" then + spans[#spans + 1] = ya.user_name and ui.Span(ya.user_name(f.cha.uid) .. ":" .. ya.group_name(f.cha.gid)) + or ui.Span("") end spans[#spans + 1] = ui.Span(" ") @@ -82,3 +85,8 @@ function Folder:by_kind(kind) return cx.active.preview.folder end end + +function Folder:window(kind) + local folder = self:by_kind(kind) + return folder and folder.window +end diff --git a/yazi-plugin/preset/components/header.lua b/yazi-plugin/preset/components/header.lua index 72cffa03..d973266d 100644 --- a/yazi-plugin/preset/components/header.lua +++ b/yazi-plugin/preset/components/header.lua @@ -3,11 +3,22 @@ Header = { } function Header:cwd(max) - local cwd = cx.active.current.cwd - local readable = ya.readable_path(tostring(cwd)) + local s = ya.readable_path(tostring(cx.active.current.cwd)) .. self:flags() + return ui.Span(ya.truncate(s, { max = max, rtl = true })):style(THEME.manager.cwd) +end - local text = cwd.is_search and string.format("%s (search: %s)", readable, cwd:frag()) or readable - return ui.Span(ya.truncate(text, { max = max, rtl = true })):style(THEME.manager.cwd) +function Header:flags() + local cwd = cx.active.current.cwd + local filter = cx.active.current.files.filter + + local s = cwd.is_search and string.format(" (search: %s", cwd:frag()) or "" + if not filter then + return s == "" and s or s .. ")" + elseif s == "" then + return string.format(" (filter: %s)", tostring(filter)) + else + return string.format("%s, filter: %s)", s, tostring(filter)) + end end function Header:count() @@ -56,26 +67,6 @@ function Header:tabs() return ui.Line(spans) end --- TODO: remove this function after v0.2.5 release -function Header:layout(area) - if not ya.deprecated_header_layout then - ya.deprecated_header_layout = true - ya.notify { - title = "Deprecated API", - content = "`Header:layout()` is deprecated, please apply the latest `Header:render()` in your `init.lua`", - timeout = 5, - level = "warn", - } - end - - self.area = area - - return ui.Layout() - :direction(ui.Layout.HORIZONTAL) - :constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) }) - :split(area) -end - function Header:render(area) self.area = area @@ -86,3 +77,9 @@ function Header:render(area) ui.Paragraph(area, { right }):align(ui.Paragraph.RIGHT), } end + +function Header:click(event, up) end + +function Header:scroll(event, step) end + +function Header:touch(event, step) end diff --git a/yazi-plugin/preset/components/parent.lua b/yazi-plugin/preset/components/parent.lua index 8d76f77f..a60ef3e8 100644 --- a/yazi-plugin/preset/components/parent.lua +++ b/yazi-plugin/preset/components/parent.lua @@ -26,3 +26,20 @@ function Parent:render(area) Folder:markers(area, markers), } end + +function Parent:click(event, up) + if up or not event.is_left then + return + end + + local window = Folder:window(Folder.PARENT) or {} + if window[event.y] then + ya.manager_emit("reveal", { window[event.y].url }) + else + ya.manager_emit("leave", {}) + end +end + +function Parent:scroll(event, step) end + +function Parent:touch(event, step) end diff --git a/yazi-plugin/preset/components/preview.lua b/yazi-plugin/preset/components/preview.lua index dd6b73cc..4bf5866d 100644 --- a/yazi-plugin/preset/components/preview.lua +++ b/yazi-plugin/preset/components/preview.lua @@ -6,3 +6,20 @@ function Preview:render(area) self.area = area return {} end + +function Preview:click(event, up) + if up or not event.is_left then + return + end + + local window = Folder:window(Folder.PREVIEW) or {} + if window[event.y] then + ya.manager_emit("reveal", { window[event.y].url }) + else + ya.manager_emit("enter", {}) + end +end + +function Preview:scroll(event, step) ya.manager_emit("seek", { step }) end + +function Preview:touch(event, step) end diff --git a/yazi-plugin/preset/components/root.lua b/yazi-plugin/preset/components/root.lua new file mode 100644 index 00000000..cccc315e --- /dev/null +++ b/yazi-plugin/preset/components/root.lua @@ -0,0 +1,7 @@ +Root = { + drag_start = ui.Rect.default, +} + +function Root:move(event) end + +function Root:drag(event) end diff --git a/yazi-plugin/preset/components/status.lua b/yazi-plugin/preset/components/status.lua index de3d4014..571b9469 100644 --- a/yazi-plugin/preset/components/status.lua +++ b/yazi-plugin/preset/components/status.lua @@ -121,3 +121,9 @@ function Status:render(area) table.unpack(Progress:render(area, right:width())), } end + +function Status:click(event, up) end + +function Status:scroll(event, step) end + +function Status:touch(event, step) end diff --git a/yazi-plugin/preset/plugins/archive.lua b/yazi-plugin/preset/plugins/archive.lua index e53a7925..3827d754 100644 --- a/yazi-plugin/preset/plugins/archive.lua +++ b/yazi-plugin/preset/plugins/archive.lua @@ -1,9 +1,64 @@ local M = {} function M:peek() - local _, bound = ya.preview_archive(self) - if bound then - ya.manager_emit("peek", { bound, only_if = self.file.url, upper_bound = true }) + local child + if ya.target_os() == "macos" then + child = self:try_spawn("7zz") or self:try_spawn("7z") + else + child = self:try_spawn("7z") or self:try_spawn("7zz") + end + + if not child then + return ya.err("spawn `7z` and `7zz` both commands failed, error code: " .. tostring(self.last_error)) + end + + local limit = self.area.h + local i, icon, names, sizes = 0, nil, {}, {} + repeat + local next, event = child:read_line() + if event ~= 0 then + break + end + + local attr, size, name = next:match("^[-%d]+%s+[:%d]+%s+([.%a]+)%s+(%d+)%s+%d+%s+(.+)[\r\n]+") + if not name then + goto continue + end + + i = i + 1 + if i <= self.skip then + goto continue + end + + icon = File({ + url = Url(name), + cha = Cha { kind = attr:sub(1, 1) == "D" and 1 or 0 }, + }):icon() + + if icon then + names[#names + 1] = ui.Line { ui.Span(" " .. icon.text .. " "):style(icon.style), ui.Span(name) } + else + names[#names + 1] = ui.Line(name) + end + + size = tonumber(size) + if size > 0 then + sizes[#sizes + 1] = ui.Line(string.format(" %s ", ya.readable_size(size))) + else + sizes[#sizes + 1] = ui.Line("") + end + + ::continue:: + until i >= self.skip + limit + + child:start_kill() + if self.skip > 0 and i < self.skip + limit then + ya.manager_emit("peek", { math.max(0, i - limit), only_if = self.file.url, upper_bound = true }) + else + ya.preview_widgets(self, { + ui.Paragraph(self.area, names), + ui.Paragraph(self.area, sizes):align(ui.Paragraph.RIGHT), + }) end end @@ -18,4 +73,12 @@ function M:seek(units) end end +function M:try_spawn(name) + local child, code = Command(name):args({ "l", "-ba", tostring(self.file.url) }):stdout(Command.PIPED):spawn() + if not child then + self.last_error = code + end + return child +end + return M diff --git a/yazi-plugin/preset/plugins/font.lua b/yazi-plugin/preset/plugins/font.lua new file mode 100644 index 00000000..f94f6248 --- /dev/null +++ b/yazi-plugin/preset/plugins/font.lua @@ -0,0 +1,52 @@ +local TEXT = "ABCDEFGHIJKLM\nNOPQRSTUVWXYZ\nabcdefghijklm\nnopqrstuvwxyz\n1234567890\n!$&*()[]{}" + +local M = {} + +function M:peek() + local cache = ya.file_cache(self) + if not cache then + return + end + + if self:preload() == 1 then + ya.image_show(cache, self.area) + ya.preview_widgets(self, {}) + end +end + +function M:seek() end + +function M:preload() + local cache = ya.file_cache(self) + if not cache or fs.cha(cache) then + return 1 + end + + local child, code = Command("magick"):args({ + "-size", + "800x560", + "-gravity", + "center", + "-font", + tostring(self.file.url), + "-pointsize", + "64", + "xc:white", + "-fill", + "black", + "-annotate", + "+0+0", + TEXT, + "JPG:" .. tostring(cache), + }):spawn() + + if not child then + ya.err("spawn `magick` command returns " .. tostring(code)) + return 0 + end + + local status = child:wait() + return status and status.success and 1 or 2 +end + +return M diff --git a/yazi-plugin/preset/plugins/fzf.lua b/yazi-plugin/preset/plugins/fzf.lua index 210bd419..70033f16 100644 --- a/yazi-plugin/preset/plugins/fzf.lua +++ b/yazi-plugin/preset/plugins/fzf.lua @@ -16,8 +16,8 @@ local function entry() local output, err = child:wait_with_output() if not output then return fail("Cannot read `fzf` output, error code %s", err) - elseif not output.status:success() and output.status:code() ~= 130 then - return fail("`fzf` exited with error code %s", output.status:code()) + elseif not output.status.success and output.status.code ~= 130 then + return fail("`fzf` exited with error code %s", output.status.code) end local target = output.stdout:gsub("\n$", "") diff --git a/yazi-plugin/preset/plugins/magick.lua b/yazi-plugin/preset/plugins/magick.lua new file mode 100644 index 00000000..75ba7a38 --- /dev/null +++ b/yazi-plugin/preset/plugins/magick.lua @@ -0,0 +1,43 @@ +local M = {} + +function M:peek() + local cache = ya.file_cache(self) + if not cache then + return + end + + if self:preload() == 1 then + ya.image_show(cache, self.area) + ya.preview_widgets(self, {}) + end +end + +function M:seek() end + +function M:preload() + local cache = ya.file_cache(self) + if not cache or fs.cha(cache) then + return 1 + end + + local child, code = Command("magick"):args({ + "-density", + "200", + tostring(self.file.url), + "-resize", + string.format("%dx%d^", PREVIEW.max_width, PREVIEW.max_height), + "-quality", + tostring(PREVIEW.image_quality), + "JPG:" .. tostring(cache), + }):spawn() + + if not child then + ya.err("spawn `magick` command returns " .. tostring(code)) + return 0 + end + + local status = child:wait() + return status and status.success and 1 or 2 +end + +return M diff --git a/yazi-plugin/preset/plugins/mime.lua b/yazi-plugin/preset/plugins/mime.lua index 33416965..548b07e4 100644 --- a/yazi-plugin/preset/plugins/mime.lua +++ b/yazi-plugin/preset/plugins/mime.lua @@ -9,7 +9,7 @@ local function match_mimetype(s) end end -function M:preload() +function M:fetch() local urls = {} for _, file in ipairs(self.files) do urls[#urls + 1] = tostring(file.url) @@ -59,4 +59,28 @@ function M:preload() return j == #urls and 3 or 2 end +-- TODO: remove this after v0.3 release +local notified = ya.sync(function (state) + if state.notified then + return true + else + state.notified = true + return false + end +end) +function M:preload() + if notified() then + return 1 + end + ya.notify { + title = "Error", + content = [[In Yazi v0.3, the `mime` plugin has been re-classified as a fetcher. Please remove it from the `preloaders` of your yazi.toml + +See https://github.com/sxyazi/yazi/issues/1046 for details.]], + timeout = 20, + level = "error", + } + return 1 +end + return M diff --git a/yazi-plugin/preset/plugins/pdf.lua b/yazi-plugin/preset/plugins/pdf.lua index 4854ba74..de4f4ef8 100644 --- a/yazi-plugin/preset/plugins/pdf.lua +++ b/yazi-plugin/preset/plugins/pdf.lua @@ -34,7 +34,7 @@ function M:preload() if not output then return 0 - elseif not output.status:success() then + elseif not output.status.success then local pages = tonumber(output.stderr:match("the last page %((%d+)%)")) or 0 if self.skip > 0 and pages > 0 then ya.manager_emit("peek", { math.max(0, pages - 1), only_if = self.file.url, upper_bound = true }) diff --git a/yazi-plugin/preset/plugins/session.lua b/yazi-plugin/preset/plugins/session.lua index 6a5fcfdf..c20d94b6 100644 --- a/yazi-plugin/preset/plugins/session.lua +++ b/yazi-plugin/preset/plugins/session.lua @@ -1,6 +1,6 @@ local function setup(_, opts) if opts.sync_yanked then - ps.sub_remote("yank", function(body) ya.manager_emit("update_yanked", { cut = body.cut, urls = body }) end) + ps.sub_remote("@yank", function(body) ya.manager_emit("update_yanked", { cut = body.cut, urls = body }) end) end end diff --git a/yazi-plugin/preset/plugins/video.lua b/yazi-plugin/preset/plugins/video.lua index e8b9fe52..50ea9528 100644 --- a/yazi-plugin/preset/plugins/video.lua +++ b/yazi-plugin/preset/plugins/video.lua @@ -55,7 +55,7 @@ function M:preload() end local status = child:wait() - return status and status:success() and 1 or 2 + return status and status.success and 1 or 2 end return M diff --git a/yazi-plugin/preset/plugins/zoxide.lua b/yazi-plugin/preset/plugins/zoxide.lua index 2a57d87b..a7b89aeb 100644 --- a/yazi-plugin/preset/plugins/zoxide.lua +++ b/yazi-plugin/preset/plugins/zoxide.lua @@ -50,10 +50,12 @@ end local function entry() local st = state() - if st.empty == true then - return fail("No directory history in the database, check out the `zoxide` docs to set it up.") - elseif st.empty == nil and head(st.cwd) < 2 then - set_state(true) + if st.empty == nil then + st.empty = head(st.cwd) < 2 + set_state(st.empty) + end + + if st.empty then return fail("No directory history in the database, check out the `zoxide` docs to set it up.") end @@ -73,8 +75,8 @@ local function entry() local output, err = child:wait_with_output() if not output then return fail("Cannot read `zoxide` output, error code %s", err) - elseif not output.status:success() and output.status:code() ~= 130 then - return fail("`zoxide` exited with error code %s", output.status:code()) + elseif not output.status.success and output.status.code ~= 130 then + return fail("`zoxide` exited with error code %s", output.status.code) end local target = output.stdout:gsub("\n$", "") diff --git a/yazi-plugin/preset/setup.lua b/yazi-plugin/preset/setup.lua index a6adcbb9..bba7d57e 100644 --- a/yazi-plugin/preset/setup.lua +++ b/yazi-plugin/preset/setup.lua @@ -1,3 +1,4 @@ +os.setlocale("") package.path = BOOT.plugin_dir .. "/?.yazi/init.lua;" .. package.path require("dds"):setup() diff --git a/yazi-plugin/src/bindings/file.rs b/yazi-plugin/src/bindings/file.rs deleted file mode 100644 index 9f714658..00000000 --- a/yazi-plugin/src/bindings/file.rs +++ /dev/null @@ -1,29 +0,0 @@ -use mlua::{AnyUserData, Lua, UserDataFields, UserDataRef}; - -use super::{Cast, Cha}; -use crate::url::Url; - -pub type FileRef<'lua> = UserDataRef<'lua, yazi_shared::fs::File>; - -pub struct File; - -impl File { - pub fn register(lua: &Lua) -> mlua::Result<()> { - lua.register_userdata_type::(|reg| { - reg.add_field_method_get("url", |lua, me| Url::cast(lua, me.url.clone())); - reg.add_field_method_get("cha", |lua, me| Cha::cast(lua, me.cha)); - reg.add_field_method_get("link_to", |lua, me| { - me.link_to.as_ref().cloned().map(|u| Url::cast(lua, u)).transpose() - }); - - // Extension - reg.add_field_method_get("name", |lua, me| { - me.url.file_name().map(|n| lua.create_string(n.as_encoded_bytes())).transpose() - }); - }) - } -} - -impl> Cast for File { - fn cast(lua: &Lua, data: T) -> mlua::Result { lua.create_any_userdata(data.into()) } -} diff --git a/yazi-plugin/src/bindings/input.rs b/yazi-plugin/src/bindings/input.rs index fd6b671c..90fccd29 100644 --- a/yazi-plugin/src/bindings/input.rs +++ b/yazi-plugin/src/bindings/input.rs @@ -1,15 +1,23 @@ +use std::pin::Pin; + use mlua::{prelude::LuaUserDataMethods, UserData}; -use tokio::sync::mpsc::UnboundedReceiver; +use tokio::pin; +use tokio_stream::StreamExt; use yazi_shared::InputError; -pub struct InputRx { - inner: UnboundedReceiver>, +pub struct InputRx>> { + inner: T, } -impl InputRx { - pub fn new(inner: UnboundedReceiver>) -> Self { Self { inner } } +impl>> InputRx { + pub fn new(inner: T) -> Self { Self { inner } } - pub fn parse(res: Result) -> (Option, u8) { + pub async fn consume(inner: T) -> (Option, u8) { + pin!(inner); + inner.next().await.map(Self::parse).unwrap_or((None, 0)) + } + + fn parse(res: Result) -> (Option, u8) { match res { Ok(s) => (Some(s), 1), Err(InputError::Canceled(s)) => (Some(s), 2), @@ -19,14 +27,11 @@ impl InputRx { } } -impl UserData for InputRx { +impl> + 'static> UserData for InputRx { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_async_method_mut("recv", |_, me, ()| async move { - let Some(res) = me.inner.recv().await else { - return Ok((None, 0)); - }; - - Ok(Self::parse(res)) + let mut inner = unsafe { Pin::new_unchecked(&mut me.inner) }; + Ok(inner.next().await.map(Self::parse).unwrap_or((None, 0))) }); } } diff --git a/yazi-plugin/src/bindings/mod.rs b/yazi-plugin/src/bindings/mod.rs index 31f2ac4c..a0b7870e 100644 --- a/yazi-plugin/src/bindings/mod.rs +++ b/yazi-plugin/src/bindings/mod.rs @@ -1,20 +1,18 @@ #![allow(clippy::module_inception)] mod bindings; -mod cha; -mod file; mod icon; mod input; +mod mouse; mod permit; mod position; mod range; mod window; pub use bindings::*; -pub use cha::*; -pub use file::*; pub use icon::*; pub use input::*; +pub use mouse::*; pub use permit::*; pub use position::*; pub use range::*; diff --git a/yazi-plugin/src/bindings/mouse.rs b/yazi-plugin/src/bindings/mouse.rs new file mode 100644 index 00000000..a3e7727f --- /dev/null +++ b/yazi-plugin/src/bindings/mouse.rs @@ -0,0 +1,35 @@ +use crossterm::event::MouseButton; +use mlua::{AnyUserData, Lua, UserDataFields}; + +use super::Cast; + +pub struct MouseEvent; + +impl MouseEvent { + pub fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("x", |_, me| Ok(me.column as u32 + 1)); + reg.add_field_method_get("y", |_, me| Ok(me.row as u32 + 1)); + reg.add_field_method_get("is_left", |_, me| { + use crossterm::event::MouseEventKind as K; + Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Left)) + }); + reg.add_field_method_get("is_right", |_, me| { + use crossterm::event::MouseEventKind as K; + Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Right)) + }); + reg.add_field_method_get("is_middle", |_, me| { + use crossterm::event::MouseEventKind as K; + Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Middle)) + }); + })?; + + Ok(()) + } +} + +impl Cast for MouseEvent { + fn cast(lua: &Lua, data: crossterm::event::MouseEvent) -> mlua::Result { + lua.create_any_userdata(data) + } +} diff --git a/yazi-plugin/src/bindings/window.rs b/yazi-plugin/src/bindings/window.rs index c8da6bde..b4d63bbb 100644 --- a/yazi-plugin/src/bindings/window.rs +++ b/yazi-plugin/src/bindings/window.rs @@ -1,5 +1,5 @@ use mlua::{FromLua, UserData}; -use yazi_shared::term::Term; +use yazi_adapter::Dimension; #[derive(Debug, Clone, Copy, FromLua)] pub struct Window { @@ -11,7 +11,7 @@ pub struct Window { impl Default for Window { fn default() -> Self { - let ws = Term::size(); + let ws = Dimension::available(); Self { rows: ws.rows, cols: ws.columns, width: ws.width, height: ws.height } } } diff --git a/yazi-plugin/src/bindings/cha.rs b/yazi-plugin/src/cha/cha.rs similarity index 50% rename from yazi-plugin/src/bindings/cha.rs rename to yazi-plugin/src/cha/cha.rs index 68b81c05..96830bee 100644 --- a/yazi-plugin/src/bindings/cha.rs +++ b/yazi-plugin/src/cha/cha.rs @@ -1,8 +1,11 @@ -use std::time::UNIX_EPOCH; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use mlua::{AnyUserData, Lua, UserDataFields, UserDataMethods}; +use mlua::{AnyUserData, ExternalError, Lua, Table, UserDataFields, UserDataMethods, UserDataRef}; +use yazi_shared::fs::ChaKind; -use super::Cast; +use crate::bindings::Cast; + +pub type UrlRef<'lua> = UserDataRef<'lua, yazi_shared::fs::Cha>; pub struct Cha; @@ -13,10 +16,10 @@ impl Cha { reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden())); reg.add_field_method_get("is_link", |_, me| Ok(me.is_link())); reg.add_field_method_get("is_orphan", |_, me| Ok(me.is_orphan())); - reg.add_field_method_get("is_block_device", |_, me| Ok(me.is_block_device())); - reg.add_field_method_get("is_char_device", |_, me| Ok(me.is_char_device())); + reg.add_field_method_get("is_block", |_, me| Ok(me.is_block())); + reg.add_field_method_get("is_char", |_, me| Ok(me.is_char())); reg.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo())); - reg.add_field_method_get("is_socket", |_, me| Ok(me.is_socket())); + reg.add_field_method_get("is_sock", |_, me| Ok(me.is_sock())); reg.add_field_method_get("is_exec", |_, me| Ok(me.is_exec())); reg.add_field_method_get("is_sticky", |_, me| Ok(me.is_sticky())); @@ -24,6 +27,7 @@ impl Cha { { reg.add_field_method_get("uid", |_, me| Ok(me.uid)); reg.add_field_method_get("gid", |_, me| Ok(me.gid)); + reg.add_field_method_get("nlink", |_, me| Ok(me.nlink)); } reg.add_field_method_get("length", |_, me| Ok(me.len)); @@ -48,6 +52,41 @@ impl Cha { Ok(()) } + + pub fn install(lua: &Lua) -> mlua::Result<()> { + #[inline] + fn parse_time(f: Option) -> mlua::Result> { + Ok(match f { + Some(n) if n >= 0.0 => Some(SystemTime::UNIX_EPOCH + Duration::from_secs_f64(n)), + Some(n) => Err(format!("Invalid timestamp: {n}").into_lua_err())?, + None => None, + }) + } + + lua.globals().raw_set( + "Cha", + lua.create_function(|lua, t: Table| { + let kind = + ChaKind::from_bits(t.raw_get("kind")?).ok_or_else(|| "Invalid kind".into_lua_err())?; + + Self::cast(lua, yazi_shared::fs::Cha { + kind, + len: t.raw_get("len").unwrap_or_default(), + accessed: parse_time(t.raw_get("atime").ok())?, + created: parse_time(t.raw_get("ctime").ok())?, + modified: parse_time(t.raw_get("mtime").ok())?, + #[cfg(unix)] + permissions: t.raw_get("permissions").unwrap_or_default(), + #[cfg(unix)] + uid: t.raw_get("uid").unwrap_or_default(), + #[cfg(unix)] + gid: t.raw_get("gid").unwrap_or_default(), + #[cfg(unix)] + nlink: t.raw_get("nlink").unwrap_or_default(), + }) + })?, + ) + } } impl> Cast for Cha { diff --git a/yazi-plugin/src/cha/mod.rs b/yazi-plugin/src/cha/mod.rs new file mode 100644 index 00000000..6c9c1cf7 --- /dev/null +++ b/yazi-plugin/src/cha/mod.rs @@ -0,0 +1,12 @@ +#![allow(clippy::module_inception)] + +mod cha; + +pub use cha::*; + +pub fn pour(lua: &mlua::Lua) -> mlua::Result<()> { + cha::Cha::register(lua)?; + cha::Cha::install(lua)?; + + Ok(()) +} diff --git a/yazi-core/src/clipboard.rs b/yazi-plugin/src/clipboard.rs similarity index 100% rename from yazi-core/src/clipboard.rs rename to yazi-plugin/src/clipboard.rs diff --git a/yazi-plugin/src/elements/clear.rs b/yazi-plugin/src/elements/clear.rs index 0c6f4c7d..1eff7978 100644 --- a/yazi-plugin/src/elements/clear.rs +++ b/yazi-plugin/src/elements/clear.rs @@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use mlua::{Lua, Table, UserData}; use ratatui::layout::Rect; -use yazi_adaptor::ADAPTOR; +use yazi_adapter::ADAPTOR; use super::{RectRef, Renderable}; diff --git a/yazi-plugin/src/external/fd.rs b/yazi-plugin/src/external/fd.rs index 4fced261..74aaca15 100644 --- a/yazi-plugin/src/external/fd.rs +++ b/yazi-plugin/src/external/fd.rs @@ -7,17 +7,18 @@ use yazi_shared::fs::{File, Url}; pub struct FdOpt { pub cwd: Url, pub hidden: bool, - pub glob: bool, pub subject: String, + pub args: Vec, } pub fn fd(opt: FdOpt) -> Result> { let mut child = Command::new("fd") .arg("--base-directory") .arg(&opt.cwd) - .args(if opt.hidden { ["--hidden", "--no-ignore"] } else { ["--no-hidden", "--ignore"] }) - .arg(if opt.glob { "--glob" } else { "--regex" }) - .arg(&opt.subject) + .arg("--regex") + .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) + .args(opt.args) + .arg(opt.subject) .kill_on_drop(true) .stdout(Stdio::piped()) .stderr(Stdio::null()) diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 584e614f..0e681b96 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -57,7 +57,7 @@ impl Highlighter { } pub async fn highlight(&self, skip: usize, limit: usize) -> Result, PeekError> { - let mut reader = BufReader::new(File::open(&self.path).await?).lines(); + let mut reader = BufReader::new(File::open(&self.path).await?); let syntax = Self::find_syntax(&self.path).await; let mut plain = syntax.is_err(); @@ -66,24 +66,30 @@ impl Highlighter { let mut after = Vec::with_capacity(limit); let mut i = 0; - while let Some(mut line) = reader.next_line().await? { + let mut buf = vec![]; + while reader.read_until(b'\n', &mut buf).await.is_ok() { i += 1; - if i > skip + limit { + if buf.is_empty() || i > skip + limit { break; } - if !plain && line.len() > 6000 { + if !plain && buf.len() > 6000 { plain = true; drop(mem::take(&mut before)); } - if i > skip { - line.push('\n'); - after.push(line); - } else if !plain { - line.push('\n'); - before.push(line); + if buf.ends_with(b"\r\n") { + buf.pop(); + buf.pop(); + buf.push(b'\n'); } + + if i > skip { + after.push(String::from_utf8_lossy(&buf).into_owned()); + } else if !plain { + before.push(String::from_utf8_lossy(&buf).into_owned()); + } + buf.clear(); } if skip > 0 && i < skip + limit { @@ -91,7 +97,8 @@ impl Highlighter { } if plain { - Ok(Text::from(after.join(""))) + let indent = " ".repeat(PREVIEW.tab_size as usize); + Ok(Text::from(after.join("").replace('\t', &indent))) } else { Self::highlight_with(before, after, syntax.unwrap()).await } diff --git a/yazi-plugin/src/external/lsar.rs b/yazi-plugin/src/external/lsar.rs deleted file mode 100644 index d48a0f88..00000000 --- a/yazi-plugin/src/external/lsar.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::path::Path; - -use anyhow::anyhow; -use serde::Deserialize; -use serde_json::Value; -use tokio::process::Command; -use yazi_shared::PeekError; - -#[derive(Debug)] -pub enum LsarAttr { - Posix(u16), - Windows(u16), - Dos(u8), -} - -#[derive(Debug, Deserialize)] -pub struct LsarFile { - #[serde(rename = "XADFileName")] - pub name: String, - #[serde(rename = "XADLastModificationDate")] - pub last_modified: String, - #[serde(rename = "XADFileSize")] - pub size: Option, - #[serde(rename = "XADCompressedSize")] - pub compressed_size: Option, - #[serde(rename = "XADCompressionName")] - pub compression_name: Option, - - #[serde(skip)] - pub attributes: Option, -} - -#[allow(clippy::manual_map)] -pub async fn lsar(path: &Path, skip: usize, limit: usize) -> Result, PeekError> { - let output = Command::new("lsar").arg("-j").arg(path).kill_on_drop(true).output().await?; - if !output.status.success() { - return Err(String::from_utf8_lossy(&output.stderr).to_string().into()); - } - - #[derive(Deserialize)] - struct Outer { - #[serde(rename = "lsarContents")] - contents: Vec, - } - - let output = String::from_utf8_lossy(&output.stdout); - let contents = serde_json::from_str::(output.trim()).map_err(|e| anyhow!(e))?.contents; - - let mut i = 0; - let mut files = Vec::with_capacity(limit); - for content in contents { - i += 1; - if i > skip + limit { - break; - } else if i <= skip { - continue; - } - - let attributes = if let Some(p) = content.get("XADPosixPermissions").and_then(|p| p.as_u64()) { - Some(LsarAttr::Posix(p as u16)) - } else if let Some(a) = content.get("XADWindowsFileAttributes").and_then(|a| a.as_u64()) { - Some(LsarAttr::Windows(a as u16)) - } else if let Some(a) = content.get("XADDOSFileAttributes").and_then(|a| a.as_u64()) { - Some(LsarAttr::Dos(a as u8)) - } else { - None - }; - - let mut file = serde_json::from_value::(content).map_err(|e| anyhow!(e))?; - file.attributes = attributes; - files.push(file); - } - - if skip > 0 && files.len() < limit { - Err(PeekError::Exceed(i.saturating_sub(limit))) - } else { - Ok(files) - } -} diff --git a/yazi-plugin/src/external/mod.rs b/yazi-plugin/src/external/mod.rs index 04bb43d8..5a7b270a 100644 --- a/yazi-plugin/src/external/mod.rs +++ b/yazi-plugin/src/external/mod.rs @@ -1,9 +1,7 @@ mod fd; mod highlighter; -mod lsar; mod rg; pub use fd::*; pub use highlighter::*; -pub use lsar::*; pub use rg::*; diff --git a/yazi-plugin/src/external/rg.rs b/yazi-plugin/src/external/rg.rs index b37a3235..7ff71f2c 100644 --- a/yazi-plugin/src/external/rg.rs +++ b/yazi-plugin/src/external/rg.rs @@ -8,14 +8,16 @@ pub struct RgOpt { pub cwd: Url, pub hidden: bool, pub subject: String, + pub args: Vec, } pub fn rg(opt: RgOpt) -> Result> { let mut child = Command::new("rg") .current_dir(&opt.cwd) .args(["--color=never", "--files-with-matches", "--smart-case"]) - .args(if opt.hidden { ["--hidden", "--no-ignore"] } else { ["--no-hidden", "--ignore"] }) - .arg(&opt.subject) + .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) + .args(opt.args) + .arg(opt.subject) .kill_on_drop(true) .stdout(Stdio::piped()) .stderr(Stdio::null()) diff --git a/yazi-plugin/src/file/file.rs b/yazi-plugin/src/file/file.rs new file mode 100644 index 00000000..5b7266ae --- /dev/null +++ b/yazi-plugin/src/file/file.rs @@ -0,0 +1,63 @@ +use mlua::{AnyUserData, Lua, Table, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry}; +use yazi_config::THEME; + +use crate::{bindings::{Cast, Icon}, cha::Cha, url::Url}; + +pub type FileRef<'lua> = UserDataRef<'lua, yazi_shared::fs::File>; + +pub struct File; + +impl File { + #[inline] + pub fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(Self::register_with) + } + + pub fn register_with(reg: &mut UserDataRegistry) + where + T: AsRef, + { + reg.add_field_method_get("url", |lua, me| Url::cast(lua, me.as_ref().url.clone())); + reg.add_field_method_get("cha", |lua, me| Cha::cast(lua, me.as_ref().cha)); + reg.add_field_method_get("link_to", |lua, me| { + me.as_ref().link_to.clone().map(|u| Url::cast(lua, u)).transpose() + }); + + // Extension + reg.add_field_method_get("name", |lua, me| { + me.as_ref().url.file_name().map(|n| lua.create_string(n.as_encoded_bytes())).transpose() + }); + + reg.add_method("icon", |lua, me, ()| { + use yazi_shared::theme::IconCache; + + let me = me.as_ref(); + match me.icon.get() { + IconCache::Missing => { + let matched = THEME.icons.matches(me); + me.icon.set(matched.map_or(IconCache::Undefined, IconCache::Icon)); + matched.map(|i| Icon::cast(lua, i)).transpose() + } + IconCache::Undefined => Ok(None), + IconCache::Icon(cached) => Some(Icon::cast(lua, cached)).transpose(), + } + }); + } + + pub fn install(lua: &Lua) -> mlua::Result<()> { + lua.globals().raw_set( + "File", + lua.create_function(|lua, t: Table| { + Self::cast(lua, yazi_shared::fs::File { + url: t.raw_get::<_, AnyUserData>("url")?.take()?, + cha: t.raw_get::<_, AnyUserData>("cha")?.take()?, + ..Default::default() + }) + })?, + ) + } +} + +impl> Cast for File { + fn cast(lua: &Lua, data: T) -> mlua::Result { lua.create_any_userdata(data.into()) } +} diff --git a/yazi-plugin/src/file/mod.rs b/yazi-plugin/src/file/mod.rs new file mode 100644 index 00000000..ac2d9fb6 --- /dev/null +++ b/yazi-plugin/src/file/mod.rs @@ -0,0 +1,12 @@ +#![allow(clippy::module_inception)] + +mod file; + +pub use file::*; + +pub fn pour(lua: &mlua::Lua) -> mlua::Result<()> { + file::File::register(lua)?; + file::File::install(lua)?; + + Ok(()) +} diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index aa3ecf9e..158b931c 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -1,7 +1,7 @@ use mlua::{IntoLuaMulti, Lua, Value}; use tokio::fs; -use crate::{bindings::{Cast, Cha}, url::UrlRef}; +use crate::{bindings::Cast, cha::Cha, url::UrlRef}; pub fn install(lua: &Lua) -> mlua::Result<()> { lua.globals().raw_set( diff --git a/yazi-plugin/src/isolate/fetch.rs b/yazi-plugin/src/isolate/fetch.rs new file mode 100644 index 00000000..bee5c63c --- /dev/null +++ b/yazi-plugin/src/isolate/fetch.rs @@ -0,0 +1,33 @@ +use mlua::{ExternalError, ExternalResult, Table, TableExt}; +use tokio::runtime::Handle; +use yazi_config::LAYOUT; + +use super::slim_lua; +use crate::{bindings::Cast, elements::Rect, file::File, loader::LOADER}; + +pub async fn fetch(name: &str, files: Vec) -> mlua::Result { + LOADER.ensure(name).await.into_lua_err()?; + + let name = name.to_owned(); + tokio::task::spawn_blocking(move || { + let lua = slim_lua(&name)?; + let plugin: Table = if let Some(b) = LOADER.read().get(&name) { + lua.load(b.as_ref()).call(())? + } else { + return Err("unloaded plugin".into_lua_err()); + }; + + let files = files.into_iter().filter_map(|f| File::cast(&lua, f).ok()).collect::>(); + if files.is_empty() { + return Err("no files".into_lua_err()); + } + + plugin.raw_set("skip", 0)?; + plugin.raw_set("area", Rect::cast(&lua, LAYOUT.load().preview)?)?; + plugin.raw_set("files", files)?; + + Handle::current().block_on(plugin.call_async_method("fetch", ())) + }) + .await + .into_lua_err()? +} diff --git a/yazi-plugin/src/isolate/isolate.rs b/yazi-plugin/src/isolate/isolate.rs index 933cff57..d82719ff 100644 --- a/yazi-plugin/src/isolate/isolate.rs +++ b/yazi-plugin/src/isolate/isolate.rs @@ -1,14 +1,15 @@ use mlua::Lua; -use crate::{bindings, elements, runtime::Runtime}; +use crate::{elements, runtime::Runtime}; pub fn slim_lua(name: &str) -> mlua::Result { let lua = Lua::new(); lua.set_named_registry_value("rt", Runtime::new(name))?; // Base - bindings::Cha::register(&lua)?; - bindings::File::register(&lua)?; + crate::bindings::Icon::register(&lua)?; + crate::cha::pour(&lua)?; + crate::file::pour(&lua)?; crate::url::pour(&lua)?; crate::fs::install(&lua)?; diff --git a/yazi-plugin/src/isolate/mod.rs b/yazi-plugin/src/isolate/mod.rs index 86391ca7..8b14c6ca 100644 --- a/yazi-plugin/src/isolate/mod.rs +++ b/yazi-plugin/src/isolate/mod.rs @@ -1,12 +1,14 @@ #![allow(clippy::module_inception)] mod entry; +mod fetch; mod isolate; mod peek; mod preload; mod seek; pub use entry::*; +pub use fetch::*; pub use isolate::*; pub use peek::*; pub use preload::*; diff --git a/yazi-plugin/src/isolate/peek.rs b/yazi-plugin/src/isolate/peek.rs index 886caf94..019b7cfa 100644 --- a/yazi-plugin/src/isolate/peek.rs +++ b/yazi-plugin/src/isolate/peek.rs @@ -6,7 +6,7 @@ use yazi_config::LAYOUT; use yazi_shared::{emit, event::Cmd, Layer}; use super::slim_lua; -use crate::{bindings::{Cast, File, Window}, elements::Rect, loader::LOADER, Opt, OptCallback, LUA}; +use crate::{bindings::{Cast, Window}, elements::Rect, file::File, loader::LOADER, Opt, OptCallback, LUA}; pub fn peek(cmd: &Cmd, file: yazi_shared::fs::File, skip: usize) -> CancellationToken { let ct = CancellationToken::new(); diff --git a/yazi-plugin/src/isolate/preload.rs b/yazi-plugin/src/isolate/preload.rs index 3ce0f76a..928d55d7 100644 --- a/yazi-plugin/src/isolate/preload.rs +++ b/yazi-plugin/src/isolate/preload.rs @@ -3,13 +3,9 @@ use tokio::runtime::Handle; use yazi_config::LAYOUT; use super::slim_lua; -use crate::{bindings::{Cast, File}, elements::Rect, loader::LOADER}; +use crate::{bindings::Cast, elements::Rect, file::File, loader::LOADER}; -pub async fn preload( - name: &str, - files: Vec, - multi: bool, -) -> mlua::Result { +pub async fn preload(name: &str, file: yazi_shared::fs::File) -> mlua::Result { LOADER.ensure(name).await.into_lua_err()?; let name = name.to_owned(); @@ -21,18 +17,9 @@ pub async fn preload( return Err("unloaded plugin".into_lua_err()); }; - let mut files = files.into_iter().filter_map(|f| File::cast(&lua, f).ok()).collect::>(); - if files.is_empty() { - return Err("no files".into_lua_err()); - } - plugin.raw_set("skip", 0)?; plugin.raw_set("area", Rect::cast(&lua, LAYOUT.load().preview)?)?; - if multi { - plugin.raw_set("files", files)?; - } else { - plugin.raw_set("file", files.remove(0))?; - } + plugin.raw_set("file", File::cast(&lua, file)?)?; Handle::current().block_on(plugin.call_async_method("preload", ())) }) diff --git a/yazi-plugin/src/isolate/seek.rs b/yazi-plugin/src/isolate/seek.rs index 74af4f86..07e86926 100644 --- a/yazi-plugin/src/isolate/seek.rs +++ b/yazi-plugin/src/isolate/seek.rs @@ -2,7 +2,7 @@ use mlua::TableExt; use yazi_config::LAYOUT; use yazi_shared::{emit, event::Cmd, Layer}; -use crate::{bindings::{Cast, File}, elements::Rect, Opt, OptCallback, LUA}; +use crate::{bindings::Cast, elements::Rect, file::File, Opt, OptCallback, LUA}; pub fn seek_sync(cmd: &Cmd, file: yazi_shared::fs::File, units: i16) { let cb: OptCallback = Box::new(move |_, plugin| { diff --git a/yazi-plugin/src/lib.rs b/yazi-plugin/src/lib.rs index 069f6649..e4fbd3ef 100644 --- a/yazi-plugin/src/lib.rs +++ b/yazi-plugin/src/lib.rs @@ -2,9 +2,12 @@ pub mod bindings; mod cast; +pub mod cha; +mod clipboard; mod config; pub mod elements; pub mod external; +pub mod file; pub mod fs; pub mod isolate; pub mod loader; @@ -17,12 +20,15 @@ pub mod url; pub mod utils; pub use cast::*; +pub use clipboard::*; pub use config::*; pub use lua::*; pub use opt::*; pub use runtime::*; pub fn init() { + CLIPBOARD.with(Default::default); + crate::loader::init(); crate::init_lua(); } diff --git a/yazi-plugin/src/loader/loader.rs b/yazi-plugin/src/loader/loader.rs index 55e8d0d5..c00fddb7 100644 --- a/yazi-plugin/src/loader/loader.rs +++ b/yazi-plugin/src/loader/loader.rs @@ -40,9 +40,11 @@ impl Loader { "code" => include_bytes!("../../preset/plugins/code.lua"), "file" => include_bytes!("../../preset/plugins/file.lua"), "folder" => include_bytes!("../../preset/plugins/folder.lua"), + "font" => include_bytes!("../../preset/plugins/font.lua"), "fzf" => include_bytes!("../../preset/plugins/fzf.lua"), "image" => include_bytes!("../../preset/plugins/image.lua"), "json" => include_bytes!("../../preset/plugins/json.lua"), + "magick" => include_bytes!("../../preset/plugins/magick.lua"), "mime" => include_bytes!("../../preset/plugins/mime.lua"), "pdf" => include_bytes!("../../preset/plugins/pdf.lua"), "video" => include_bytes!("../../preset/plugins/video.lua"), diff --git a/yazi-plugin/src/lua.rs b/yazi-plugin/src/lua.rs index a320c7bd..fd7f10de 100644 --- a/yazi-plugin/src/lua.rs +++ b/yazi-plugin/src/lua.rs @@ -21,12 +21,13 @@ fn stage_1(lua: &'static Lua) -> Result<()> { // Base lua.set_named_registry_value("rt", Runtime::default())?; lua.load(include_str!("../preset/ya.lua")).exec()?; - crate::bindings::Cha::register(lua)?; - crate::bindings::File::register(lua)?; crate::bindings::Icon::register(lua)?; + crate::bindings::MouseEvent::register(lua)?; crate::elements::pour(lua)?; crate::loader::install(lua)?; crate::pubsub::install(lua)?; + crate::cha::pour(lua)?; + crate::file::pour(lua)?; crate::url::pour(lua)?; // Components @@ -38,6 +39,7 @@ fn stage_1(lua: &'static Lua) -> Result<()> { lua.load(include_str!("../preset/components/parent.lua")).exec()?; lua.load(include_str!("../preset/components/preview.lua")).exec()?; lua.load(include_str!("../preset/components/progress.lua")).exec()?; + lua.load(include_str!("../preset/components/root.lua")).exec()?; lua.load(include_str!("../preset/components/status.lua")).exec()?; Ok(()) diff --git a/yazi-plugin/src/process/child.rs b/yazi-plugin/src/process/child.rs index 268726ab..a56ad39f 100644 --- a/yazi-plugin/src/process/child.rs +++ b/yazi-plugin/src/process/child.rs @@ -1,25 +1,25 @@ use std::time::Duration; use futures::future::try_join3; -use mlua::{AnyUserData, IntoLuaMulti, Table, UserData, Value}; -use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, BufReader}, process::{ChildStderr, ChildStdin, ChildStdout}, select}; +use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, Value}; +use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, process::{ChildStderr, ChildStdin, ChildStdout}, select}; use super::Status; use crate::process::Output; pub struct Child { inner: tokio::process::Child, - _stdin: Option, + stdin: Option>, stdout: Option>, stderr: Option>, } impl Child { pub fn new(mut inner: tokio::process::Child) -> Self { - let stdin = inner.stdin.take(); + let stdin = inner.stdin.take().map(BufWriter::new); let stdout = inner.stdout.take().map(BufReader::new); let stderr = inner.stderr.take().map(BufReader::new); - Self { inner, _stdin: stdin, stdout, stderr } + Self { inner, stdin, stdout, stderr } } } @@ -69,6 +69,26 @@ impl UserData for Child { Err(_) => Ok((String::new(), 3u8)), } }); + + methods.add_async_method_mut("write_all", |lua, me, src: mlua::String| async move { + let Some(stdin) = &mut me.stdin else { + return Err("stdin is not piped".into_lua_err()); + }; + match stdin.write_all(src.as_bytes()).await { + Ok(()) => (true, Value::Nil).into_lua_multi(lua), + Err(e) => (false, e.raw_os_error()).into_lua_multi(lua), + } + }); + methods.add_async_method_mut("flush", |lua, me, ()| async move { + let Some(stdin) = &mut me.stdin else { + return Err("stdin is not piped".into_lua_err()); + }; + match stdin.flush().await { + Ok(()) => (true, Value::Nil).into_lua_multi(lua), + Err(e) => (false, e.raw_os_error()).into_lua_multi(lua), + } + }); + methods.add_async_method_mut("wait", |lua, me, ()| async move { match me.inner.wait().await { Ok(status) => (Status::new(status), Value::Nil).into_lua_multi(lua), @@ -107,5 +127,18 @@ impl UserData for Child { Ok(_) => (true, Value::Nil).into_lua_multi(lua), Err(e) => (false, e.raw_os_error()).into_lua_multi(lua), }); + + methods.add_method_mut("take_stdin", |lua, me, ()| match me.stdin.take() { + Some(stdin) => lua.create_any_userdata(stdin.into_inner())?.into_lua(lua), + None => Ok(Value::Nil), + }); + methods.add_method_mut("take_stdout", |lua, me, ()| match me.stdout.take() { + Some(stdout) => lua.create_any_userdata(stdout.into_inner())?.into_lua(lua), + None => Ok(Value::Nil), + }); + methods.add_method_mut("take_stderr", |lua, me, ()| match me.stderr.take() { + Some(stderr) => lua.create_any_userdata(stderr.into_inner())?.into_lua(lua), + None => Ok(Value::Nil), + }); } } diff --git a/yazi-plugin/src/process/command.rs b/yazi-plugin/src/process/command.rs index be57d68a..5877345c 100644 --- a/yazi-plugin/src/process/command.rs +++ b/yazi-plugin/src/process/command.rs @@ -1,6 +1,7 @@ use std::process::Stdio; -use mlua::{AnyUserData, IntoLuaMulti, Lua, Table, UserData, Value}; +use mlua::{AnyUserData, ExternalError, IntoLuaMulti, Lua, Table, UserData, Value}; +use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; use super::{output::Output, Child}; @@ -36,6 +37,33 @@ impl Command { impl UserData for Command { fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + #[inline] + fn make_stdio(v: Value) -> mlua::Result { + match v { + Value::Integer(n) => { + return Ok(match n as u8 { + PIPED => Stdio::piped(), + INHERIT => Stdio::inherit(), + _ => Stdio::null(), + }); + } + Value::UserData(ud) => { + if let Ok(stdin) = ud.take::() { + return Ok(stdin.try_into()?); + } else if let Ok(stdout) = ud.take::() { + return Ok(stdout.try_into()?); + } else if let Ok(stderr) = ud.take::() { + return Ok(stderr.try_into()?); + } + } + _ => {} + } + + Err( + "must be one of Command.NULL, Command.PIPED, Command.INHERIT, or a ChildStdin, ChildStdout, or ChildStderr".into_lua_err(), + ) + } + methods.add_function("arg", |_, (ud, arg): (AnyUserData, mlua::String)| { ud.borrow_mut::()?.inner.arg(arg.to_string_lossy().as_ref()); Ok(ud) @@ -62,28 +90,16 @@ impl UserData for Command { Ok(ud) }, ); - methods.add_function("stdin", |_, (ud, stdio): (AnyUserData, u8)| { - ud.borrow_mut::()?.inner.stdin(match stdio { - PIPED => Stdio::piped(), - INHERIT => Stdio::inherit(), - _ => Stdio::null(), - }); + methods.add_function("stdin", |_, (ud, stdio): (AnyUserData, Value)| { + ud.borrow_mut::()?.inner.stdin(make_stdio(stdio)?); Ok(ud) }); - methods.add_function("stdout", |_, (ud, stdio): (AnyUserData, u8)| { - ud.borrow_mut::()?.inner.stdout(match stdio { - PIPED => Stdio::piped(), - INHERIT => Stdio::inherit(), - _ => Stdio::null(), - }); + methods.add_function("stdout", |_, (ud, stdio): (AnyUserData, Value)| { + ud.borrow_mut::()?.inner.stdout(make_stdio(stdio)?); Ok(ud) }); - methods.add_function("stderr", |_, (ud, stdio): (AnyUserData, u8)| { - ud.borrow_mut::()?.inner.stderr(match stdio { - PIPED => Stdio::piped(), - INHERIT => Stdio::inherit(), - _ => Stdio::null(), - }); + methods.add_function("stderr", |_, (ud, stdio): (AnyUserData, Value)| { + ud.borrow_mut::()?.inner.stderr(make_stdio(stdio)?); Ok(ud) }); methods.add_method_mut("spawn", |lua, me, ()| match me.inner.spawn() { diff --git a/yazi-plugin/src/process/status.rs b/yazi-plugin/src/process/status.rs index 7c8e05f7..ef7116f2 100644 --- a/yazi-plugin/src/process/status.rs +++ b/yazi-plugin/src/process/status.rs @@ -9,8 +9,8 @@ impl Status { } impl UserData for Status { - fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method("success", |_, me, ()| Ok(me.inner.success())); - methods.add_method("code", |_, me, ()| Ok(me.inner.code())); + fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("success", |_, me| Ok(me.inner.success())); + fields.add_field_method_get("code", |_, me| Ok(me.inner.code())); } } diff --git a/yazi-plugin/src/pubsub/pubsub.rs b/yazi-plugin/src/pubsub/pubsub.rs index eb505648..acc81d9e 100644 --- a/yazi-plugin/src/pubsub/pubsub.rs +++ b/yazi-plugin/src/pubsub/pubsub.rs @@ -25,14 +25,6 @@ impl Pubsub { })?, )?; - ps.raw_set( - "pub_static", - lua.create_function(|_, (severity, kind, value): (u16, mlua::String, Value)| { - yazi_dds::Pubsub::pub_static(severity, Body::from_lua(kind.to_str()?, value)?); - Ok(()) - })?, - )?; - ps.raw_set( "sub", lua.create_function(|lua, (kind, f): (mlua::String, Function)| { diff --git a/yazi-plugin/src/utils/cache.rs b/yazi-plugin/src/utils/cache.rs index b0f9a31a..a090436b 100644 --- a/yazi-plugin/src/utils/cache.rs +++ b/yazi-plugin/src/utils/cache.rs @@ -3,7 +3,7 @@ use mlua::{Lua, Table}; use yazi_config::PREVIEW; use super::Utils; -use crate::{bindings::{Cast, FileRef}, url::Url}; +use crate::{bindings::Cast, file::FileRef, url::Url}; impl Utils { pub(super) fn cache(lua: &Lua, ya: &Table) -> mlua::Result<()> { diff --git a/yazi-plugin/src/utils/call.rs b/yazi-plugin/src/utils/call.rs index c788aea7..2a2655a1 100644 --- a/yazi-plugin/src/utils/call.rs +++ b/yazi-plugin/src/utils/call.rs @@ -49,6 +49,14 @@ impl Utils { })?, )?; + ya.raw_set( + "input_emit", + lua.create_function(|_, (name, args): (String, Table)| { + emit!(Call(Cmd { name, args: Self::parse_args(args)? }, Layer::Input)); + Ok(()) + })?, + )?; + Ok(()) } } diff --git a/yazi-plugin/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index 9a13e684..92d8c464 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -1,16 +1,16 @@ use mlua::{IntoLuaMulti, Lua, Table, Value}; -use yazi_adaptor::{Image, ADAPTOR}; +use yazi_adapter::{Image, ADAPTOR}; use super::Utils; -use crate::{elements::RectRef, url::UrlRef}; +use crate::{bindings::Cast, elements::{Rect, RectRef}, url::UrlRef}; impl Utils { pub(super) fn image(lua: &Lua, ya: &Table) -> mlua::Result<()> { ya.raw_set( "image_show", lua.create_async_function(|lua, (url, rect): (UrlRef, RectRef)| async move { - if let Ok(size) = ADAPTOR.image_show(&url, *rect).await { - size.into_lua_multi(lua) + if let Ok(area) = ADAPTOR.image_show(&url, *rect).await { + Rect::cast(lua, area)?.into_lua_multi(lua) } else { Value::Nil.into_lua_multi(lua) } diff --git a/yazi-plugin/src/utils/layer.rs b/yazi-plugin/src/utils/layer.rs index 0ed0335d..40c49d8a 100644 --- a/yazi-plugin/src/utils/layer.rs +++ b/yazi-plugin/src/utils/layer.rs @@ -1,10 +1,11 @@ -use std::str::FromStr; +use std::{str::FromStr, time::Duration}; use mlua::{ExternalError, ExternalResult, IntoLuaMulti, Lua, Table, Value}; use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; use yazi_config::{keymap::{Control, Key}, popup::InputCfg}; use yazi_proxy::{AppProxy, InputProxy}; -use yazi_shared::{emit, event::Cmd, Layer}; +use yazi_shared::{emit, event::Cmd, Debounce, Layer}; use super::Utils; use crate::bindings::{InputRx, Position}; @@ -59,7 +60,7 @@ impl Utils { "input", lua.create_async_function(|lua, t: Table| async move { let realtime = t.raw_get("realtime").unwrap_or_default(); - let mut rx = InputProxy::show(InputCfg { + let rx = UnboundedReceiverStream::new(InputProxy::show(InputCfg { title: t.raw_get("title")?, value: t.raw_get("value").unwrap_or_default(), cursor: None, // TODO @@ -67,14 +68,20 @@ impl Utils { realtime, completion: false, highlight: false, - }); + })); - if realtime { + if !realtime { + return InputRx::consume(rx).await.into_lua_multi(lua); + } + + let debounce = t.raw_get::<_, f64>("debounce").unwrap_or_default(); + if debounce < 0.0 { + Err("negative debounce duration".into_lua_err()) + } else if debounce == 0.0 { (InputRx::new(rx), Value::Nil).into_lua_multi(lua) - } else if let Some(res) = rx.recv().await { - InputRx::parse(res).into_lua_multi(lua) } else { - (Value::Nil, 0).into_lua_multi(lua) + (InputRx::new(Debounce::new(rx, Duration::from_secs_f64(debounce))), Value::Nil) + .into_lua_multi(lua) } })?, )?; diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index 73a6aed9..231e9019 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -2,7 +2,7 @@ use mlua::{AnyUserData, IntoLuaMulti, Lua, Table, Value}; use yazi_shared::{emit, event::Cmd, Layer, PeekError}; use super::Utils; -use crate::{bindings::{FileRef, Window}, cast_to_renderable, elements::{Paragraph, RectRef, Renderable}, external::{self, Highlighter}}; +use crate::{bindings::Window, cast_to_renderable, elements::{Paragraph, RectRef, Renderable}, external::Highlighter, file::FileRef}; pub struct PreviewLock { pub url: yazi_shared::fs::Url, @@ -49,29 +49,6 @@ impl Utils { })?, )?; - ya.raw_set( - "preview_archive", - lua.create_async_function(|lua, t: Table| async move { - let area: RectRef = t.raw_get("area")?; - let mut lock = PreviewLock::try_from(t)?; - - let lines: Vec<_> = match external::lsar(&lock.url, lock.skip, area.height as usize).await { - Ok(items) => items.into_iter().map(|f| ratatui::text::Line::from(f.name)).collect(), - Err(PeekError::Exceed(max)) => return (false, max).into_lua_multi(lua), - Err(_) => return (false, Value::Nil).into_lua_multi(lua), - }; - - lock.data = vec![Box::new(Paragraph { - area: *area, - text: ratatui::text::Text::from(lines), - ..Default::default() - })]; - - emit!(Call(Cmd::new("preview").with_any("lock", lock), Layer::Manager)); - (true, Value::Nil).into_lua_multi(lua) - })?, - )?; - ya.raw_set( "preview_widgets", lua.create_async_function(|_, (t, widgets): (Table, Vec)| async move { diff --git a/yazi-plugin/src/utils/target.rs b/yazi-plugin/src/utils/target.rs index ab9ec164..2663c806 100644 --- a/yazi-plugin/src/utils/target.rs +++ b/yazi-plugin/src/utils/target.rs @@ -4,23 +4,8 @@ use super::Utils; impl Utils { pub(super) fn target(lua: &Lua, ya: &Table) -> mlua::Result<()> { - ya.raw_set( - "target_family", - lua.create_function(|_, ()| { - #[cfg(unix)] - { - Ok("unix") - } - #[cfg(windows)] - { - Ok("windows") - } - #[cfg(wasm)] - { - Ok("wasm") - } - })?, - )?; + ya.raw_set("target_os", lua.create_function(|_, ()| Ok(std::env::consts::OS))?)?; + ya.raw_set("target_family", lua.create_function(|_, ()| Ok(std::env::consts::FAMILY))?)?; Ok(()) } diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index 92044180..b95f9887 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -4,16 +4,18 @@ use mlua::{Lua, Table}; use unicode_width::UnicodeWidthChar; use super::Utils; +use crate::CLIPBOARD; impl Utils { pub(super) fn text(lua: &Lua, ya: &Table) -> mlua::Result<()> { ya.raw_set( "quote", - lua.create_function(|_, s: mlua::String| { - #[cfg(unix)] - let s = shell_escape::unix::escape(s.to_str()?.into()); - #[cfg(windows)] - let s = shell_escape::windows::escape(s.to_str()?.into()); + lua.create_function(|_, (s, unix): (mlua::String, Option)| { + let s = match unix { + Some(true) => shell_escape::unix::escape(s.to_str()?.into()), + Some(false) => shell_escape::windows::escape(s.to_str()?.into()), + None => shell_escape::escape(s.to_str()?.into()), + }; Ok(s.into_owned()) })?, )?; @@ -31,6 +33,18 @@ impl Utils { })?, )?; + ya.raw_set( + "clipboard", + lua.create_async_function(|lua, text: Option| async move { + if let Some(text) = text { + CLIPBOARD.set(text).await; + Ok(None) + } else { + Some(lua.create_string(CLIPBOARD.get().await.as_encoded_bytes())).transpose() + } + })?, + )?; + Ok(()) } diff --git a/yazi-proxy/Cargo.toml b/yazi-proxy/Cargo.toml index 09f5a61c..4d1c3ca4 100644 --- a/yazi-proxy/Cargo.toml +++ b/yazi-proxy/Cargo.toml @@ -17,6 +17,6 @@ yazi-config = { path = "../yazi-config", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -anyhow = "1.0.82" -mlua = { version = "0.9.7", features = [ "lua54" ] } -tokio = { version = "1.37.0", features = [ "full" ] } +anyhow = "1.0.86" +mlua = { version = "0.9.9", features = [ "lua54" ] } +tokio = { version = "1.38.0", features = [ "full" ] } diff --git a/yazi-proxy/src/app.rs b/yazi-proxy/src/app.rs index add88327..fcdcb470 100644 --- a/yazi-proxy/src/app.rs +++ b/yazi-proxy/src/app.rs @@ -37,4 +37,17 @@ impl AppProxy { Layer::App )); } + + #[inline] + pub fn notify_error(title: &str, content: &str) { + emit!(Call( + Cmd::new("notify").with_any("option", NotifyOpt { + title: title.to_owned(), + content: content.to_owned(), + level: NotifyLevel::Error, + timeout: Duration::from_secs(10), + }), + Layer::App + )); + } } diff --git a/yazi-proxy/src/options/notify.rs b/yazi-proxy/src/options/notify.rs index d4b10962..bded3d23 100644 --- a/yazi-proxy/src/options/notify.rs +++ b/yazi-proxy/src/options/notify.rs @@ -2,7 +2,8 @@ use std::{str::FromStr, time::Duration}; use anyhow::bail; use mlua::{ExternalError, ExternalResult}; -use yazi_shared::event::Cmd; +use yazi_config::THEME; +use yazi_shared::{event::Cmd, theme::Style}; pub struct NotifyOpt { pub title: String, @@ -41,7 +42,7 @@ impl<'a> TryFrom> for NotifyOpt { } } -#[derive(Default)] +#[derive(Clone, Copy, Default)] pub enum NotifyLevel { #[default] Info, @@ -49,6 +50,26 @@ pub enum NotifyLevel { Error, } +impl NotifyLevel { + #[inline] + pub fn icon(self) -> &'static str { + match self { + Self::Info => &THEME.notify.icon_info, + Self::Warn => &THEME.notify.icon_warn, + Self::Error => &THEME.notify.icon_error, + } + } + + #[inline] + pub fn style(self) -> &'static Style { + match self { + Self::Info => &THEME.notify.title_info, + Self::Warn => &THEME.notify.title_warn, + Self::Error => &THEME.notify.title_error, + } + } +} + impl FromStr for NotifyLevel { type Err = anyhow::Error; diff --git a/yazi-scheduler/Cargo.toml b/yazi-scheduler/Cargo.toml index d35b11e1..d42b4020 100644 --- a/yazi-scheduler/Cargo.toml +++ b/yazi-scheduler/Cargo.toml @@ -16,18 +16,18 @@ yazi-proxy = { path = "../yazi-proxy", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -anyhow = "1.0.82" +anyhow = "1.0.86" async-priority-channel = "0.2.0" futures = "0.3.30" -parking_lot = "0.12.1" +parking_lot = "0.12.3" scopeguard = "1.2.0" -tokio = { version = "1.37.0", features = [ "full" ] } +tokio = { version = "1.38.0", features = [ "full" ] } # Logging tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } [target."cfg(unix)".dependencies] -libc = "0.2.153" +libc = "0.2.155" [target.'cfg(not(target_os = "android"))'.dependencies] -trash = "4.1.0" +trash = "5.0.0" diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 8b495e26..0373a59c 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,13 +1,12 @@ use std::{borrow::Cow, collections::VecDeque, fs::Metadata, path::{Path, PathBuf}}; use anyhow::{anyhow, Result}; -use futures::{future::BoxFuture, FutureExt}; use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::TASKS; -use yazi_shared::fs::{accessible, calculate_size, copy_with_progress, path_relative_to, Url}; +use yazi_shared::fs::{calculate_size, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to, Url}; -use super::{FileOp, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash}; +use super::{FileOp, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash}; use crate::{TaskOp, TaskProg, LOW, NORMAL}; pub struct File { @@ -26,12 +25,9 @@ impl File { pub async fn work(&self, op: FileOp) -> Result<()> { match op { FileOp::Paste(mut task) => { - match fs::remove_file(&task.to).await { - Err(e) if e.kind() != NotFound => Err(e)?, - _ => {} - } - + ok_or_not_found(fs::remove_file(&task.to).await)?; let mut it = copy_with_progress(&task.from, &task.to, task.meta.as_ref().unwrap()); + while let Some(res) = it.recv().await { match res { Ok(0) => { @@ -42,7 +38,7 @@ impl File { } Ok(n) => self.prog.send(TaskProg::Adv(task.id, 0, n))?, Err(e) if e.kind() == NotFound => { - warn!("Paste task partially done: {:?}", task); + warn!("Paste task partially done: {task:?}"); break; } // Operation not permitted (os error 1) @@ -68,7 +64,7 @@ impl File { match fs::read_link(&task.from).await { Ok(p) => Cow::Owned(p), Err(e) if e.kind() == NotFound => { - self.log(task.id, format!("Link task partially done: {:?}", task))?; + warn!("Link task partially done: {task:?}"); return Ok(self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?); } Err(e) => Err(e)?, @@ -83,21 +79,17 @@ impl File { src }; - match fs::remove_file(&task.to).await { - Err(e) if e.kind() != NotFound => Err(e)?, - _ => { - #[cfg(unix)] - { - fs::symlink(src, &task.to).await? - } - #[cfg(windows)] - { - if meta.is_dir() { - fs::symlink_dir(src, &task.to).await? - } else { - fs::symlink_file(src, &task.to).await? - } - } + ok_or_not_found(fs::remove_file(&task.to).await)?; + #[cfg(unix)] + { + fs::symlink(src, &task.to).await?; + } + #[cfg(windows)] + { + if meta.is_dir() { + fs::symlink_dir(src, &task.to).await?; + } else { + fs::symlink_file(src, &task.to).await?; } } @@ -106,9 +98,29 @@ impl File { } self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?; } + FileOp::Hardlink(task) => { + let meta = task.meta.as_ref().unwrap(); + let src = if !task.follow { + Cow::Borrowed(task.from.as_path()) + } else if let Ok(p) = fs::canonicalize(&task.from).await { + Cow::Owned(p) + } else { + Cow::Borrowed(task.from.as_path()) + }; + + ok_or_not_found(fs::remove_file(&task.to).await)?; + match fs::hard_link(src, &task.to).await { + Err(e) if e.kind() == NotFound => { + warn!("Hardlink task partially done: {task:?}"); + } + v => v?, + } + + self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?; + } FileOp::Delete(task) => { if let Err(e) = fs::remove_file(&task.target).await { - if e.kind() != NotFound && accessible(&task.target).await { + if e.kind() != NotFound && maybe_exists(&task.target).await { self.fail(task.id, format!("Delete task failed: {:?}, {e}", task))?; Err(e)? } @@ -134,12 +146,8 @@ impl File { } pub async fn paste(&self, mut task: FileOpPaste) -> Result<()> { - if task.cut { - match fs::rename(&task.from, &task.to).await { - Ok(_) => return self.succ(task.id), - Err(e) if e.kind() == NotFound => return self.succ(task.id), - _ => {} - } + if task.cut && ok_or_not_found(fs::rename(&task.from, &task.to).await).is_ok() { + return self.succ(task.id); } if task.meta.is_none() { @@ -217,6 +225,61 @@ impl File { self.succ(id) } + pub async fn hardlink(&self, mut task: FileOpHardlink) -> Result<()> { + if task.meta.is_none() { + task.meta = Some(Self::metadata(&task.from, task.follow).await?); + } + + let meta = task.meta.as_ref().unwrap(); + if !meta.is_dir() { + let id = task.id; + self.prog.send(TaskProg::New(id, meta.len()))?; + self.queue(FileOp::Hardlink(task), NORMAL).await?; + return self.succ(id); + } + + macro_rules! continue_unless_ok { + ($result:expr) => { + match $result { + Ok(v) => v, + Err(e) => { + self.prog.send(TaskProg::New(task.id, 0))?; + self.fail(task.id, format!("An error occurred while hardlinking: {e}"))?; + continue; + } + } + }; + } + + let root = &task.to; + let skip = task.from.components().count(); + let mut dirs = VecDeque::from([task.from.clone()]); + + while let Some(src) = dirs.pop_front() { + let dest = root.join(src.components().skip(skip).collect::()); + continue_unless_ok!(match fs::create_dir(&dest).await { + Err(e) if e.kind() != AlreadyExists => Err(e), + _ => Ok(()), + }); + + let mut it = continue_unless_ok!(fs::read_dir(&src).await); + while let Ok(Some(entry)) = it.next_entry().await { + let from = Url::from(entry.path()); + let meta = continue_unless_ok!(Self::metadata(&from, task.follow).await); + + if meta.is_dir() { + dirs.push_back(from); + continue; + } + + let to = dest.join(from.file_name().unwrap()); + self.prog.send(TaskProg::New(task.id, meta.len()))?; + self.queue(FileOp::Hardlink(task.spawn(from, to, meta)), NORMAL).await?; + } + } + self.succ(task.id) + } + pub async fn delete(&self, mut task: FileOpDelete) -> Result<()> { let meta = fs::symlink_metadata(&task.target).await?; if !meta.is_dir() { @@ -257,6 +320,7 @@ impl File { self.succ(id) } + #[inline] async fn metadata(path: &Path, follow: bool) -> io::Result { if !follow { return fs::symlink_metadata(path).await; @@ -266,24 +330,18 @@ impl File { if meta.is_ok() { meta } else { fs::symlink_metadata(path).await } } - pub(crate) fn remove_empty_dirs(dir: &Path) -> BoxFuture<()> { - async move { - let mut it = match fs::read_dir(dir).await { - Ok(it) => it, - Err(_) => return, - }; + pub(crate) async fn remove_empty_dirs(dir: &Path) { + let Ok(mut it) = fs::read_dir(dir).await else { return }; - while let Ok(Some(entry)) = it.next_entry().await { - if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) { - let path = entry.path(); - Self::remove_empty_dirs(&path).await; - fs::remove_dir(path).await.ok(); - } + while let Ok(Some(entry)) = it.next_entry().await { + if entry.file_type().await.is_ok_and(|t| t.is_dir()) { + let path = entry.path(); + Box::pin(Self::remove_empty_dirs(&path)).await; + fs::remove_dir(path).await.ok(); } - - fs::remove_dir(dir).await.ok(); } - .boxed() + + fs::remove_dir(dir).await.ok(); } } diff --git a/yazi-scheduler/src/file/op.rs b/yazi-scheduler/src/file/op.rs index 3eabec69..7f898fce 100644 --- a/yazi-scheduler/src/file/op.rs +++ b/yazi-scheduler/src/file/op.rs @@ -6,6 +6,7 @@ use yazi_shared::fs::Url; pub enum FileOp { Paste(FileOpPaste), Link(FileOpLink), + Hardlink(FileOpHardlink), Delete(FileOpDelete), Trash(FileOpTrash), } @@ -15,12 +16,14 @@ impl FileOp { match self { Self::Paste(op) => op.id, Self::Link(op) => op.id, + Self::Hardlink(op) => op.id, Self::Delete(op) => op.id, Self::Trash(op) => op.id, } } } +// --- Paste #[derive(Clone, Debug)] pub struct FileOpPaste { pub id: usize, @@ -46,6 +49,7 @@ impl FileOpPaste { } } +// --- Link #[derive(Clone, Debug)] pub struct FileOpLink { pub id: usize, @@ -71,6 +75,23 @@ impl From for FileOpLink { } } +// --- Hardlink +#[derive(Clone, Debug)] +pub struct FileOpHardlink { + pub id: usize, + pub from: Url, + pub to: Url, + pub meta: Option, + pub follow: bool, +} + +impl FileOpHardlink { + pub(super) fn spawn(&self, from: Url, to: Url, meta: Metadata) -> Self { + Self { id: self.id, from, to, meta: Some(meta), follow: self.follow } + } +} + +// --- Delete #[derive(Clone, Debug)] pub struct FileOpDelete { pub id: usize, @@ -78,6 +99,7 @@ pub struct FileOpDelete { pub length: u64, } +// --- Trash #[derive(Clone, Debug)] pub struct FileOpTrash { pub id: usize, diff --git a/yazi-scheduler/src/lib.rs b/yazi-scheduler/src/lib.rs index f744e551..6e9ed29f 100644 --- a/yazi-scheduler/src/lib.rs +++ b/yazi-scheduler/src/lib.rs @@ -4,7 +4,7 @@ mod file; mod ongoing; mod op; mod plugin; -mod preload; +mod prework; mod process; mod scheduler; mod task; diff --git a/yazi-scheduler/src/op.rs b/yazi-scheduler/src/op.rs index 6afcbebe..c94d614a 100644 --- a/yazi-scheduler/src/op.rs +++ b/yazi-scheduler/src/op.rs @@ -1,10 +1,10 @@ -use crate::{file::FileOp, plugin::PluginOp, preload::PreloadOp}; +use crate::{file::FileOp, plugin::PluginOp, prework::PreworkOp}; #[derive(Debug)] pub enum TaskOp { File(Box), Plugin(Box), - Preload(Box), + Prework(Box), } impl TaskOp { @@ -12,7 +12,7 @@ impl TaskOp { match self { TaskOp::File(op) => op.id(), TaskOp::Plugin(op) => op.id(), - TaskOp::Preload(op) => op.id(), + TaskOp::Prework(op) => op.id(), } } } @@ -25,6 +25,6 @@ impl From for TaskOp { fn from(op: PluginOp) -> Self { Self::Plugin(Box::new(op)) } } -impl From for TaskOp { - fn from(op: PreloadOp) -> Self { Self::Preload(Box::new(op)) } +impl From for TaskOp { + fn from(op: PreworkOp) -> Self { Self::Prework(Box::new(op)) } } diff --git a/yazi-scheduler/src/preload/op.rs b/yazi-scheduler/src/preload/op.rs deleted file mode 100644 index 6e4b4d01..00000000 --- a/yazi-scheduler/src/preload/op.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::sync::Arc; - -use yazi_config::plugin::PluginProps; -use yazi_shared::{fs::Url, Throttle}; - -#[derive(Debug)] -pub enum PreloadOp { - Rule(PreloadOpRule), - Size(PreloadOpSize), -} - -impl PreloadOp { - pub fn id(&self) -> usize { - match self { - Self::Rule(op) => op.id, - Self::Size(op) => op.id, - } - } -} - -#[derive(Clone, Debug)] -pub struct PreloadOpRule { - pub id: usize, - pub plugin: PluginProps, - pub targets: Vec, -} - -#[derive(Debug)] -pub struct PreloadOpSize { - pub id: usize, - pub target: Url, - pub throttle: Arc>, -} diff --git a/yazi-scheduler/src/preload/preload.rs b/yazi-scheduler/src/preload/preload.rs deleted file mode 100644 index 554dd29f..00000000 --- a/yazi-scheduler/src/preload/preload.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use anyhow::{anyhow, Result}; -use parking_lot::RwLock; -use tokio::sync::mpsc; -use tracing::error; -use yazi_config::Priority; -use yazi_plugin::isolate; -use yazi_shared::fs::{calculate_size, FilesOp, Url}; - -use super::{PreloadOp, PreloadOpRule, PreloadOpSize}; -use crate::{TaskOp, TaskProg, HIGH, NORMAL}; - -pub struct Preload { - macro_: async_priority_channel::Sender, - prog: mpsc::UnboundedSender, - - pub rule_loaded: RwLock>, - pub size_loading: RwLock>, -} - -impl Preload { - pub fn new( - macro_: async_priority_channel::Sender, - prog: mpsc::UnboundedSender, - ) -> Self { - Self { macro_, prog, rule_loaded: Default::default(), size_loading: Default::default() } - } - - pub async fn work(&self, op: PreloadOp) -> Result<()> { - match op { - PreloadOp::Rule(task) => { - let urls: Vec<_> = task.targets.iter().map(|f| f.url()).collect(); - let result = isolate::preload(&task.plugin.name, task.targets, task.plugin.multi).await; - if let Err(e) = result { - self.fail(task.id, format!("Preload task failed:\n{e}"))?; - return Err(e.into()); - }; - - let code = result.unwrap(); - if code & 1 == 0 { - error!("Preload task `{}` returned {code}", task.plugin.name); - } - if code >> 1 & 1 != 0 { - let mut loaded = self.rule_loaded.write(); - for url in urls { - loaded.get_mut(&url).map(|x| *x ^= 1 << task.plugin.id); - } - } - self.prog.send(TaskProg::Adv(task.id, 1, 0))?; - } - PreloadOp::Size(task) => { - let length = calculate_size(&task.target).await; - task.throttle.done((task.target, length), |buf| { - { - let mut loading = self.size_loading.write(); - for (path, _) in &buf { - loading.remove(path); - } - } - - let parent = buf[0].0.parent_url().unwrap(); - FilesOp::Size(parent, HashMap::from_iter(buf)).emit(); - }); - self.prog.send(TaskProg::Adv(task.id, 1, 0))?; - } - } - Ok(()) - } - - pub async fn rule(&self, task: PreloadOpRule) -> Result<()> { - let id = task.id; - self.prog.send(TaskProg::New(id, 0))?; - - match task.plugin.prio { - Priority::Low => self.queue(PreloadOp::Rule(task), NORMAL).await?, - Priority::Normal => self.queue(PreloadOp::Rule(task), HIGH).await?, - Priority::High => self.work(PreloadOp::Rule(task)).await?, - } - self.succ(id) - } - - pub async fn size(&self, task: PreloadOpSize) -> Result<()> { - let id = task.id; - - self.prog.send(TaskProg::New(id, 0))?; - self.work(PreloadOp::Size(task)).await?; - self.succ(id) - } -} - -impl Preload { - #[inline] - fn succ(&self, id: usize) -> Result<()> { Ok(self.prog.send(TaskProg::Succ(id))?) } - - #[inline] - fn fail(&self, id: usize, reason: String) -> Result<()> { - Ok(self.prog.send(TaskProg::Fail(id, reason))?) - } - - #[inline] - async fn queue(&self, op: impl Into, priority: u8) -> Result<()> { - self.macro_.send(op.into(), priority).await.map_err(|_| anyhow!("Failed to send task")) - } -} diff --git a/yazi-scheduler/src/preload/mod.rs b/yazi-scheduler/src/prework/mod.rs similarity index 64% rename from yazi-scheduler/src/preload/mod.rs rename to yazi-scheduler/src/prework/mod.rs index 829ef0e5..4c89c36c 100644 --- a/yazi-scheduler/src/preload/mod.rs +++ b/yazi-scheduler/src/prework/mod.rs @@ -1,7 +1,7 @@ #![allow(clippy::module_inception)] mod op; -mod preload; +mod prework; pub use op::*; -pub use preload::*; +pub use prework::*; diff --git a/yazi-scheduler/src/prework/op.rs b/yazi-scheduler/src/prework/op.rs new file mode 100644 index 00000000..1b9fb278 --- /dev/null +++ b/yazi-scheduler/src/prework/op.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; + +use yazi_config::plugin::{FetcherProps, PreloaderProps}; +use yazi_shared::{fs::Url, Throttle}; + +#[derive(Debug)] +pub enum PreworkOp { + Fetch(PreworkOpFetch), + Load(PreworkOpLoad), + Size(PreworkOpSize), +} + +impl PreworkOp { + pub fn id(&self) -> usize { + match self { + Self::Fetch(op) => op.id, + Self::Load(op) => op.id, + Self::Size(op) => op.id, + } + } +} + +#[derive(Clone, Debug)] +pub struct PreworkOpFetch { + pub id: usize, + pub plugin: FetcherProps, + pub targets: Vec, +} + +#[derive(Clone, Debug)] +pub struct PreworkOpLoad { + pub id: usize, + pub plugin: PreloaderProps, + pub target: yazi_shared::fs::File, +} + +#[derive(Debug)] +pub struct PreworkOpSize { + pub id: usize, + pub target: Url, + pub throttle: Arc>, +} diff --git a/yazi-scheduler/src/prework/prework.rs b/yazi-scheduler/src/prework/prework.rs new file mode 100644 index 00000000..ae0dc3c6 --- /dev/null +++ b/yazi-scheduler/src/prework/prework.rs @@ -0,0 +1,149 @@ +use std::collections::{HashMap, HashSet}; + +use anyhow::{anyhow, Result}; +use parking_lot::{Mutex, RwLock}; +use tokio::sync::mpsc; +use tracing::error; +use yazi_config::Priority; +use yazi_plugin::isolate; +use yazi_shared::fs::{calculate_size, FilesOp, Url}; + +use super::{PreworkOp, PreworkOpFetch, PreworkOpLoad, PreworkOpSize}; +use crate::{TaskOp, TaskProg, HIGH, NORMAL}; + +pub struct Prework { + macro_: async_priority_channel::Sender, + prog: mpsc::UnboundedSender, + + pub loaded: Mutex>, + pub size_loading: RwLock>, +} + +impl Prework { + pub fn new( + macro_: async_priority_channel::Sender, + prog: mpsc::UnboundedSender, + ) -> Self { + Self { macro_, prog, loaded: Default::default(), size_loading: Default::default() } + } + + pub async fn work(&self, op: PreworkOp) -> Result<()> { + match op { + PreworkOp::Fetch(task) => { + let urls: Vec<_> = task.targets.iter().map(|f| f.url()).collect(); + let result = isolate::fetch(&task.plugin.name, task.targets).await; + if let Err(e) = result { + self.fail( + task.id, + format!( + "Failed to run fetcher `{}` with:\n{}\n\nError message:\n{e}", + task.plugin.name, + urls.iter().map(ToString::to_string).collect::>().join("\n") + ), + )?; + return Err(e.into()); + }; + + let code = result.unwrap(); + if code & 1 == 0 { + error!( + "Returned {code} when running fetcher `{}` with:\n{}", + task.plugin.name, + urls.iter().map(ToString::to_string).collect::>().join("\n") + ); + } + if code >> 1 & 1 != 0 { + let mut loaded = self.loaded.lock(); + for url in urls { + loaded.get_mut(&url).map(|x| *x ^= 1 << task.plugin.id); + } + } + self.prog.send(TaskProg::Adv(task.id, 1, 0))?; + } + PreworkOp::Load(task) => { + let url = task.target.url(); + let result = isolate::preload(&task.plugin.name, task.target).await; + if let Err(e) = result { + self.fail( + task.id, + format!("Failed to run preloader `{}` with `{url}`:\n{e}", task.plugin.name), + )?; + return Err(e.into()); + }; + + let code = result.unwrap(); + if code & 1 == 0 { + error!("Returned {code} when running preloader `{}` with `{url}`", task.plugin.name); + } + if code >> 1 & 1 != 0 { + let mut loaded = self.loaded.lock(); + loaded.get_mut(&url).map(|x| *x ^= 1 << task.plugin.id); + } + self.prog.send(TaskProg::Adv(task.id, 1, 0))?; + } + PreworkOp::Size(task) => { + let length = calculate_size(&task.target).await; + task.throttle.done((task.target, length), |buf| { + { + let mut loading = self.size_loading.write(); + for (path, _) in &buf { + loading.remove(path); + } + } + + let parent = buf[0].0.parent_url().unwrap(); + FilesOp::Size(parent, HashMap::from_iter(buf)).emit(); + }); + self.prog.send(TaskProg::Adv(task.id, 1, 0))?; + } + } + Ok(()) + } + + pub async fn fetch(&self, task: PreworkOpFetch) -> Result<()> { + let id = task.id; + self.prog.send(TaskProg::New(id, 0))?; + + match task.plugin.prio { + Priority::Low => self.queue(PreworkOp::Fetch(task), NORMAL).await?, + Priority::Normal => self.queue(PreworkOp::Fetch(task), HIGH).await?, + Priority::High => self.work(PreworkOp::Fetch(task)).await?, + } + self.succ(id) + } + + pub async fn load(&self, task: PreworkOpLoad) -> Result<()> { + let id = task.id; + self.prog.send(TaskProg::New(id, 0))?; + + match task.plugin.prio { + Priority::Low => self.queue(PreworkOp::Load(task), NORMAL).await?, + Priority::Normal => self.queue(PreworkOp::Load(task), HIGH).await?, + Priority::High => self.work(PreworkOp::Load(task)).await?, + } + self.succ(id) + } + + pub async fn size(&self, task: PreworkOpSize) -> Result<()> { + let id = task.id; + + self.prog.send(TaskProg::New(id, 0))?; + self.work(PreworkOp::Size(task)).await?; + self.succ(id) + } +} + +impl Prework { + #[inline] + fn succ(&self, id: usize) -> Result<()> { Ok(self.prog.send(TaskProg::Succ(id))?) } + + #[inline] + fn fail(&self, id: usize, reason: String) -> Result<()> { + Ok(self.prog.send(TaskProg::Fail(id, reason))?) + } + + #[inline] + async fn queue(&self, op: impl Into, priority: u8) -> Result<()> { + self.macro_.send(op.into(), priority).await.map_err(|_| anyhow!("Failed to send task")) + } +} diff --git a/yazi-scheduler/src/process/shell.rs b/yazi-scheduler/src/process/shell.rs index 8f5f8195..fb3ad667 100644 --- a/yazi-scheduler/src/process/shell.rs +++ b/yazi-scheduler/src/process/shell.rs @@ -36,8 +36,8 @@ pub fn shell(opt: ShellOpt) -> Result { .args(opt.args) .kill_on_drop(!opt.orphan) .pre_exec(move || { - if opt.orphan && libc::setpgid(0i32, 0i32) < 0 { - libc::perror(std::ptr::null()); + if opt.orphan && libc::setpgid(0, 0) < 0 { + return Err(std::io::Error::last_os_error()); } Ok(()) }) diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index db83c69c..a4137958 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -4,17 +4,17 @@ use anyhow::Result; use futures::{future::BoxFuture, FutureExt}; use parking_lot::Mutex; use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, task::JoinHandle}; -use yazi_config::{open::Opener, plugin::PluginRule, TASKS}; +use yazi_config::{open::Opener, plugin::{Fetcher, Preloader}, TASKS}; use yazi_dds::Pump; use yazi_shared::{event::Data, fs::{unique_path, Url}, Throttle}; use super::{Ongoing, TaskProg, TaskStage}; -use crate::{file::{File, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, preload::{Preload, PreloadOpRule, PreloadOpSize}, process::{Process, ProcessOpBg, ProcessOpBlock, ProcessOpOrphan}, TaskKind, TaskOp, HIGH, LOW, NORMAL}; +use crate::{file::{File, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, prework::{Prework, PreworkOpFetch, PreworkOpLoad, PreworkOpSize}, process::{Process, ProcessOpBg, ProcessOpBlock, ProcessOpOrphan}, TaskKind, TaskOp, HIGH, LOW, NORMAL}; pub struct Scheduler { pub file: Arc, pub plugin: Arc, - pub preload: Arc, + pub prework: Arc, pub process: Arc, micro: async_priority_channel::Sender, u8>, @@ -32,7 +32,7 @@ impl Scheduler { let mut scheduler = Self { file: Arc::new(File::new(macro_tx.clone(), prog_tx.clone())), plugin: Arc::new(Plugin::new(macro_tx.clone(), prog_tx.clone())), - preload: Arc::new(Preload::new(macro_tx.clone(), prog_tx.clone())), + prework: Arc::new(Prework::new(macro_tx.clone(), prog_tx.clone())), process: Arc::new(Process::new(prog_tx.clone())), micro: micro_tx, @@ -154,6 +154,28 @@ impl Scheduler { ); } + pub fn file_hardlink(&self, from: Url, mut to: Url, force: bool, follow: bool) { + let name = format!("Hardlink {:?} to {:?}", from, to); + let id = self.ongoing.lock().add(TaskKind::User, name); + + if to.starts_with(&from) && to != from { + self.new_and_fail(id, "Cannot hardlink directory into itself").ok(); + return; + } + + let file = self.file.clone(); + _ = self.micro.try_send( + async move { + if !force { + to = unique_path(to).await; + } + file.hardlink(FileOpHardlink { id, from, to, meta: None, follow }).await.ok(); + } + .boxed(), + LOW, + ); + } + pub fn file_delete(&self, target: Url) { let mut ongoing = self.ongoing.lock(); let id = ongoing.add(TaskKind::User, format!("Delete {:?}", target)); @@ -218,25 +240,40 @@ impl Scheduler { self.plugin.macro_(PluginOpEntry { id, name, args }).ok(); } - pub fn preload_paged(&self, rule: &PluginRule, targets: Vec<&yazi_shared::fs::File>) { + pub fn fetch_paged(&self, fetcher: &Fetcher, targets: Vec) { let id = self.ongoing.lock().add( TaskKind::Preload, - format!("Run preloader `{}` with {} target(s)", rule.cmd.name, targets.len()), + format!("Run fetcher `{}` with {} target(s)", fetcher.run.name, targets.len()), ); - let plugin = rule.into(); - let targets = targets.into_iter().cloned().collect(); - let preload = self.preload.clone(); + let plugin = fetcher.into(); + let prework = self.prework.clone(); _ = self.micro.try_send( async move { - preload.rule(PreloadOpRule { id, plugin, targets }).await.ok(); + prework.fetch(PreworkOpFetch { id, plugin, targets }).await.ok(); } .boxed(), NORMAL, ); } - pub fn preload_size(&self, targets: Vec<&Url>) { + pub fn preload_paged(&self, preloader: &Preloader, target: &yazi_shared::fs::File) { + let id = + self.ongoing.lock().add(TaskKind::Preload, format!("Run preloader `{}`", preloader.run.name)); + + let plugin = preloader.into(); + let target = target.clone(); + let prework = self.prework.clone(); + _ = self.micro.try_send( + async move { + prework.load(PreworkOpLoad { id, plugin, target }).await.ok(); + } + .boxed(), + NORMAL, + ); + } + + pub fn prework_size(&self, targets: Vec<&Url>) { let throttle = Arc::new(Throttle::new(targets.len(), Duration::from_millis(300))); let mut ongoing = self.ongoing.lock(); @@ -245,10 +282,10 @@ impl Scheduler { let target = target.clone(); let throttle = throttle.clone(); - let preload = self.preload.clone(); + let prework = self.prework.clone(); _ = self.micro.try_send( async move { - preload.size(PreloadOpSize { id, target, throttle }).await.ok(); + prework.size(PreworkOpSize { id, target, throttle }).await.ok(); } .boxed(), NORMAL, @@ -329,7 +366,7 @@ impl Scheduler { ) -> JoinHandle<()> { let file = self.file.clone(); let plugin = self.plugin.clone(); - let preload = self.preload.clone(); + let prework = self.prework.clone(); let prog = self.prog.clone(); let ongoing = self.ongoing.clone(); @@ -349,11 +386,11 @@ impl Scheduler { let result = match op { TaskOp::File(op) => file.work(*op).await, TaskOp::Plugin(op) => plugin.work(*op).await, - TaskOp::Preload(op) => preload.work(*op).await, + TaskOp::Prework(op) => prework.work(*op).await, }; if let Err(e) = result { - prog.send(TaskProg::Fail(id, format!("Failed to work on this task: {:?}", e))).ok(); + prog.send(TaskProg::Fail(id, format!("Failed to work on this task: {e:?}"))).ok(); } } } diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 5a121d3e..f43ea71e 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -1,29 +1,30 @@ [package] -name = "yazi-shared" -version = "0.2.5" -edition = "2021" -license = "MIT" -authors = [ "sxyazi " ] -description = "Yazi shared library" -homepage = "https://yazi-rs.github.io" -repository = "https://github.com/sxyazi/yazi" +name = "yazi-shared" +version = "0.2.5" +edition = "2021" +license = "MIT" +authors = [ "sxyazi " ] +description = "Yazi shared library" +homepage = "https://yazi-rs.github.io" +repository = "https://github.com/sxyazi/yazi" +rust-version = "1.78.0" [dependencies] -anyhow = "1.0.82" +anyhow = "1.0.86" bitflags = "2.5.0" crossterm = "0.27.0" dirs = "5.0.1" -filetime = "0.2.23" futures = "0.3.30" -parking_lot = "0.12.1" +parking_lot = "0.12.3" percent-encoding = "2.3.1" -ratatui = "=0.26.1" -regex = "1.10.4" -serde = "1.0.198" -tokio = { version = "1.37.0", features = [ "full" ] } - -# Logging -tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } +ratatui = "0.27.0" +regex = "1.10.5" +serde = { version = "1.0.203", features = [ "derive" ] } +shell-words = "1.1.0" +tokio = { version = "1.38.0", features = [ "full" ] } [target."cfg(unix)".dependencies] -libc = "0.2.153" +libc = "0.2.155" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.52.0", features = [ "Win32_Storage_FileSystem" ] } diff --git a/yazi-shared/src/chars.rs b/yazi-shared/src/chars.rs index 83fa989b..33f55ea9 100644 --- a/yazi-shared/src/chars.rs +++ b/yazi-shared/src/chars.rs @@ -18,3 +18,13 @@ impl CharKind { } } } + +pub fn strip_trailing_newline(mut s: String) -> String { + if s.ends_with('\n') { + s.pop(); + } + if s.ends_with('\r') { + s.pop(); + } + s +} diff --git a/yazi-shared/src/event/cmd.rs b/yazi-shared/src/event/cmd.rs index 82685827..37c9f54c 100644 --- a/yazi-shared/src/event/cmd.rs +++ b/yazi-shared/src/event/cmd.rs @@ -1,4 +1,7 @@ -use std::{any::Any, collections::HashMap, fmt::{self, Display}}; +use std::{any::Any, collections::HashMap, fmt::{self, Display}, mem, str::FromStr}; + +use anyhow::bail; +use serde::{de, Deserialize}; use super::Data; @@ -57,6 +60,11 @@ impl Cmd { self.args.get(name).and_then(Data::as_bool).unwrap_or(false) } + #[inline] + pub fn maybe_bool(&self, name: &str) -> Option { + self.args.get(name).and_then(Data::as_bool) + } + #[inline] pub fn first(&self) -> Option<&Data> { self.args.get("0") } @@ -112,3 +120,46 @@ impl Display for Cmd { Ok(()) } } + +impl FromStr for Cmd { + type Err = anyhow::Error; + + #[allow(clippy::explicit_counter_loop)] + fn from_str(s: &str) -> Result { + let mut args = shell_words::split(s)?; + if args.is_empty() || args[0].is_empty() { + bail!("command name cannot be empty"); + } + + let mut cmd = Cmd { name: mem::take(&mut args[0]), ..Default::default() }; + let mut i = 0usize; + for arg in args.into_iter().skip(1) { + let Some(arg) = arg.strip_prefix("--") else { + cmd.args.insert(i.to_string(), Data::String(arg)); + i += 1; + continue; + }; + + let mut parts = arg.splitn(2, '='); + let Some(key) = parts.next().map(|s| s.to_owned()) else { + bail!("invalid argument: {arg}"); + }; + + if let Some(val) = parts.next() { + cmd.args.insert(key, Data::String(val.to_owned())); + } else { + cmd.args.insert(key, Data::Boolean(true)); + } + } + Ok(cmd) + } +} + +impl<'de> Deserialize<'de> for Cmd { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + <_>::from_str(&String::deserialize(deserializer)?).map_err(de::Error::custom) + } +} diff --git a/yazi-shared/src/event/data.rs b/yazi-shared/src/event/data.rs index 927b7567..2b833e7e 100644 --- a/yazi-shared/src/event/data.rs +++ b/yazi-shared/src/event/data.rs @@ -25,6 +25,8 @@ impl Data { pub fn as_bool(&self) -> Option { match self { Self::Boolean(b) => Some(*b), + Self::String(s) if s == "no" => Some(false), + Self::String(s) if s == "yes" => Some(true), _ => None, } } diff --git a/yazi-shared/src/event/event.rs b/yazi-shared/src/event/event.rs index 6acb504d..bee1f911 100644 --- a/yazi-shared/src/event/event.rs +++ b/yazi-shared/src/event/event.rs @@ -1,6 +1,6 @@ use std::{collections::VecDeque, ffi::OsString}; -use crossterm::event::KeyEvent; +use crossterm::event::{KeyEvent, MouseEvent}; use tokio::sync::mpsc; use super::Cmd; @@ -15,6 +15,7 @@ pub enum Event { Seq(VecDeque, Layer), Render, Key(KeyEvent), + Mouse(MouseEvent), Resize, Paste(String), Quit(EventQuit), diff --git a/yazi-shared/src/fs/cha.rs b/yazi-shared/src/fs/cha.rs index a1b116ba..310ec706 100644 --- a/yazi-shared/src/fs/cha.rs +++ b/yazi-shared/src/fs/cha.rs @@ -5,16 +5,16 @@ use bitflags::bitflags; bitflags! { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ChaKind: u8 { - const DIR = 0b00000001; + const DIR = 0b00000001; - const HIDDEN = 0b00000010; - const LINK = 0b00000100; - const ORPHAN = 0b00001000; + const HIDDEN = 0b00000010; + const LINK = 0b00000100; + const ORPHAN = 0b00001000; - const BLOCK_DEVICE = 0b00010000; - const CHAR_DEVICE = 0b00100000; - const FIFO = 0b01000000; - const SOCKET = 0b10000000; + const BLOCK = 0b00010000; + const CHAR = 0b00100000; + const FIFO = 0b01000000; + const SOCKET = 0b10000000; } } @@ -28,9 +28,11 @@ pub struct Cha { #[cfg(unix)] pub permissions: libc::mode_t, #[cfg(unix)] - pub uid: u32, + pub uid: libc::uid_t, #[cfg(unix)] - pub gid: u32, + pub gid: libc::gid_t, + #[cfg(unix)] + pub nlink: libc::nlink_t, } impl From for Cha { @@ -44,10 +46,10 @@ impl From for Cha { { use std::os::unix::prelude::FileTypeExt; if m.file_type().is_block_device() { - ck |= ChaKind::BLOCK_DEVICE; + ck |= ChaKind::BLOCK; } if m.file_type().is_char_device() { - ck |= ChaKind::CHAR_DEVICE; + ck |= ChaKind::CHAR; } if m.file_type().is_fifo() { ck |= ChaKind::FIFO; @@ -61,24 +63,28 @@ impl From for Cha { kind: ck, len: m.len(), accessed: m.accessed().ok(), - // TODO: remove this once https://github.com/rust-lang/rust/issues/108277 is fixed. - created: None, + created: m.created().ok(), modified: m.modified().ok(), #[cfg(unix)] permissions: { use std::os::unix::prelude::PermissionsExt; - m.permissions().mode() as libc::mode_t + m.permissions().mode() as _ }, #[cfg(unix)] uid: { use std::os::unix::fs::MetadataExt; - m.uid() + m.uid() as _ }, #[cfg(unix)] gid: { use std::os::unix::fs::MetadataExt; - m.gid() + m.gid() as _ + }, + #[cfg(unix)] + nlink: { + use std::os::unix::fs::MetadataExt; + m.nlink() as _ }, } } @@ -106,16 +112,16 @@ impl Cha { pub fn is_orphan(&self) -> bool { self.kind.contains(ChaKind::ORPHAN) } #[inline] - pub fn is_block_device(&self) -> bool { self.kind.contains(ChaKind::BLOCK_DEVICE) } + pub fn is_block(&self) -> bool { self.kind.contains(ChaKind::BLOCK) } #[inline] - pub fn is_char_device(&self) -> bool { self.kind.contains(ChaKind::CHAR_DEVICE) } + pub fn is_char(&self) -> bool { self.kind.contains(ChaKind::CHAR) } #[inline] pub fn is_fifo(&self) -> bool { self.kind.contains(ChaKind::FIFO) } #[inline] - pub fn is_socket(&self) -> bool { self.kind.contains(ChaKind::SOCKET) } + pub fn is_sock(&self) -> bool { self.kind.contains(ChaKind::SOCKET) } #[inline] pub fn is_exec(&self) -> bool { diff --git a/yazi-shared/src/fs/file.rs b/yazi-shared/src/fs/file.rs index 90841bf5..eb3eebf5 100644 --- a/yazi-shared/src/fs/file.rs +++ b/yazi-shared/src/fs/file.rs @@ -20,6 +20,11 @@ impl Deref for File { fn deref(&self) -> &Self::Target { &self.cha } } +impl AsRef for File { + #[inline] + fn as_ref(&self) -> &File { self } +} + impl File { #[inline] pub async fn from(url: Url) -> Result { diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index 97934573..e201807b 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -1,16 +1,162 @@ -use std::{collections::VecDeque, fs::Metadata, path::{Path, PathBuf}}; +use std::{borrow::Cow, collections::{HashMap, VecDeque}, ffi::{OsStr, OsString}, fs::Metadata, path::{Path, PathBuf}}; -use anyhow::Result; -use filetime::{set_file_mtime, FileTime}; +use anyhow::{bail, Result}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; -pub async fn accessible(path: &Path) -> bool { - match fs::symlink_metadata(path).await { +#[inline] +pub async fn must_exists(p: impl AsRef) -> bool { fs::symlink_metadata(p).await.is_ok() } + +#[inline] +pub async fn maybe_exists(p: impl AsRef) -> bool { + match fs::symlink_metadata(p).await { Ok(_) => true, Err(e) => e.kind() != io::ErrorKind::NotFound, } } +#[inline] +pub fn ok_or_not_found(result: io::Result<()>) -> io::Result<()> { + match result { + Ok(()) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + Err(_) => result, + } +} + +#[inline] +pub async fn paths_to_same_file(a: impl AsRef, b: impl AsRef) -> bool { + _paths_to_same_file(a.as_ref(), b.as_ref()).await.unwrap_or(false) +} + +#[cfg(unix)] +async fn _paths_to_same_file(a: &Path, b: &Path) -> io::Result { + use std::os::unix::fs::MetadataExt; + + let (a_, b_) = (fs::symlink_metadata(a).await?, fs::symlink_metadata(b).await?); + Ok( + a_.ino() == b_.ino() + && a_.dev() == b_.dev() + && fs::canonicalize(a).await? == fs::canonicalize(b).await?, + ) +} + +#[cfg(windows)] +async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result { + use std::os::windows::{ffi::OsStringExt, io::AsRawHandle}; + + use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{GetFinalPathNameByHandleW, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, VOLUME_NAME_DOS}}; + + async fn final_name(p: &Path) -> std::io::Result { + let file = tokio::fs::OpenOptions::new() + .access_mode(0) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) + .open(p) + .await?; + + tokio::task::spawn_blocking(move || { + let mut buf = [0u16; MAX_PATH as usize]; + let len = unsafe { + GetFinalPathNameByHandleW( + file.as_raw_handle() as HANDLE, + buf.as_mut_ptr(), + buf.len() as u32, + VOLUME_NAME_DOS, + ) + }; + + if len == 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize]))) + } + }) + .await? + } + + Ok(final_name(a).await? == final_name(b).await?) +} + +pub async fn symlink_realpath(path: &Path) -> Result { + let p = fs::canonicalize(path).await?; + if p == path { + return Ok(p); + } + + let Some(parent) = path.parent() else { bail!("no parent") }; + symlink_realname(path, &mut HashMap::new()).await.map(|n| parent.join(n)) +} + +#[cfg(unix)] +#[tokio::test] +async fn test_symlink_realpath() { + fs::remove_dir_all("/tmp/issue-1173").await.ok(); + fs::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap(); + fs::File::create("/tmp/issue-1173/A").await.unwrap(); + fs::File::create("/tmp/issue-1173/b").await.unwrap(); + fs::File::create("/tmp/issue-1173/real-dir/C").await.unwrap(); + fs::symlink("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap(); + fs::symlink("real-dir", "/tmp/issue-1173/link-dir").await.unwrap(); + + async fn check(a: &str, b: &str) { + let expected = if a == b || cfg!(windows) || cfg!(target_os = "macos") { + Some(PathBuf::from(b)) + } else { + None + }; + assert_eq!(symlink_realpath(Path::new(a)).await.ok(), expected); + } + + check("/tmp/issue-1173/a", "/tmp/issue-1173/A").await; + check("/tmp/issue-1173/A", "/tmp/issue-1173/A").await; + + check("/tmp/issue-1173/b", "/tmp/issue-1173/b").await; + check("/tmp/issue-1173/B", "/tmp/issue-1173/b").await; + + check("/tmp/issue-1173/link-dir/c", "/tmp/issue-1173/link-dir/C").await; + check("/tmp/issue-1173/link-dir/C", "/tmp/issue-1173/link-dir/C").await; + + check("/tmp/issue-1173/d", "/tmp/issue-1173/D").await; + check("/tmp/issue-1173/D", "/tmp/issue-1173/D").await; +} + +// realpath(3) without resolving symlinks. This is useful for case-insensitive +// filesystems. +// +// Make sure the file of the path exists. +pub async fn symlink_realname<'a>( + path: &'a Path, + cached: &'a mut HashMap>, +) -> Result> { + let Some(name) = path.file_name() else { bail!("no file name") }; + let Some(parent) = path.parent() else { return Ok(name.into()) }; + + if !cached.contains_key(parent) { + let mut map = HashMap::new(); + let mut it = fs::read_dir(parent).await?; + while let Some(entry) = it.next_entry().await? { + let n = entry.file_name(); + if n.as_encoded_bytes().iter().all(|&b| b.is_ascii_lowercase()) { + map.insert(n, OsString::new()); + } else { + map.insert(n.to_ascii_lowercase(), n); + } + } + cached.insert(parent.to_owned(), map); + } + + let c = &cached[parent]; + if let Some(s) = c.get(name) { + return if s.is_empty() { Ok(name.into()) } else { Ok(s.into()) }; + } + + let lowercased = name.to_ascii_lowercase(); + if let Some(s) = c.get(&lowercased) { + return if s.is_empty() { Ok(lowercased.into()) } else { Ok(s.into()) }; + } + + Ok(name.into()) +} + pub async fn calculate_size(path: &Path) -> u64 { let mut total = 0; let mut stack = VecDeque::from([path.to_path_buf()]); @@ -45,12 +191,28 @@ pub fn copy_with_progress( tokio::spawn({ let (from, to) = (from.to_owned(), to.to_owned()); - let mtime = FileTime::from_last_modification_time(meta); + + let mut ft = std::fs::FileTimes::new(); + meta.accessed().map(|t| ft = ft.set_accessed(t)).ok(); + meta.modified().map(|t| ft = ft.set_modified(t)).ok(); + #[cfg(target_os = "macos")] + { + use std::os::macos::fs::FileTimesExt; + meta.created().map(|t| ft = ft.set_created(t)).ok(); + } + #[cfg(windows)] + { + use std::os::windows::fs::FileTimesExt; + meta.created().map(|t| ft = ft.set_created(t)).ok(); + } async move { _ = match fs::copy(&from, &to).await { Ok(len) => { - set_file_mtime(to, mtime).ok(); + _ = tokio::task::spawn_blocking(move || { + std::fs::File::options().write(true).open(to).and_then(|f| f.set_times(ft)).ok(); + }) + .await; tick_tx.send(Ok(len)) } Err(e) => tick_tx.send(Err(e)), diff --git a/yazi-shared/src/fs/path.rs b/yazi-shared/src/fs/path.rs index 155a120b..d21847ae 100644 --- a/yazi-shared/src/fs/path.rs +++ b/yazi-shared/src/fs/path.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, env, ffi::OsString, path::{Component, Path, PathBuf, MAIN_SEPARATOR}}; -use super::accessible; +use super::maybe_exists; use crate::fs::Url; #[inline] @@ -26,14 +26,18 @@ fn _expand_path(p: &Path) -> PathBuf { env::var(name.as_str()).unwrap_or_else(|_| caps.get(0).unwrap().as_str().to_owned()) }); + // Windows paths that only have a drive letter but no root, e.g. "D:" + #[cfg(windows)] + if s.len() == 2 { + let b = s.as_bytes(); + if b[1] == b':' && b[0].is_ascii_alphabetic() { + return PathBuf::from(s.to_uppercase() + "\\"); + } + } + let p = Path::new(s.as_ref()); if let Ok(rest) = p.strip_prefix("~") { - #[cfg(unix)] - let home = env::var_os("HOME"); - #[cfg(windows)] - let home = env::var_os("USERPROFILE"); - - return if let Some(p) = home { PathBuf::from(p).join(rest) } else { rest.to_path_buf() }; + return dirs::home_dir().unwrap_or_default().join(rest); } if p.is_absolute() { @@ -67,7 +71,7 @@ pub async fn unique_path(mut p: Url) -> Url { .unwrap_or_default(); let mut i = 0; - while accessible(&p).await { + while maybe_exists(&p).await { i += 1; let mut name = OsString::with_capacity(stem.len() + ext.len() + 5); diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 1a81d158..dc11a9b3 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -12,10 +12,11 @@ mod natsort; mod number; mod os; mod ro_cell; -pub mod term; +mod terminal; pub mod theme; mod throttle; mod time; +mod translit; mod xdg; pub use chars::*; @@ -26,10 +27,13 @@ pub use errors::*; pub use layer::*; pub use natsort::*; pub use number::*; +#[cfg(unix)] pub use os::*; pub use ro_cell::*; +pub use terminal::*; pub use throttle::*; pub use time::*; +pub use translit::*; pub use xdg::*; pub fn init() { event::Event::init(); } diff --git a/yazi-shared/src/natsort.rs b/yazi-shared/src/natsort.rs index 70e25969..0a3a7c98 100644 --- a/yazi-shared/src/natsort.rs +++ b/yazi-shared/src/natsort.rs @@ -135,12 +135,12 @@ mod tests { #[test] fn test_natsort() { - let dates = vec!["1999-3-3", "1999-12-25", "2000-1-2", "2000-1-10", "2000-3-23"]; - let fractions = vec![ + let dates = ["1999-3-3", "1999-12-25", "2000-1-2", "2000-1-10", "2000-3-23"]; + let fractions = [ "1.002.01", "1.002.03", "1.002.08", "1.009.02", "1.009.10", "1.009.20", "1.010.12", "1.011.02", ]; - let words = vec![ + let words = [ "1-02", "1-2", "1-20", diff --git a/yazi-shared/src/ro_cell.rs b/yazi-shared/src/ro_cell.rs index 3783f618..4ed209f3 100644 --- a/yazi-shared/src/ro_cell.rs +++ b/yazi-shared/src/ro_cell.rs @@ -13,7 +13,7 @@ impl RoCell { #[inline] pub fn init(&self, value: T) { - debug_assert!(!self.is_initialized()); + debug_assert!(!self.initialized()); unsafe { *self.0.get() = Some(value); } @@ -29,25 +29,25 @@ impl RoCell { #[inline] pub fn replace(&self, value: T) -> T { - debug_assert!(self.is_initialized()); + debug_assert!(self.initialized()); unsafe { mem::replace(&mut *self.0.get(), Some(value)).unwrap_unchecked() } } #[inline] pub fn drop(&self) -> T { - debug_assert!(self.is_initialized()); + debug_assert!(self.initialized()); unsafe { mem::take(&mut *self.0.get()).unwrap_unchecked() } } #[inline] - fn is_initialized(&self) -> bool { unsafe { (*self.0.get()).is_some() } } + fn initialized(&self) -> bool { unsafe { (*self.0.get()).is_some() } } } impl Deref for RoCell { type Target = T; fn deref(&self) -> &Self::Target { - debug_assert!(self.is_initialized()); + debug_assert!(self.initialized()); unsafe { (*self.0.get()).as_ref().unwrap_unchecked() } } } diff --git a/yazi-shared/src/term/csi_u.rs b/yazi-shared/src/term/csi_u.rs deleted file mode 100644 index 335fe59f..00000000 --- a/yazi-shared/src/term/csi_u.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::time::Duration; - -use anyhow::{bail, Result}; -use tokio::{io::{stdin, AsyncReadExt, BufReader}, time::timeout}; -use tracing::error; - -use super::Term; - -impl Term { - pub async fn read_until_da1() -> Result { - let read = async { - let mut stdin = BufReader::new(stdin()); - let mut buf = String::with_capacity(200); - loop { - let mut c = [0; 1]; - if stdin.read(&mut c).await? == 0 { - bail!("unexpected EOF"); - } - if c[0] == b'c' && buf.contains("\x1b[?") { - break; - } - buf.push(c[0] as char); - } - Ok(buf) - }; - - let timeout = timeout(Duration::from_secs(10), read).await; - if let Err(ref e) = timeout { - error!("read_until_da1: {e:?}"); - } - - timeout? - } -} diff --git a/yazi-shared/src/term/cursor.rs b/yazi-shared/src/term/cursor.rs deleted file mode 100644 index 4cbbd0f2..00000000 --- a/yazi-shared/src/term/cursor.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::io::{stderr, BufWriter, StderrLock, Write}; - -use anyhow::Result; -use crossterm::{cursor::{MoveTo, RestorePosition, SavePosition, SetCursorStyle}, queue}; - -use super::Term; - -impl Term { - #[inline] - pub fn move_to(w: &mut impl Write, x: u16, y: u16) -> Result<()> { Ok(queue!(w, MoveTo(x, y))?) } - - // FIXME: remove this function - #[inline] - pub fn move_lock((x, y): (u16, u16), cb: F) -> Result - where - F: FnOnce(&mut BufWriter) -> Result, - { - let mut buf = BufWriter::new(stderr().lock()); - #[cfg(windows)] - { - use std::{thread, time::Duration}; - - use crossterm::cursor::{Hide, Show}; - queue!(buf, SavePosition, MoveTo(x, y), Show)?; - - // I really don't want to add this, - // but on Windows the cursor position will not synchronize in time occasionally - buf.flush()?; - thread::sleep(Duration::from_millis(1)); - - let result = cb(&mut buf); - queue!(buf, Hide, RestorePosition)?; - buf.flush()?; - result - } - #[cfg(unix)] - { - queue!(buf, SavePosition, MoveTo(x, y))?; - let result = cb(&mut buf); - queue!(buf, RestorePosition)?; - buf.flush()?; - result - } - } - - #[inline] - pub fn set_cursor_block() -> Result<()> { Ok(queue!(stderr(), SetCursorStyle::BlinkingBlock)?) } - - #[inline] - pub fn set_cursor_bar() -> Result<()> { Ok(queue!(stderr(), SetCursorStyle::BlinkingBar)?) } -} diff --git a/yazi-shared/src/term/mod.rs b/yazi-shared/src/term/mod.rs deleted file mode 100644 index 91aab0b9..00000000 --- a/yazi-shared/src/term/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![allow(clippy::module_inception)] - -mod csi_u; -mod cursor; -mod term; - -pub use term::*; diff --git a/yazi-shared/src/terminal.rs b/yazi-shared/src/terminal.rs new file mode 100644 index 00000000..ed66d0cc --- /dev/null +++ b/yazi-shared/src/terminal.rs @@ -0,0 +1,10 @@ +use std::io::Write; + +use crossterm::queue; + +#[inline] +pub fn terminal_clear(w: &mut impl Write) -> std::io::Result<()> { + queue!(w, crossterm::terminal::Clear(crossterm::terminal::ClearType::All))?; + writeln!(w)?; + w.flush() +} diff --git a/yazi-shared/src/theme/color.rs b/yazi-shared/src/theme/color.rs index 5cde2172..97c357be 100644 --- a/yazi-shared/src/theme/color.rs +++ b/yazi-shared/src/theme/color.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use anyhow::Result; +use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Deserialize)] @@ -11,7 +11,7 @@ impl FromStr for Color { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - ratatui::style::Color::from_str(s).map(Self).map_err(|_| anyhow::anyhow!("invalid color")) + ratatui::style::Color::from_str(s).map(Self).map_err(|_| anyhow!("invalid color: {s}")) } } diff --git a/yazi-shared/src/theme/style.rs b/yazi-shared/src/theme/style.rs index 8054d4a4..529d4e2e 100644 --- a/yazi-shared/src/theme/style.rs +++ b/yazi-shared/src/theme/style.rs @@ -3,7 +3,7 @@ use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; use super::Color; -#[derive(Clone, Copy, Debug, Deserialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize)] #[serde(from = "StyleShadow")] pub struct Style { pub fg: Option, diff --git a/yazi-shared/src/translit/mod.rs b/yazi-shared/src/translit/mod.rs new file mode 100644 index 00000000..a51cf385 --- /dev/null +++ b/yazi-shared/src/translit/mod.rs @@ -0,0 +1,5 @@ +mod table; +mod traits; + +use table::*; +pub use traits::*; diff --git a/yazi-shared/src/translit/table.rs b/yazi-shared/src/translit/table.rs new file mode 100644 index 00000000..1a5fa832 --- /dev/null +++ b/yazi-shared/src/translit/table.rs @@ -0,0 +1,762 @@ +const TABLE_0: [&str; 496] = [ + "A", // À 192 + "A", // Á 193 + "A", //  194 + "A", // à 195 + "A", // Ä 196 + "A", // Å 197 + "AE", // Æ 198 + "C", // Ç 199 + "E", // È 200 + "E", // É 201 + "E", // Ê 202 + "E", // Ë 203 + "I", // Ì 204 + "I", // Í 205 + "I", // Î 206 + "I", // Ï 207 + "D", // Ð 208 + "N", // Ñ 209 + "O", // Ò 210 + "O", // Ó 211 + "O", // Ô 212 + "O", // Õ 213 + "O", // Ö 214 + "×", // × 215 + "O", // Ø 216 + "U", // Ù 217 + "U", // Ú 218 + "U", // Û 219 + "U", // Ü 220 + "Y", // Ý 221 + "T", // Þ 222 + "ss", // ß 223 + "a", // à 224 + "a", // á 225 + "a", // â 226 + "a", // ã 227 + "a", // ä 228 + "a", // å 229 + "ae", // æ 230 + "c", // ç 231 + "e", // è 232 + "e", // é 233 + "e", // ê 234 + "e", // ë 235 + "i", // ì 236 + "i", // í 237 + "i", // î 238 + "i", // ï 239 + "d", // ð 240 + "n", // ñ 241 + "o", // ò 242 + "o", // ó 243 + "o", // ô 244 + "o", // õ 245 + "o", // ö 246 + "÷", // ÷ 247 + "o", // ø 248 + "u", // ù 249 + "u", // ú 250 + "u", // û 251 + "u", // ü 252 + "y", // ý 253 + "t", // þ 254 + "y", // ÿ 255 + "A", // Ā 256 + "a", // ā 257 + "A", // Ă 258 + "a", // ă 259 + "A", // Ą 260 + "a", // ą 261 + "C", // Ć 262 + "c", // ć 263 + "C", // Ĉ 264 + "c", // ĉ 265 + "C", // Ċ 266 + "c", // ċ 267 + "C", // Č 268 + "c", // č 269 + "D", // Ď 270 + "d", // ď 271 + "D", // Đ 272 + "d", // đ 273 + "E", // Ē 274 + "e", // ē 275 + "E", // Ĕ 276 + "e", // ĕ 277 + "E", // Ė 278 + "e", // ė 279 + "E", // Ę 280 + "e", // ę 281 + "E", // Ě 282 + "e", // ě 283 + "G", // Ĝ 284 + "g", // ĝ 285 + "G", // Ğ 286 + "g", // ğ 287 + "G", // Ġ 288 + "g", // ġ 289 + "G", // Ģ 290 + "g", // ģ 291 + "H", // Ĥ 292 + "h", // ĥ 293 + "H", // Ħ 294 + "h", // ħ 295 + "I", // Ĩ 296 + "i", // ĩ 297 + "I", // Ī 298 + "i", // ī 299 + "I", // Ĭ 300 + "i", // ĭ 301 + "I", // Į 302 + "i", // į 303 + "I", // İ 304 + "i", // ı 305 + "IJ", // IJ 306 + "ij", // ij 307 + "J", // Ĵ 308 + "j", // ĵ 309 + "K", // Ķ 310 + "k", // ķ 311 + "k", // ĸ 312 + "L", // Ĺ 313 + "l", // ĺ 314 + "L", // Ļ 315 + "l", // ļ 316 + "L", // Ľ 317 + "l", // ľ 318 + "L", // Ŀ 319 + "l", // ŀ 320 + "L", // Ł 321 + "l", // ł 322 + "N", // Ń 323 + "n", // ń 324 + "N", // Ņ 325 + "n", // ņ 326 + "N", // Ň 327 + "n", // ň 328 + "n", // ʼn 329 + "N", // Ŋ 330 + "n", // ŋ 331 + "O", // Ō 332 + "o", // ō 333 + "O", // Ŏ 334 + "o", // ŏ 335 + "O", // Ő 336 + "o", // ő 337 + "OE", // Œ 338 + "oe", // œ 339 + "R", // Ŕ 340 + "r", // ŕ 341 + "R", // Ŗ 342 + "r", // ŗ 343 + "R", // Ř 344 + "r", // ř 345 + "S", // Ś 346 + "s", // ś 347 + "S", // Ŝ 348 + "s", // ŝ 349 + "S", // Ş 350 + "s", // ş 351 + "S", // Š 352 + "s", // š 353 + "T", // Ţ 354 + "t", // ţ 355 + "T", // Ť 356 + "t", // ť 357 + "T", // Ŧ 358 + "t", // ŧ 359 + "U", // Ũ 360 + "u", // ũ 361 + "U", // Ū 362 + "u", // ū 363 + "U", // Ŭ 364 + "u", // ŭ 365 + "U", // Ů 366 + "u", // ů 367 + "U", // Ű 368 + "u", // ű 369 + "U", // Ų 370 + "u", // ų 371 + "W", // Ŵ 372 + "w", // ŵ 373 + "Y", // Ŷ 374 + "y", // ŷ 375 + "Y", // Ÿ 376 + "Z", // Ź 377 + "z", // ź 378 + "Z", // Ż 379 + "z", // ż 380 + "Z", // Ž 381 + "z", // ž 382 + "s", // ſ 383 + "ƀ", // ƀ 384 + "B", // Ɓ 385 + "Ƃ", // Ƃ 386 + "ƃ", // ƃ 387 + "Ƅ", // Ƅ 388 + "ƅ", // ƅ 389 + "O", // Ɔ 390 + "Ƈ", // Ƈ 391 + "ƈ", // ƈ 392 + "Ɖ", // Ɖ 393 + "D", // Ɗ 394 + "Ƌ", // Ƌ 395 + "ƌ", // ƌ 396 + "ƍ", // ƍ 397 + "Ǝ", // Ǝ 398 + "E", // Ə 399 + "E", // Ɛ 400 + "F", // Ƒ 401 + "f", // ƒ 402 + "Ɠ", // Ɠ 403 + "Ɣ", // Ɣ 404 + "ƕ", // ƕ 405 + "Ɩ", // Ɩ 406 + "Ɨ", // Ɨ 407 + "K", // Ƙ 408 + "k", // ƙ 409 + "l", // ƚ 410 + "l", // ƛ 411 + "Ɯ", // Ɯ 412 + "N", // Ɲ 413 + "ƞ", // ƞ 414 + "O", // Ɵ 415 + "O", // Ơ 416 + "o", // ơ 417 + "Ƣ", // Ƣ 418 + "ƣ", // ƣ 419 + "Ƥ", // Ƥ 420 + "ƥ", // ƥ 421 + "Ʀ", // Ʀ 422 + "Ƨ", // Ƨ 423 + "ƨ", // ƨ 424 + "Ʃ", // Ʃ 425 + "ƪ", // ƪ 426 + "ƫ", // ƫ 427 + "Ƭ", // Ƭ 428 + "ƭ", // ƭ 429 + "Ʈ", // Ʈ 430 + "U", // Ư 431 + "u", // ư 432 + "Ʊ", // Ʊ 433 + "Ʋ", // Ʋ 434 + "Y", // Ƴ 435 + "y", // ƴ 436 + "Z", // Ƶ 437 + "z", // ƶ 438 + "Ʒ", // Ʒ 439 + "Ƹ", // Ƹ 440 + "ƹ", // ƹ 441 + "ƺ", // ƺ 442 + "ƻ", // ƻ 443 + "Ƽ", // Ƽ 444 + "ƽ", // ƽ 445 + "ƾ", // ƾ 446 + "ƿ", // ƿ 447 + "ǀ", // ǀ 448 + "ǁ", // ǁ 449 + "ǂ", // ǂ 450 + "ǃ", // ǃ 451 + "DZ", // DŽ 452 + "Dz", // Dž 453 + "dz", // dž 454 + "LJ", // LJ 455 + "Lj", // Lj 456 + "lj", // lj 457 + "NJ", // NJ 458 + "Nj", // Nj 459 + "nj", // nj 460 + "A", // Ǎ 461 + "a", // ǎ 462 + "I", // Ǐ 463 + "i", // ǐ 464 + "O", // Ǒ 465 + "o", // ǒ 466 + "U", // Ǔ 467 + "u", // ǔ 468 + "U", // Ǖ 469 + "u", // ǖ 470 + "U", // Ǘ 471 + "u", // ǘ 472 + "U", // Ǚ 473 + "u", // ǚ 474 + "U", // Ǜ 475 + "u", // ǜ 476 + "ǝ", // ǝ 477 + "Ǟ", // Ǟ 478 + "ǟ", // ǟ 479 + "Ǡ", // Ǡ 480 + "ǡ", // ǡ 481 + "Ǣ", // Ǣ 482 + "ǣ", // ǣ 483 + "Ǥ", // Ǥ 484 + "ǥ", // ǥ 485 + "G", // Ǧ 486 + "g", // ǧ 487 + "Ǩ", // Ǩ 488 + "ǩ", // ǩ 489 + "O", // Ǫ 490 + "o", // ǫ 491 + "Ǭ", // Ǭ 492 + "ǭ", // ǭ 493 + "Ǯ", // Ǯ 494 + "e", // ǯ 495 + "j", // ǰ 496 + "DZ", // DZ 497 + "Dz", // Dz 498 + "dz", // dz 499 + "G", // Ǵ 500 + "g", // ǵ 501 + "Ƕ", // Ƕ 502 + "Ƿ", // Ƿ 503 + "N", // Ǹ 504 + "n", // ǹ 505 + "A", // Ǻ 506 + "a", // ǻ 507 + "AE", // Ǽ 508 + "ae", // ǽ 509 + "O", // Ǿ 510 + "o", // ǿ 511 + "Ȁ", // Ȁ 512 + "ȁ", // ȁ 513 + "Ȃ", // Ȃ 514 + "ȃ", // ȃ 515 + "Ȅ", // Ȅ 516 + "ȅ", // ȅ 517 + "Ȇ", // Ȇ 518 + "ȇ", // ȇ 519 + "Ȉ", // Ȉ 520 + "ȉ", // ȉ 521 + "Ȋ", // Ȋ 522 + "ȋ", // ȋ 523 + "Ȍ", // Ȍ 524 + "ȍ", // ȍ 525 + "Ȏ", // Ȏ 526 + "ȏ", // ȏ 527 + "Ȑ", // Ȑ 528 + "ȑ", // ȑ 529 + "Ȓ", // Ȓ 530 + "ȓ", // ȓ 531 + "Ȕ", // Ȕ 532 + "ȕ", // ȕ 533 + "Ȗ", // Ȗ 534 + "ȗ", // ȗ 535 + "S", // Ș 536 + "s", // ș 537 + "T", // Ț 538 + "t", // ț 539 + "Ȝ", // Ȝ 540 + "ȝ", // ȝ 541 + "Ȟ", // Ȟ 542 + "ȟ", // ȟ 543 + "Ƞ", // Ƞ 544 + "ȡ", // ȡ 545 + "Ȣ", // Ȣ 546 + "ȣ", // ȣ 547 + "Ȥ", // Ȥ 548 + "ȥ", // ȥ 549 + "Ȧ", // Ȧ 550 + "ȧ", // ȧ 551 + "Ȩ", // Ȩ 552 + "ȩ", // ȩ 553 + "Ȫ", // Ȫ 554 + "ȫ", // ȫ 555 + "Ȭ", // Ȭ 556 + "ȭ", // ȭ 557 + "Ȯ", // Ȯ 558 + "ȯ", // ȯ 559 + "Ȱ", // Ȱ 560 + "ȱ", // ȱ 561 + "Y", // Ȳ 562 + "y", // ȳ 563 + "ȴ", // ȴ 564 + "ȵ", // ȵ 565 + "ȶ", // ȶ 566 + "j", // ȷ 567 + "ȸ", // ȸ 568 + "ȹ", // ȹ 569 + "Ⱥ", // Ⱥ 570 + "Ȼ", // Ȼ 571 + "ȼ", // ȼ 572 + "L", // Ƚ 573 + "Ⱦ", // Ⱦ 574 + "ȿ", // ȿ 575 + "ɀ", // ɀ 576 + "Ɂ", // Ɂ 577 + "ɂ", // ɂ 578 + "Ƀ", // Ƀ 579 + "Ʉ", // Ʉ 580 + "Ʌ", // Ʌ 581 + "Ɇ", // Ɇ 582 + "ɇ", // ɇ 583 + "Ɉ", // Ɉ 584 + "ɉ", // ɉ 585 + "Ɋ", // Ɋ 586 + "ɋ", // ɋ 587 + "Ɍ", // Ɍ 588 + "ɍ", // ɍ 589 + "Ɏ", // Ɏ 590 + "ɏ", // ɏ 591 + "a", // ɐ 592 + "a", // ɑ 593 + "a", // ɒ 594 + "b", // ɓ 595 + "o", // ɔ 596 + "c", // ɕ 597 + "d", // ɖ 598 + "d", // ɗ 599 + "e", // ɘ 600 + "e", // ə 601 + "e", // ɚ 602 + "o", // ɛ 603 + "e", // ɜ 604 + "e", // ɝ 605 + "e", // ɞ 606 + "j", // ɟ 607 + "g", // ɠ 608 + "g", // ɡ 609 + "ɢ", // ɢ 610 + "g", // ɣ 611 + "ɤ", // ɤ 612 + "h", // ɥ 613 + "h", // ɦ 614 + "h", // ɧ 615 + "i", // ɨ 616 + "i", // ɩ 617 + "ɪ", // ɪ 618 + "l", // ɫ 619 + "l", // ɬ 620 + "l", // ɭ 621 + "le", // ɮ 622 + "m", // ɯ 623 + "m", // ɰ 624 + "m", // ɱ 625 + "n", // ɲ 626 + "n", // ɳ 627 + "ɴ", // ɴ 628 + "o", // ɵ 629 + "OE", // ɶ 630 + "ɷ", // ɷ 631 + "p", // ɸ 632 + "r", // ɹ 633 + "r", // ɺ 634 + "r", // ɻ 635 + "r", // ɼ 636 + "r", // ɽ 637 + "r", // ɾ 638 + "r", // ɿ 639 + "ʀ", // ʀ 640 + "R", // ʁ 641 + "s", // ʂ 642 + "s", // ʃ 643 + "j", // ʄ 644 + "s", // ʅ 645 + "s", // ʆ 646 + "t", // ʇ 647 + "t", // ʈ 648 + "u", // ʉ 649 + "u", // ʊ 650 + "v", // ʋ 651 + "v", // ʌ 652 + "w", // ʍ 653 + "y", // ʎ 654 + "ʏ", // ʏ 655 + "z", // ʐ 656 + "z", // ʑ 657 + "e", // ʒ 658 + "e", // ʓ 659 + "ʔ", // ʔ 660 + "ʕ", // ʕ 661 + "ʖ", // ʖ 662 + "C", // ʗ 663 + "o", // ʘ 664 + "ʙ", // ʙ 665 + "e", // ʚ 666 + "G", // ʛ 667 + "ʜ", // ʜ 668 + "j", // ʝ 669 + "k", // ʞ 670 + "ʟ", // ʟ 671 + "q", // ʠ 672 + "ʡ", // ʡ 673 + "ʢ", // ʢ 674 + "dz", // ʣ 675 + "de", // ʤ 676 + "dz", // ʥ 677 + "ts", // ʦ 678 + "ts", // ʧ 679 + "tc", // ʨ 680 + "fn", // ʩ 681 + "ls", // ʪ 682 + "lz", // ʫ 683 + "W", // ʬ 684 + "ʭ", // ʭ 685 + "h", // ʮ 686 + "h", // ʯ 687 +]; + +const TABLE_1: [&str; 246] = [ + "B", // Ḅ 7684 + "b", // ḅ 7685 + "Ḇ", // Ḇ 7686 + "ḇ", // ḇ 7687 + "Ḉ", // Ḉ 7688 + "ḉ", // ḉ 7689 + "Ḋ", // Ḋ 7690 + "ḋ", // ḋ 7691 + "D", // Ḍ 7692 + "d", // ḍ 7693 + "D", // Ḏ 7694 + "d", // ḏ 7695 + "Ḑ", // Ḑ 7696 + "ḑ", // ḑ 7697 + "D", // Ḓ 7698 + "d", // ḓ 7699 + "Ḕ", // Ḕ 7700 + "ḕ", // ḕ 7701 + "Ḗ", // Ḗ 7702 + "ḗ", // ḗ 7703 + "Ḙ", // Ḙ 7704 + "ḙ", // ḙ 7705 + "Ḛ", // Ḛ 7706 + "ḛ", // ḛ 7707 + "Ḝ", // Ḝ 7708 + "ḝ", // ḝ 7709 + "Ḟ", // Ḟ 7710 + "ḟ", // ḟ 7711 + "G", // Ḡ 7712 + "g", // ḡ 7713 + "Ḣ", // Ḣ 7714 + "ḣ", // ḣ 7715 + "H", // Ḥ 7716 + "h", // ḥ 7717 + "Ḧ", // Ḧ 7718 + "ḧ", // ḧ 7719 + "Ḩ", // Ḩ 7720 + "ḩ", // ḩ 7721 + "H", // Ḫ 7722 + "h", // ḫ 7723 + "Ḭ", // Ḭ 7724 + "ḭ", // ḭ 7725 + "Ḯ", // Ḯ 7726 + "ḯ", // ḯ 7727 + "Ḱ", // Ḱ 7728 + "ḱ", // ḱ 7729 + "K", // Ḳ 7730 + "k", // ḳ 7731 + "K", // Ḵ 7732 + "k", // ḵ 7733 + "L", // Ḷ 7734 + "l", // ḷ 7735 + "L", // Ḹ 7736 + "l", // ḹ 7737 + "L", // Ḻ 7738 + "l", // ḻ 7739 + "L", // Ḽ 7740 + "l", // ḽ 7741 + "M", // Ḿ 7742 + "m", // ḿ 7743 + "M", // Ṁ 7744 + "m", // ṁ 7745 + "M", // Ṃ 7746 + "m", // ṃ 7747 + "N", // Ṅ 7748 + "n", // ṅ 7749 + "N", // Ṇ 7750 + "n", // ṇ 7751 + "N", // Ṉ 7752 + "n", // ṉ 7753 + "N", // Ṋ 7754 + "n", // ṋ 7755 + "Ṍ", // Ṍ 7756 + "ṍ", // ṍ 7757 + "Ṏ", // Ṏ 7758 + "ṏ", // ṏ 7759 + "Ṑ", // Ṑ 7760 + "ṑ", // ṑ 7761 + "Ṓ", // Ṓ 7762 + "ṓ", // ṓ 7763 + "Ṕ", // Ṕ 7764 + "ṕ", // ṕ 7765 + "Ṗ", // Ṗ 7766 + "ṗ", // ṗ 7767 + "R", // Ṙ 7768 + "r", // ṙ 7769 + "R", // Ṛ 7770 + "r", // ṛ 7771 + "R", // Ṝ 7772 + "r", // ṝ 7773 + "R", // Ṟ 7774 + "r", // ṟ 7775 + "S", // Ṡ 7776 + "s", // ṡ 7777 + "S", // Ṣ 7778 + "s", // ṣ 7779 + "Ṥ", // Ṥ 7780 + "ṥ", // ṥ 7781 + "Ṧ", // Ṧ 7782 + "ṧ", // ṧ 7783 + "Ṩ", // Ṩ 7784 + "ṩ", // ṩ 7785 + "Ṫ", // Ṫ 7786 + "ṫ", // ṫ 7787 + "T", // Ṭ 7788 + "t", // ṭ 7789 + "T", // Ṯ 7790 + "t", // ṯ 7791 + "T", // Ṱ 7792 + "t", // ṱ 7793 + "Ṳ", // Ṳ 7794 + "ṳ", // ṳ 7795 + "Ṵ", // Ṵ 7796 + "ṵ", // ṵ 7797 + "Ṷ", // Ṷ 7798 + "ṷ", // ṷ 7799 + "Ṹ", // Ṹ 7800 + "ṹ", // ṹ 7801 + "Ṻ", // Ṻ 7802 + "ṻ", // ṻ 7803 + "Ṽ", // Ṽ 7804 + "ṽ", // ṽ 7805 + "Ṿ", // Ṿ 7806 + "ṿ", // ṿ 7807 + "W", // Ẁ 7808 + "w", // ẁ 7809 + "W", // Ẃ 7810 + "w", // ẃ 7811 + "W", // Ẅ 7812 + "w", // ẅ 7813 + "Ẇ", // Ẇ 7814 + "ẇ", // ẇ 7815 + "Ẉ", // Ẉ 7816 + "ẉ", // ẉ 7817 + "Ẋ", // Ẋ 7818 + "ẋ", // ẋ 7819 + "Ẍ", // Ẍ 7820 + "ẍ", // ẍ 7821 + "Y", // Ẏ 7822 + "y", // ẏ 7823 + "Ẑ", // Ẑ 7824 + "ẑ", // ẑ 7825 + "Z", // Ẓ 7826 + "z", // ẓ 7827 + "Z", // Ẕ 7828 + "z", // ẕ 7829 + "h", // ẖ 7830 + "t", // ẗ 7831 + "ẘ", // ẘ 7832 + "ẙ", // ẙ 7833 + "ẚ", // ẚ 7834 + "ẛ", // ẛ 7835 + "ẜ", // ẜ 7836 + "ẝ", // ẝ 7837 + "SS", // ẞ 7838 + "ẟ", // ẟ 7839 + "A", // Ạ 7840 + "a", // ạ 7841 + "A", // Ả 7842 + "a", // ả 7843 + "A", // Ấ 7844 + "a", // ấ 7845 + "A", // Ầ 7846 + "a", // ầ 7847 + "A", // Ẩ 7848 + "a", // ẩ 7849 + "A", // Ẫ 7850 + "a", // ẫ 7851 + "A", // Ậ 7852 + "a", // ậ 7853 + "A", // Ắ 7854 + "a", // ắ 7855 + "A", // Ằ 7856 + "a", // ằ 7857 + "A", // Ẳ 7858 + "a", // ẳ 7859 + "A", // Ẵ 7860 + "a", // ẵ 7861 + "A", // Ặ 7862 + "a", // ặ 7863 + "E", // Ẹ 7864 + "e", // ẹ 7865 + "E", // Ẻ 7866 + "e", // ẻ 7867 + "E", // Ẽ 7868 + "e", // ẽ 7869 + "E", // Ế 7870 + "e", // ế 7871 + "E", // Ề 7872 + "e", // ề 7873 + "E", // Ể 7874 + "e", // ể 7875 + "E", // Ễ 7876 + "e", // ễ 7877 + "E", // Ệ 7878 + "e", // ệ 7879 + "I", // Ỉ 7880 + "i", // ỉ 7881 + "I", // Ị 7882 + "i", // ị 7883 + "O", // Ọ 7884 + "o", // ọ 7885 + "O", // Ỏ 7886 + "o", // ỏ 7887 + "O", // Ố 7888 + "o", // ố 7889 + "O", // Ồ 7890 + "o", // ồ 7891 + "O", // Ổ 7892 + "o", // ổ 7893 + "O", // Ỗ 7894 + "o", // ỗ 7895 + "O", // Ộ 7896 + "o", // ộ 7897 + "O", // Ớ 7898 + "o", // ớ 7899 + "O", // Ờ 7900 + "o", // ờ 7901 + "O", // Ở 7902 + "o", // ở 7903 + "O", // Ỡ 7904 + "o", // ỡ 7905 + "O", // Ợ 7906 + "o", // ợ 7907 + "U", // Ụ 7908 + "u", // ụ 7909 + "U", // Ủ 7910 + "u", // ủ 7911 + "U", // Ứ 7912 + "u", // ứ 7913 + "U", // Ừ 7914 + "u", // ừ 7915 + "U", // Ử 7916 + "u", // ử 7917 + "U", // Ữ 7918 + "u", // ữ 7919 + "U", // Ự 7920 + "u", // ự 7921 + "Y", // Ỳ 7922 + "y", // ỳ 7923 + "Y", // Ỵ 7924 + "y", // ỵ 7925 + "Y", // Ỷ 7926 + "y", // ỷ 7927 + "Y", // Ỹ 7928 + "y", // ỹ 7929 +]; + +const TABLE_2: [&str; 2] = [ + "fi", // fi 64257 + "fl", // fl 64258 +]; + +#[inline(always)] +pub(super) fn lookup(c: char) -> Option<&'static str> { + match c as u16 { + 192..=687 => unsafe { Some(TABLE_0.get_unchecked((c as u16 - 192) as usize)) }, + 7684..=7929 => unsafe { Some(TABLE_1.get_unchecked((c as u16 - 7684) as usize)) }, + 64257..=64258 => unsafe { Some(TABLE_2.get_unchecked((c as u16 - 64257) as usize)) }, + _ => None, + } +} diff --git a/yazi-shared/src/translit/traits.rs b/yazi-shared/src/translit/traits.rs new file mode 100644 index 00000000..3f862e48 --- /dev/null +++ b/yazi-shared/src/translit/traits.rs @@ -0,0 +1,69 @@ +use core::str; +use std::borrow::Cow; + +pub trait Transliterator { + fn transliterate(&self) -> Cow; +} + +impl Transliterator for &[u8] { + fn transliterate(&self) -> Cow { + // Fast path to skip over ASCII chars at the beginning of the string + let ascii_len = self.iter().take_while(|&&c| c < 0x7f).count(); + if ascii_len >= self.len() { + return Cow::Borrowed(unsafe { str::from_utf8_unchecked(self) }); + } + + let (ascii, rest) = self.split_at(ascii_len); + let ascii = unsafe { str::from_utf8_unchecked(ascii) }; + + // Reserve a bit more space to avoid reallocations on longer transliterations + // but instead of `+ 16` uses `| 15` to stay in the smallest allocation bucket + // for short strings + let mut out = String::new(); + out.try_reserve_exact(self.len() | 15).unwrap_or_else(|_| panic!()); + out.push_str(ascii); + + for c in String::from_utf8_lossy(rest).chars() { + if let Some(s) = super::lookup(c) { + out.push_str(s); + } else { + out.push(c); + } + } + Cow::Owned(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_transliterate() { + assert_eq!("Æcœ".as_bytes().transliterate(), "AEcoe"); + assert_eq!( + "ěřůøĉĝĥĵŝŭèùÿėįųāēīūļķņģőűëïąćęłńśźżõșțčďĺľňŕšťýžéíñóúüåäöçîşûğăâđêôơưáàãảạ" + .as_bytes() + .transliterate(), + "eruocghjsueuyeiuaeiulkngoueiacelnszzostcdllnrstyzeinouuaaocisugaadeoouaaaaa", + ); + assert_eq!( + "áạàảãăắặằẳẵâấậầẩẫéẹèẻẽêếệềểễiíịìỉĩoóọòỏõôốộồổỗơớợờởỡúụùủũưứựừửữyýỵỳỷỹđ" + .as_bytes() + .transliterate(), + "aaaaaaaaaaaaaaaaaeeeeeeeeeeeiiiiiioooooooooooooooooouuuuuuuuuuuyyyyyyd", + ); + assert_ne!( + "ěřůøĉĝĥĵŝŭèùÿėįųāēīūļķņģőűëïąćęłńśźżõșțčďĺľňŕšťýžéíñóúüåäöçîşûğăâđêôơưáàãảạfifl" + .as_bytes() + .transliterate(), + "ěřůøĉĝĥĵŝŭèùÿėįųāēīūļķņģőűëïąćęłńśźżõșțčďĺľňŕšťýžéíñóúüåäöçîşûğăâđêôơưáàãảạfifl" + ); + assert_eq!( + "THEQUICKBROWNFOXJUMPEDOVERTHELAZYDOGthequickbrownfoxjumpedoverthelazydog" + .as_bytes() + .transliterate(), + "THEQUICKBROWNFOXJUMPEDOVERTHELAZYDOGthequickbrownfoxjumpedoverthelazydog" + ); + } +}