mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-22 07:11:03 +00:00
Compare commits
No commits in common. "main" and "v0.1.5" have entirely different histories.
1538 changed files with 15846 additions and 85417 deletions
|
|
@ -1,16 +0,0 @@
|
|||
[env]
|
||||
MACOSX_DEPLOYMENT_TARGET = "10.12"
|
||||
JEMALLOC_SYS_WITH_LG_PAGE = "16"
|
||||
|
||||
# environment variable for tikv-jemalloc-sys
|
||||
#
|
||||
# https://jemalloc.net/jemalloc.3.html#opt.narenas
|
||||
# narenas is the maximum number of arenas to use for automatic multiplexing
|
||||
# of threads and arenas. The default is four times the number of CPUs,
|
||||
# or one if there is a single CPU.
|
||||
#
|
||||
# Improve memory efficiency by reducing fragmentation and ensuring all threads allocate from the same pool
|
||||
JEMALLOC_SYS_WITH_MALLOC_CONF = "narenas:1"
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = [ "-Ctarget-cpu=apple-m1" ]
|
||||
1
.envrc
1
.envrc
|
|
@ -1 +0,0 @@
|
|||
use flake
|
||||
56
.github/DISCUSSION_TEMPLATE/1-q-a.yml
vendored
56
.github/DISCUSSION_TEMPLATE/1-q-a.yml
vendored
|
|
@ -1,56 +0,0 @@
|
|||
body:
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: What system are you running Yazi on?
|
||||
options:
|
||||
- Linux X11
|
||||
- Linux Wayland
|
||||
- macOS
|
||||
- Windows
|
||||
- Windows WSL
|
||||
- FreeBSD X11
|
||||
- FreeBSD Wayland
|
||||
- Android
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: terminal
|
||||
attributes:
|
||||
label: What terminal are you running Yazi in?
|
||||
placeholder: "ex: kitty v0.32.2"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: env
|
||||
attributes:
|
||||
label: "`ya env` output"
|
||||
description: Run `ya env` and paste the environment information here.
|
||||
render: Shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the question
|
||||
description: A clear and concise description of what the question is
|
||||
placeholder: Tell us what you want to know
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Add any other context about the problem here. You can attach screenshots by clicking
|
||||
this area to highlight it and then drag the files in.
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Before submitting the post, please make sure you have completed the following
|
||||
options:
|
||||
- label: I have read all the documentation
|
||||
required: true
|
||||
- label: I have searched the existing discussions/issues
|
||||
required: true
|
||||
70
.github/ISSUE_TEMPLATE/bug.yml
vendored
70
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
|
@ -1,70 +0,0 @@
|
|||
name: 🐞 Bug Report
|
||||
description: Create a report to help us improve
|
||||
labels: [bug]
|
||||
assignees: []
|
||||
body:
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: What system are you running Yazi on?
|
||||
options:
|
||||
- Linux X11
|
||||
- Linux Wayland
|
||||
- macOS
|
||||
- Windows
|
||||
- Windows WSL
|
||||
- FreeBSD X11
|
||||
- FreeBSD Wayland
|
||||
- Android
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: terminal
|
||||
attributes:
|
||||
label: What terminal are you running Yazi in?
|
||||
placeholder: "ex: kitty v0.32.2"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: env
|
||||
attributes:
|
||||
label: "`ya env` output"
|
||||
description: Run `ya env` and paste the environment information here.
|
||||
render: Shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is
|
||||
placeholder: Tell us what happened
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproducer
|
||||
attributes:
|
||||
label: Minimal reproducer
|
||||
description: A [minimal reproducer](https://stackoverflow.com/help/minimal-reproducible-example) is required, otherwise the issue might be closed without further notice.
|
||||
placeholder: |
|
||||
Please include as much information as possible that can help to reproduce and understand the issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Add any other context about the problem here. You can attach screenshots by clicking
|
||||
this area to highlight it and then drag the files in.
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Before submitting the issue, please make sure you have completed the following
|
||||
options:
|
||||
- label: I tried the [latest nightly build](https://yazi-rs.github.io/docs/installation#binaries), and the issue is still reproducible
|
||||
required: true
|
||||
- label: I updated the environment information (`ya env`) field to the nightly that I tried
|
||||
required: true
|
||||
- label: I can reproduce it after disabling all custom configs/plugins (`mv ~/.config/yazi ~/.config/yazi-backup`)
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
14
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,14 +0,0 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🚧 Build Issues
|
||||
url: https://github.com/sxyazi/yazi/discussions/new?category=3-build-issues
|
||||
about: If you have issues building Yazi from source code
|
||||
- name: 📝 Documentation Improvement
|
||||
url: https://github.com/yazi-rs/yazi-rs.github.io
|
||||
about: If you'd like to help improve the documentation
|
||||
- name: 💬 GitHub Discussions
|
||||
url: https://github.com/sxyazi/yazi/discussions/new?category=1-q-a
|
||||
about: When you have questions that are not bug reports or feature requests
|
||||
- name: 🌐 Discord Server / Telegram Group
|
||||
url: https://github.com/sxyazi/yazi#discussion
|
||||
about: If you'd prefer more realtime conversation with the community
|
||||
49
.github/ISSUE_TEMPLATE/feature.yml
vendored
49
.github/ISSUE_TEMPLATE/feature.yml
vendored
|
|
@ -1,49 +0,0 @@
|
|||
name: 💡 Feature Request
|
||||
description: Suggest an idea for this project
|
||||
labels: [feature]
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: env
|
||||
attributes:
|
||||
label: "`ya env` output"
|
||||
description: Run `ya env` and paste the environment information here.
|
||||
render: Shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Please describe the problem you're trying to solve
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Would you be willing to contribute this feature?
|
||||
description: The feature has a much higher chance of completion if you are willing to get involved!
|
||||
options:
|
||||
- label: Yes, I'll give it a shot
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Before submitting the issue, please make sure you have completed the following
|
||||
options:
|
||||
- label: I have searched the existing issues/discussions
|
||||
required: true
|
||||
- label: The [latest nightly build](https://yazi-rs.github.io/docs/installation/#binaries) doesn't already have this feature
|
||||
required: true
|
||||
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
|
|
@ -1,15 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/scripts/validate-form"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
24
.github/pull_request_template.md
vendored
24
.github/pull_request_template.md
vendored
|
|
@ -1,24 +0,0 @@
|
|||
## Which issue does this PR resolve?
|
||||
|
||||
<!--
|
||||
For any fixes and enhancements, we usually require an associated issue to be filed where clearly detailed your proposed changes and why they are necessary, which ensures we are aligned and reduces the risk of re-work.
|
||||
|
||||
You can use GitHub syntax to link an issue to this PR, such as `Resolves #1000`, which indicates this PR will resolve issue #1000.
|
||||
-->
|
||||
|
||||
Resolves #
|
||||
|
||||
## Rationale of this PR
|
||||
|
||||
<!--
|
||||
A clear and concise description of the rationale of the changes, to help our reviewers understand your intent and why it is necessary.
|
||||
|
||||
If it has already been detailed in the associated issue, please skip this section.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read [CONTRIBUTING.md](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md)
|
||||
- [ ] I confirm this PR follows the [AI Policy](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md#ai-policy)
|
||||
|
||||
<!-- AI bots are not allowed to open PRs in this repository. All PRs must be made by humans and comply with the AI policy. -->
|
||||
30
.github/workflows/cachix.yml
vendored
30
.github/workflows/cachix.yml
vendored
|
|
@ -1,30 +0,0 @@
|
|||
name: Cachix
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish Flake
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Authenticate with Cachix
|
||||
uses: cachix/cachix-action@v17
|
||||
with:
|
||||
name: yazi
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Build Flake
|
||||
run: nix build -L
|
||||
58
.github/workflows/check.yml
vendored
58
.github/workflows/check.yml
vendored
|
|
@ -1,58 +0,0 @@
|
|||
name: Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
SCCACHE_GHA_ENABLED: true
|
||||
RUSTC_WRAPPER: sccache
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
run: |
|
||||
rustup toolchain install stable --profile minimal
|
||||
rustup component add clippy
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.10
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --all
|
||||
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
run: |
|
||||
rustup toolchain install nightly --profile minimal
|
||||
rustup component add rustfmt --toolchain nightly
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.10
|
||||
|
||||
- name: Rustfmt
|
||||
run: find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} +
|
||||
|
||||
stylua:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: JohnnyMorganz/stylua-action@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: latest
|
||||
args: --color always --check .
|
||||
260
.github/workflows/draft.yml
vendored
260
.github/workflows/draft.yml
vendored
|
|
@ -1,260 +0,0 @@
|
|||
name: Draft
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v[0-9]+.[0-9]+.[0-9]+"]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
SCCACHE_GHA_ENABLED: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-unix:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
gcc: gcc-aarch64-linux-gnu
|
||||
- os: ubuntu-latest
|
||||
target: i686-unknown-linux-gnu
|
||||
gcc: gcc-i686-linux-gnu
|
||||
- os: ubuntu-latest
|
||||
target: riscv64gc-unknown-linux-gnu
|
||||
gcc: gcc-riscv64-linux-gnu
|
||||
- os: ubuntu-latest
|
||||
target: sparc64-unknown-linux-gnu
|
||||
gcc: gcc-sparc64-linux-gnu
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
||||
CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER: i686-linux-gnu-gcc
|
||||
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
|
||||
CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install gcc
|
||||
if: matrix.gcc != ''
|
||||
run: sudo apt update && sudo apt install -yq ${{ matrix.gcc }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.10
|
||||
|
||||
- name: Build
|
||||
run: ./scripts/build.sh ${{ matrix.target }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: |
|
||||
yazi-${{ matrix.target }}.zip
|
||||
yazi-${{ matrix.target }}.deb
|
||||
|
||||
build-windows:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
YAZI_GEN_COMPLETIONS: true
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
|
||||
CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.10
|
||||
|
||||
- name: Build
|
||||
run: cargo build --profile release-windows --locked --target ${{ matrix.target }}
|
||||
|
||||
- name: Pack artifact
|
||||
env:
|
||||
TARGET_NAME: yazi-${{ matrix.target }}
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path "${env:TARGET_NAME}\completions"
|
||||
Copy-Item -Path "target\${{ matrix.target }}\release-windows\ya.exe" -Destination ${env:TARGET_NAME}
|
||||
Copy-Item -Path "target\${{ matrix.target }}\release-windows\yazi.exe" -Destination ${env:TARGET_NAME}
|
||||
Copy-Item -Path "yazi-cli\completions\*" -Destination "${env:TARGET_NAME}\completions"
|
||||
Copy-Item -Path "yazi-boot\completions\*" -Destination "${env:TARGET_NAME}\completions"
|
||||
Copy-Item -Path "README.md", "LICENSE" -Destination ${env:TARGET_NAME}
|
||||
Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: yazi-${{ matrix.target }}.zip
|
||||
|
||||
build-musl:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-musl
|
||||
- target: aarch64-unknown-linux-musl
|
||||
container:
|
||||
image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.10
|
||||
|
||||
- name: Build
|
||||
run: ./scripts/build.sh ${{ matrix.target }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: |
|
||||
yazi-${{ matrix.target }}.zip
|
||||
yazi-${{ matrix.target }}.deb
|
||||
|
||||
build-snap:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup LXD
|
||||
uses: canonical/setup-lxd@v1
|
||||
|
||||
- name: Setup snapcraft
|
||||
run: sudo snap install --classic snapcraft
|
||||
|
||||
- name: Build snap
|
||||
run: snapcraft --verbose
|
||||
|
||||
- name: Rename snap
|
||||
run: mv yazi_*.snap yazi-${{ matrix.arch }}.snap
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: snap-${{ matrix.arch }}
|
||||
path: yazi-${{ matrix.arch }}.snap
|
||||
|
||||
snap:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-snap]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: snap-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Setup snapcraft
|
||||
run: sudo snap install --classic snapcraft
|
||||
|
||||
- name: Push snap to candidate channel
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
run: |
|
||||
parallel 'snapcraft upload -v --release latest/candidate {}' ::: yazi-*.snap || true
|
||||
|
||||
- name: Push snap to edge channel
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
run: |
|
||||
parallel 'snapcraft upload -v --release latest/edge {}' ::: yazi-*.snap || true
|
||||
|
||||
draft:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-unix, build-windows, build-musl, build-snap]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Draft
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
yazi-*.zip
|
||||
yazi-*.deb
|
||||
yazi-*.snap
|
||||
generate_release_notes: true
|
||||
|
||||
nightly:
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-unix, build-windows, build-musl, build-snap]
|
||||
steps:
|
||||
- run: |
|
||||
echo 'NIGHTLY_BODY<<EOF' >> $GITHUB_ENV
|
||||
echo "From commit: ${GITHUB_SHA:0:8}" >> $GITHUB_ENV
|
||||
echo "Generated on: $(date -u +"%Y-%m-%d %H:%M") UTC" >> $GITHUB_ENV
|
||||
echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Update the tag
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git tag --force nightly && git push --force origin tag nightly
|
||||
|
||||
- name: Nightly
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: nightly
|
||||
prerelease: true
|
||||
files: |
|
||||
yazi-*.zip
|
||||
yazi-*.deb
|
||||
yazi-*.snap
|
||||
name: Nightly Build
|
||||
body: ${{ env.NIGHTLY_BODY }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
31
.github/workflows/lock.yml
vendored
31
.github/workflows/lock.yml
vendored
|
|
@ -1,31 +0,0 @@
|
|||
name: Lock Threads
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "5 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v6
|
||||
with:
|
||||
issue-inactive-days: "30"
|
||||
issue-comment: >
|
||||
I'm going to lock this issue because it has been closed for _30 days_. ⏳
|
||||
|
||||
This helps our maintainers find and focus on the active issues.
|
||||
If you have found a problem that seems similar to this, please file a new
|
||||
issue and complete the issue template so we can capture all the details
|
||||
necessary to investigate further.
|
||||
pr-inactive-days: "30"
|
||||
discussion-inactive-days: "30"
|
||||
process-only: "issues,prs,discussions"
|
||||
19
.github/workflows/no-response.yml
vendored
19
.github/workflows/no-response.yml
vendored
|
|
@ -1,19 +0,0 @@
|
|||
name: No Response
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
schedule:
|
||||
- cron: "10 * * * *"
|
||||
|
||||
jobs:
|
||||
no-response:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: lee-dohm/no-response@v0.5.0
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
daysUntilClose: 7
|
||||
responseRequiredLabel: waiting on op
|
||||
37
.github/workflows/publish.yml
vendored
37
.github/workflows/publish.yml
vendored
|
|
@ -1,37 +0,0 @@
|
|||
name: Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
winget:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish to Winget
|
||||
uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: sxyazi.yazi
|
||||
installers-regex: 'yazi-(x86_64|aarch64)-pc-windows-msvc\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
snapcraft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Promote snap to stable
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
run: |
|
||||
sudo snap install --classic snapcraft
|
||||
snapcraft promote yazi --from-channel latest/candidate --to-channel latest/stable --yes
|
||||
68
.github/workflows/release.yml
vendored
Normal file
68
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Add aarch64 target
|
||||
if: matrix.target == 'aarch64-apple-darwin'
|
||||
run: rustup target add aarch64-apple-darwin
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --locked --target ${{ matrix.target }}
|
||||
|
||||
- name: Pack artifacts [Linux & macOS]
|
||||
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest'
|
||||
env:
|
||||
TARGET_NAME: yazi-${{ matrix.target }}
|
||||
run: |
|
||||
mkdir $TARGET_NAME
|
||||
cp target/${{ matrix.target }}/release/yazi $TARGET_NAME
|
||||
cp -r config/completions $TARGET_NAME
|
||||
cp README.md LICENSE $TARGET_NAME
|
||||
zip -r $TARGET_NAME.zip $TARGET_NAME
|
||||
|
||||
- name: Pack artifacts [Windows]
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
TARGET_NAME: yazi-${{ matrix.target }}
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path ${env:TARGET_NAME}
|
||||
Copy-Item -Path "target\${{ matrix.target }}\release\yazi.exe" -Destination ${env:TARGET_NAME}
|
||||
Copy-Item -Path "config\completions" -Destination ${env:TARGET_NAME} -Recurse
|
||||
Copy-Item -Path "README.md", "LICENSE" -Destination ${env:TARGET_NAME}
|
||||
Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip"
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
files: yazi-${{ matrix.target }}.zip
|
||||
generate_release_notes: true
|
||||
24
.github/workflows/test.yml
vendored
24
.github/workflows/test.yml
vendored
|
|
@ -2,35 +2,29 @@ name: Test
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [ "main" ]
|
||||
|
||||
env:
|
||||
SCCACHE_GHA_ENABLED: true
|
||||
RUSTC_WRAPPER: sccache
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
run: rustup toolchain install stable --profile minimal
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.10
|
||||
- name: Cache dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
|
||||
- name: Test
|
||||
run: cargo test --workspace --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
|
|
|
|||
34
.github/workflows/validate-issue.yml
vendored
34
.github/workflows/validate-issue.yml
vendored
|
|
@ -1,34 +0,0 @@
|
|||
name: Validate Issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
schedule:
|
||||
- cron: "20 * * * *"
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
cd scripts/validate-form
|
||||
npm ci
|
||||
|
||||
- name: Validate Issue
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const script = require('./scripts/validate-form/main.js')
|
||||
await script({github, context, core})
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
36
.github/workflows/validate-pr.yml
vendored
36
.github/workflows/validate-pr.yml
vendored
|
|
@ -1,36 +0,0 @@
|
|||
name: Validate PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
check-list:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
cd scripts/validate-form
|
||||
npm ci
|
||||
|
||||
- name: Validate PR
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const script = require('./scripts/validate-form/main.js')
|
||||
await script({github, context, core})
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,14 +1,8 @@
|
|||
target/
|
||||
yazi-*/completions
|
||||
node_modules/
|
||||
config/completions
|
||||
|
||||
.DS_Store
|
||||
|
||||
result
|
||||
result-*
|
||||
.direnv
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
*.snap
|
||||
.devenv*
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
|
||||
"runtime.version": "Lua 5.5",
|
||||
"runtime.special": {
|
||||
"fail": "error"
|
||||
},
|
||||
"workspace.library": ["~/.config/yazi/plugins/types.yazi/"],
|
||||
"diagnostics.disable": ["redefined-local"]
|
||||
}
|
||||
1785
CHANGELOG.md
1785
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,76 +0,0 @@
|
|||
# Code of Conduct
|
||||
|
||||
## Goal
|
||||
|
||||
Our goal is to create a welcoming and safe space where anyone can contribute and seek help for this project in a respectful, collaborative
|
||||
and harassment-free way.
|
||||
|
||||
All contributions are welcome and encourage everyone to participate regardless of age, body size, disability, ethnicity, sex characteristics,
|
||||
gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion,
|
||||
or sexual identity and orientation.
|
||||
|
||||
## Standards
|
||||
|
||||
Our standards are as follows:
|
||||
|
||||
- Respect different viewpoints and opinions
|
||||
- Do not harass, attack, or discriminate against others for any reason. We have zero tolerance for harassment
|
||||
- Communicate professionally and constructively. Do not post spam or go off-topic
|
||||
- Assume goodwill in conversations. Misunderstandings happen, so give others the benefit of the doubt before jumping to conclusions
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- Trolling, insulting or derogatory comments, and personal attacks
|
||||
- Harassing or bullying another person
|
||||
- Publishing others' private information without their explicit permission
|
||||
- Posting things unrelated to the topic being discussed
|
||||
- Other conduct that could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported here: https://discord.com/users/638378964804698112
|
||||
All reporters are guaranteed privacy, and your reports will always be kept private and secure.
|
||||
|
||||
All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate.
|
||||
The following is a list of consequences that you may face if you are found to be breaking the terms of the CoC.
|
||||
|
||||
### 1. Correction
|
||||
|
||||
A private warning with an explanation as to why the behavior was not appropriate.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
A private or public warning with consequences for continued behavior.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
A temporary ban from any sort of interaction or public communication with the community for a specified period of time.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the
|
||||
community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official
|
||||
social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
174
CONTRIBUTING.md
174
CONTRIBUTING.md
|
|
@ -1,174 +0,0 @@
|
|||
# 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 Requests](#pull-requests)
|
||||
6. [AI Policy](#ai-policy)
|
||||
|
||||
## 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/<your-username>/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-binding/ # Yazi Lua bindings
|
||||
├── yazi-boot/ # Yazi bootstrapper
|
||||
├── yazi-cli/ # Yazi command-line interface
|
||||
├── yazi-codegen/ # Yazi code generator
|
||||
├── yazi-config/ # Yazi configuration file parser
|
||||
├── yazi-core/ # Yazi core logic
|
||||
├── yazi-dds/ # Yazi data distribution service
|
||||
├── yazi-ffi/ # Yazi foreign function interface
|
||||
├── yazi-fm/ # Yazi file manager
|
||||
├── yazi-fs/ # Yazi file system
|
||||
├── yazi-macro/ # Yazi macros
|
||||
├── yazi-plugin/ # Yazi plugin system
|
||||
├── yazi-proxy/ # Yazi event proxy
|
||||
├── yazi-scheduler/ # Yazi task scheduler
|
||||
├── yazi-shared/ # Yazi shared library
|
||||
├── yazi-term/ # Yazi terminal extensions
|
||||
├── yazi-widgets/ # Yazi user interface widgets
|
||||
├── .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 --workspace --verbose
|
||||
```
|
||||
|
||||
4. Format the code (requires `rustfmt` nightly):
|
||||
|
||||
```sh
|
||||
rustup component add rustfmt --toolchain nightly
|
||||
rustfmt +nightly **/*.rs
|
||||
```
|
||||
|
||||
## How to Contribute
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
If you encounter a bug and have found a way to reliably reproduce it on the latest `main` branch, please file a [bug report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) with a [minimal reproducer](https://stackoverflow.com/help/minimal-reproducible-example).
|
||||
|
||||
### Suggesting Features
|
||||
|
||||
If you want to request a feature, please file a [feature request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml). Please make sure to search for existing issues and discussions before submitting.
|
||||
|
||||
### 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 there.
|
||||
|
||||
### Improving Icons
|
||||
|
||||
Yazi's icon originates from [`nvim-web-devicons`](https://github.com/nvim-tree/nvim-web-devicons), and it is periodically grabbed and updated with the latest changes from upstream via [`generate.lua`](https://github.com/sxyazi/yazi/blob/main/scripts/icons/generate.lua).
|
||||
|
||||
Contributions related to the icon should be made upstream to facilitate easier automation of this process.
|
||||
|
||||
### 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 Requests
|
||||
|
||||
If you have an idea, before raising a pull request, we encourage you to file an issue to propose it, ensuring that we are aligned and reducing the risk of re-work.
|
||||
|
||||
We want you to succeed, and it can be discouraging to find that a lot of re-work is needed.
|
||||
|
||||
### 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.
|
||||
|
||||
## AI Policy
|
||||
|
||||
1. All issue, PR, and discussion descriptions must be written by humans, not AI.
|
||||
2. Any use of AI must be disclosed. You must declare which model you used and the extent of AI assistance.
|
||||
3. Any AI-generated code must be reviewed, tested, and simplified by a human before publishing. This requires you to fully understand how it interacts with the greater system without AI assistance.
|
||||
4. Any AI tools used must explicitly state they do not assert copyright over the work.
|
||||
4886
Cargo.lock
generated
4886
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
106
Cargo.toml
106
Cargo.toml
|
|
@ -1,97 +1,15 @@
|
|||
[workspace]
|
||||
resolver = "3"
|
||||
members = [ "yazi-*" ]
|
||||
default-members = [ "yazi-fm", "yazi-cli" ]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
version = "26.5.6"
|
||||
license = "MIT"
|
||||
authors = [ "sxyazi <sxyazi@gmail.com>" ]
|
||||
homepage = "https://yazi-rs.github.io"
|
||||
repository = "https://github.com/sxyazi/yazi"
|
||||
rust-version = "1.95.0"
|
||||
|
||||
[profile.dev]
|
||||
debug = "line-tables-only"
|
||||
resolver = "2"
|
||||
members = [
|
||||
"adaptor",
|
||||
"app",
|
||||
"config",
|
||||
"core",
|
||||
"plugin",
|
||||
"shared",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
[profile.release-windows]
|
||||
inherits = "release"
|
||||
panic = "unwind"
|
||||
|
||||
[profile.dev-opt]
|
||||
inherits = "release"
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
lto = false
|
||||
|
||||
[profile.dev.package."*"]
|
||||
debug = false
|
||||
|
||||
[workspace.dependencies]
|
||||
ansi-to-tui = "8.0.1"
|
||||
anyhow = "1.0.104"
|
||||
arc-swap = { version = "1.9.2", features = [ "serde" ] }
|
||||
base64 = "0.22.1"
|
||||
bitflags = { version = "2.13.1", features = [ "serde" ] }
|
||||
chrono = "0.4.45"
|
||||
clap = { version = "4.6.2", features = [ "derive" ] }
|
||||
compact_str = { version = "0.10.0", features = [ "serde" ] }
|
||||
core-foundation-sys = "0.8.7"
|
||||
data-encoding = "2.11.0"
|
||||
dirs = "6.0.0"
|
||||
dyn-clone = "1.0.20"
|
||||
either = { version = "1.16.0" }
|
||||
foldhash = "0.2.0"
|
||||
futures = "0.3.33"
|
||||
globset = "0.4.19"
|
||||
hashbrown = { version = "0.17.1", features = [ "serde" ] }
|
||||
image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] }
|
||||
indexmap = { version = "2.14.0", features = [ "serde" ] }
|
||||
inventory = "0.3.24"
|
||||
libc = "0.2.186"
|
||||
lru = "0.18.1"
|
||||
mlua = { version = "0.12.0", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] }
|
||||
objc2 = "0.6.4"
|
||||
ordered-float = { version = "5.3.0", features = [ "serde" ] }
|
||||
parking_lot = "0.12.5"
|
||||
paste = "1.0.15"
|
||||
percent-encoding = "2.3.2"
|
||||
rand = { version = "0.10.2", default-features = false, features = [ "std", "sys_rng" ] }
|
||||
ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] }
|
||||
ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] }
|
||||
regex = "1.13.1"
|
||||
russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] }
|
||||
scopeguard = "1.2.0"
|
||||
serde = { version = "1.0.229", features = [ "derive" ] }
|
||||
serde_json = "1.0.151"
|
||||
serde_with = "3.21.0"
|
||||
strum = { version = "0.28.0", features = [ "derive" ] }
|
||||
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
|
||||
thiserror = "2.0.19"
|
||||
tokio = { version = "1.53.0", features = [ "full" ] }
|
||||
tokio-stream = "0.1.18"
|
||||
tokio-util = "0.7.18"
|
||||
toml = { version = "1.1.3" }
|
||||
tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] }
|
||||
twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
|
||||
typed-path = "0.12.3"
|
||||
unicode-width = { version = "0.2.2", default-features = false }
|
||||
uzers = "0.12.2"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
format_push_string = "warn"
|
||||
if_same_then_else = "allow"
|
||||
implicit_clone = "warn"
|
||||
len_without_is_empty = "allow"
|
||||
missing_safety_doc = "allow"
|
||||
module_inception = "allow"
|
||||
option_map_unit_fn = "allow"
|
||||
unit_arg = "allow"
|
||||
use_self = "warn"
|
||||
lto = true
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
||||
97
README.md
97
README.md
|
|
@ -1,44 +1,28 @@
|
|||
<div align="center">
|
||||
<sup>Special thanks to:</sup><br>
|
||||
|
||||
| <a href="https://go.warp.dev/yazi" target="_blank"><img alt="Warp sponsorship" width=350 src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-02.png"><br><b>Warp, built for coding with multiple AI agents</b><br><sup>Available for macOS, Linux and Windows</sup></a> |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
</div>
|
||||
|
||||
## Yazi - ⚡️ Blazing Fast Terminal File Manager
|
||||
|
||||
Yazi (means "duck") is a terminal file manager written in Rust, based on non-blocking async I/O. It aims to provide an efficient, user-friendly, and customizable file management experience.
|
||||
Yazi ("duck" in Chinese) is a terminal file manager written in Rust, based on non-blocking async I/O. It aims to provide an efficient, user-friendly, and customizable file management experience.
|
||||
|
||||
💡 A new article explaining its internal workings: [Why is Yazi Fast?](https://yazi-rs.github.io/blog/why-is-yazi-fast)
|
||||
💫 A new article explaining its internal workings: [Why is Yazi Fast?](https://github.com/sxyazi/yazi/issues/143)
|
||||
|
||||
- 🚀 **Full Asynchronous Support**: All I/O operations are asynchronous, CPU tasks are spread across multiple threads, making the most of available resources.
|
||||
- 💪 **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++ and Chafa, covering almost all terminals.
|
||||
- 🌟 **Built-in Code Highlighting and Image Decoding**: Combined with the pre-loading mechanism, greatly accelerates image and normal file loading.
|
||||
- 🔌 **Concurrent Plugin System**: UI plugins (rewriting most of the UI), functional plugins, custom previewer/preloader/spotter/fetcher; Just some pieces of Lua.
|
||||
- ☁️ **Virtual Filesystem**: Remote file management, custom search engines.
|
||||
- 📡 **Data Distribution Service**: Built on a client-server architecture (no additional server process required), integrated with a Lua-based publish-subscribe model, achieving cross-instance communication and state persistence.
|
||||
- 📦 **Package Manager**: Install plugins and themes with one command, keeping them up-to-date, or pin them to a specific version.
|
||||
- 🧰 Integration with ripgrep, fd, fzf, zoxide
|
||||
- 💫 Vim-like input/pick/confirm/which/notify component, auto-completion for cd paths
|
||||
- 🏷️ Multi-Tab Support, Cross-directory selection, Scrollable Preview (for videos, PDFs, archives, code, directories, etc.)
|
||||
- 🔄 Bulk Rename/Create, Archive Extraction, Visual Mode, File Chooser, [Git Integration](https://github.com/yazi-rs/plugins/tree/main/git.yazi), [Mount Manager](https://github.com/yazi-rs/plugins/tree/main/mount.yazi)
|
||||
- 🎨 Theme System, Mouse Support, Drag and Drop, Trash Bin, Custom Layouts, CSI u, OSC 52
|
||||
- 🖼️ **Built-in Support for Multiple Image Protocols**: Also integrated with Überzug++, covering almost all terminals.
|
||||
- 🌟 **Built-in Code Highlighting and Image Encoding**: Combined with the pre-caching mechanism, greatly accelerates image and normal file loading.
|
||||
- 🧰 Integration with fd, rg, fzf, zoxide
|
||||
- 💫 Vim-like Input component, and Select component
|
||||
- 🏷️ Multi-Tab Support, Scrollable Preview (for videos, PDFs, archives, directories, code, etc.)
|
||||
- 🔄 Batch Renaming, Visual Mode, File Chooser
|
||||
- 🎨 Theme System, Custom Layouts, Trash Bin, CSI u
|
||||
- ... and more!
|
||||
|
||||
https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7
|
||||
|
||||
## Project status
|
||||
|
||||
Public beta, can be used as a daily driver.
|
||||
|
||||
Yazi is currently in heavy development, expect breaking changes.
|
||||
⚠️ Note: Yazi is currently in active development and may be unstable. The API is subject to change without prior notice.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Usage: https://yazi-rs.github.io/docs/installation
|
||||
- Features: https://yazi-rs.github.io/features
|
||||
- Usage: https://yazi-rs.github.io/usage/installation
|
||||
- Showcase: https://yazi-rs.github.io/showcase
|
||||
|
||||
## Discussion
|
||||
|
||||
|
|
@ -47,48 +31,31 @@ Yazi is currently in heavy development, expect breaking changes.
|
|||
|
||||
## Image Preview
|
||||
|
||||
| Platform | Protocol | Support |
|
||||
| ---------------------------------------------------------------------------- | -------------------------------------- | -------------------------------------- |
|
||||
| [kitty](https://github.com/kovidgoyal/kitty) (>= 0.28.0) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
|
||||
| [iTerm2](https://iterm2.com) | [Inline images protocol][iip] | ✅ Built-in |
|
||||
| [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol][iip] | ✅ Built-in |
|
||||
| [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol][kgp-old] | ✅ Built-in |
|
||||
| [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format][sixel] | ✅ Built-in |
|
||||
| [Ghostty](https://github.com/ghostty-org/ghostty) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
|
||||
| [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.10352.0) | [Sixel graphics format][sixel] | ✅ Built-in |
|
||||
| [st with Sixel patch](https://github.com/bakkeby/st-flexipatch) | [Sixel graphics format][sixel] | ✅ Built-in |
|
||||
| [Warp](https://www.warp.dev) (macOS/Linux only) | [Inline images protocol][iip] | ✅ Built-in |
|
||||
| [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol][iip] | ✅ Built-in |
|
||||
| [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol][iip] | ✅ Built-in |
|
||||
| [Rio](https://github.com/raphamorim/rio) (>= 0.3.9) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
|
||||
| [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format][sixel] | ✅ Built-in |
|
||||
| [Bobcat](https://github.com/ismail-yilmaz/Bobcat) | [Inline images protocol][iip] | ✅ Built-in |
|
||||
| X11 / Wayland | Window system protocol | ☑️ [Überzug++][ueberzug] required |
|
||||
| Fallback | [ASCII art (Unicode block)][ascii-art] | ☑️ [Chafa][chafa] required (>= 1.16.0) |
|
||||
| Platform | Protocol | Support |
|
||||
| ----------------- | -------------------------------------------------------------------------------- | --------------------- |
|
||||
| Kitty | [Terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ✅ Built-in |
|
||||
| WezTerm | [Terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ✅ Built-in |
|
||||
| Konsole | [Terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ✅ Built-in |
|
||||
| iTerm2 | [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 |
|
||||
| Hyper | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in |
|
||||
| foot | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in |
|
||||
| Black Box | [Sixel graphics format](https://www.vt100.net/docs/vt3xx-gp/chapter14.html) | ✅ Built-in |
|
||||
| X11 / Wayland | Window system protocol | ☑️ Überzug++ required |
|
||||
| Fallback | [Chafa](https://hpjansson.org/chafa/) | ☑️ Überzug++ required |
|
||||
|
||||
See https://yazi-rs.github.io/docs/image-preview for details.
|
||||
Yazi automatically selects the appropriate preview method for you, based on the priority from top to bottom.
|
||||
That's relying on the `$TERM`, `$TERM_PROGRAM`, and `$XDG_SESSION_TYPE` variables, make sure you don't overwrite them by mistake!
|
||||
|
||||
<!-- Protocols -->
|
||||
For instance, if your terminal is Alacritty, which doesn't support displaying images itself, but you are running on an X11/Wayland environment,
|
||||
it will automatically use the "Window system protocol" to display images -- this requires you to have [Überzug++](https://github.com/jstkdng/ueberzugpp) installed.
|
||||
|
||||
[kgp]: https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders
|
||||
[kgp-old]: https://github.com/sxyazi/yazi/blob/main/yazi-adapter/src/drivers/kgp_old.rs
|
||||
[iip]: https://iterm2.com/documentation-images.html
|
||||
[sixel]: https://www.vt100.net/docs/vt3xx-gp/chapter14.html
|
||||
[ascii-art]: https://en.wikipedia.org/wiki/ASCII_art
|
||||
Here is a guide for tmux users: [Image preview within tmux](https://github.com/sxyazi/yazi/wiki/Image-preview-within-tmux)
|
||||
|
||||
<!-- Dependencies -->
|
||||
## TODO
|
||||
|
||||
[ueberzug]: https://github.com/jstkdng/ueberzugpp
|
||||
[chafa]: https://hpjansson.org/chafa/
|
||||
|
||||
## Special Thanks
|
||||
|
||||
<img alt="RustRover logo" align="right" width="200" src="https://resources.jetbrains.com/storage/products/company/brand/logos/RustRover.svg">
|
||||
|
||||
Thanks to RustRover team for providing open-source licenses to support the maintenance of Yazi.
|
||||
|
||||
Active code contributors can contact @sxyazi to get a license (if any are still available).
|
||||
See [Feature requests](https://github.com/sxyazi/yazi/issues/51) for more details.
|
||||
|
||||
## License
|
||||
|
||||
Yazi is MIT-licensed. For more information check the [LICENSE](LICENSE) file.
|
||||
Yazi is MIT licensed. For more information check the [LICENSE](LICENSE) file.
|
||||
|
|
|
|||
19
adaptor/Cargo.toml
Normal file
19
adaptor/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "adaptor"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
config = { path = "../config" }
|
||||
shared = { path = "../shared" }
|
||||
|
||||
# External dependencies
|
||||
anyhow = "^1"
|
||||
base64 = "^0"
|
||||
color_quant = "^1"
|
||||
image = "^0"
|
||||
ratatui = "^0"
|
||||
tokio = { version = "^1", features = [ "parking_lot" ] }
|
||||
|
||||
# Logging
|
||||
tracing = "^0"
|
||||
145
adaptor/src/adaptor.rs
Normal file
145
adaptor/src/adaptor.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
use std::{env, path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use config::PREVIEW;
|
||||
use ratatui::prelude::Rect;
|
||||
use shared::RoCell;
|
||||
use tokio::{fs, sync::mpsc::UnboundedSender};
|
||||
|
||||
use super::{Iterm2, Kitty};
|
||||
use crate::{ueberzug::Ueberzug, Sixel, TMUX};
|
||||
|
||||
static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
static UEBERZUG: RoCell<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> = RoCell::new();
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Adaptor {
|
||||
Kitty,
|
||||
Iterm2,
|
||||
Sixel,
|
||||
|
||||
// Supported by Überzug++
|
||||
X11,
|
||||
Wayland,
|
||||
Chafa,
|
||||
}
|
||||
|
||||
impl Adaptor {
|
||||
pub(super) fn detect() -> Self {
|
||||
let vars = [
|
||||
("ZELLIJ_SESSION_NAME", Self::Sixel),
|
||||
("KITTY_WINDOW_ID", Self::Kitty),
|
||||
("KONSOLE_VERSION", Self::Kitty),
|
||||
("ITERM_SESSION_ID", Self::Iterm2),
|
||||
("WEZTERM_EXECUTABLE", cfg!(windows).then_some(Self::Iterm2).unwrap_or(Self::Kitty)),
|
||||
("VSCODE_INJECTION", Self::Sixel),
|
||||
];
|
||||
if let Some(var) = vars.iter().find(|v| env::var_os(v.0).is_some()) {
|
||||
return var.1;
|
||||
}
|
||||
|
||||
let (term, program) = Self::term_program();
|
||||
match program.as_str() {
|
||||
"iTerm.app" => return Self::Iterm2,
|
||||
"WezTerm" => return cfg!(windows).then_some(Self::Iterm2).unwrap_or(Self::Kitty),
|
||||
"BlackBox" => return Self::Sixel,
|
||||
"vscode" => return Self::Sixel,
|
||||
"Hyper" => return Self::Sixel,
|
||||
"mintty" => return Self::Iterm2,
|
||||
_ => {}
|
||||
}
|
||||
match term.as_str() {
|
||||
"xterm-kitty" => return Self::Kitty,
|
||||
"foot" => return Self::Sixel,
|
||||
"foot-extra" => return Self::Sixel,
|
||||
_ => {}
|
||||
}
|
||||
match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() {
|
||||
"x11" => Self::X11,
|
||||
"wayland" => Self::Wayland,
|
||||
_ => Self::Chafa,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn term_program() -> (String, String) {
|
||||
fn tmux_env(name: &str) -> Result<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Adaptor {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::Kitty => "kitty",
|
||||
Self::Iterm2 => "iterm2",
|
||||
Self::Sixel => "sixel",
|
||||
Self::X11 => "x11",
|
||||
Self::Wayland => "wayland",
|
||||
Self::Chafa => "chafa",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Adaptor {
|
||||
pub(super) fn start(self) {
|
||||
UEBERZUG.init(if self.needs_ueberzug() { Ueberzug::start(self).ok() } else { None });
|
||||
}
|
||||
|
||||
pub async fn image_show(self, mut path: &Path, rect: Rect) -> Result<()> {
|
||||
let cache = PREVIEW.cache(path, 0);
|
||||
if fs::metadata(&cache).await.is_ok() {
|
||||
path = cache.as_path();
|
||||
}
|
||||
|
||||
self.image_hide(rect).ok();
|
||||
IMAGE_SHOWN.store(true, Ordering::Relaxed);
|
||||
|
||||
match self {
|
||||
Self::Kitty => Kitty::image_show(path, rect).await,
|
||||
Self::Iterm2 => Iterm2::image_show(path, rect).await,
|
||||
Self::Sixel => Sixel::image_show(path, rect).await,
|
||||
_ => Ok(if let Some(tx) = &*UEBERZUG {
|
||||
tx.send(Some((path.to_path_buf(), rect)))?;
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image_hide(self, rect: Rect) -> Result<()> {
|
||||
if !IMAGE_SHOWN.swap(false, Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match self {
|
||||
Self::Kitty => Kitty::image_hide(),
|
||||
Self::Iterm2 => Iterm2::image_hide(rect),
|
||||
Self::Sixel => Sixel::image_hide(rect),
|
||||
_ => Ok(if let Some(tx) = &*UEBERZUG {
|
||||
tx.send(None)?;
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn needs_ueberzug(self) -> bool {
|
||||
!matches!(self, Self::Kitty | Self::Iterm2 | Self::Sixel)
|
||||
}
|
||||
}
|
||||
62
adaptor/src/image.rs
Normal file
62
adaptor/src/image.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use config::PREVIEW;
|
||||
use image::{imageops::FilterType, DynamicImage, ImageFormat};
|
||||
use shared::Term;
|
||||
use tokio::fs;
|
||||
|
||||
pub struct Image;
|
||||
|
||||
impl Image {
|
||||
pub(super) async fn crop(path: &Path, size: (u16, u16)) -> Result<DynamicImage> {
|
||||
let (w, h) = Term::ratio()
|
||||
.map(|(w, h)| {
|
||||
let (w, h) = ((size.0 as f64 * w) as u32, (size.1 as f64 * h) as u32);
|
||||
(w.min(PREVIEW.max_width), h.min(PREVIEW.max_height))
|
||||
})
|
||||
.unwrap_or((PREVIEW.max_width, PREVIEW.max_height));
|
||||
|
||||
let img = fs::read(path).await?;
|
||||
let img = tokio::task::spawn_blocking(move || -> Result<DynamicImage> {
|
||||
let img = image::load_from_memory(&img)?;
|
||||
Ok(if img.width() > w || img.height() > h {
|
||||
img.resize(w, h, FilterType::Triangle)
|
||||
} else {
|
||||
img
|
||||
})
|
||||
});
|
||||
|
||||
img.await?
|
||||
}
|
||||
|
||||
pub async fn precache(img: Arc<Vec<u8>>, cache: impl AsRef<Path>) -> Result<bool> {
|
||||
let cache = cache.as_ref().to_owned();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let img = image::load_from_memory(&img)?;
|
||||
let (w, h) = (PREVIEW.max_width, PREVIEW.max_height);
|
||||
|
||||
if img.width() <= w && img.height() <= h {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match img.resize(w, h, FilterType::Triangle) {
|
||||
DynamicImage::ImageRgb8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg),
|
||||
DynamicImage::ImageRgba8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg),
|
||||
buf => buf.to_rgb8().save_with_format(cache, ImageFormat::Jpeg),
|
||||
}?;
|
||||
|
||||
Ok(true)
|
||||
});
|
||||
|
||||
result.await?
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn precache_anyway(img: Arc<Vec<u8>>, cache: impl AsRef<Path>) -> Result<()> {
|
||||
Ok(match Self::precache(img.clone(), &cache).await {
|
||||
Ok(true) => (),
|
||||
_ => fs::write(cache, &*img).await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
56
adaptor/src/iterm2.rs
Normal file
56
adaptor/src/iterm2.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use std::{io::{stdout, BufWriter, Write}, path::Path};
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use image::{codecs::jpeg::JpegEncoder, DynamicImage};
|
||||
use ratatui::prelude::Rect;
|
||||
use shared::Term;
|
||||
|
||||
use super::image::Image;
|
||||
use crate::{CLOSE, START};
|
||||
|
||||
pub(super) struct Iterm2;
|
||||
|
||||
impl Iterm2 {
|
||||
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> {
|
||||
let img = Image::crop(path, (rect.width, rect.height)).await?;
|
||||
let b = Self::encode(img).await?;
|
||||
|
||||
Self::image_hide(rect)?;
|
||||
Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?))
|
||||
}
|
||||
|
||||
pub(super) fn image_hide(rect: Rect) -> Result<()> {
|
||||
let stdout = BufWriter::new(stdout().lock());
|
||||
Term::move_lock(stdout, (0, 0), |stdout| {
|
||||
for y in rect.top()..rect.bottom() {
|
||||
Term::move_to(stdout, rect.x, y)?;
|
||||
Term::kill_to_end(stdout)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||
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?
|
||||
}
|
||||
}
|
||||
76
adaptor/src/kitty.rs
Normal file
76
adaptor/src/kitty.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use std::{io::{stdout, Write}, path::Path};
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use image::DynamicImage;
|
||||
use ratatui::prelude::Rect;
|
||||
use shared::Term;
|
||||
|
||||
use super::image::Image;
|
||||
use crate::{CLOSE, ESCAPE, START};
|
||||
|
||||
pub(super) struct Kitty;
|
||||
|
||||
impl Kitty {
|
||||
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> {
|
||||
let img = Image::crop(path, (rect.width, rect.height)).await?;
|
||||
let b = Self::encode(img).await?;
|
||||
|
||||
Self::image_hide()?;
|
||||
Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn image_hide() -> Result<()> {
|
||||
let mut stdout = stdout().lock();
|
||||
stdout.write_all(format!("{}_Ga=d,d=A{}\\{}", START, ESCAPE, CLOSE).as_bytes())?;
|
||||
stdout.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||
fn output(raw: &[u8], format: u8, size: (u32, u32)) -> Result<Vec<u8>> {
|
||||
let b64 = general_purpose::STANDARD.encode(raw).chars().collect::<Vec<_>>();
|
||||
|
||||
let mut it = b64.chunks(4096).peekable();
|
||||
let mut buf = Vec::with_capacity(b64.len() + it.len() * 50);
|
||||
if let Some(first) = it.next() {
|
||||
write!(
|
||||
buf,
|
||||
"{}_Ga=T,f={},s={},v={},m={};{}{}\\{}",
|
||||
START,
|
||||
format,
|
||||
size.0,
|
||||
size.1,
|
||||
it.peek().is_some() as u8,
|
||||
first.iter().collect::<String>(),
|
||||
ESCAPE,
|
||||
CLOSE
|
||||
)?;
|
||||
}
|
||||
|
||||
while let Some(chunk) = it.next() {
|
||||
write!(
|
||||
buf,
|
||||
"{}_Gm={};{}{}\\{}",
|
||||
START,
|
||||
it.peek().is_some() as u8,
|
||||
chunk.iter().collect::<String>(),
|
||||
ESCAPE,
|
||||
CLOSE
|
||||
)?;
|
||||
}
|
||||
|
||||
buf.write_all(CLOSE.as_bytes())?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
let size = (img.width(), img.height());
|
||||
tokio::task::spawn_blocking(move || match img {
|
||||
DynamicImage::ImageRgb8(v) => output(v.as_raw(), 24, size),
|
||||
DynamicImage::ImageRgba8(v) => output(v.as_raw(), 32, size),
|
||||
v => output(v.to_rgb8().as_raw(), 24, size),
|
||||
})
|
||||
.await?
|
||||
}
|
||||
}
|
||||
34
adaptor/src/lib.rs
Normal file
34
adaptor/src/lib.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#![allow(clippy::unit_arg)]
|
||||
|
||||
mod adaptor;
|
||||
mod image;
|
||||
mod iterm2;
|
||||
mod kitty;
|
||||
mod sixel;
|
||||
mod ueberzug;
|
||||
|
||||
use adaptor::*;
|
||||
use iterm2::*;
|
||||
use kitty::*;
|
||||
use shared::RoCell;
|
||||
use sixel::*;
|
||||
|
||||
pub use crate::image::*;
|
||||
|
||||
pub static ADAPTOR: RoCell<Adaptor> = RoCell::new();
|
||||
|
||||
// Tmux support
|
||||
static TMUX: RoCell<bool> = RoCell::new();
|
||||
static ESCAPE: RoCell<&'static str> = RoCell::new();
|
||||
static START: RoCell<&'static str> = RoCell::new();
|
||||
static CLOSE: RoCell<&'static str> = RoCell::new();
|
||||
|
||||
pub fn init() {
|
||||
TMUX.init(std::env::var_os("TMUX").is_some());
|
||||
START.init(if *TMUX { "\x1bPtmux;\x1b\x1b" } else { "\x1b" });
|
||||
CLOSE.init(if *TMUX { "\x1b\\" } else { "" });
|
||||
ESCAPE.init(if *TMUX { "\x1b\x1b" } else { "\x1b" });
|
||||
|
||||
ADAPTOR.init(Adaptor::detect());
|
||||
ADAPTOR.start();
|
||||
}
|
||||
98
adaptor/src/sixel.rs
Normal file
98
adaptor/src/sixel.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use std::{io::{stdout, BufWriter, Write}, path::Path};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use color_quant::NeuQuant;
|
||||
use image::DynamicImage;
|
||||
use ratatui::prelude::Rect;
|
||||
use shared::Term;
|
||||
|
||||
use crate::{Image, CLOSE, ESCAPE, START};
|
||||
|
||||
pub(super) struct Sixel;
|
||||
|
||||
impl Sixel {
|
||||
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> {
|
||||
let img = Image::crop(path, (rect.width, rect.height)).await?;
|
||||
let b = Self::encode(img).await?;
|
||||
|
||||
Self::image_hide(rect)?;
|
||||
Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?))
|
||||
}
|
||||
|
||||
pub(super) fn image_hide(rect: Rect) -> Result<()> {
|
||||
let stdout = BufWriter::new(stdout().lock());
|
||||
Term::move_lock(stdout, (0, 0), |stdout| {
|
||||
for y in rect.top()..rect.bottom() {
|
||||
Term::move_to(stdout, rect.x, y)?;
|
||||
Term::kill_to_end(stdout)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||
let alpha = img.color().has_alpha();
|
||||
if img.width() == 0 || img.height() == 0 {
|
||||
bail!("image is empty");
|
||||
}
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let img = img.into_rgba8();
|
||||
let nq = NeuQuant::new(10, 256 - alpha as usize, &img);
|
||||
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(1 << 16);
|
||||
write!(buf, "{}P0;1;8q\"1;1;{};{}", START, img.width(), img.height())?;
|
||||
|
||||
// Palette
|
||||
for (i, c) in nq.color_map_rgba().chunks(4).enumerate() {
|
||||
write!(
|
||||
buf,
|
||||
"#{};2;{};{};{}",
|
||||
i + alpha as usize,
|
||||
c[0] as u16 * 100 / 255,
|
||||
c[1] as u16 * 100 / 255,
|
||||
c[2] as u16 * 100 / 255
|
||||
)?;
|
||||
}
|
||||
|
||||
for y in 0..img.height() {
|
||||
let c = (b'?' + (1 << (y % 6))) as char;
|
||||
|
||||
let mut last = 0;
|
||||
let mut repeat = 0usize;
|
||||
for x in 0..img.width() {
|
||||
let pixel = img.get_pixel(x, y).0;
|
||||
let idx = if pixel[3] == 0 { 0 } else { nq.index_of(&pixel) as u8 + alpha as u8 };
|
||||
|
||||
if idx == last || repeat == 0 {
|
||||
(last, repeat) = (idx, repeat + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if repeat > 1 {
|
||||
write!(buf, "#{last}!{repeat}{c}")?;
|
||||
} else {
|
||||
write!(buf, "#{last}{c}")?;
|
||||
}
|
||||
|
||||
(last, repeat) = (idx, 1);
|
||||
}
|
||||
|
||||
if repeat > 1 {
|
||||
write!(buf, "#{last}!{repeat}{c}")?;
|
||||
} else {
|
||||
write!(buf, "#{last}{c}")?;
|
||||
}
|
||||
|
||||
write!(buf, "$")?;
|
||||
if y % 6 == 5 {
|
||||
write!(buf, "-")?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(buf, "{}\\{}", ESCAPE, CLOSE)?;
|
||||
Ok(buf)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
}
|
||||
65
adaptor/src/ueberzug.rs
Normal file
65
adaptor/src/ueberzug.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use std::{path::PathBuf, process::Stdio};
|
||||
|
||||
use anyhow::Result;
|
||||
use ratatui::prelude::Rect;
|
||||
use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}};
|
||||
|
||||
use crate::Adaptor;
|
||||
|
||||
pub(super) struct Ueberzug;
|
||||
|
||||
impl Ueberzug {
|
||||
pub(super) fn start(adaptor: Adaptor) -> Result<UnboundedSender<Option<(PathBuf, Rect)>>> {
|
||||
let mut child = Self::create_demon(adaptor).ok();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
let exit = child.as_mut().and_then(|c| c.try_wait().ok());
|
||||
if exit != Some(None) {
|
||||
child = None;
|
||||
}
|
||||
if child.is_none() {
|
||||
child = Self::create_demon(adaptor).ok();
|
||||
}
|
||||
if let Some(c) = &mut child {
|
||||
Self::send_command(c, cmd).await.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
fn create_demon(adaptor: Adaptor) -> Result<Child> {
|
||||
Ok(
|
||||
Command::new("ueberzug")
|
||||
.args(["layer", "-so", &adaptor.to_string()])
|
||||
.kill_on_drop(true)
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?,
|
||||
)
|
||||
}
|
||||
|
||||
async fn send_command(child: &mut Child, cmd: Option<(PathBuf, Rect)>) -> Result<()> {
|
||||
let stdin = child.stdin.as_mut().unwrap();
|
||||
if let Some((path, rect)) = cmd {
|
||||
let s = format!(
|
||||
r#"{{"action":"add","identifier":"yazi","x":{},"y":{},"max_width":{},"max_height":{},"path":"{}"}}{}"#,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
path.to_string_lossy(),
|
||||
"\n"
|
||||
);
|
||||
stdin.write_all(s.as_bytes()).await?;
|
||||
} else {
|
||||
stdin
|
||||
.write_all(format!(r#"{{"action":"remove","identifier":"yazi"}}{}"#, "\n").as_bytes())
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
33
app/Cargo.toml
Normal file
33
app/Cargo.toml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "app"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
adaptor = { path = "../adaptor" }
|
||||
config = { path = "../config" }
|
||||
core = { path = "../core" }
|
||||
plugin = { path = "../plugin" }
|
||||
shared = { path = "../shared" }
|
||||
|
||||
# External dependencies
|
||||
ansi-to-tui = "^3"
|
||||
anyhow = "^1"
|
||||
crossterm = { version = "^0", features = [ "event-stream" ] }
|
||||
futures = "^0"
|
||||
ratatui = "^0"
|
||||
tokio = { version = "^1", features = [ "parking_lot" ] }
|
||||
unicode-width = "^0"
|
||||
|
||||
# Logging
|
||||
tracing = "^0"
|
||||
tracing-appender = "^0"
|
||||
tracing-subscriber = "^0"
|
||||
|
||||
[target."cfg(unix)".dependencies]
|
||||
libc = "^0"
|
||||
signal-hook-tokio = { version = "^0", features = [ "futures-v0_3" ] }
|
||||
|
||||
[[bin]]
|
||||
name = "yazi"
|
||||
path = "src/main.rs"
|
||||
220
app/src/app.rs
Normal file
220
app/src/app.rs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
use core::{emit, files::FilesOp, input::InputMode, Ctx, Event};
|
||||
use std::ffi::OsString;
|
||||
|
||||
use anyhow::{Ok, Result};
|
||||
use config::{keymap::{Exec, Key, KeymapLayer}, BOOT};
|
||||
use crossterm::event::KeyEvent;
|
||||
use shared::{expand_url, Term};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::{Executor, Logs, Root, Signals};
|
||||
|
||||
pub(super) struct App {
|
||||
cx: Ctx,
|
||||
term: Option<Term>,
|
||||
signals: Signals,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(super) async fn run() -> Result<()> {
|
||||
let _log = Logs::init()?;
|
||||
let term = Term::start()?;
|
||||
|
||||
let signals = Signals::start()?;
|
||||
let mut app = Self { cx: Ctx::make(), term: Some(term), signals };
|
||||
|
||||
while let Some(event) = app.signals.recv().await {
|
||||
match event {
|
||||
Event::Quit(no_cwd_file) => {
|
||||
app.dispatch_quit(no_cwd_file);
|
||||
break;
|
||||
}
|
||||
Event::Key(key) => app.dispatch_key(key),
|
||||
Event::Paste(str) => app.dispatch_paste(str),
|
||||
Event::Render(_) => app.dispatch_render(),
|
||||
Event::Resize(..) => app.dispatch_resize(),
|
||||
Event::Stop(state, tx) => app.dispatch_stop(state, tx),
|
||||
Event::Call(exec, layer) => app.dispatch_call(exec, layer),
|
||||
event => app.dispatch_module(event),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dispatch_quit(&mut self, no_cwd_file: bool) {
|
||||
if let Some(p) = BOOT.cwd_file.as_ref().filter(|_| !no_cwd_file) {
|
||||
let cwd = self.cx.manager.cwd().as_os_str();
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::fs::write(p, cwd.to_string_lossy().as_bytes()).ok();
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
std::fs::write(p, cwd.as_bytes()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_key(&mut self, key: KeyEvent) {
|
||||
let key = Key::from(key);
|
||||
if Executor::handle(&mut self.cx, key) {
|
||||
emit!(Render);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_paste(&mut self, str: String) {
|
||||
if self.cx.layer() == KeymapLayer::Input {
|
||||
let input = &mut self.cx.input;
|
||||
if input.mode() == InputMode::Insert && input.type_str(&str) {
|
||||
emit!(Render);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_render(&mut self) {
|
||||
if let Some(term) = &mut self.term {
|
||||
_ = term.draw(|f| {
|
||||
plugin::scope(&self.cx, |_| {
|
||||
f.render_widget(Root::new(&self.cx), f.size());
|
||||
});
|
||||
|
||||
if let Some((x, y)) = self.cx.cursor() {
|
||||
f.set_cursor(x, y);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_resize(&mut self) {
|
||||
self.cx.manager.current_mut().set_page(true);
|
||||
self.cx.manager.active_mut().preview.reset(|_| true);
|
||||
self.cx.manager.peek(true, self.cx.image_layer());
|
||||
emit!(Render);
|
||||
}
|
||||
|
||||
fn dispatch_stop(&mut self, state: bool, tx: Option<oneshot::Sender<()>>) {
|
||||
self.cx.manager.active_mut().preview.reset(|l| l.is_image());
|
||||
if state {
|
||||
self.signals.stop_term(true);
|
||||
self.term = None;
|
||||
} else {
|
||||
self.term = Some(Term::start().unwrap());
|
||||
self.signals.stop_term(false);
|
||||
emit!(Render);
|
||||
emit!(Hover);
|
||||
}
|
||||
if let Some(tx) = tx {
|
||||
tx.send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn dispatch_call(&mut self, exec: Vec<Exec>, layer: KeymapLayer) {
|
||||
if Executor::dispatch(&mut self.cx, &exec, layer) {
|
||||
emit!(Render);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_module(&mut self, event: Event) {
|
||||
let manager = &mut self.cx.manager;
|
||||
let tasks = &mut self.cx.tasks;
|
||||
match event {
|
||||
Event::Cd(url) => {
|
||||
futures::executor::block_on(async {
|
||||
manager.active_mut().cd(expand_url(url)).await;
|
||||
});
|
||||
}
|
||||
Event::Refresh => {
|
||||
manager.refresh();
|
||||
}
|
||||
Event::Files(op) => {
|
||||
let calc = !matches!(op, FilesOp::Size(..) | FilesOp::IOErr(_));
|
||||
let b = match op {
|
||||
FilesOp::IOErr(..) => manager.update_ioerr(op),
|
||||
_ => manager.update_read(op),
|
||||
};
|
||||
if b {
|
||||
emit!(Render);
|
||||
}
|
||||
if calc {
|
||||
tasks.precache_size(&manager.current().files);
|
||||
}
|
||||
}
|
||||
Event::Pages(page) => {
|
||||
if manager.current().page == page {
|
||||
let targets = self.cx.manager.current().paginate();
|
||||
tasks.precache_mime(targets, &self.cx.manager.mimetype);
|
||||
}
|
||||
}
|
||||
Event::Mimetype(mimes) => {
|
||||
if manager.update_mimetype(mimes, tasks) {
|
||||
emit!(Render);
|
||||
emit!(Peek);
|
||||
}
|
||||
}
|
||||
Event::Hover(url) => {
|
||||
if manager.current_mut().repos(url) {
|
||||
emit!(Render);
|
||||
}
|
||||
emit!(Peek);
|
||||
}
|
||||
Event::Peek(sequent) => {
|
||||
if let Some((max, url)) = sequent {
|
||||
manager.active_mut().update_peek(max, url);
|
||||
self.cx.manager.peek(true, self.cx.image_layer());
|
||||
} else {
|
||||
self.cx.manager.peek(false, self.cx.image_layer());
|
||||
}
|
||||
}
|
||||
Event::Preview(lock) => {
|
||||
if manager.active_mut().update_preview(lock) {
|
||||
emit!(Render);
|
||||
}
|
||||
}
|
||||
|
||||
Event::Select(opt, tx) => {
|
||||
self.cx.select.show(opt, tx);
|
||||
emit!(Render);
|
||||
}
|
||||
Event::Input(opt, tx) => {
|
||||
self.cx.input.show(opt, tx);
|
||||
emit!(Render);
|
||||
}
|
||||
|
||||
Event::Open(targets, opener) => {
|
||||
if let Some(p) = &BOOT.chooser_file {
|
||||
let paths = targets.into_iter().fold(OsString::new(), |mut s, (p, _)| {
|
||||
s.push(p);
|
||||
s.push("\n");
|
||||
s
|
||||
});
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::fs::write(p, paths.to_string_lossy().as_bytes()).ok();
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
std::fs::write(p, paths.as_bytes()).ok();
|
||||
}
|
||||
return emit!(Quit(false));
|
||||
}
|
||||
|
||||
if let Some(opener) = opener {
|
||||
tasks.file_open_with(&opener, &targets.into_iter().map(|(f, _)| f).collect::<Vec<_>>());
|
||||
} else {
|
||||
tasks.file_open(&targets);
|
||||
}
|
||||
}
|
||||
Event::Progress(progress) => {
|
||||
tasks.progress = progress;
|
||||
emit!(Render);
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
279
app/src/executor.rs
Normal file
279
app/src/executor.rs
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
use core::{emit, input::InputMode, tab::FinderCase, Ctx};
|
||||
|
||||
use config::{keymap::{Control, Exec, Key, KeymapLayer}, KEYMAP};
|
||||
use shared::{optional_bool, Url};
|
||||
|
||||
pub(super) struct Executor;
|
||||
|
||||
impl Executor {
|
||||
pub(super) fn handle(cx: &mut Ctx, key: Key) -> bool {
|
||||
let layer = cx.layer();
|
||||
if layer == KeymapLayer::Which {
|
||||
return cx.which.press(key);
|
||||
}
|
||||
|
||||
if layer == KeymapLayer::Input && cx.input.type_(&key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if layer == KeymapLayer::Help && cx.help.type_(&key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for Control { on, exec, .. } in KEYMAP.get(layer) {
|
||||
if on.is_empty() || on[0] != key {
|
||||
continue;
|
||||
}
|
||||
|
||||
return if on.len() > 1 {
|
||||
cx.which.show(&key, layer)
|
||||
} else {
|
||||
Self::dispatch(cx, exec, layer)
|
||||
};
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn dispatch(cx: &mut Ctx, exec: &[Exec], layer: KeymapLayer) -> bool {
|
||||
let mut render = false;
|
||||
for e in exec {
|
||||
render |= match layer {
|
||||
KeymapLayer::Manager => Self::manager(cx, e),
|
||||
KeymapLayer::Tasks => Self::tasks(cx, e),
|
||||
KeymapLayer::Select => Self::select(cx, e),
|
||||
KeymapLayer::Input => Self::input(cx, e),
|
||||
KeymapLayer::Help => Self::help(cx, e),
|
||||
KeymapLayer::Which => unreachable!(),
|
||||
};
|
||||
}
|
||||
render
|
||||
}
|
||||
|
||||
fn manager(cx: &mut Ctx, exec: &Exec) -> bool {
|
||||
match exec.cmd.as_str() {
|
||||
"escape" => cx.manager.active_mut().escape(exec),
|
||||
"quit" => cx.manager.quit(&cx.tasks, exec.named.contains_key("no-cwd-file")),
|
||||
"close" => cx.manager.close(&cx.tasks),
|
||||
"suspend" => cx.manager.suspend(),
|
||||
|
||||
// Navigation
|
||||
"arrow" => {
|
||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or_default();
|
||||
cx.manager.active_mut().arrow(step)
|
||||
}
|
||||
"peek" => {
|
||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
cx.manager.active_mut().preview.arrow(step);
|
||||
cx.manager.peek(true, cx.image_layer())
|
||||
}
|
||||
"leave" => cx.manager.active_mut().leave(),
|
||||
"enter" => cx.manager.active_mut().enter(),
|
||||
"back" => cx.manager.active_mut().back(),
|
||||
"forward" => cx.manager.active_mut().forward(),
|
||||
"cd" => {
|
||||
let url = exec.args.get(0).map(Url::from).unwrap_or_default();
|
||||
if exec.named.contains_key("interactive") {
|
||||
cx.manager.active_mut().cd_interactive(url)
|
||||
} else {
|
||||
emit!(Cd(url));
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Selection
|
||||
"select" => {
|
||||
let state = exec.named.get("state").cloned().unwrap_or("none".to_string());
|
||||
cx.manager.active_mut().select(optional_bool(&state))
|
||||
}
|
||||
"select_all" => {
|
||||
let state = exec.named.get("state").cloned().unwrap_or("none".to_string());
|
||||
cx.manager.active_mut().select_all(optional_bool(&state))
|
||||
}
|
||||
"visual_mode" => cx.manager.active_mut().visual_mode(exec.named.contains_key("unset")),
|
||||
|
||||
// Operation
|
||||
"open" => cx.manager.open(exec.named.contains_key("interactive")),
|
||||
"yank" => cx.manager.yank(exec.named.contains_key("cut")),
|
||||
"paste" => {
|
||||
let dest = cx.manager.cwd();
|
||||
let (cut, ref src) = cx.manager.yanked;
|
||||
|
||||
let force = exec.named.contains_key("force");
|
||||
if cut { cx.tasks.file_cut(src, dest, force) } else { cx.tasks.file_copy(src, dest, force) }
|
||||
}
|
||||
"link" => {
|
||||
let (cut, ref src) = cx.manager.yanked;
|
||||
!cut
|
||||
&& cx.tasks.file_link(
|
||||
src,
|
||||
cx.manager.cwd(),
|
||||
exec.named.contains_key("relative"),
|
||||
exec.named.contains_key("force"),
|
||||
)
|
||||
}
|
||||
"remove" => {
|
||||
let targets = cx.manager.selected().into_iter().map(|f| f.url()).collect();
|
||||
let force = exec.named.contains_key("force");
|
||||
let permanently = exec.named.contains_key("permanently");
|
||||
cx.tasks.file_remove(targets, force, permanently)
|
||||
}
|
||||
"create" => cx.manager.create(exec.named.contains_key("force")),
|
||||
"rename" => cx.manager.rename(exec.named.contains_key("force")),
|
||||
"copy" => cx.manager.active().copy(exec.args.get(0).map(|s| s.as_str()).unwrap_or("")),
|
||||
"shell" => cx.manager.active().shell(
|
||||
exec.args.get(0).map(|e| e.as_str()).unwrap_or(""),
|
||||
exec.named.contains_key("block"),
|
||||
exec.named.contains_key("confirm"),
|
||||
),
|
||||
"hidden" => cx.manager.active_mut().hidden(exec),
|
||||
"linemode" => cx.manager.active_mut().linemode(exec),
|
||||
"search" => match exec.args.get(0).map(|s| s.as_str()).unwrap_or("") {
|
||||
"rg" => cx.manager.active_mut().search(true),
|
||||
"fd" => cx.manager.active_mut().search(false),
|
||||
_ => cx.manager.active_mut().search_stop(),
|
||||
},
|
||||
"jump" => match exec.args.get(0).map(|s| s.as_str()).unwrap_or("") {
|
||||
"fzf" => cx.manager.active_mut().jump(true),
|
||||
"zoxide" => cx.manager.active_mut().jump(false),
|
||||
_ => false,
|
||||
},
|
||||
|
||||
// Find
|
||||
"find" => {
|
||||
let query = exec.args.get(0).map(|s| s.as_str());
|
||||
let prev = exec.named.contains_key("previous");
|
||||
let case = match (exec.named.contains_key("smart"), exec.named.contains_key("insensitive"))
|
||||
{
|
||||
(true, _) => FinderCase::Smart,
|
||||
(_, false) => FinderCase::Sensitive,
|
||||
(_, true) => FinderCase::Insensitive,
|
||||
};
|
||||
cx.manager.active_mut().find(query, prev, case)
|
||||
}
|
||||
"find_arrow" => cx.manager.active_mut().find_arrow(exec.named.contains_key("previous")),
|
||||
|
||||
// Sorting
|
||||
"sort" => {
|
||||
let b = cx.manager.active_mut().sort(exec);
|
||||
cx.tasks.precache_size(&cx.manager.current().files);
|
||||
b
|
||||
}
|
||||
|
||||
// Tabs
|
||||
"tab_create" => {
|
||||
let path = if exec.named.contains_key("current") {
|
||||
cx.manager.cwd().to_owned()
|
||||
} else {
|
||||
exec.args.get(0).map(Url::from).unwrap_or_else(|| Url::from("/"))
|
||||
};
|
||||
cx.manager.tabs.create(&path)
|
||||
}
|
||||
"tab_close" => {
|
||||
let idx = exec.args.get(0).and_then(|i| i.parse().ok()).unwrap_or(0);
|
||||
cx.manager.tabs.close(idx)
|
||||
}
|
||||
"tab_switch" => {
|
||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
let rel = exec.named.contains_key("relative");
|
||||
cx.manager.tabs.switch(step, rel)
|
||||
}
|
||||
"tab_swap" => {
|
||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
cx.manager.tabs.swap(step)
|
||||
}
|
||||
|
||||
// Tasks
|
||||
"tasks_show" => cx.tasks.toggle(),
|
||||
|
||||
// Help
|
||||
"help" => cx.help.toggle(cx.layer()),
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn tasks(cx: &mut Ctx, exec: &Exec) -> bool {
|
||||
match exec.cmd.as_str() {
|
||||
"close" => cx.tasks.toggle(),
|
||||
|
||||
"arrow" => {
|
||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
if step > 0 { cx.tasks.next() } else { cx.tasks.prev() }
|
||||
}
|
||||
|
||||
"inspect" => cx.tasks.inspect(),
|
||||
"cancel" => cx.tasks.cancel(),
|
||||
|
||||
"help" => cx.help.toggle(cx.layer()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn select(cx: &mut Ctx, exec: &Exec) -> bool {
|
||||
match exec.cmd.as_str() {
|
||||
"close" => cx.select.close(exec.named.contains_key("submit")),
|
||||
|
||||
"arrow" => {
|
||||
let step: isize = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
if step > 0 { cx.select.next(step as usize) } else { cx.select.prev(step.unsigned_abs()) }
|
||||
}
|
||||
|
||||
"help" => cx.help.toggle(cx.layer()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn input(cx: &mut Ctx, exec: &Exec) -> bool {
|
||||
match exec.cmd.as_str() {
|
||||
"close" => return cx.input.close(exec.named.contains_key("submit")),
|
||||
"escape" => return cx.input.escape(),
|
||||
|
||||
"move" => {
|
||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
let in_operating = exec.named.contains_key("in-operating");
|
||||
return if in_operating { cx.input.move_in_operating(step) } else { cx.input.move_(step) };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match cx.input.mode() {
|
||||
InputMode::Normal => match exec.cmd.as_str() {
|
||||
"insert" => cx.input.insert(exec.named.contains_key("append")),
|
||||
"visual" => cx.input.visual(),
|
||||
|
||||
"backward" => cx.input.backward(),
|
||||
"forward" => cx.input.forward(exec.named.contains_key("end-of-word")),
|
||||
"delete" => {
|
||||
cx.input.delete(exec.named.contains_key("cut"), exec.named.contains_key("insert"))
|
||||
}
|
||||
|
||||
"yank" => cx.input.yank(),
|
||||
"paste" => cx.input.paste(exec.named.contains_key("before")),
|
||||
|
||||
"undo" => cx.input.undo(),
|
||||
"redo" => cx.input.redo(),
|
||||
|
||||
"help" => cx.help.toggle(cx.layer()),
|
||||
_ => false,
|
||||
},
|
||||
InputMode::Insert => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn help(cx: &mut Ctx, exec: &Exec) -> bool {
|
||||
match exec.cmd.as_str() {
|
||||
"close" => cx.help.toggle(cx.layer()),
|
||||
"escape" => cx.help.escape(),
|
||||
|
||||
"arrow" => {
|
||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
cx.help.arrow(step)
|
||||
}
|
||||
|
||||
"filter" => cx.help.filter(),
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/src/help/bindings.rs
Normal file
54
app/src/help/bindings.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use core::Ctx;
|
||||
|
||||
use config::THEME;
|
||||
use ratatui::{layout::{self, Constraint}, prelude::{Buffer, Direction, Rect}, widgets::{List, ListItem, Widget}};
|
||||
|
||||
pub(super) struct Bindings<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
||||
impl<'a> Bindings<'a> {
|
||||
pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||
}
|
||||
|
||||
impl Widget for Bindings<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let bindings = &self.cx.help.window();
|
||||
if bindings.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// On
|
||||
let col1 = bindings
|
||||
.iter()
|
||||
.map(|c| ListItem::new(c.on()).style(THEME.help.on.into()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Exec
|
||||
let col2 = bindings
|
||||
.iter()
|
||||
.map(|c| ListItem::new(c.exec()).style(THEME.help.exec.into()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Desc
|
||||
let col3 = bindings
|
||||
.iter()
|
||||
.map(|c| ListItem::new(c.desc.as_deref().unwrap_or("-")).style(THEME.help.desc.into()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let chunks = layout::Layout::new()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(2, 10), Constraint::Ratio(3, 10), Constraint::Ratio(5, 10)])
|
||||
.split(area);
|
||||
|
||||
let cursor = self.cx.help.rel_cursor() as u16;
|
||||
buf.set_style(
|
||||
Rect { x: area.x, y: area.y + cursor, width: area.width, height: 1 },
|
||||
THEME.help.hovered.into(),
|
||||
);
|
||||
|
||||
List::new(col1).render(chunks[0], buf);
|
||||
List::new(col2).render(chunks[1], buf);
|
||||
List::new(col3).render(chunks[2], buf);
|
||||
}
|
||||
}
|
||||
32
app/src/help/layout.rs
Normal file
32
app/src/help/layout.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use core::Ctx;
|
||||
|
||||
use config::THEME;
|
||||
use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, widgets::{Clear, Paragraph, Widget}};
|
||||
|
||||
use super::Bindings;
|
||||
|
||||
pub(crate) struct Layout<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
||||
impl<'a> Layout<'a> {
|
||||
pub fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||
}
|
||||
|
||||
impl<'a> Widget for Layout<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let chunks = layout::Layout::new()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(1)])
|
||||
.split(area);
|
||||
|
||||
Clear.render(area, buf);
|
||||
|
||||
let help = &self.cx.help;
|
||||
Paragraph::new(help.keyword().unwrap_or_else(|| format!("{}.help", help.layer)))
|
||||
.style(THEME.help.footer.into())
|
||||
.render(chunks[1], buf);
|
||||
|
||||
Bindings::new(self.cx).render(chunks[0], buf);
|
||||
}
|
||||
}
|
||||
5
app/src/help/mod.rs
Normal file
5
app/src/help/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod bindings;
|
||||
mod layout;
|
||||
|
||||
use bindings::*;
|
||||
pub(super) use layout::*;
|
||||
59
app/src/input/input.rs
Normal file
59
app/src/input/input.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use core::{input::InputMode, Ctx};
|
||||
use std::ops::Range;
|
||||
|
||||
use ansi_to_tui::IntoText;
|
||||
use config::THEME;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget}};
|
||||
use shared::Term;
|
||||
|
||||
pub(crate) struct Input<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
||||
impl<'a> Input<'a> {
|
||||
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||
}
|
||||
|
||||
impl<'a> Widget for Input<'a> {
|
||||
fn render(self, win: Rect, buf: &mut Buffer) {
|
||||
let input = &self.cx.input;
|
||||
let area = self.cx.area(&input.position);
|
||||
|
||||
let value = if let Ok(v) = input.value_pretty() {
|
||||
v.into_text().unwrap()
|
||||
} else {
|
||||
Text::from(input.value())
|
||||
};
|
||||
|
||||
Clear.render(area, buf);
|
||||
Paragraph::new(value)
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(THEME.input.border.into())
|
||||
.title({
|
||||
let mut line = Line::from(input.title());
|
||||
line.patch_style(THEME.input.title.into());
|
||||
line
|
||||
}),
|
||||
)
|
||||
.style(THEME.input.value.into())
|
||||
.render(area, buf);
|
||||
|
||||
if let Some(Range { start, end }) = input.selected() {
|
||||
let x = win.width.min(area.x + 1 + start);
|
||||
let y = win.height.min(area.y + 1);
|
||||
|
||||
buf.set_style(
|
||||
Rect { x, y, width: (end - start).min(win.width - x), height: 1.min(win.height - y) },
|
||||
THEME.input.selected.into(),
|
||||
)
|
||||
}
|
||||
|
||||
_ = match input.mode() {
|
||||
InputMode::Insert => Term::set_cursor_bar(),
|
||||
_ => Term::set_cursor_block(),
|
||||
};
|
||||
}
|
||||
}
|
||||
3
app/src/input/mod.rs
Normal file
3
app/src/input/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod input;
|
||||
|
||||
pub(super) use input::*;
|
||||
21
app/src/logs.rs
Normal file
21
app/src/logs.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use anyhow::{Context, Result};
|
||||
use config::BOOT;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::{fmt, prelude::__tracing_subscriber_SubscriberExt, Registry};
|
||||
|
||||
pub(super) struct Logs;
|
||||
|
||||
impl Logs {
|
||||
pub(super) fn init() -> Result<WorkerGuard> {
|
||||
let appender = tracing_appender::rolling::never(&BOOT.state_dir, "yazi.log");
|
||||
let (handle, guard) = tracing_appender::non_blocking(appender);
|
||||
|
||||
// let filter = EnvFilter::from_default_env();
|
||||
let subscriber = Registry::default().with(fmt::layer().pretty().with_writer(handle));
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("setting default subscriber failed")?;
|
||||
|
||||
Ok(guard)
|
||||
}
|
||||
}
|
||||
33
app/src/main.rs
Normal file
33
app/src/main.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#![allow(clippy::module_inception)]
|
||||
|
||||
mod app;
|
||||
mod executor;
|
||||
mod help;
|
||||
mod input;
|
||||
mod logs;
|
||||
mod root;
|
||||
mod select;
|
||||
mod signals;
|
||||
mod tasks;
|
||||
mod which;
|
||||
|
||||
use app::*;
|
||||
use executor::*;
|
||||
use logs::*;
|
||||
use root::*;
|
||||
use signals::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// console_subscriber::init();
|
||||
|
||||
config::init();
|
||||
|
||||
core::init();
|
||||
|
||||
plugin::init();
|
||||
|
||||
adaptor::init();
|
||||
|
||||
App::run().await
|
||||
}
|
||||
48
app/src/root.rs
Normal file
48
app/src/root.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use core::Ctx;
|
||||
|
||||
use plugin::components;
|
||||
use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget};
|
||||
|
||||
use super::{input, select, tasks, which};
|
||||
use crate::help;
|
||||
|
||||
pub(super) struct Root<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
||||
impl<'a> Root<'a> {
|
||||
pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||
}
|
||||
|
||||
impl<'a> Widget for Root<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let chunks = Layout::new()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)])
|
||||
.split(area);
|
||||
|
||||
components::Header::new(self.cx).render(chunks[0], buf);
|
||||
components::Manager::new(self.cx).render(chunks[1], buf);
|
||||
components::Status::new(self.cx).render(chunks[2], buf);
|
||||
|
||||
if self.cx.tasks.visible {
|
||||
tasks::Layout::new(self.cx).render(area, buf);
|
||||
}
|
||||
|
||||
if self.cx.select.visible {
|
||||
select::Select::new(self.cx).render(area, buf);
|
||||
}
|
||||
|
||||
if self.cx.input.visible {
|
||||
input::Input::new(self.cx).render(area, buf);
|
||||
}
|
||||
|
||||
if self.cx.help.visible {
|
||||
help::Layout::new(self.cx).render(area, buf);
|
||||
}
|
||||
|
||||
if self.cx.which.visible {
|
||||
which::Which::new(self.cx).render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
app/src/select/mod.rs
Normal file
3
app/src/select/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod select;
|
||||
|
||||
pub(super) use select::*;
|
||||
43
app/src/select/select.rs
Normal file
43
app/src/select/select.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use core::Ctx;
|
||||
|
||||
use config::THEME;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}};
|
||||
|
||||
pub(crate) struct Select<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
||||
impl<'a> Select<'a> {
|
||||
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||
}
|
||||
|
||||
impl<'a> Widget for Select<'a> {
|
||||
fn render(self, _: Rect, buf: &mut Buffer) {
|
||||
let select = &self.cx.select;
|
||||
let area = self.cx.area(&select.position);
|
||||
|
||||
let items = select
|
||||
.window()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
if i != select.rel_cursor() {
|
||||
return ListItem::new(format!(" {v}")).style(THEME.select.inactive.into());
|
||||
}
|
||||
|
||||
ListItem::new(format!(" {v}")).style(THEME.select.active.into())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Clear.render(area, buf);
|
||||
List::new(items)
|
||||
.block(
|
||||
Block::new()
|
||||
.title(select.title())
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(THEME.select.border.into()),
|
||||
)
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
107
app/src/signals.rs
Normal file
107
app/src/signals.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
use core::Event;
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind};
|
||||
use futures::StreamExt;
|
||||
use tokio::{select, sync::{mpsc::{self, UnboundedReceiver, UnboundedSender}, oneshot}, task::JoinHandle};
|
||||
|
||||
pub(super) struct Signals {
|
||||
tx: UnboundedSender<Event>,
|
||||
rx: UnboundedReceiver<Event>,
|
||||
|
||||
term_stop_tx: Option<oneshot::Sender<()>>,
|
||||
term_stop_rx: Option<oneshot::Receiver<()>>,
|
||||
}
|
||||
|
||||
impl Signals {
|
||||
pub(super) fn start() -> Result<Self> {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let (term_tx, term_rx) = oneshot::channel();
|
||||
|
||||
let mut signals =
|
||||
Self { tx: tx.clone(), rx, term_stop_tx: Some(term_tx), term_stop_rx: Some(term_rx) };
|
||||
|
||||
signals.spawn_system_task()?;
|
||||
signals.spawn_crossterm_task();
|
||||
|
||||
Event::init(tx);
|
||||
Ok(signals)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) async fn recv(&mut self) -> Option<Event> { self.rx.recv().await }
|
||||
|
||||
pub(super) fn stop_term(&mut self, state: bool) {
|
||||
if state == self.term_stop_tx.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(tx) = self.term_stop_tx.take() {
|
||||
tx.send(()).ok();
|
||||
} else {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
(self.term_stop_tx, self.term_stop_rx) = (Some(tx), Some(rx));
|
||||
self.spawn_crossterm_task();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn spawn_system_task(&self) -> Result<()> { Ok(()) }
|
||||
|
||||
#[cfg(unix)]
|
||||
fn spawn_system_task(&self) -> Result<JoinHandle<()>> {
|
||||
use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM};
|
||||
|
||||
let tx = self.tx.clone();
|
||||
let mut signals = signal_hook_tokio::Signals::new([
|
||||
// Terminating signals
|
||||
SIGHUP, SIGTERM, SIGQUIT, SIGINT, //
|
||||
// Job control signals
|
||||
SIGCONT,
|
||||
])?;
|
||||
|
||||
Ok(tokio::spawn(async move {
|
||||
while let Some(signal) = signals.next().await {
|
||||
match signal {
|
||||
SIGHUP | SIGTERM | SIGQUIT | SIGINT => {
|
||||
if tx.send(Event::Quit(false)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
SIGCONT => {
|
||||
tx.send(Event::Stop(false, None)).ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn spawn_crossterm_task(&mut self) -> JoinHandle<()> {
|
||||
let tx = self.tx.clone();
|
||||
let mut stop_rx = self.term_stop_rx.take().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut reader = EventStream::new();
|
||||
|
||||
loop {
|
||||
select! {
|
||||
_ = &mut stop_rx => break,
|
||||
Some(Ok(event)) = reader.next() => {
|
||||
let event = match event {
|
||||
// We need to check key event kind;
|
||||
// otherwise event will be dispatched twice.
|
||||
CrosstermEvent::Key(key @ KeyEvent { kind: KeyEventKind::Press, .. }) => Event::Key(key),
|
||||
CrosstermEvent::Paste(str) => Event::Paste(str),
|
||||
CrosstermEvent::Resize(cols, rows) => Event::Resize(cols, rows),
|
||||
_ => continue,
|
||||
};
|
||||
if tx.send(event).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
19
app/src/tasks/clear.rs
Normal file
19
app/src/tasks/clear.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{self, Widget}};
|
||||
|
||||
pub(super) struct Clear;
|
||||
|
||||
impl Widget for Clear {
|
||||
fn render(self, mut area: Rect, buf: &mut Buffer) {
|
||||
if area.x > 0 {
|
||||
area.x -= 1;
|
||||
area.width += 2;
|
||||
}
|
||||
|
||||
if area.y > 0 {
|
||||
area.y -= 1;
|
||||
area.height += 2;
|
||||
}
|
||||
|
||||
widgets::Clear.render(area, buf)
|
||||
}
|
||||
}
|
||||
70
app/src/tasks/layout.rs
Normal file
70
app/src/tasks/layout.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use core::{tasks::TASKS_PERCENT, Ctx};
|
||||
|
||||
use config::THEME;
|
||||
use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Direction, Rect}, text::Line, widgets::{Block, BorderType, Borders, List, ListItem, Padding, Widget}};
|
||||
|
||||
use super::Clear;
|
||||
|
||||
pub(crate) struct Layout<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
||||
impl<'a> Layout<'a> {
|
||||
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||
|
||||
pub(super) fn area(area: Rect) -> Rect {
|
||||
let chunk = layout::Layout::new()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
|
||||
Constraint::Percentage(TASKS_PERCENT),
|
||||
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
|
||||
])
|
||||
.split(area)[1];
|
||||
|
||||
layout::Layout::new()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
|
||||
Constraint::Percentage(TASKS_PERCENT),
|
||||
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
|
||||
])
|
||||
.split(chunk)[1]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Layout<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = Self::area(area);
|
||||
|
||||
Clear.render(area, buf);
|
||||
let block = Block::new()
|
||||
.title({
|
||||
let mut line = Line::from("Tasks");
|
||||
line.patch_style(THEME.tasks.title.into());
|
||||
line
|
||||
})
|
||||
.title_alignment(Alignment::Center)
|
||||
.padding(Padding::new(0, 0, 1, 1))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(THEME.tasks.border.into());
|
||||
block.clone().render(area, buf);
|
||||
|
||||
let tasks = &self.cx.tasks;
|
||||
let items = tasks
|
||||
.paginate()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
let mut item = ListItem::new(v.name.clone());
|
||||
if i == tasks.cursor {
|
||||
item = item.style(THEME.tasks.hovered.into());
|
||||
}
|
||||
item
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
List::new(items).render(block.inner(area), buf);
|
||||
}
|
||||
}
|
||||
5
app/src/tasks/mod.rs
Normal file
5
app/src/tasks/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod clear;
|
||||
mod layout;
|
||||
|
||||
use clear::*;
|
||||
pub(super) use layout::*;
|
||||
48
app/src/which/layout.rs
Normal file
48
app/src/which/layout.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use core::Ctx;
|
||||
|
||||
use config::THEME;
|
||||
use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::{Block, Clear, Widget}};
|
||||
|
||||
use super::Side;
|
||||
|
||||
pub(crate) struct Which<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
||||
impl<'a> Which<'a> {
|
||||
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||
}
|
||||
|
||||
impl Widget for Which<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let which = &self.cx.which;
|
||||
let mut cands: (Vec<_>, Vec<_>, Vec<_>) = Default::default();
|
||||
for (i, c) in which.cands.iter().enumerate() {
|
||||
match i % 3 {
|
||||
0 => cands.0.push(c),
|
||||
1 => cands.1.push(c),
|
||||
2 => cands.2.push(c),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
let height = cands.0.len() as u16 + 2;
|
||||
let area = Rect {
|
||||
x: 1,
|
||||
y: area.height.saturating_sub(height + 2),
|
||||
width: area.width.saturating_sub(2),
|
||||
height,
|
||||
};
|
||||
|
||||
let chunks = layout::Layout::new()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3)])
|
||||
.split(area);
|
||||
|
||||
Clear.render(area, buf);
|
||||
Block::new().style(THEME.which.mask.into()).render(area, buf);
|
||||
Side::new(which.times, cands.0).render(chunks[0], buf);
|
||||
Side::new(which.times, cands.1).render(chunks[1], buf);
|
||||
Side::new(which.times, cands.2).render(chunks[2], buf);
|
||||
}
|
||||
}
|
||||
5
app/src/which/mod.rs
Normal file
5
app/src/which/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod layout;
|
||||
mod side;
|
||||
|
||||
pub(super) use layout::*;
|
||||
use side::*;
|
||||
44
app/src/which/side.rs
Normal file
44
app/src/which/side.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use config::{keymap::Control, THEME};
|
||||
use ratatui::{prelude::{Buffer, Rect}, text::{Line, Span}, widgets::{Block, List, ListItem, Padding, Widget}};
|
||||
|
||||
pub(super) struct Side<'a> {
|
||||
times: usize,
|
||||
cands: Vec<&'a Control>,
|
||||
}
|
||||
|
||||
impl<'a> Side<'a> {
|
||||
pub(super) fn new(times: usize, cands: Vec<&'a Control>) -> Self { Self { times, cands } }
|
||||
}
|
||||
|
||||
impl Widget for Side<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let items = self
|
||||
.cands
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let mut spans = Vec::with_capacity(10);
|
||||
|
||||
// Keys
|
||||
let keys = c.on[self.times..].iter().map(ToString::to_string).collect::<Vec<_>>();
|
||||
spans.push(Span::raw(" ".repeat(10usize.saturating_sub(keys.join("").len()))));
|
||||
spans.push(Span::styled(keys[0].clone(), THEME.which.cand.into()));
|
||||
spans.extend(
|
||||
keys.iter().skip(1).map(|k| Span::styled(k.to_string(), THEME.which.rest.into())),
|
||||
);
|
||||
|
||||
// Separator
|
||||
spans.push(Span::styled(
|
||||
THEME.which.separator.to_string(),
|
||||
THEME.which.separator_style.into(),
|
||||
));
|
||||
|
||||
// Desc / Exec
|
||||
spans.push(Span::styled(c.desc_or_exec(), THEME.which.desc.into()));
|
||||
|
||||
ListItem::new(Line::from(spans))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
List::new(items).block(Block::new().padding(Padding::new(0, 1, 1, 1))).render(area, buf);
|
||||
}
|
||||
}
|
||||
BIN
assets/logo.png
BIN
assets/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 517 KiB |
|
|
@ -1,11 +0,0 @@
|
|||
[Desktop Entry]
|
||||
Name=Yazi File Manager
|
||||
Icon=yazi
|
||||
Comment=Blazing fast terminal file manager written in Rust, based on async I/O
|
||||
Terminal=true
|
||||
TryExec=yazi
|
||||
Exec=yazi %f
|
||||
Type=Application
|
||||
MimeType=inode/directory
|
||||
Categories=System;FileManager;FileTools;ConsoleOnly
|
||||
Keywords=File;Manager;Explorer;Browser;Launcher
|
||||
28
config/Cargo.toml
Normal file
28
config/Cargo.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "config"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
shared = { path = "../shared" }
|
||||
|
||||
# External dependencies
|
||||
anyhow = "^1"
|
||||
clap = { version = "^4", features = [ "derive" ] }
|
||||
crossterm = "^0"
|
||||
dirs = "^5"
|
||||
futures = "^0"
|
||||
glob = "^0"
|
||||
indexmap = "^2"
|
||||
md-5 = "^0"
|
||||
ratatui = "^0"
|
||||
serde = { version = "^1", features = [ "derive" ] }
|
||||
shell-words = "^1"
|
||||
toml = { version = "^0", features = [ "preserve_order" ] }
|
||||
validator = { version = "^0", features = [ "derive" ] }
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version = "^4", features = [ "derive" ] }
|
||||
clap_complete = "^4"
|
||||
clap_complete_nushell = "^4"
|
||||
clap_complete_fig = "^4"
|
||||
24
config/build.rs
Normal file
24
config/build.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#[path = "src/boot/cli.rs"]
|
||||
mod cli;
|
||||
|
||||
use std::{fs, io};
|
||||
|
||||
use clap::CommandFactory;
|
||||
use clap_complete::{generate_to, Shell};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let cmd = &mut cli::Args::command();
|
||||
let bin = "yazi";
|
||||
let out = "completions";
|
||||
|
||||
fs::create_dir_all(out)?;
|
||||
generate_to(Shell::Bash, cmd, bin, out)?;
|
||||
generate_to(Shell::Fish, cmd, bin, out)?;
|
||||
generate_to(Shell::Zsh, cmd, bin, out)?;
|
||||
generate_to(Shell::Elvish, cmd, bin, out)?;
|
||||
generate_to(Shell::PowerShell, cmd, bin, out)?;
|
||||
generate_to(clap_complete_nushell::Nushell, cmd, bin, out)?;
|
||||
generate_to(clap_complete_fig::Fig, cmd, bin, out)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
239
config/preset/keymap.toml
Normal file
239
config/preset/keymap.toml
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
[manager]
|
||||
|
||||
keymap = [
|
||||
{ on = [ "<Esc>" ], exec = "escape", desc = "Exit visual mode, clear selected, or cancel search" },
|
||||
{ on = [ "q" ], exec = "quit", desc = "Exit the process" },
|
||||
{ on = [ "Q" ], exec = "quit --no-cwd-file", desc = "Exit the process without writing cwd-file" },
|
||||
{ on = [ "<C-q>" ], exec = "close", desc = "Close the current tab, or quit if it is last tab" },
|
||||
{ on = [ "<C-z>" ], exec = "suspend", desc = "Suspend the process" },
|
||||
|
||||
# Navigation
|
||||
{ on = [ "k" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||
{ on = [ "j" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||
|
||||
{ on = [ "K" ], exec = "arrow -5", desc = "Move cursor up 5 lines" },
|
||||
{ on = [ "J" ], exec = "arrow 5", desc = "Move cursor down 5 lines" },
|
||||
|
||||
{ on = [ "<C-u>" ], exec = "arrow -50%", desc = "Move cursor up half page" },
|
||||
{ on = [ "<C-d>" ], exec = "arrow 50%", desc = "Move cursor down half page" },
|
||||
{ on = [ "<C-b>" ], exec = "arrow -100%", desc = "Move cursor up one page" },
|
||||
{ on = [ "<C-f>" ], exec = "arrow 100%", desc = "Move cursor down one page" },
|
||||
|
||||
{ on = [ "h" ], exec = [ "leave", "escape --visual --select" ], desc = "Go back to the parent directory" },
|
||||
{ on = [ "l" ], exec = [ "enter", "escape --visual --select" ], desc = "Enter the child directory" },
|
||||
|
||||
{ on = [ "H" ], exec = "back", desc = "Go back to the previous directory" },
|
||||
{ on = [ "L" ], exec = "forward", desc = "Go forward to the next directory" },
|
||||
|
||||
{ on = [ "<A-k>" ], exec = "peek -5", desc = "Peek up 5 units in the preview" },
|
||||
{ on = [ "<A-j>" ], exec = "peek 5", desc = "Peek down 5 units in the preview" },
|
||||
|
||||
{ on = [ "<Up>" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||
{ on = [ "<Down>" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||
{ on = [ "<Left>" ], exec = "leave", desc = "Go back to the parent directory" },
|
||||
{ on = [ "<Right>" ], exec = "enter", desc = "Enter the child directory" },
|
||||
|
||||
{ on = [ "g", "g" ], exec = "arrow -99999999", desc = "Move cursor to the top" },
|
||||
{ on = [ "G" ], exec = "arrow 99999999", desc = "Move cursor to the bottom" },
|
||||
|
||||
# Selection
|
||||
{ on = [ "<Space>" ], exec = [ "select --state=none", "arrow 1" ], desc = "Toggle the current selection state" },
|
||||
{ on = [ "v" ], exec = "visual_mode", desc = "Enter visual mode (selection mode)" },
|
||||
{ on = [ "V" ], exec = "visual_mode --unset", desc = "Enter visual mode (unset mode)" },
|
||||
{ on = [ "<C-a>" ], exec = "select_all --state=true", desc = "Select all files" },
|
||||
{ on = [ "<C-r>" ], exec = "select_all --state=none", desc = "Inverse selection of all files" },
|
||||
|
||||
# Operation
|
||||
{ on = [ "o" ], exec = "open", desc = "Open the selected files" },
|
||||
{ on = [ "O" ], exec = "open --interactive", desc = "Open the selected files interactively" },
|
||||
{ on = [ "<Enter>" ], exec = "open", desc = "Open the selected files" },
|
||||
{ on = [ "<C-Enter>" ], exec = "open --interactive", desc = "Open the selected files interactively" },
|
||||
{ on = [ "y" ], exec = [ "yank", "escape --visual --select" ], desc = "Copy the selected files" },
|
||||
{ on = [ "x" ], exec = [ "yank --cut", "escape --visual --select" ], desc = "Cut the selected files" },
|
||||
{ on = [ "p" ], exec = "paste", desc = "Paste the files" },
|
||||
{ on = [ "P" ], exec = "paste --force", desc = "Paste the files (overwrite if the destination exists)" },
|
||||
{ on = [ "-" ], exec = "link", desc = "Symlink the absolute path of files" },
|
||||
{ on = [ "_" ], exec = "link --relative", desc = "Symlink the relative path of files" },
|
||||
{ on = [ "d" ], exec = [ "remove", "escape --visual --select" ], desc = "Move the files to the trash" },
|
||||
{ on = [ "D" ], exec = [ "remove --permanently", "escape --visual --select" ], desc = "Permanently delete the files" },
|
||||
{ on = [ "a" ], exec = "create", desc = "Create a file or directory (ends with / for directories)" },
|
||||
{ on = [ "r" ], exec = "rename", desc = "Rename a file or directory" },
|
||||
{ on = [ ";" ], exec = "shell", desc = "Run a shell command" },
|
||||
{ on = [ ":" ], exec = "shell --block", desc = "Run a shell command (block the UI until the command finishes)" },
|
||||
{ on = [ "." ], exec = "hidden toggle", desc = "Toggle the visibility of hidden files" },
|
||||
{ on = [ "s" ], exec = "search fd", desc = "Search files by name using fd" },
|
||||
{ on = [ "S" ], exec = "search rg", desc = "Search files by content using ripgrep" },
|
||||
{ on = [ "<C-s>" ], exec = "search none", desc = "Cancel the ongoing search" },
|
||||
{ on = [ "z" ], exec = "jump zoxide", desc = "Jump to a directory using zoxide" },
|
||||
{ on = [ "Z" ], exec = "jump fzf", desc = "Jump to a directory, or reveal a file using fzf" },
|
||||
|
||||
# Linemode
|
||||
{ on = [ "m", "s" ], exec = "linemode size", desc = "Set linemode to size" },
|
||||
{ on = [ "m", "p" ], exec = "linemode permissions", desc = "Set linemode to permissions" },
|
||||
{ on = [ "m", "m" ], exec = "linemode mtime", desc = "Set linemode to mtime" },
|
||||
{ on = [ "m", "n" ], exec = "linemode none", desc = "Set linemode to none" },
|
||||
|
||||
# Copy
|
||||
{ on = [ "c", "c" ], exec = "copy path", desc = "Copy the absolute path" },
|
||||
{ on = [ "c", "d" ], exec = "copy dirname", desc = "Copy the path of the parent directory" },
|
||||
{ on = [ "c", "f" ], exec = "copy filename", desc = "Copy the name of the file" },
|
||||
{ on = [ "c", "n" ], exec = "copy name_without_ext", desc = "Copy the name of the file without the extension" },
|
||||
|
||||
# Find
|
||||
{ on = [ "/" ], exec = "find --smart" },
|
||||
{ on = [ "?" ], exec = "find --previous --smart" },
|
||||
{ on = [ "n" ], exec = "find_arrow" },
|
||||
{ on = [ "N" ], exec = "find_arrow --previous" },
|
||||
|
||||
# Sorting
|
||||
{ on = [ ",", "a" ], exec = "sort alphabetical --dir_first", desc = "Sort alphabetically" },
|
||||
{ on = [ ",", "A" ], exec = "sort alphabetical --reverse --dir_first", desc = "Sort alphabetically (reverse)" },
|
||||
{ on = [ ",", "c" ], exec = "sort created --dir_first", desc = "Sort by creation time" },
|
||||
{ on = [ ",", "C" ], exec = "sort created --reverse --dir_first", desc = "Sort by creation time (reverse)" },
|
||||
{ on = [ ",", "m" ], exec = "sort modified --dir_first", desc = "Sort by modified time" },
|
||||
{ on = [ ",", "M" ], exec = "sort modified --reverse --dir_first", desc = "Sort by modified time (reverse)" },
|
||||
{ on = [ ",", "n" ], exec = "sort natural --dir_first", desc = "Sort naturally" },
|
||||
{ on = [ ",", "N" ], exec = "sort natural --reverse --dir_first", desc = "Sort naturally (reverse)" },
|
||||
{ on = [ ",", "s" ], exec = "sort size --dir_first", desc = "Sort by size" },
|
||||
{ on = [ ",", "S" ], exec = "sort size --reverse --dir_first", desc = "Sort by size (reverse)" },
|
||||
|
||||
# Tabs
|
||||
{ on = [ "t" ], exec = "tab_create --current", desc = "Create a new tab using the current path" },
|
||||
|
||||
{ on = [ "1" ], exec = "tab_switch 0", desc = "Switch to the first tab" },
|
||||
{ on = [ "2" ], exec = "tab_switch 1", desc = "Switch to the second tab" },
|
||||
{ on = [ "3" ], exec = "tab_switch 2", desc = "Switch to the third tab" },
|
||||
{ on = [ "4" ], exec = "tab_switch 3", desc = "Switch to the fourth tab" },
|
||||
{ on = [ "5" ], exec = "tab_switch 4", desc = "Switch to the fifth tab" },
|
||||
{ on = [ "6" ], exec = "tab_switch 5", desc = "Switch to the sixth tab" },
|
||||
{ on = [ "7" ], exec = "tab_switch 6", desc = "Switch to the seventh tab" },
|
||||
{ on = [ "8" ], exec = "tab_switch 7", desc = "Switch to the eighth tab" },
|
||||
{ on = [ "9" ], exec = "tab_switch 8", desc = "Switch to the ninth tab" },
|
||||
|
||||
{ on = [ "[" ], exec = "tab_switch -1 --relative", desc = "Switch to the previous tab" },
|
||||
{ on = [ "]" ], exec = "tab_switch 1 --relative", desc = "Switch to the next tab" },
|
||||
|
||||
{ on = [ "{" ], exec = "tab_swap -1", desc = "Swap the current tab with the previous tab" },
|
||||
{ on = [ "}" ], exec = "tab_swap 1", desc = "Swap the current tab with the next tab" },
|
||||
|
||||
# Tasks
|
||||
{ on = [ "w" ], exec = "tasks_show", desc = "Show the tasks manager" },
|
||||
|
||||
# Goto
|
||||
{ on = [ "g", "h" ], exec = "cd ~", desc = "Go to the home directory" },
|
||||
{ on = [ "g", "c" ], exec = "cd ~/.config", desc = "Go to the config directory" },
|
||||
{ on = [ "g", "d" ], exec = "cd ~/Downloads", desc = "Go to the downloads directory" },
|
||||
{ on = [ "g", "t" ], exec = "cd /tmp", desc = "Go to the temporary directory" },
|
||||
{ on = [ "g", "<Space>" ], exec = "cd --interactive", desc = "Go to a directory interactively" },
|
||||
|
||||
# Help
|
||||
{ on = [ "~" ], exec = "help", desc = "Open help" },
|
||||
]
|
||||
|
||||
[tasks]
|
||||
|
||||
keymap = [
|
||||
{ on = [ "<Esc>" ], exec = "close", desc = "Hide the task manager" },
|
||||
{ on = [ "<C-q>" ], exec = "close", desc = "Hide the task manager" },
|
||||
{ on = [ "w" ], exec = "close", desc = "Hide the task manager" },
|
||||
|
||||
{ on = [ "k" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||
{ on = [ "j" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||
|
||||
{ on = [ "<Up>" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||
{ on = [ "<Down>" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||
|
||||
{ on = [ "<Enter>" ], exec = "inspect", desc = "Inspect the task" },
|
||||
{ on = [ "x" ], exec = "cancel", desc = "Cancel the task" },
|
||||
|
||||
{ on = [ "~" ], exec = "help", desc = "Open help" }
|
||||
]
|
||||
|
||||
[select]
|
||||
|
||||
keymap = [
|
||||
{ on = [ "<C-q>" ], exec = "close", desc = "Cancel selection" },
|
||||
{ on = [ "<Esc>" ], exec = "close", desc = "Cancel selection" },
|
||||
{ on = [ "<Enter>" ], exec = "close --submit", desc = "Submit the selection" },
|
||||
|
||||
{ on = [ "k" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||
{ on = [ "j" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||
|
||||
{ on = [ "K" ], exec = "arrow -5", desc = "Move cursor up 5 lines" },
|
||||
{ on = [ "J" ], exec = "arrow 5", desc = "Move cursor down 5 lines" },
|
||||
|
||||
{ on = [ "<Up>" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||
{ on = [ "<Down>" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||
|
||||
{ on = [ "~" ], exec = "help", desc = "Open help" }
|
||||
]
|
||||
|
||||
[input]
|
||||
|
||||
keymap = [
|
||||
{ on = [ "<C-q>" ], exec = "close", desc = "Cancel input" },
|
||||
{ on = [ "<Enter>" ], exec = "close --submit", desc = "Submit the input" },
|
||||
{ on = [ "<Esc>" ], exec = "escape", desc = "Go back the normal mode, or cancel input" },
|
||||
|
||||
# Mode
|
||||
{ on = [ "i" ], exec = "insert", desc = "Enter insert mode" },
|
||||
{ on = [ "a" ], exec = "insert --append", desc = "Enter append mode" },
|
||||
{ on = [ "v" ], exec = "visual", desc = "Enter visual mode" },
|
||||
{ on = [ "V" ], exec = [ "move -999", "visual", "move 999" ], desc = "Enter visual mode and select all" },
|
||||
|
||||
# Navigation
|
||||
{ on = [ "h" ], exec = "move -1", desc = "Move cursor left" },
|
||||
{ on = [ "l" ], exec = "move 1", desc = "Move cursor right" },
|
||||
|
||||
{ on = [ "0" ], exec = "move -999", desc = "Move to the BOL" },
|
||||
{ on = [ "$" ], exec = "move 999", desc = "Move to the EOL" },
|
||||
{ on = [ "I" ], exec = [ "move -999", "insert" ], desc = "Move to the BOL, and enter insert mode" },
|
||||
{ on = [ "A" ], exec = [ "move 999", "insert --append" ], desc = "Move to the EOL, and enter append mode" },
|
||||
|
||||
{ on = [ "<Left>" ], exec = "move -1", desc = "Move cursor left" },
|
||||
{ on = [ "<Right>" ], exec = "move 1", desc = "Move cursor right" },
|
||||
|
||||
{ on = [ "b" ], exec = "backward", desc = "Move to the beginning of the previous word" },
|
||||
{ on = [ "w" ], exec = "forward", desc = "Move to the beginning of the next word" },
|
||||
{ on = [ "e" ], exec = "forward --end-of-word", desc = "Move to the end of the next word" },
|
||||
|
||||
# Deletion
|
||||
{ on = [ "d" ], exec = "delete --cut", desc = "Cut the selected characters" },
|
||||
{ on = [ "D" ], exec = [ "delete --cut", "move 999" ], desc = "Cut until the EOL" },
|
||||
{ on = [ "c" ], exec = "delete --cut --insert", desc = "Cut the selected characters, and enter insert mode" },
|
||||
{ on = [ "C" ], exec = [ "delete --cut --insert", "move 999" ], desc = "Cut until the EOL, and enter insert mode" },
|
||||
{ on = [ "x" ], exec = [ "delete --cut", "move 1 --in-operating" ], desc = "Cut the current character" },
|
||||
|
||||
# Yank/Paste
|
||||
{ on = [ "y" ], exec = "yank", desc = "Copy the selected characters" },
|
||||
{ on = [ "p" ], exec = "paste", desc = "Paste the copied characters after the cursor" },
|
||||
{ on = [ "P" ], exec = "paste --before", desc = "Paste the copied characters before the cursor" },
|
||||
|
||||
# Undo/Redo
|
||||
{ on = [ "u" ], exec = "undo", desc = "Undo the last operation" },
|
||||
{ on = [ "<C-r>" ], exec = "redo", desc = "Redo the last operation" },
|
||||
|
||||
# Help
|
||||
{ on = [ "~" ], exec = "help", desc = "Open help" }
|
||||
]
|
||||
|
||||
[help]
|
||||
|
||||
keymap = [
|
||||
{ on = [ "<Esc>" ], exec = "escape", desc = "Clear the filter, or hide the help" },
|
||||
{ on = [ "q" ], exec = "close", desc = "Exit the process" },
|
||||
{ on = [ "<C-q>" ], exec = "close", desc = "Hide the help" },
|
||||
|
||||
# Navigation
|
||||
{ on = [ "k" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||
{ on = [ "j" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||
|
||||
{ on = [ "K" ], exec = "arrow -5", desc = "Move cursor up 5 lines" },
|
||||
{ on = [ "J" ], exec = "arrow 5", desc = "Move cursor down 5 lines" },
|
||||
|
||||
{ on = [ "<Up>" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||
{ on = [ "<Down>" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||
|
||||
# Filtering
|
||||
{ on = [ "/" ], exec = "filter", desc = "Apply a filter for the help items" },
|
||||
]
|
||||
244
config/preset/theme.toml
Normal file
244
config/preset/theme.toml
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
# vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
# : Manager {{{
|
||||
|
||||
[manager]
|
||||
cwd = { fg = "cyan" }
|
||||
|
||||
# Hovered
|
||||
hovered = { fg = "black", bg = "lightblue" }
|
||||
preview_hovered = { underline = true }
|
||||
|
||||
# Find
|
||||
find_keyword = { fg = "yellow", italic = true }
|
||||
find_position = { fg = "magenta", bg = "reset", italic = true }
|
||||
|
||||
# Marker
|
||||
marker_selected = { fg = "lightgreen", bg = "lightgreen" }
|
||||
marker_copied = { fg = "lightyellow", bg = "lightyellow" }
|
||||
marker_cut = { fg = "lightred", bg = "lightred" }
|
||||
|
||||
# Tab
|
||||
tab_active = { fg = "black", bg = "lightblue" }
|
||||
tab_inactive = { fg = "white", bg = "darkgray" }
|
||||
tab_width = 1
|
||||
|
||||
# Border
|
||||
border_symbol = "│"
|
||||
border_style = { fg = "gray" }
|
||||
|
||||
# Offset
|
||||
folder_offset = [ 1, 0, 1, 0 ]
|
||||
preview_offset = [ 1, 1, 1, 1 ]
|
||||
|
||||
# Highlighting
|
||||
syntect_theme = ""
|
||||
|
||||
# : }}}
|
||||
|
||||
|
||||
# : Status {{{
|
||||
|
||||
[status]
|
||||
separator_open = ""
|
||||
separator_close = ""
|
||||
separator_style = { fg = "darkgray", bg = "darkgray" }
|
||||
|
||||
# Mode
|
||||
mode_normal = { fg = "black", bg = "lightblue", bold = true }
|
||||
mode_select = { fg = "black", bg = "lightgreen", bold = true }
|
||||
mode_unset = { fg = "black", bg = "lightmagenta", bold = true }
|
||||
|
||||
# Progress
|
||||
progress_label = { bold = true }
|
||||
progress_normal = { fg = "blue", bg = "black" }
|
||||
progress_error = { fg = "red", bg = "black" }
|
||||
|
||||
# Permissions
|
||||
permissions_t = { fg = "lightgreen" }
|
||||
permissions_r = { fg = "lightyellow" }
|
||||
permissions_w = { fg = "lightred" }
|
||||
permissions_x = { fg = "lightcyan" }
|
||||
permissions_s = { fg = "darkgray" }
|
||||
|
||||
# : }}}
|
||||
|
||||
|
||||
# : Input {{{
|
||||
|
||||
[input]
|
||||
border = { fg = "blue" }
|
||||
title = {}
|
||||
value = {}
|
||||
selected = { reversed = true }
|
||||
|
||||
# : }}}
|
||||
|
||||
|
||||
# : Select {{{
|
||||
|
||||
[select]
|
||||
border = { fg = "blue" }
|
||||
active = { fg = "magenta" }
|
||||
inactive = {}
|
||||
|
||||
# : }}}
|
||||
|
||||
|
||||
# : Tasks {{{
|
||||
|
||||
[tasks]
|
||||
border = { fg = "blue" }
|
||||
title = {}
|
||||
hovered = { underline = true }
|
||||
|
||||
# : }}}
|
||||
|
||||
|
||||
# : Which {{{
|
||||
|
||||
[which]
|
||||
mask = { bg = "black" }
|
||||
cand = { fg = "lightcyan" }
|
||||
rest = { fg = "darkgray" }
|
||||
desc = { fg = "magenta" }
|
||||
separator = " "
|
||||
separator_style = { fg = "darkgray" }
|
||||
|
||||
# : }}}
|
||||
|
||||
|
||||
# : Help {{{
|
||||
|
||||
[help]
|
||||
on = { fg = "magenta" }
|
||||
exec = { fg = "cyan" }
|
||||
desc = { fg = "gray" }
|
||||
hovered = { bg = "darkgray", bold = true }
|
||||
footer = { fg = "black", bg = "white" }
|
||||
|
||||
# : }}}
|
||||
|
||||
|
||||
# : File-specific styles {{{
|
||||
|
||||
[filetype]
|
||||
|
||||
rules = [
|
||||
# Images
|
||||
{ mime = "image/*", fg = "cyan" },
|
||||
|
||||
# Videos
|
||||
{ mime = "video/*", fg = "yellow" },
|
||||
{ mime = "audio/*", fg = "yellow" },
|
||||
|
||||
# Archives
|
||||
{ mime = "application/zip", fg = "magenta" },
|
||||
{ mime = "application/gzip", fg = "magenta" },
|
||||
{ mime = "application/x-tar", fg = "magenta" },
|
||||
{ mime = "application/x-bzip", fg = "magenta" },
|
||||
{ mime = "application/x-bzip2", fg = "magenta" },
|
||||
{ mime = "application/x-7z-compressed", fg = "magenta" },
|
||||
{ mime = "application/x-rar", fg = "magenta" },
|
||||
|
||||
# Fallback
|
||||
# { name = "*", fg = "white" },
|
||||
{ name = "*/", fg = "blue" }
|
||||
]
|
||||
|
||||
[icons]
|
||||
|
||||
"Desktop/" = ""
|
||||
"Documents/" = ""
|
||||
"Downloads/" = ""
|
||||
"Pictures/" = ""
|
||||
"Music/" = ""
|
||||
"Movies/" = ""
|
||||
"Videos/" = ""
|
||||
"Public/" = ""
|
||||
"Library/" = ""
|
||||
"Development/" = ""
|
||||
".config/" = ""
|
||||
|
||||
# Git
|
||||
".git/" = ""
|
||||
".gitignore" = ""
|
||||
".gitmodules" = ""
|
||||
".gitattributes" = ""
|
||||
|
||||
# Dotfiles
|
||||
".DS_Store" = ""
|
||||
".bashrc" = ""
|
||||
".bashprofile" = ""
|
||||
".zshrc" = ""
|
||||
".zshenv" = ""
|
||||
".zprofile" = ""
|
||||
".vimrc" = ""
|
||||
|
||||
# Text
|
||||
"*.txt" = ""
|
||||
"*.md" = ""
|
||||
|
||||
# Archives
|
||||
"*.zip" = ""
|
||||
"*.tar" = ""
|
||||
"*.gz" = ""
|
||||
"*.7z" = ""
|
||||
|
||||
# Audio
|
||||
"*.mp3" = ""
|
||||
"*.flac" = ""
|
||||
"*.wav" = ""
|
||||
|
||||
# Movies
|
||||
"*.mp4" = ""
|
||||
"*.mkv" = ""
|
||||
"*.avi" = ""
|
||||
"*.mov" = ""
|
||||
|
||||
# Images
|
||||
"*.jpg" = ""
|
||||
"*.jpeg" = ""
|
||||
"*.png" = ""
|
||||
"*.gif" = ""
|
||||
"*.webp" = ""
|
||||
"*.avif" = ""
|
||||
"*.bmp" = ""
|
||||
"*.ico" = ""
|
||||
"*.svg" = ""
|
||||
|
||||
# Programming
|
||||
"*.c" = ""
|
||||
"*.cpp" = ""
|
||||
"*.h" = ""
|
||||
"*.hpp" = ""
|
||||
"*.rs" = ""
|
||||
"*.go" = ""
|
||||
"*.py" = ""
|
||||
"*.js" = ""
|
||||
"*.ts" = ""
|
||||
"*.tsx" = ""
|
||||
"*.jsx" = ""
|
||||
"*.rb" = ""
|
||||
"*.php" = ""
|
||||
"*.java" = ""
|
||||
"*.sh" = ""
|
||||
"*.fish" = ""
|
||||
"*.swift" = ""
|
||||
"*.vim" = ""
|
||||
"*.lua" = ""
|
||||
"*.html" = ""
|
||||
"*.css" = ""
|
||||
"*.scss" = ""
|
||||
"*.json" = ""
|
||||
"*.toml" = ""
|
||||
"*.yml" = ""
|
||||
"*.yaml" = ""
|
||||
"*.ini" = ""
|
||||
"*.conf" = ""
|
||||
|
||||
# Default
|
||||
"*" = ""
|
||||
"*/" = ""
|
||||
|
||||
# : }}}
|
||||
75
config/preset/yazi.toml
Normal file
75
config/preset/yazi.toml
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
[manager]
|
||||
layout = [ 1, 4, 3 ]
|
||||
sort_by = "modified"
|
||||
sort_sensitive = true
|
||||
sort_reverse = true
|
||||
sort_dir_first = true
|
||||
linemode = "none"
|
||||
show_hidden = false
|
||||
show_symlink = true
|
||||
|
||||
[preview]
|
||||
tab_size = 2
|
||||
max_width = 600
|
||||
max_height = 900
|
||||
cache_dir = ""
|
||||
|
||||
[opener]
|
||||
edit = [
|
||||
{ exec = '$EDITOR "$@"', block = true, for = "unix" },
|
||||
{ exec = 'code "%*"', orphan = true, for = "windows" },
|
||||
]
|
||||
open = [
|
||||
{ exec = 'xdg-open "$@"', desc = "Open", for = "linux" },
|
||||
{ exec = 'open "$@"', desc = "Open", for = "macos" },
|
||||
{ exec = 'start "%1"', orphan = true, desc = "Open", for = "windows" }
|
||||
]
|
||||
reveal = [
|
||||
{ exec = 'open -R "$1"', desc = "Reveal", for = "macos" },
|
||||
{ exec = 'explorer /select,%1', orphan = true, desc = "Reveal", for = "windows" },
|
||||
{ exec = '''exiftool "$1"; echo "Press enter to exit"; read''', block = true, desc = "Show EXIF", for = "unix" },
|
||||
]
|
||||
extract = [
|
||||
{ exec = 'unar "$1"', desc = "Extract here", for = "unix" },
|
||||
{ exec = 'unar "%1"', desc = "Extract here", for = "windows" },
|
||||
]
|
||||
play = [
|
||||
{ exec = 'mpv "$@"', orphan = true, for = "unix" },
|
||||
{ exec = 'mpv "%1"', orphan = true, for = "windows" },
|
||||
{ exec = '''mediainfo "$1"; echo "Press enter to exit"; read''', block = true, desc = "Show media info", for = "unix" },
|
||||
]
|
||||
|
||||
[open]
|
||||
rules = [
|
||||
{ name = "*/", use = [ "edit", "open", "reveal" ] },
|
||||
|
||||
{ mime = "text/*", use = [ "edit", "reveal" ] },
|
||||
{ mime = "image/*", use = [ "open", "reveal" ] },
|
||||
{ mime = "video/*", use = [ "play", "reveal" ] },
|
||||
{ mime = "audio/*", use = [ "play", "reveal" ] },
|
||||
{ mime = "inode/x-empty", use = [ "edit", "reveal" ] },
|
||||
|
||||
{ mime = "application/json", use = [ "edit", "reveal" ] },
|
||||
{ mime = "*/javascript", use = [ "edit", "reveal" ] },
|
||||
|
||||
{ mime = "application/zip", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/gzip", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-tar", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-bzip", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-bzip2", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-7z-compressed", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-rar", use = [ "extract", "reveal" ] },
|
||||
|
||||
{ mime = "*", use = [ "open", "reveal" ] },
|
||||
]
|
||||
|
||||
[tasks]
|
||||
micro_workers = 5
|
||||
macro_workers = 10
|
||||
bizarre_retry = 5
|
||||
|
||||
[plugins]
|
||||
preload = []
|
||||
|
||||
[log]
|
||||
enabled = false
|
||||
54
config/src/boot/boot.rs
Normal file
54
config/src/boot/boot.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use std::{env, fs, path::PathBuf, process};
|
||||
|
||||
use clap::Parser;
|
||||
use shared::expand_path;
|
||||
|
||||
use super::cli::Args;
|
||||
use crate::{Xdg, PREVIEW};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Boot {
|
||||
pub cwd: PathBuf,
|
||||
pub state_dir: PathBuf,
|
||||
|
||||
pub cwd_file: Option<PathBuf>,
|
||||
pub chooser_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for Boot {
|
||||
fn default() -> Self {
|
||||
let args = Args::parse();
|
||||
|
||||
let cwd = args.cwd.map(expand_path).filter(|p| p.is_dir()).or_else(|| env::current_dir().ok());
|
||||
|
||||
let boot = Self {
|
||||
cwd: cwd.unwrap_or("/".into()),
|
||||
state_dir: Xdg::state_dir().unwrap(),
|
||||
|
||||
cwd_file: args.cwd_file,
|
||||
chooser_file: args.chooser_file,
|
||||
};
|
||||
|
||||
if !boot.state_dir.is_dir() {
|
||||
fs::create_dir_all(&boot.state_dir).unwrap();
|
||||
}
|
||||
if !PREVIEW.cache_dir.is_dir() {
|
||||
fs::create_dir(&PREVIEW.cache_dir).unwrap();
|
||||
}
|
||||
|
||||
if args.clear_cache {
|
||||
if PREVIEW.cache_dir == Xdg::cache_dir() {
|
||||
println!("Clearing cache directory: \n{:?}", PREVIEW.cache_dir);
|
||||
fs::remove_dir_all(&PREVIEW.cache_dir).unwrap();
|
||||
} else {
|
||||
println!(
|
||||
"You've changed the default cache directory, for your data's safety, please clear it manually: \n{:?}",
|
||||
PREVIEW.cache_dir
|
||||
);
|
||||
}
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
boot
|
||||
}
|
||||
}
|
||||
22
config/src/boot/cli.rs
Normal file
22
config/src/boot/cli.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::{command, Parser};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "yazi", version)]
|
||||
pub(super) struct Args {
|
||||
/// Set the current working directory
|
||||
#[arg(index = 1)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
|
||||
/// Write the cwd on exit to this file
|
||||
#[arg(long)]
|
||||
pub cwd_file: Option<PathBuf>,
|
||||
/// Write the selected files on open emitted by the chooser mode
|
||||
#[arg(long)]
|
||||
pub chooser_file: Option<PathBuf>,
|
||||
|
||||
/// Clear the cache directory
|
||||
#[arg(long, action)]
|
||||
pub clear_cache: bool,
|
||||
}
|
||||
4
config/src/boot/mod.rs
Normal file
4
config/src/boot/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod boot;
|
||||
mod cli;
|
||||
|
||||
pub use boot::*;
|
||||
41
config/src/keymap/control.rs
Normal file
41
config/src/keymap/control.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{Exec, Key};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Control {
|
||||
pub on: Vec<Key>,
|
||||
#[serde(deserialize_with = "Exec::deserialize")]
|
||||
pub exec: Vec<Exec>,
|
||||
pub desc: Option<String>,
|
||||
}
|
||||
|
||||
impl Control {
|
||||
#[inline]
|
||||
pub fn to_call(&self) -> Vec<Exec> { self.exec.clone() }
|
||||
}
|
||||
|
||||
impl Control {
|
||||
#[inline]
|
||||
pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() }
|
||||
|
||||
#[inline]
|
||||
pub fn exec(&self) -> String {
|
||||
self.exec.iter().map(|e| e.to_string()).collect::<Vec<_>>().join("; ")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn desc_or_exec(&self) -> Cow<str> {
|
||||
if let Some(ref s) = self.desc { Cow::Borrowed(s) } else { self.exec().into() }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn contains(&self, s: &str) -> bool {
|
||||
let s = s.to_lowercase();
|
||||
self.desc.as_ref().map(|d| d.to_lowercase().contains(&s)) == Some(true)
|
||||
|| self.exec().to_lowercase().contains(&s)
|
||||
|| self.on().to_lowercase().contains(&s)
|
||||
}
|
||||
}
|
||||
111
config/src/keymap/exec.rs
Normal file
111
config/src/keymap/exec.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
use std::{collections::BTreeMap, fmt::{self, Debug, Display}};
|
||||
|
||||
use anyhow::bail;
|
||||
use serde::{de::{self, Visitor}, Deserializer};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Exec {
|
||||
pub cmd: String,
|
||||
pub args: Vec<String>,
|
||||
pub named: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Exec {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
let s = shell_words::split(s)?;
|
||||
if s.is_empty() {
|
||||
bail!("`exec` cannot be empty");
|
||||
}
|
||||
|
||||
let mut exec = Self { cmd: s[0].clone(), args: Vec::new(), named: BTreeMap::new() };
|
||||
for arg in s.into_iter().skip(1) {
|
||||
if arg.starts_with("--") {
|
||||
let mut arg = arg.splitn(2, '=');
|
||||
let key = arg.next().unwrap().trim_start_matches('-');
|
||||
let val = arg.next().unwrap_or("").to_string();
|
||||
exec.named.insert(key.to_string(), val);
|
||||
} else {
|
||||
exec.args.push(arg);
|
||||
}
|
||||
}
|
||||
Ok(exec)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Exec {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.cmd)?;
|
||||
if !self.args.is_empty() {
|
||||
write!(f, " {}", self.args.join(" "))?;
|
||||
}
|
||||
for (k, v) in &self.named {
|
||||
write!(f, " --{k}")?;
|
||||
if !v.is_empty() {
|
||||
write!(f, "={v}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Exec {
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Exec>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ExecVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ExecVisitor {
|
||||
type Value = Vec<Exec>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a exec string, e.g. tab_switch 0")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
{
|
||||
let mut execs = Vec::new();
|
||||
while let Some(value) = &seq.next_element::<String>()? {
|
||||
execs.push(Exec::try_from(value.as_str()).map_err(de::Error::custom)?);
|
||||
}
|
||||
Ok(execs)
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(vec![Exec::try_from(value).map_err(de::Error::custom)?])
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ExecVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Exec {
|
||||
#[inline]
|
||||
pub fn call(cwd: &str, args: Vec<String>) -> Self {
|
||||
Exec { cmd: cwd.to_owned(), args, named: Default::default() }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn call_named(cwd: &str, named: BTreeMap<String, String>) -> Self {
|
||||
Exec { cmd: cwd.to_owned(), args: Default::default(), named }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn vec(self) -> Vec<Self> { vec![self] }
|
||||
|
||||
#[inline]
|
||||
pub fn with_bool(mut self, name: &str, state: bool) -> Self {
|
||||
if state {
|
||||
self.named.insert(name.to_string(), "".to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
184
config/src/keymap/key.rs
Normal file
184
config/src/keymap/key.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
use anyhow::bail;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(try_from = "String")]
|
||||
pub struct Key {
|
||||
pub code: KeyCode,
|
||||
pub shift: bool,
|
||||
pub ctrl: bool,
|
||||
pub alt: bool,
|
||||
}
|
||||
|
||||
impl Key {
|
||||
#[inline]
|
||||
pub fn plain(&self) -> Option<char> {
|
||||
match self.code {
|
||||
KeyCode::Char(c) if !self.ctrl && !self.alt => 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 } }
|
||||
}
|
||||
|
||||
impl From<KeyEvent> for Key {
|
||||
fn from(value: KeyEvent) -> Self {
|
||||
// For alphabet:
|
||||
// Unix : <S-a> => Char("A") + SHIFT
|
||||
// Windows : <S-a> => Char("A") + SHIFT
|
||||
//
|
||||
// For non-alphabet:
|
||||
// Unix : <S-`> => Char("~") + NULL
|
||||
// Windows : <S-`> => Char("~") + SHIFT
|
||||
//
|
||||
// So we detect `Char("~") + SHIFT`, and change it to `Char("~") + NULL`
|
||||
// for consistent behavior between OSs.
|
||||
|
||||
let shift = match (value.code, value.modifiers) {
|
||||
(KeyCode::Char(c), _) => c.is_ascii_uppercase(),
|
||||
(KeyCode::BackTab, _) => false,
|
||||
(_, m) => m.contains(KeyModifiers::SHIFT),
|
||||
};
|
||||
|
||||
Self {
|
||||
code: value.code,
|
||||
shift,
|
||||
ctrl: value.modifiers.contains(KeyModifiers::CONTROL),
|
||||
alt: value.modifiers.contains(KeyModifiers::ALT),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Key {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
if s.is_empty() {
|
||||
bail!("empty key")
|
||||
}
|
||||
|
||||
let mut key = Self::default();
|
||||
if !s.starts_with('<') || !s.ends_with('>') {
|
||||
let c = s.chars().next().unwrap();
|
||||
key.code = KeyCode::Char(c);
|
||||
key.shift = c.is_ascii_uppercase();
|
||||
return Ok(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,
|
||||
|
||||
"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}"),
|
||||
}
|
||||
}
|
||||
|
||||
if key.code == KeyCode::Null {
|
||||
bail!("empty key")
|
||||
}
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Key {
|
||||
fn to_string(&self) -> String {
|
||||
if let Some(c) = self.plain() {
|
||||
let c = if self.shift { c.to_ascii_uppercase() } else { c };
|
||||
return if c == ' ' { "<Space>".to_string() } else { c.to_string() };
|
||||
}
|
||||
|
||||
let mut s = "<".to_string();
|
||||
if self.ctrl {
|
||||
s += "C-";
|
||||
}
|
||||
if self.alt {
|
||||
s += "A-";
|
||||
}
|
||||
if self.shift && !matches!(self.code, KeyCode::Char(_)) {
|
||||
s += "S-";
|
||||
}
|
||||
|
||||
let code = match self.code {
|
||||
KeyCode::Backspace => "Backspace",
|
||||
KeyCode::Enter => "Enter",
|
||||
KeyCode::Left => "Left",
|
||||
KeyCode::Right => "Right",
|
||||
KeyCode::Up => "Up",
|
||||
KeyCode::Down => "Down",
|
||||
KeyCode::Home => "Home",
|
||||
KeyCode::End => "End",
|
||||
KeyCode::PageUp => "PageUp",
|
||||
KeyCode::PageDown => "PageDown",
|
||||
KeyCode::Tab => "Tab",
|
||||
KeyCode::BackTab => "BackTab",
|
||||
KeyCode::Delete => "Delete",
|
||||
KeyCode::Insert => "Insert",
|
||||
KeyCode::F(1) => "F1",
|
||||
KeyCode::F(2) => "F2",
|
||||
KeyCode::F(3) => "F3",
|
||||
KeyCode::F(4) => "F4",
|
||||
KeyCode::F(5) => "F5",
|
||||
KeyCode::F(6) => "F6",
|
||||
KeyCode::F(7) => "F7",
|
||||
KeyCode::F(8) => "F8",
|
||||
KeyCode::F(9) => "F9",
|
||||
KeyCode::F(10) => "F10",
|
||||
KeyCode::F(11) => "F11",
|
||||
KeyCode::F(12) => "F12",
|
||||
KeyCode::Esc => "Esc",
|
||||
|
||||
KeyCode::Char(' ') => "Space",
|
||||
KeyCode::Char(c) => {
|
||||
s.push(if self.shift { c.to_ascii_uppercase() } else { c });
|
||||
""
|
||||
}
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
s + code + ">"
|
||||
}
|
||||
}
|
||||
86
config/src/keymap/keymap.rs
Normal file
86
config/src/keymap/keymap.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use std::fmt::{self, Display};
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::Control;
|
||||
use crate::MERGED_KEYMAP;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Keymap {
|
||||
pub manager: Vec<Control>,
|
||||
pub tasks: Vec<Control>,
|
||||
pub select: Vec<Control>,
|
||||
pub input: Vec<Control>,
|
||||
pub help: Vec<Control>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Keymap {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Shadow {
|
||||
manager: Inner,
|
||||
tasks: Inner,
|
||||
select: Inner,
|
||||
input: Inner,
|
||||
help: Inner,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Inner {
|
||||
keymap: Vec<Control>,
|
||||
}
|
||||
|
||||
let shadow = Shadow::deserialize(deserializer)?;
|
||||
Ok(Self {
|
||||
manager: shadow.manager.keymap,
|
||||
tasks: shadow.tasks.keymap,
|
||||
select: shadow.select.keymap,
|
||||
input: shadow.input.keymap,
|
||||
help: shadow.help.keymap,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Keymap {
|
||||
fn default() -> Self { toml::from_str(&MERGED_KEYMAP).unwrap() }
|
||||
}
|
||||
|
||||
impl Keymap {
|
||||
#[inline]
|
||||
pub fn get(&self, layer: KeymapLayer) -> &Vec<Control> {
|
||||
match layer {
|
||||
KeymapLayer::Manager => &self.manager,
|
||||
KeymapLayer::Tasks => &self.tasks,
|
||||
KeymapLayer::Select => &self.select,
|
||||
KeymapLayer::Input => &self.input,
|
||||
KeymapLayer::Help => &self.help,
|
||||
KeymapLayer::Which => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum KeymapLayer {
|
||||
#[default]
|
||||
Manager,
|
||||
Tasks,
|
||||
Select,
|
||||
Input,
|
||||
Help,
|
||||
Which,
|
||||
}
|
||||
|
||||
impl Display for KeymapLayer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
KeymapLayer::Manager => write!(f, "manager"),
|
||||
KeymapLayer::Tasks => write!(f, "tasks"),
|
||||
KeymapLayer::Select => write!(f, "select"),
|
||||
KeymapLayer::Input => write!(f, "input"),
|
||||
KeymapLayer::Help => write!(f, "help"),
|
||||
KeymapLayer::Which => write!(f, "which"),
|
||||
}
|
||||
}
|
||||
}
|
||||
9
config/src/keymap/mod.rs
Normal file
9
config/src/keymap/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
mod control;
|
||||
mod exec;
|
||||
mod key;
|
||||
mod keymap;
|
||||
|
||||
pub use control::*;
|
||||
pub use exec::*;
|
||||
pub use key::*;
|
||||
pub use keymap::*;
|
||||
53
config/src/lib.rs
Normal file
53
config/src/lib.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#![allow(clippy::module_inception)]
|
||||
|
||||
use shared::RoCell;
|
||||
|
||||
mod boot;
|
||||
pub mod keymap;
|
||||
mod log;
|
||||
pub mod manager;
|
||||
pub mod open;
|
||||
mod pattern;
|
||||
pub mod plugins;
|
||||
mod preset;
|
||||
pub mod preview;
|
||||
mod tasks;
|
||||
pub mod theme;
|
||||
mod validation;
|
||||
mod xdg;
|
||||
|
||||
pub(crate) use pattern::*;
|
||||
pub(crate) use preset::*;
|
||||
pub(crate) use xdg::*;
|
||||
|
||||
static MERGED_KEYMAP: RoCell<String> = RoCell::new();
|
||||
static MERGED_THEME: RoCell<String> = RoCell::new();
|
||||
static MERGED_YAZI: RoCell<String> = RoCell::new();
|
||||
|
||||
pub static KEYMAP: RoCell<keymap::Keymap> = RoCell::new();
|
||||
pub static LOG: RoCell<log::Log> = RoCell::new();
|
||||
pub static MANAGER: RoCell<manager::Manager> = RoCell::new();
|
||||
pub static OPEN: RoCell<open::Open> = RoCell::new();
|
||||
pub static PLUGINS: RoCell<plugins::Plugins> = RoCell::new();
|
||||
pub static PREVIEW: RoCell<preview::Preview> = RoCell::new();
|
||||
pub static TASKS: RoCell<tasks::Tasks> = RoCell::new();
|
||||
pub static THEME: RoCell<theme::Theme> = RoCell::new();
|
||||
|
||||
pub static BOOT: RoCell<boot::Boot> = RoCell::new();
|
||||
|
||||
pub fn init() {
|
||||
MERGED_KEYMAP.with(Preset::keymap);
|
||||
MERGED_THEME.with(Preset::theme);
|
||||
MERGED_YAZI.with(Preset::yazi);
|
||||
|
||||
KEYMAP.with(Default::default);
|
||||
LOG.with(Default::default);
|
||||
MANAGER.with(Default::default);
|
||||
OPEN.with(Default::default);
|
||||
PLUGINS.with(Default::default);
|
||||
PREVIEW.with(Default::default);
|
||||
TASKS.with(Default::default);
|
||||
THEME.with(Default::default);
|
||||
|
||||
BOOT.with(Default::default);
|
||||
}
|
||||
32
config/src/log/log.rs
Normal file
32
config/src/log/log.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use crate::MERGED_YAZI;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Log {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for Log {
|
||||
fn default() -> Self { toml::from_str(&MERGED_YAZI).unwrap() }
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Log {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Outer {
|
||||
log: Shadow,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Shadow {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
let outer = Outer::deserialize(deserializer)?;
|
||||
|
||||
Ok(Self { enabled: outer.log.enabled })
|
||||
}
|
||||
}
|
||||
3
config/src/log/mod.rs
Normal file
3
config/src/log/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod log;
|
||||
|
||||
pub use log::*;
|
||||
71
config/src/manager/layout.rs
Normal file
71
config/src/manager/layout.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
use anyhow::bail;
|
||||
use crossterm::terminal::WindowSize;
|
||||
use ratatui::{prelude::Rect, widgets::{Block, Padding}};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared::Term;
|
||||
|
||||
use crate::THEME;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(try_from = "Vec<u32>")]
|
||||
pub struct ManagerLayout {
|
||||
pub parent: u32,
|
||||
pub current: u32,
|
||||
pub preview: u32,
|
||||
pub all: u32,
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u32>> for ManagerLayout {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(ratio: Vec<u32>) -> Result<Self, Self::Error> {
|
||||
if ratio.len() != 3 {
|
||||
bail!("invalid layout ratio: {:?}", ratio);
|
||||
}
|
||||
if ratio.iter().all(|&r| r == 0) {
|
||||
bail!("at least one layout ratio must be non-zero: {:?}", ratio);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
parent: ratio[0],
|
||||
current: ratio[1],
|
||||
preview: ratio[2],
|
||||
all: ratio[0] + ratio[1] + ratio[2],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagerLayout {
|
||||
pub fn preview_rect(&self) -> Rect {
|
||||
let WindowSize { columns, rows, .. } = Term::size();
|
||||
|
||||
let width = (columns as u32 * self.preview) as f64 / self.all as f64;
|
||||
let width = if width.fract() > 0.5 { width.ceil() as u16 } else { width.floor() as u16 };
|
||||
|
||||
let offset = THEME.manager.preview_offset;
|
||||
Block::default().padding(Padding::new(offset.3, offset.1, offset.0, offset.2)).inner(Rect {
|
||||
x: columns.saturating_sub(width),
|
||||
y: 0,
|
||||
width,
|
||||
height: rows,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn preview_height(&self) -> usize { self.preview_rect().height as usize }
|
||||
|
||||
pub fn folder_rect(&self) -> Rect {
|
||||
let WindowSize { columns, rows, .. } = Term::size();
|
||||
|
||||
let offset = THEME.manager.folder_offset;
|
||||
Block::default().padding(Padding::new(offset.3, offset.1, offset.0, offset.2)).inner(Rect {
|
||||
x: (columns as u32 * self.parent / self.all) as u16,
|
||||
y: 0,
|
||||
width: (columns as u32 * self.current / self.all) as u16,
|
||||
height: rows,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn folder_height(&self) -> usize { self.folder_rect().height as usize }
|
||||
}
|
||||
36
config/src/manager/manager.rs
Normal file
36
config/src/manager/manager.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
use super::{ManagerLayout, SortBy};
|
||||
use crate::{validation::check_validation, MERGED_YAZI};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Validate)]
|
||||
pub struct Manager {
|
||||
pub layout: ManagerLayout,
|
||||
|
||||
// Sorting
|
||||
pub sort_by: SortBy,
|
||||
pub sort_sensitive: bool,
|
||||
pub sort_reverse: bool,
|
||||
pub sort_dir_first: bool,
|
||||
|
||||
// Display
|
||||
#[validate(length(min = 1, max = 20, message = "must be between 1 and 20 characters"))]
|
||||
pub linemode: String,
|
||||
pub show_hidden: bool,
|
||||
pub show_symlink: bool,
|
||||
}
|
||||
|
||||
impl Default for Manager {
|
||||
fn default() -> Self {
|
||||
#[derive(Deserialize)]
|
||||
struct Outer {
|
||||
manager: Manager,
|
||||
}
|
||||
|
||||
let manager = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().manager;
|
||||
|
||||
check_validation(manager.validate());
|
||||
manager
|
||||
}
|
||||
}
|
||||
7
config/src/manager/mod.rs
Normal file
7
config/src/manager/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
mod layout;
|
||||
mod manager;
|
||||
mod sorting;
|
||||
|
||||
pub use layout::*;
|
||||
pub use manager::*;
|
||||
pub use sorting::*;
|
||||
52
config/src/manager/sorting.rs
Normal file
52
config/src/manager/sorting.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use anyhow::bail;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(try_from = "String")]
|
||||
pub enum SortBy {
|
||||
#[default]
|
||||
None,
|
||||
Alphabetical,
|
||||
Created,
|
||||
Modified,
|
||||
Natural,
|
||||
Size,
|
||||
}
|
||||
|
||||
impl FromStr for SortBy {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"none" => Self::None,
|
||||
"alphabetical" => Self::Alphabetical,
|
||||
"created" => Self::Created,
|
||||
"modified" => Self::Modified,
|
||||
"natural" => Self::Natural,
|
||||
"size" => Self::Size,
|
||||
_ => bail!("invalid sort_by value: {s}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for SortBy {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(&s) }
|
||||
}
|
||||
|
||||
impl ToString for SortBy {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::Alphabetical => "alphabetical",
|
||||
Self::Created => "created",
|
||||
Self::Modified => "modified",
|
||||
Self::Natural => "natural",
|
||||
Self::Size => "size",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
7
config/src/open/mod.rs
Normal file
7
config/src/open/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
mod open;
|
||||
mod opener;
|
||||
mod rule;
|
||||
|
||||
pub use open::*;
|
||||
pub use opener::*;
|
||||
use rule::*;
|
||||
84
config/src/open/open.rs
Normal file
84
config/src/open/open.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use std::{collections::BTreeMap, path::Path};
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use shared::MIME_DIR;
|
||||
|
||||
use super::Opener;
|
||||
use crate::{open::OpenRule, MERGED_YAZI};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Open {
|
||||
rules: Vec<OpenRule>,
|
||||
openers: BTreeMap<String, IndexSet<Opener>>,
|
||||
}
|
||||
|
||||
impl Default for Open {
|
||||
fn default() -> Self { toml::from_str(&MERGED_YAZI).unwrap() }
|
||||
}
|
||||
|
||||
impl Open {
|
||||
pub fn openers<P, M>(&self, path: P, mime: M) -> Option<IndexSet<&Opener>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
M: AsRef<str>,
|
||||
{
|
||||
let is_folder = Some(mime.as_ref() == MIME_DIR);
|
||||
self.rules.iter().find_map(|rule| {
|
||||
if rule.mime.as_ref().is_some_and(|m| m.matches(&mime))
|
||||
|| rule.name.as_ref().is_some_and(|n| n.match_path(&path, is_folder))
|
||||
{
|
||||
let openers = rule
|
||||
.use_
|
||||
.iter()
|
||||
.filter_map(|use_| self.openers.get(use_))
|
||||
.flatten()
|
||||
.collect::<IndexSet<_>>();
|
||||
|
||||
if openers.is_empty() { None } else { Some(openers) }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn block_opener<P, M>(&self, path: P, mime: M) -> Option<&Opener>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
M: AsRef<str>,
|
||||
{
|
||||
self.openers(path, mime).and_then(|o| o.into_iter().find(|o| o.block))
|
||||
}
|
||||
|
||||
pub fn common_openers(&self, targets: &[(impl AsRef<Path>, impl AsRef<str>)]) -> Vec<&Opener> {
|
||||
let grouped: Vec<_> = targets.iter().filter_map(|(p, m)| self.openers(p, m)).collect();
|
||||
let flat: IndexSet<_> = grouped.iter().flatten().copied().collect();
|
||||
flat.into_iter().filter(|&o| grouped.iter().all(|g| g.contains(o))).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Open {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Outer {
|
||||
opener: BTreeMap<String, Vec<Opener>>,
|
||||
open: OuterOpen,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct OuterOpen {
|
||||
rules: Vec<OpenRule>,
|
||||
}
|
||||
|
||||
let outer = Outer::deserialize(deserializer)?;
|
||||
let openers = outer
|
||||
.opener
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.into_iter().filter_map(|o| o.take()).collect::<IndexSet<_>>()))
|
||||
.collect();
|
||||
Ok(Self { rules: outer.open.rules, openers })
|
||||
}
|
||||
}
|
||||
80
config/src/open/opener.rs
Normal file
80
config/src/open/opener.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Opener {
|
||||
pub exec: String,
|
||||
pub block: bool,
|
||||
pub orphan: bool,
|
||||
pub desc: String,
|
||||
pub for_: Option<String>,
|
||||
pub spread: bool,
|
||||
}
|
||||
|
||||
impl Opener {
|
||||
pub fn take(mut self) -> Option<Self> {
|
||||
if let Some(for_) = self.for_.take() {
|
||||
match for_.as_bytes() {
|
||||
b"unix" if cfg!(unix) => {}
|
||||
b"windows" if cfg!(windows) => {}
|
||||
b"linux" if cfg!(target_os = "linux") => {}
|
||||
b"macos" if cfg!(target_os = "macos") => {}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Opener {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
pub struct Shadow {
|
||||
exec: String,
|
||||
#[serde(default)]
|
||||
block: bool,
|
||||
#[serde(default)]
|
||||
orphan: bool,
|
||||
desc: Option<String>,
|
||||
#[serde(rename = "for")]
|
||||
for_: Option<String>,
|
||||
|
||||
// TODO: remove this when v1.0.5 is released --
|
||||
display_name: Option<String>,
|
||||
// TODO: -- remove this when v1.0.5 is released
|
||||
}
|
||||
|
||||
let mut shadow = Shadow::deserialize(deserializer)?;
|
||||
if shadow.exec.is_empty() {
|
||||
return Err(serde::de::Error::custom("`exec` cannot be empty"));
|
||||
}
|
||||
|
||||
// TODO: remove this when v1.0.5 is released --
|
||||
if shadow.display_name.is_some() {
|
||||
println!(
|
||||
"WARNING: `display_name` is deprecated and will be removed in Yazi v1.0.6. Use `desc` instead.\ne.g. {}\n\n",
|
||||
r#"{ exec = 'nvim "$@"', display_name = "Edit" } ==> { exec = 'nvim "$@"', desc = "Edit" }"#
|
||||
);
|
||||
}
|
||||
if shadow.display_name.is_some() && shadow.desc.is_none() {
|
||||
shadow.desc = shadow.display_name.clone();
|
||||
}
|
||||
// TODO: -- remove this when v1.0.5 is released
|
||||
|
||||
let desc =
|
||||
shadow.desc.unwrap_or_else(|| shadow.exec.split_whitespace().next().unwrap().to_string());
|
||||
|
||||
let spread =
|
||||
shadow.exec.contains("$@") || shadow.exec.contains("%*") || shadow.exec.contains("$*");
|
||||
Ok(Self {
|
||||
exec: shadow.exec,
|
||||
block: shadow.block,
|
||||
orphan: shadow.orphan,
|
||||
desc,
|
||||
for_: shadow.for_,
|
||||
spread,
|
||||
})
|
||||
}
|
||||
}
|
||||
58
config/src/open/rule.rs
Normal file
58
config/src/open/rule.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use std::fmt;
|
||||
|
||||
use serde::{de::{self, Visitor}, Deserialize, Deserializer};
|
||||
|
||||
use crate::pattern::Pattern;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct OpenRule {
|
||||
pub(super) name: Option<Pattern>,
|
||||
pub(super) mime: Option<Pattern>,
|
||||
#[serde(rename = "use")]
|
||||
#[serde(deserialize_with = "OpenRule::deserialize")]
|
||||
pub(super) use_: Vec<String>,
|
||||
}
|
||||
|
||||
impl OpenRule {
|
||||
fn deserialize<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct UseVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for UseVisitor {
|
||||
type Value = Vec<String>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string, or array of strings")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
{
|
||||
let mut uses = Vec::new();
|
||||
while let Some(use_) = seq.next_element::<String>()? {
|
||||
uses.push(use_);
|
||||
}
|
||||
Ok(uses)
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(vec![value.to_owned()])
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(vec![v])
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(UseVisitor)
|
||||
}
|
||||
}
|
||||
56
config/src/pattern.rs
Normal file
56
config/src/pattern.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use std::path::Path;
|
||||
|
||||
use glob::MatchOptions;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(try_from = "String")]
|
||||
pub struct Pattern {
|
||||
inner: glob::Pattern,
|
||||
sensitive: bool,
|
||||
is_folder: bool,
|
||||
full_path: bool,
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
#[inline]
|
||||
pub fn matches(&self, str: impl AsRef<str>) -> bool {
|
||||
self.inner.matches_with(str.as_ref(), MatchOptions {
|
||||
case_sensitive: self.sensitive,
|
||||
require_literal_separator: self.full_path,
|
||||
require_literal_leading_dot: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn match_path(&self, path: impl AsRef<Path>, is_folder: Option<bool>) -> bool {
|
||||
let path = path.as_ref();
|
||||
let s = if self.full_path {
|
||||
path.to_str()
|
||||
} else {
|
||||
path.file_name().and_then(|n| n.to_str()).or_else(|| path.to_str())
|
||||
};
|
||||
is_folder.map_or(true, |f| f == self.is_folder) && s.is_some_and(|s| self.matches(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Pattern {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
let a = s.trim_start_matches("\\s");
|
||||
let b = a.trim_end_matches('/');
|
||||
Ok(Self {
|
||||
inner: glob::Pattern::new(b)?,
|
||||
sensitive: a.len() < s.len(),
|
||||
is_folder: b.len() < a.len(),
|
||||
full_path: b.contains('/'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Pattern {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> { Self::try_from(s.as_str()) }
|
||||
}
|
||||
3
config/src/plugins/mod.rs
Normal file
3
config/src/plugins/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod plugins;
|
||||
|
||||
pub use plugins::*;
|
||||
29
config/src/plugins/plugins.rs
Normal file
29
config/src/plugins/plugins.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
use shared::expand_path;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::MERGED_YAZI;
|
||||
|
||||
#[derive(Debug, Deserialize, Validate)]
|
||||
pub struct Plugins {
|
||||
pub preload: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for Plugins {
|
||||
fn default() -> Self {
|
||||
#[derive(Deserialize)]
|
||||
struct Outer {
|
||||
plugins: Plugins,
|
||||
}
|
||||
|
||||
let mut plugins = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().plugins;
|
||||
|
||||
plugins.preload.iter_mut().for_each(|p| {
|
||||
*p = expand_path(&p);
|
||||
});
|
||||
|
||||
plugins
|
||||
}
|
||||
}
|
||||
54
config/src/preset.rs
Normal file
54
config/src/preset.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use std::fs;
|
||||
|
||||
use toml::Table;
|
||||
|
||||
use crate::xdg::Xdg;
|
||||
|
||||
pub(crate) struct Preset;
|
||||
|
||||
impl Preset {
|
||||
fn merge(a: &mut Table, b: &Table, max: u8) {
|
||||
for (k, v) in b {
|
||||
let Some(a) = a.get_mut(k) else {
|
||||
a.insert(k.clone(), v.clone());
|
||||
continue;
|
||||
};
|
||||
|
||||
if k == "icons" || max <= 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(a) = a.as_table_mut() {
|
||||
if let Some(b) = v.as_table() {
|
||||
Self::merge(a, b, max - 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
*a = v.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_str(user: &str, base: &str) -> String {
|
||||
let path = Xdg::config_dir().unwrap().join(user);
|
||||
let mut user = fs::read_to_string(path).unwrap_or_default().parse::<Table>().unwrap();
|
||||
|
||||
let base = base.parse::<Table>().unwrap();
|
||||
Self::merge(&mut user, &base, 2);
|
||||
user.to_string()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn keymap() -> String {
|
||||
Self::merge_str("keymap.toml", include_str!("../preset/keymap.toml"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn theme() -> String {
|
||||
Self::merge_str("theme.toml", include_str!("../preset/theme.toml"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn yazi() -> String {
|
||||
Self::merge_str("yazi.toml", include_str!("../preset/yazi.toml"))
|
||||
}
|
||||
}
|
||||
3
config/src/preview/mod.rs
Normal file
3
config/src/preview/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod preview;
|
||||
|
||||
pub use preview::*;
|
||||
61
config/src/preview/preview.rs
Normal file
61
config/src/preview/preview.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
use std::{path::{Path, PathBuf}, time::{self, SystemTime}};
|
||||
|
||||
use md5::{Digest, Md5};
|
||||
use serde::Deserialize;
|
||||
use shared::expand_path;
|
||||
|
||||
use crate::{xdg::Xdg, MERGED_YAZI};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Preview {
|
||||
pub tab_size: u32,
|
||||
pub max_width: u32,
|
||||
pub max_height: u32,
|
||||
|
||||
pub cache_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for Preview {
|
||||
fn default() -> Self {
|
||||
#[derive(Deserialize)]
|
||||
struct Outer {
|
||||
preview: Shadow,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Shadow {
|
||||
tab_size: u32,
|
||||
max_width: u32,
|
||||
max_height: u32,
|
||||
|
||||
cache_dir: Option<String>,
|
||||
}
|
||||
|
||||
let preview = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().preview;
|
||||
|
||||
let cache_dir =
|
||||
preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path);
|
||||
|
||||
Preview {
|
||||
tab_size: preview.tab_size,
|
||||
max_width: preview.max_width,
|
||||
max_height: preview.max_height,
|
||||
|
||||
cache_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Preview {
|
||||
#[inline]
|
||||
pub fn cache(&self, path: &Path, skip: usize) -> PathBuf {
|
||||
self
|
||||
.cache_dir
|
||||
.join(format!("{:x}", Md5::new_with_prefix(format!("{:?}///{}", path, skip)).finalize()))
|
||||
}
|
||||
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
3
config/src/tasks/mod.rs
Normal file
3
config/src/tasks/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod tasks;
|
||||
|
||||
pub use tasks::*;
|
||||
28
config/src/tasks/tasks.rs
Normal file
28
config/src/tasks/tasks.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use serde::Deserialize;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{validation::check_validation, MERGED_YAZI};
|
||||
|
||||
#[derive(Debug, Deserialize, Validate)]
|
||||
pub struct Tasks {
|
||||
#[validate(range(min = 3, message = "Cannot be less than 3"))]
|
||||
pub micro_workers: u8,
|
||||
#[validate(range(min = 5, message = "Cannot be less than 5"))]
|
||||
pub macro_workers: u8,
|
||||
#[validate(range(min = 3, message = "Cannot be less than 3"))]
|
||||
pub bizarre_retry: u8,
|
||||
}
|
||||
|
||||
impl Default for Tasks {
|
||||
fn default() -> Self {
|
||||
#[derive(Deserialize)]
|
||||
struct Outer {
|
||||
tasks: Tasks,
|
||||
}
|
||||
|
||||
let tasks = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().tasks;
|
||||
check_validation(tasks.validate());
|
||||
|
||||
tasks
|
||||
}
|
||||
}
|
||||
35
config/src/theme/color.rs
Normal file
35
config/src/theme/color.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Deserialize)]
|
||||
#[serde(try_from = "String")]
|
||||
pub struct Color(ratatui::style::Color);
|
||||
|
||||
impl FromStr for Color {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
ratatui::style::Color::from_str(s).map(Self).map_err(|_| anyhow::anyhow!("invalid color"))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Color {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(&s) }
|
||||
}
|
||||
|
||||
impl From<Color> for ratatui::style::Color {
|
||||
fn from(value: Color) -> Self { value.0 }
|
||||
}
|
||||
|
||||
impl Serialize for Color {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.0.to_string().serialize(serializer)
|
||||
}
|
||||
}
|
||||
87
config/src/theme/filetype.rs
Normal file
87
config/src/theme/filetype.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::{Color, Style, StyleShadow};
|
||||
use crate::Pattern;
|
||||
|
||||
pub struct Filetype {
|
||||
pub name: Option<Pattern>,
|
||||
pub mime: Option<Pattern>,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl Filetype {
|
||||
pub fn matches(&self, path: &Path, mime: Option<impl AsRef<str>>, is_dir: bool) -> bool {
|
||||
if self.name.as_ref().is_some_and(|e| e.match_path(path, Some(is_dir))) {
|
||||
return true;
|
||||
}
|
||||
if let Some(mime) = mime {
|
||||
return self.mime.as_ref().is_some_and(|m| m.matches(mime));
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Filetype {
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Filetype>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct FiletypeOuter {
|
||||
rules: Vec<FiletypeRule>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct FiletypeRule {
|
||||
name: Option<Pattern>,
|
||||
mime: Option<Pattern>,
|
||||
|
||||
fg: Option<Color>,
|
||||
bg: Option<Color>,
|
||||
#[serde(default)]
|
||||
bold: bool,
|
||||
#[serde(default)]
|
||||
dim: bool,
|
||||
#[serde(default)]
|
||||
italic: bool,
|
||||
#[serde(default)]
|
||||
underline: bool,
|
||||
#[serde(default)]
|
||||
blink: bool,
|
||||
#[serde(default)]
|
||||
blink_rapid: bool,
|
||||
#[serde(default)]
|
||||
reversed: bool,
|
||||
#[serde(default)]
|
||||
hidden: bool,
|
||||
#[serde(default)]
|
||||
crossed: bool,
|
||||
}
|
||||
|
||||
Ok(
|
||||
FiletypeOuter::deserialize(deserializer)?
|
||||
.rules
|
||||
.into_iter()
|
||||
.map(|r| Filetype {
|
||||
name: r.name,
|
||||
mime: r.mime,
|
||||
style: StyleShadow {
|
||||
fg: r.fg,
|
||||
bg: r.bg,
|
||||
bold: r.bold,
|
||||
dim: r.dim,
|
||||
italic: r.italic,
|
||||
underline: r.underline,
|
||||
blink: r.blink,
|
||||
blink_rapid: r.blink_rapid,
|
||||
reversed: r.reversed,
|
||||
hidden: r.hidden,
|
||||
crossed: r.crossed,
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
45
config/src/theme/icon.rs
Normal file
45
config/src/theme/icon.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use std::fmt;
|
||||
|
||||
use serde::{de::{self, Visitor}, Deserializer};
|
||||
|
||||
use crate::Pattern;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Icon {
|
||||
pub name: Pattern,
|
||||
pub display: String,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Icon>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct IconVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for IconVisitor {
|
||||
type Value = Vec<Icon>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a icon rule, e.g. \"*.md\" = \"\"")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::MapAccess<'de>,
|
||||
{
|
||||
let mut icons = Vec::new();
|
||||
while let Some((key, value)) = &map.next_entry::<String, String>()? {
|
||||
icons.push(Icon {
|
||||
name: Pattern::try_from(key.clone())
|
||||
.map_err(|e| de::Error::custom(e.to_string()))?,
|
||||
display: value.clone(),
|
||||
});
|
||||
}
|
||||
Ok(icons)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(IconVisitor)
|
||||
}
|
||||
}
|
||||
11
config/src/theme/mod.rs
Normal file
11
config/src/theme/mod.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
mod color;
|
||||
mod filetype;
|
||||
mod icon;
|
||||
mod style;
|
||||
mod theme;
|
||||
|
||||
pub use color::*;
|
||||
pub use filetype::*;
|
||||
pub use icon::*;
|
||||
pub use style::*;
|
||||
pub use theme::*;
|
||||
98
config/src/theme/style.rs
Normal file
98
config/src/theme/style.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use ratatui::style::Modifier;
|
||||
use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
|
||||
|
||||
use super::Color;
|
||||
|
||||
#[derive(Clone, Copy, Deserialize)]
|
||||
#[serde(from = "StyleShadow")]
|
||||
pub struct Style {
|
||||
pub fg: Option<Color>,
|
||||
pub bg: Option<Color>,
|
||||
pub modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Serialize for Style {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(3))?;
|
||||
map.serialize_entry("fg", &self.fg)?;
|
||||
map.serialize_entry("bg", &self.bg)?;
|
||||
map.serialize_entry("modifier", &self.modifier.bits())?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Style> for ratatui::style::Style {
|
||||
fn from(value: Style) -> Self {
|
||||
ratatui::style::Style {
|
||||
fg: value.fg.map(Into::into),
|
||||
bg: value.bg.map(Into::into),
|
||||
underline_color: None,
|
||||
add_modifier: value.modifier,
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct StyleShadow {
|
||||
#[serde(default)]
|
||||
pub(super) fg: Option<Color>,
|
||||
#[serde(default)]
|
||||
pub(super) bg: Option<Color>,
|
||||
#[serde(default)]
|
||||
pub(super) bold: bool,
|
||||
#[serde(default)]
|
||||
pub(super) dim: bool,
|
||||
#[serde(default)]
|
||||
pub(super) italic: bool,
|
||||
#[serde(default)]
|
||||
pub(super) underline: bool,
|
||||
#[serde(default)]
|
||||
pub(super) blink: bool,
|
||||
#[serde(default)]
|
||||
pub(super) blink_rapid: bool,
|
||||
#[serde(default)]
|
||||
pub(super) reversed: bool,
|
||||
#[serde(default)]
|
||||
pub(super) hidden: bool,
|
||||
#[serde(default)]
|
||||
pub(super) crossed: bool,
|
||||
}
|
||||
|
||||
impl From<StyleShadow> for Style {
|
||||
fn from(value: StyleShadow) -> Self {
|
||||
let mut modifier = Modifier::empty();
|
||||
if value.bold {
|
||||
modifier |= Modifier::BOLD;
|
||||
}
|
||||
if value.dim {
|
||||
modifier |= Modifier::DIM;
|
||||
}
|
||||
if value.italic {
|
||||
modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.underline {
|
||||
modifier |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.blink {
|
||||
modifier |= Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.blink_rapid {
|
||||
modifier |= Modifier::RAPID_BLINK;
|
||||
}
|
||||
if value.reversed {
|
||||
modifier |= Modifier::REVERSED;
|
||||
}
|
||||
if value.hidden {
|
||||
modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.crossed {
|
||||
modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
|
||||
Self { fg: value.fg, bg: value.bg, modifier }
|
||||
}
|
||||
}
|
||||
139
config/src/theme/theme.rs
Normal file
139
config/src/theme/theme.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared::expand_path;
|
||||
use validator::Validate;
|
||||
|
||||
use super::{Filetype, Icon, Style};
|
||||
use crate::{validation::check_validation, MERGED_THEME};
|
||||
|
||||
#[derive(Deserialize, Serialize, Validate)]
|
||||
pub struct Manager {
|
||||
cwd: Style,
|
||||
|
||||
// Hovered
|
||||
hovered: Style,
|
||||
preview_hovered: Style,
|
||||
|
||||
// Find
|
||||
find_keyword: Style,
|
||||
find_position: Style,
|
||||
|
||||
// Marker
|
||||
marker_selected: Style,
|
||||
marker_copied: Style,
|
||||
marker_cut: Style,
|
||||
|
||||
// Tab
|
||||
tab_active: Style,
|
||||
tab_inactive: Style,
|
||||
#[validate(range(min = 1, message = "Must be greater than 0"))]
|
||||
tab_width: u8,
|
||||
|
||||
// Border
|
||||
pub border_symbol: String,
|
||||
pub border_style: Style,
|
||||
|
||||
// Offset
|
||||
pub(crate) folder_offset: (u16, u16, u16, u16),
|
||||
pub(crate) preview_offset: (u16, u16, u16, u16),
|
||||
|
||||
// Highlighting
|
||||
pub syntect_theme: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Status {
|
||||
pub separator_open: String,
|
||||
pub separator_close: String,
|
||||
pub separator_style: Style,
|
||||
|
||||
// Mode
|
||||
pub mode_normal: Style,
|
||||
pub mode_select: Style,
|
||||
pub mode_unset: Style,
|
||||
|
||||
// Progress
|
||||
pub progress_label: Style,
|
||||
pub progress_normal: Style,
|
||||
pub progress_error: Style,
|
||||
|
||||
// Permissions
|
||||
pub permissions_t: Style,
|
||||
pub permissions_r: Style,
|
||||
pub permissions_w: Style,
|
||||
pub permissions_x: Style,
|
||||
pub permissions_s: Style,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Input {
|
||||
pub border: Style,
|
||||
pub title: Style,
|
||||
pub value: Style,
|
||||
pub selected: Style,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Select {
|
||||
pub border: Style,
|
||||
pub active: Style,
|
||||
pub inactive: Style,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Tasks {
|
||||
pub border: Style,
|
||||
pub title: Style,
|
||||
pub hovered: Style,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Which {
|
||||
pub mask: Style,
|
||||
pub cand: Style,
|
||||
pub rest: Style,
|
||||
pub desc: Style,
|
||||
|
||||
pub separator: String,
|
||||
pub separator_style: Style,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Help {
|
||||
pub on: Style,
|
||||
pub exec: Style,
|
||||
pub desc: Style,
|
||||
|
||||
pub hovered: Style,
|
||||
pub footer: Style,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Theme {
|
||||
pub manager: Manager,
|
||||
status: Status,
|
||||
pub input: Input,
|
||||
pub select: Select,
|
||||
pub tasks: Tasks,
|
||||
pub which: Which,
|
||||
pub help: Help,
|
||||
|
||||
// File-specific styles
|
||||
#[serde(rename = "filetype", deserialize_with = "Filetype::deserialize", skip_serializing)]
|
||||
pub filetypes: Vec<Filetype>,
|
||||
#[serde(deserialize_with = "Icon::deserialize", skip_serializing)]
|
||||
pub icons: Vec<Icon>,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
fn default() -> Self {
|
||||
let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap();
|
||||
|
||||
check_validation(theme.manager.validate());
|
||||
|
||||
theme.manager.syntect_theme = expand_path(&theme.manager.syntect_theme);
|
||||
|
||||
theme
|
||||
}
|
||||
}
|
||||
31
config/src/validation.rs
Normal file
31
config/src/validation.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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 {
|
||||
print!("Config `{field}[{i}]` format error: ");
|
||||
check_validation(Err(*errors));
|
||||
println!();
|
||||
}
|
||||
}
|
||||
ValidationErrorsKind::Field(error) => {
|
||||
for e in error {
|
||||
println!(
|
||||
"Config `{field}` format error: {}\n",
|
||||
e.message.unwrap_or(Cow::Borrowed("unknown error"))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
44
config/src/xdg.rs
Normal file
44
config/src/xdg.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use std::{env, path::PathBuf};
|
||||
|
||||
use shared::expand_path;
|
||||
|
||||
pub(super) struct Xdg;
|
||||
|
||||
impl Xdg {
|
||||
pub(super) fn config_dir() -> Option<PathBuf> {
|
||||
if let Some(s) = env::var_os("YAZI_CONFIG_HOME").filter(|s| !s.is_empty()) {
|
||||
return Some(expand_path(s));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
dirs::config_dir().map(|p| p.join("yazi").join("config"))
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|p| p.is_absolute())
|
||||
.or_else(|| dirs::home_dir().map(|h| h.join(".config")))
|
||||
.map(|p| p.join("yazi"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn state_dir() -> Option<PathBuf> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
dirs::data_dir().map(|p| p.join("yazi").join("state"))
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
env::var_os("XDG_STATE_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|p| p.is_absolute())
|
||||
.or_else(|| dirs::home_dir().map(|h| h.join(".local/state")))
|
||||
.map(|p| p.join("yazi"))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn cache_dir() -> PathBuf { env::temp_dir().join("yazi") }
|
||||
}
|
||||
35
core/Cargo.toml
Normal file
35
core/Cargo.toml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "core"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
adaptor = { path = "../adaptor" }
|
||||
config = { path = "../config" }
|
||||
shared = { path = "../shared" }
|
||||
|
||||
# External dependencies
|
||||
anyhow = "^1"
|
||||
async-channel = "^1"
|
||||
crossterm = "^0"
|
||||
futures = "^0"
|
||||
indexmap = "^2"
|
||||
libc = "^0"
|
||||
notify = { version = "^6", default-features = false, features = [ "macos_fsevent" ] }
|
||||
parking_lot = "^0"
|
||||
ratatui = "^0"
|
||||
regex = "^1"
|
||||
serde = "^1"
|
||||
serde_json = "^1"
|
||||
syntect = "^5"
|
||||
tokio = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "time", "fs", "process", "io-std", "io-util" ] }
|
||||
tokio-stream = "^0"
|
||||
trash = "^3"
|
||||
unicode-width = "^0"
|
||||
yazi-prebuild = "^0"
|
||||
|
||||
# Logging
|
||||
tracing = "^0"
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
clipboard-win = "^4"
|
||||
6
core/src/blocker.rs
Normal file
6
core/src/blocker.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use shared::RoCell;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
pub static BLOCKER: RoCell<Semaphore> = RoCell::new();
|
||||
|
||||
pub(super) fn init_blocker() { BLOCKER.init(Semaphore::new(1)) }
|
||||
92
core/src/context.rs
Normal file
92
core/src/context.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
use config::keymap::KeymapLayer;
|
||||
use crossterm::terminal::WindowSize;
|
||||
use ratatui::prelude::Rect;
|
||||
use shared::Term;
|
||||
|
||||
use crate::{help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position};
|
||||
|
||||
pub struct Ctx {
|
||||
pub manager: Manager,
|
||||
pub which: Which,
|
||||
pub help: Help,
|
||||
pub input: Input,
|
||||
pub select: Select,
|
||||
pub tasks: Tasks,
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
pub fn make() -> Self {
|
||||
Self {
|
||||
manager: Manager::make(),
|
||||
which: Default::default(),
|
||||
help: Default::default(),
|
||||
input: Default::default(),
|
||||
select: Default::default(),
|
||||
tasks: Tasks::start(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn area(&self, pos: &Position) -> Rect {
|
||||
let WindowSize { columns, rows, .. } = Term::size();
|
||||
|
||||
let (x, y) = match pos {
|
||||
Position::None => return Rect::default(),
|
||||
Position::Top(Rect { mut x, mut y, width, height }) => {
|
||||
x = x.min(columns.saturating_sub(*width));
|
||||
y = y.min(rows.saturating_sub(*height));
|
||||
((columns / 2).saturating_sub(width / 2) + x, y)
|
||||
}
|
||||
Position::Hovered(rect @ Rect { mut x, y, width, height }) => {
|
||||
let Some(r) =
|
||||
self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url))
|
||||
else {
|
||||
return self.area(&Position::Top(*rect));
|
||||
};
|
||||
|
||||
x = x.min(columns.saturating_sub(*width));
|
||||
if y + height + r.y + r.height > rows {
|
||||
(x + r.x, r.y.saturating_sub(height.saturating_sub(1)))
|
||||
} else {
|
||||
(x + r.x, y + r.y + r.height)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (w, h) = pos.dimension().unwrap();
|
||||
Rect { x, y, width: w.min(columns.saturating_sub(x)), height: h.min(rows.saturating_sub(y)) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cursor(&self) -> Option<(u16, u16)> {
|
||||
if self.input.visible {
|
||||
let Rect { x, y, .. } = self.area(&self.input.position);
|
||||
return Some((x + 1 + self.input.cursor(), y + 1));
|
||||
}
|
||||
if let Some((x, y)) = self.help.cursor() {
|
||||
return Some((x, y));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn layer(&self) -> KeymapLayer {
|
||||
if self.which.visible {
|
||||
KeymapLayer::Which
|
||||
} else if self.help.visible {
|
||||
KeymapLayer::Help
|
||||
} else if self.input.visible {
|
||||
KeymapLayer::Input
|
||||
} else if self.select.visible {
|
||||
KeymapLayer::Select
|
||||
} else if self.tasks.visible {
|
||||
KeymapLayer::Tasks
|
||||
} else {
|
||||
KeymapLayer::Manager
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn image_layer(&self) -> bool {
|
||||
!matches!(self.layer(), KeymapLayer::Which | KeymapLayer::Help | KeymapLayer::Tasks)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue