Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

1420 changed files with 7449 additions and 85944 deletions

View file

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

@ -1 +0,0 @@
use flake

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,36 +0,0 @@
name: Test
on:
push:
branches: [main]
pull_request:
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
- name: Setup Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.10
- name: Build
run: cargo build --verbose
- name: Test
run: cargo test --workspace --verbose

View file

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

View file

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

15
.gitignore vendored
View file

@ -1,14 +1 @@
target/
yazi-*/completions
node_modules/
.DS_Store
result
result-*
.direnv
.idea/
.vscode/
*.snap
/target

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

5118
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,97 +1,39 @@
[workspace]
resolver = "3"
members = [ "yazi-*" ]
default-members = [ "yazi-fm", "yazi-cli" ]
[package]
name = "yazi"
version = "0.1.0"
edition = "2021"
[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"
[dependencies]
ansi-to-tui = "^3"
anyhow = "^1"
async-channel = "^1"
base64 = "^0"
console-subscriber = "^0"
crossterm = { version = "^0", features = [ "event-stream" ] }
futures = "^0"
glob = "^0"
image = "^0"
indexmap = "^2"
libc = "^0"
md5 = "^0"
notify = { version = "^6", default-features = false, features = [ "macos_fsevent" ] }
once_cell = "^1"
parking_lot = "^0"
ratatui = "^0"
serde = { version = "^1", features = [ "derive" ] }
serde_json = "^1"
signal-hook-tokio = { version = "^0", features = [ "futures-v0_3" ] }
syntect = "^5"
tokio = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "fs", "process", "io-std", "io-util" ] }
toml = { version = "^0", features = [ "preserve_order" ] }
tracing = "^0"
tracing-appender = "^0"
tracing-subscriber = "^0"
trash = "^3"
unicode-width = "^0"
xdg = "^2"
[profile.dev]
debug = "line-tables-only"
[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"
# [profile.release]
# strip = true
# lto = true
# panic = "abort"

View file

View file

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

113
README.md
View file

@ -1,94 +1,55 @@
<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 configurable file management experience.
💡 A new article explaining its internal workings: [Why is Yazi Fast?](https://yazi-rs.github.io/blog/why-is-yazi-fast)
https://github.com/sxyazi/yazi/assets/17523360/740a41f4-3d24-4287-952c-3aec51520a32
- 🚀 **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
- ... and more!
⚠️ Note: Yazi is currently in active development and may be unstable. The API is subject to change without prior notice.
https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7
## Installation
## Project status
Before getting started, ensure that the following dependencies are installed on your system:
Public beta, can be used as a daily driver.
- nerd-fonts (required, for icons)
- jq (optional, for JSON preview)
- unar (optional, for archive preview)
- ffmpegthumbnailer (optional, for video thumbnails)
- fd (optional, for file searching)
- rg (optional, for file content searching)
- fzf (optional, for directory jumping)
- zoxide (optional, for directory jumping)
Yazi is currently in heavy development, expect breaking changes.
```bash
# Arch Linux
pacman -S ttf-nerd-fonts-symbols jq unarchiver ffmpegthumbnailer fd ripgrep fzf zoxide
## Documentation
# macOS
brew install jq unar ffmpegthumbnailer fd ripgrep fzf zoxide
brew tap homebrew/cask-fonts && brew install --cask font-symbols-only-nerd-font
```
- Usage: https://yazi-rs.github.io/docs/installation
- Features: https://yazi-rs.github.io/features
Execute the following commands to clone the project and build Yazi:
## Discussion
```bash
git clone https://github.com/sxyazi/yazi.git
cd yazi
cargo build --release
```
- Discord Server (English mainly): https://discord.gg/qfADduSdJu
- Telegram Group (Chinese mainly): https://t.me/yazi_rs
## Usage
## Image Preview
```bash
./target/release/yazi
```
| 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) |
## TODO
See https://yazi-rs.github.io/docs/image-preview for details.
<!-- Protocols -->
[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
<!-- Dependencies -->
[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).
- [x] Add example config for general usage, currently please see my [another repo](https://github.com/sxyazi/dotfiles/tree/main/yazi) instead
- [x] Integration with fzf, zoxide for fast directory navigation
- [x] Integration with fd, rg for fuzzy file searching
- [ ] Support for Überzug++ for image previews with X11/wayland environment
- [ ] Batch renaming support
## License
Yazi is MIT-licensed. For more information check the [LICENSE](LICENSE) file.
Yazi is MIT licensed.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 KiB

View file

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

11
build.sh Executable file
View file

@ -0,0 +1,11 @@
cargo build --release --target aarch64-apple-darwin
cargo build --release --target x86_64-apple-darwin
cargo build --release --target x86_64-unknown-linux-gnu
mv target/aarch64-apple-darwin/release/yazi target/yazi-aarch64-apple-darwin
mv target/x86_64-apple-darwin/release/yazi target/yazi-x86_64-apple-darwin
mv target/x86_64-unknown-linux-gnu/release/yazi target/yazi-x86_64-unknown-linux-gnu
zip -j yazi-aarch64-apple-darwin.zip target/yazi-aarch64-apple-darwin
zip -j yazi-x86_64-apple-darwin.zip target/yazi-x86_64-apple-darwin
zip -j yazi-x86_64-unknown-linux-gnu.zip target/yazi-x86_64-unknown-linux-gnu

120
config/keymap.toml Normal file
View file

@ -0,0 +1,120 @@
[manager]
keymap = [
{ on = [ "<Esc>" ], exec = "escape" },
{ on = [ "q" ], exec = "quit" },
{ on = [ "<C-q>" ], exec = "close" },
# Navigation
{ on = [ "j" ], exec = "arrow 1" },
{ on = [ "k" ], exec = "arrow -1" },
{ on = [ "J" ], exec = "arrow 5" },
{ on = [ "K" ], exec = "arrow -5" },
{ on = [ "h" ], exec = "leave" },
{ on = [ "l" ], exec = "enter" },
{ on = [ "H" ], exec = "back" },
{ on = [ "L" ], exec = "forward" },
# Selection
{ on = [ "<Space>" ], exec = "select --state=none" },
{ on = [ "v" ], exec = "visual_mode" },
{ on = [ "V" ], exec = "visual_mode --unselect" },
{ on = [ "<C-a>" ], exec = "select_all --state=true" },
{ on = [ "<C-r>" ], exec = "select_all --state=none" },
# Operation
{ on = [ "o" ], exec = "open" },
{ on = [ "O" ], exec = "open --select" },
{ on = [ "y" ], exec = "yank" },
{ on = [ "x" ], exec = "yank --cut" },
{ on = [ "p" ], exec = "paste" },
{ on = [ "P" ], exec = "paste --force" },
{ on = [ "f" ], exec = "paste --follow" },
{ on = [ "F" ], exec = "paste --follow --force" },
{ on = [ "d" ], exec = "remove" },
{ on = [ "D" ], exec = "remove --permanently" },
{ on = [ "c" ], exec = "create" },
{ on = [ "r" ], exec = "rename" },
{ on = [ "." ], exec = "hidden toggle" },
{ on = [ "s" ], exec = "search fd" },
{ on = [ "S" ], exec = "search rg" },
{ on = [ "<C-s>" ], exec = "search none" },
{ on = [ "z" ], exec = "jump zoxide" },
{ on = [ "Z" ], exec = "jump fzf" },
# Tabs
{ on = [ "t" ], exec = "tab_create --current" },
{ on = [ "1" ], exec = "tab_switch 0" },
{ on = [ "2" ], exec = "tab_switch 1" },
{ on = [ "3" ], exec = "tab_switch 2" },
{ on = [ "4" ], exec = "tab_switch 3" },
{ on = [ "5" ], exec = "tab_switch 4" },
{ on = [ "[" ], exec = "tab_switch -1 --relative" },
{ on = [ "]" ], exec = "tab_switch 1 --relative" },
{ on = [ "{" ], exec = "tab_swap -1" },
{ on = [ "}" ], exec = "tab_swap 1" },
# Tasks
{ on = [ "w" ], exec = "tasks_show" },
]
[tasks]
keymap = [
{ on = [ "<C-q>" ], exec = "close" },
{ on = [ "<Esc>" ], exec = "close" },
{ on = [ "w" ], exec = "close" },
{ on = [ "j" ], exec = "arrow 1" },
{ on = [ "k" ], exec = "arrow -1" },
{ on = [ "x" ], exec = "cancel" },
]
[select]
keymap = [
{ on = [ "<C-q>" ], exec = "close" },
{ on = [ "<Esc>" ], exec = "close" },
{ on = [ "<Enter>" ], exec = "close --submit" },
{ on = [ "j" ], exec = "arrow 1" },
{ on = [ "k" ], exec = "arrow -1" },
{ on = [ "J" ], exec = "arrow 5" },
{ on = [ "K" ], exec = "arrow -5" },
]
[input]
keymap = [
{ on = [ "<C-q>" ], exec = "close" },
{ on = [ "<Enter>" ], exec = "close --submit" },
{ on = [ "<Esc>" ], exec = "escape" },
{ on = [ "<Backspace>" ], exec = "backspace" },
# Mode
{ on = [ "i" ], exec = "insert" },
{ on = [ "a" ], exec = "insert --append" },
{ on = [ "v" ], exec = "visual" },
{ on = [ "h" ], exec = "move -1" },
{ on = [ "l" ], exec = "move 1" },
{ on = [ "H" ], exec = "move -999" },
{ on = [ "L" ], exec = "move 999" },
{ on = [ "b" ], exec = "backward" },
{ on = [ "w" ], exec = "forward" },
{ on = [ "e" ], exec = "forward --end-of-word" },
{ on = [ "d" ], exec = "delete" },
{ on = [ "c" ], exec = "delete --insert" },
{ on = [ "x" ], exec = [ "delete", "move 1" ] }
]

145
config/theme.toml Normal file
View file

@ -0,0 +1,145 @@
[tab]
active = { fg = "#1E2031", bg = "#80AEFA" }
inactive = { fg = "#C8D3F8", bg = "#484D66" }
[status]
primary = { normal = "#80AEFA", select = "#CD9EFC", unselect = "#FFA577" }
secondary = { normal = "#1E2031", select = "#23273B", unselect = "#23273B" }
tertiary = { normal = "#6D738F", select = "#6D738F", unselect = "#6D738F" }
body = { normal = "#484D66", select = "#484D66", unselect = "#484D66" }
emphasis = { normal = "#C8D3F8", select = "#C8D3F8", unselect = "#C8D3F8" }
info = { normal = "#7AD9E5", select = "#7AD9E5", unselect = "#7AD9E5" }
success = { normal = "#97DC8D", select = "#97DC8D", unselect = "#97DC8D" }
warning = { normal = "#F3D398", select = "#F3D398", unselect = "#F3D398" }
danger = { normal = "#FA7F94", select = "#FA7F94", unselect = "#FA7F94" }
[progress]
gauge = { fg = "#FFA577", bg = "#484D66" }
label = { fg = "#FFFFFF", bold = true }
[selection]
hovered = { fg = "#1E2031", bg = "#80AEFA" }
[marker]
selecting = { fg = "#97DC8D", bg = "#97DC8D" }
selected = { fg = "#F3D398", bg = "#F3D398" }
[preview]
hovered = { underline = true }
syntect_theme = "~/.config/bat/themes/Catppuccin-macchiato.tmTheme"
[filetype]
rules = [
# Images
{ mime = "image/*", fg = "#7AD9E5" },
# Videos
{ mime = "video/*", fg = "#F3D398" },
{ mime = "audio/*", fg = "#F3D398" },
# Archives
{ mime = "application/x-bzip", fg = "#CD9EFC" },
{ mime = "application/x-bzip2", fg = "#CD9EFC" },
{ mime = "application/gzip", fg = "#CD9EFC" },
{ mime = "application/vnd.rar", fg = "#CD9EFC" },
{ mime = "application/x-tar", fg = "#CD9EFC" },
{ mime = "application/zip", fg = "#CD9EFC" },
{ mime = "application/x-7z-compressed", fg = "#CD9EFC" },
# Fallback
{ name = "*", fg = "#C8D3F8" },
{ name = "*/", fg = "#80AEFA" }
]
[icons]
"Desktop/" = ""
"Documents/" = ""
"Downloads/" = ""
"Music/" = ""
"Pictures/" = ""
"Public/" = ""
"Videos/" = ""
".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
"*" = ""
"*/" = ""

50
config/yazi.toml Normal file
View file

@ -0,0 +1,50 @@
[manager]
sort_by = "modified"
sort_reverse = true
show_hidden = false
[preview]
tab_size = 2
max_width = 600
max_height = 900
[opener]
archive = [
{ cmd = "unar", args = [ "$1" ] },
]
text = [
{ cmd = "vi", args = [ "$*" ], block = true },
]
image = [
{ cmd = "open", args = [ "$*" ] },
{ cmd = "exiftool", args = [ "$1" ], block = true },
]
video = [
{ cmd = "mpv", args = [ "$*" ] },
{ cmd = "mediainfo", args = [ "$1" ], block = true },
]
audio = [
{ cmd = "mpv", args = [ "$*" ] },
{ cmd = "mediainfo", args = [ "$1" ], block = true },
]
fallback = [
{ cmd = "open", args = [ "$*" ] },
]
[open]
rules = [
{ name = "*.html", use = "fallback" },
{ mime = "text/*", use = "text" },
{ mime = "image/*", use = "image" },
{ mime = "video/*", use = "video" },
{ mime = "audio/*", use = "audio" },
{ mime = "application/x-bzip", use = "archive" },
{ mime = "application/x-bzip2", use = "archive" },
{ mime = "application/gzip", use = "archive" },
{ mime = "application/vnd.rar", use = "archive" },
{ mime = "application/x-tar", use = "archive" },
{ mime = "application/zip", use = "archive" },
{ mime = "application/x-7z-compressed", use = "archive" },
]

View file

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

82
flake.lock generated
View file

@ -1,82 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1783279667,
"narHash": "sha256-/NAkDSsve+GNM0Bt6tleJdCGfsTlK89nPjkVOzZMo0s=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f205b5574fd0cb7da5b702a2da51507b7f4fdd1b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1783320166,
"narHash": "sha256-l7C/OsjcnWDOk2K3ssj+SBduwL67LashjBqis9+t468=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "20ee15370c9256669d66968b89ee20a4b0a4e673",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,72 +0,0 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
rust-overlay,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
toolchain = pkgs.rust-bin.stable.latest.default;
rustPlatform = pkgs.makeRustPlatform {
cargo = toolchain;
rustc = toolchain;
};
rev = self.shortRev or self.dirtyShortRev or "dirty";
date = self.lastModifiedDate or self.lastModified or "19700101";
version =
(builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.package.version
+ "pre${builtins.substring 0 8 date}_${rev}";
in
{
packages = {
yazi-unwrapped = pkgs.callPackage ./nix/yazi-unwrapped.nix {
inherit
version
rev
date
rustPlatform
;
};
yazi = pkgs.callPackage ./nix/yazi.nix {
inherit (self.packages.${system}) yazi-unwrapped;
};
default = self.packages.${system}.yazi;
};
devShells = {
default = pkgs.callPackage ./nix/shell.nix {
inherit toolchain;
inherit (self.packages.${system}) yazi yazi-unwrapped;
};
};
formatter = pkgs.nixfmt-tree;
}
)
// {
overlays = {
default = self.overlays.yazi;
yazi = _: prev: {
inherit (self.packages.${prev.stdenv.system}) yazi yazi-unwrapped;
};
};
};
}

View file

@ -1,25 +0,0 @@
{
mkShell,
yazi,
toolchain,
cspell,
yazi-unwrapped,
}:
mkShell {
packages = yazi.passthru.runtimePaths ++ [
(toolchain.override {
extensions = [
"rust-src"
"rustfmt"
"rust-analyzer"
"clippy"
];
})
cspell
];
inputsFrom = [ yazi-unwrapped ];
env.RUST_BACKTRACE = "1";
}

View file

@ -1,88 +0,0 @@
{
rustPlatform,
version ? "git",
rev ? "unknown",
date ? "19700101",
lib,
installShellFiles,
fetchFromGitHub,
rust-jemalloc-sys,
imagemagick,
}:
let
src = lib.fileset.toSource {
root = ../.;
fileset = lib.fileset.unions [
../assets
../Cargo.toml
../Cargo.lock
(lib.fileset.fromSource (lib.sources.sourceByRegex ../. [ "^yazi-.*" ]))
];
};
in
rustPlatform.buildRustPackage (finalAttrs: {
pname = "yazi";
inherit version src;
cargoLock = {
lockFile = "${src}/Cargo.lock";
};
env = {
YAZI_GEN_COMPLETIONS = true;
VERGEN_GIT_SHA = rev;
VERGEN_BUILD_DATE = builtins.concatStringsSep "-" (builtins.match "(.{4})(.{2})(.{2}).*" date);
};
nativeBuildInputs = [
installShellFiles
imagemagick
];
buildInputs = [
rust-jemalloc-sys
];
postInstall = ''
installShellCompletion --cmd yazi \
--bash ./yazi-boot/completions/yazi.bash \
--fish ./yazi-boot/completions/yazi.fish \
--zsh ./yazi-boot/completions/_yazi
installShellCompletion --cmd ya \
--bash ./yazi-cli/completions/ya.bash \
--fish ./yazi-cli/completions/ya.fish \
--zsh ./yazi-cli/completions/_ya
# Resize logo
for RES in 16 24 32 48 64 128 256; do
mkdir -p $out/share/icons/hicolor/"$RES"x"$RES"/apps
magick assets/logo.png -resize "$RES"x"$RES" $out/share/icons/hicolor/"$RES"x"$RES"/apps/yazi.png
done
installManPage ${finalAttrs.passthru.srcs.man_src}/yazi{.1,-config.5}
mkdir -p $out/share/applications
install -m644 assets/yazi.desktop $out/share/applications/
'';
passthru.srcs = {
man_src = fetchFromGitHub {
name = "manpages"; # needed to ensure name is unique
owner = "yazi-rs";
repo = "manpages";
rev = "8950e968f4a1ad0b83d5836ec54a070855068dbf";
hash = "sha256-kEVXejDg4ChFoMNBvKlwdFEyUuTcY2VuK9j0PdafKus=";
};
};
meta = {
description = "Blazing fast terminal file manager written in Rust, based on async I/O";
homepage = "https://github.com/sxyazi/yazi";
license = lib.licenses.mit;
mainProgram = "yazi";
};
})

View file

@ -1,109 +0,0 @@
{
lib,
formats,
runCommand,
makeWrapper,
runtimeDeps ? (ps: ps),
# deps
file,
yazi-unwrapped,
# default optional deps
jq,
poppler-utils,
_7zz,
ffmpeg,
fd,
ripgrep,
resvg,
fzf,
zoxide,
imagemagick,
chafa,
settings ? { },
plugins ? { },
flavors ? { },
initLua ? null,
}:
let
inherit (lib)
concatStringsSep
concatMapStringsSep
optionalString
makeBinPath
mapAttrsToList
;
defaultDeps = [
jq
poppler-utils
_7zz
ffmpeg
fd
ripgrep
resvg
fzf
zoxide
imagemagick
chafa
];
runtimePaths = [ file ] ++ (runtimeDeps defaultDeps);
settingsFormat = formats.toml { };
files = [
"yazi"
"theme"
"keymap"
];
configHome =
if (settings == { } && initLua == null && plugins == { } && flavors == { }) then
null
else
runCommand "YAZI_CONFIG_HOME" { } ''
mkdir -p $out
${concatMapStringsSep "\n" (
name:
optionalString (settings ? ${name} && settings.${name} != { }) ''
ln -s ${settingsFormat.generate "${name}.toml" settings.${name}} $out/${name}.toml
''
) files}
mkdir $out/plugins
${optionalString (plugins != { }) ''
${concatStringsSep "\n" (
mapAttrsToList (name: value: "ln -s ${value} $out/plugins/${name}") plugins
)}
''}
mkdir $out/flavors
${optionalString (flavors != { }) ''
${concatStringsSep "\n" (
mapAttrsToList (name: value: "ln -s ${value} $out/flavors/${name}") flavors
)}
''}
${optionalString (initLua != null) "ln -s ${initLua} $out/init.lua"}
'';
in
runCommand yazi-unwrapped.name
{
inherit (yazi-unwrapped) pname version meta;
nativeBuildInputs = [ makeWrapper ];
passthru.runtimePaths = runtimePaths;
}
''
mkdir -p $out/bin
ln -s ${yazi-unwrapped}/share $out/share
ln -s ${yazi-unwrapped}/bin/ya $out/bin/ya
makeWrapper ${yazi-unwrapped}/bin/yazi $out/bin/yazi \
--prefix PATH : "${makeBinPath runtimePaths}" \
${optionalString (configHome != null) "--set YAZI_CONFIG_HOME ${configHome}"}
''

View file

@ -1,31 +0,0 @@
color = "Never"
condense_wildcard_suffixes = true
edition = "2024"
enum_discrim_align_threshold = 99
fn_single_line = true
format_code_in_doc_comments = false
format_generated_files = false
format_macro_matchers = true
format_macro_bodies = true
format_strings = false
hard_tabs = true
hex_literal_case = "Lower"
show_parse_errors = false
imports_indent = "Visual"
imports_layout = "Horizontal"
imports_granularity = "Crate"
newline_style = "Unix"
normalize_comments = true
normalize_doc_attributes = false
overflow_delimited_expr = true
reorder_impl_items = true
group_imports = "StdExternalCrate"
reorder_modules = true
struct_field_align_threshold = 99
tab_spaces = 2
unstable_features = true
use_field_init_shorthand = true
use_small_heuristics = "Max"
use_try_shorthand = true
style_edition = "2024"
wrap_comments = true

View file

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

View file

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

View file

@ -1,51 +0,0 @@
local dark = {
icons_by_filename = require("default.icons_by_filename"),
icons_by_file_extension = require("default.icons_by_file_extension"),
}
local light = {
icons_by_filename = require("light.icons_by_filename"),
icons_by_file_extension = require("light.icons_by_file_extension"),
}
function rearrange(by)
local map = {}
local source = by == "exts" and "icons_by_file_extension" or "icons_by_filename"
for k, v in pairs(dark[source]) do
map[k] = map[k] or {}
map[k].icon = v.icon
map[k].fg_dark = v.color:lower()
end
for k, v in pairs(light[source]) do
map[k].fg_light = v.color:lower()
end
return map
end
function dump(map)
local list = {}
for k, v in pairs(map) do
list[#list + 1] = { name = k, text = v.icon, fg_dark = v.fg_dark, fg_light = v.fg_light }
end
table.sort(list, function(a, b) return a.name:lower() < b.name:lower() end)
local dark, light = "", ""
for _, v in ipairs(list) do
-- stylua: ignore
dark = dark .. string.format('\t{ name = "%s", text = "%s", fg = "%s" },\n', v.name, v.text, v.fg_dark)
light = light .. string.format('\t{ name = "%s", text = "%s", fg = "%s" },\n', v.name, v.text, v.fg_light)
end
return dark, light
end
function save(typ, files, exts)
local p = string.format("../../yazi-config/preset/theme-%s.toml", typ)
local s = io.open(p, "r"):read("*a")
s = s:gsub("files = %[\n(.-)\n%]", string.format("files = [\n%s]", files))
s = s:gsub("exts = %[\n(.-)\n%]", string.format("exts = [\n%s]", exts))
io.open(p, "w"):write(s)
end
local dark_files, light_files = dump(rearrange("files"))
local dark_exts, light_exts = dump(rearrange("exts"))
save("dark", dark_files, dark_exts)
save("light", light_files, light_exts)

View file

@ -1,269 +0,0 @@
const LABEL_NAME = "needs info"
const RE_VERSION = /Yazi\s+Version\s*:\s\d+\.\d+\.\d+\s\(/gm
const RE_DEPENDENCIES = /Dependencies\s+[/a-z]+\s*:\s/gm
const RE_CHECKLIST = /#{3}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm
const RE_PR_CHECKLIST = /#{2}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm
function pullRequestBody(content) {
if (RE_PR_CHECKLIST.test(content)) {
return null
}
return "All required checklist items must be checked in the PR description."
}
function bugReportBody(creator, content, hash) {
if (RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content) && new RegExp(` \\(${hash}[a-f0-9]? `).test(content)) {
return null
}
return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that:
- The bug can still be reproduced on the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries).
- The environment info (\`ya env\`) is updated for the newest nightly.
- The non-optional items in the checklist are checked.
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏
`
}
function featureRequestBody(creator, content) {
if (RE_VERSION.test(content) && RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content)) {
return null
}
return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that:
- The requested feature does not exist in the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries).
- The environment info (\`ya env\`) is updated for the newest nightly.
- The non-optional items in the checklist are checked.
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏
`
}
function skipValidation(context) {
const login = context.payload.issue?.user?.login || context.payload.pull_request?.user?.login
const owner = context.payload.repository?.owner?.login || context.repo.owner
return !!login && login === owner
}
module.exports = async ({ github, context, core }) => {
async function nightlyHash() {
try {
const { data: tagRef } = await github.rest.git.getRef({ owner: "sxyazi", repo: "yazi", ref: "tags/nightly" })
return tagRef.object.sha.slice(0, 7)
} catch (e) {
if (e.status === 404) {
core.error("Nightly tag not found")
} else {
core.error(`Error fetching nightly version: ${e.message}`)
}
return null
}
}
async function hasLabel(id, label) {
try {
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
...context.repo,
issue_number: id,
})
return labels.some(l => l.name === label)
} catch (e) {
core.error(`Error checking labels: ${e.message}`)
return false
}
}
async function lastLabeledAt(id) {
try {
const { data: events } = await github.rest.issues.listEvents({
...context.repo,
issue_number: id,
per_page: 100,
})
const all = events.filter(v => v.event === "labeled" && v.label?.name === LABEL_NAME)
return all.at(-1)?.created_at
} catch (e) {
core.error(`Error getting label timestamp: ${e.message}`)
return null
}
}
async function removedLabelManually(id) {
try {
const { data: events } = await github.rest.issues.listEvents({
...context.repo,
issue_number: id,
per_page: 100,
})
const all = events.filter(v => v.event === "unlabeled" && v.label?.name === LABEL_NAME)
return all.length === 0 ? false : !all.at(-1).actor.login.endsWith("[bot]")
} catch (e) {
core.error(`Error checking label removal history: ${e.message}`)
return false
}
}
async function updateLabels(id, mark, body) {
try {
const marked = await hasLabel(id, LABEL_NAME)
if (!mark && marked) {
await github.rest.issues.removeLabel({
...context.repo,
issue_number: id,
name: LABEL_NAME,
})
await hideOldComments(id)
} else if (mark && !marked && !await removedLabelManually(id)) {
await github.rest.issues.addLabels({
...context.repo,
issue_number: id,
labels: [LABEL_NAME],
})
await hideOldComments(id)
await github.rest.issues.createComment({
...context.repo,
issue_number: id,
body,
})
}
} catch (e) {
core.error(`Error updating labels: ${e.message}`)
}
}
async function hideOldComments(id) {
try {
const comments = await github.paginate(github.rest.issues.listComments, {
...context.repo,
issue_number: id,
per_page: 100,
})
for (const c of comments) {
const byBot = c.user?.login?.endsWith("[bot]") || c.user?.type === "Bot"
const contains = c?.body?.includes("or closed after 2 days of inactivity")
if (!byBot || !contains || !c.node_id) continue
try {
await github.graphql(
`mutation($subjectId: ID!, $classifier: ReportedContentClassifiers!) {
minimizeComment(input: {subjectId: $subjectId, classifier: $classifier}) {
minimizedComment { isMinimized }
}
}`,
{ subjectId: c.node_id, classifier: "OUTDATED" },
)
} catch (e) {
core.error(`Error minimizing comment ${c.id}: ${e.message}`)
}
}
} catch (e) {
core.error(`Error listing comments: ${e.message}`)
}
}
async function closeOldIssues() {
try {
const { data: issues } = await github.rest.issues.listForRepo({
...context.repo,
state: "open",
labels: LABEL_NAME,
})
const now = new Date()
const twoDaysAgo = new Date(now - 2 * 24 * 60 * 60 * 1000)
for (const issue of issues) {
const markedAt = new Date(await lastLabeledAt(issue.number) || issue.created_at)
if (markedAt < twoDaysAgo) {
await github.rest.issues.update({
...context.repo,
issue_number: issue.number,
state: "closed",
state_reason: "not_planned",
})
await github.rest.issues.createComment({
...context.repo,
issue_number: issue.number,
body: `This issue has been automatically closed because it was marked as \`${LABEL_NAME}\` for more than 2 days without updates.
If the problem persists, please file a new issue and complete the issue template so we can capture all the details necessary to investigate further.`,
})
}
}
} catch (e) {
core.error(`Error checking old issues: ${e.message}`)
}
}
async function closeUnsupportedIssue(id) {
try {
await github.rest.issues.update({
...context.repo,
issue_number: id,
state: "closed",
state_reason: "not_planned",
})
await github.rest.issues.createComment({
...context.repo,
issue_number: id,
body: `Unsupported issue template.
Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) or [Feature Request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml) template should be used.`,
})
} catch (e) {
core.error(`Error closing unsupported issue: ${e.message}`)
}
}
async function main() {
if (context.eventName === "schedule") {
await closeOldIssues()
return
}
if (skipValidation(context)) {
return
}
if (context.eventName === "issues") {
const id = context.payload.issue.number
const content = context.payload.issue.body || ""
const creator = context.payload.issue.user.login
const hash = await nightlyHash()
if (!hash) return
if (await hasLabel(id, "bug")) {
const body = bugReportBody(creator, content, hash)
await updateLabels(id, !!body, body)
} else if (await hasLabel(id, "feature")) {
const body = featureRequestBody(creator, content)
await updateLabels(id, !!body, body)
} else if (context.payload.action === "opened") {
await closeUnsupportedIssue(id)
}
} else if (context.eventName === "pull_request" || context.eventName === "pull_request_target") {
const content = context.payload.pull_request.body || ""
const body = pullRequestBody(content)
if (body) {
core.setFailed(body)
}
}
}
await main()
}

View file

@ -1,256 +0,0 @@
{
"name": "validate-form",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "validate-form",
"version": "1.0.0",
"dependencies": {
"@actions/core": "^3.0.1",
"@actions/github": "^9.1.1"
}
},
"node_modules/@actions/core": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz",
"integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==",
"license": "MIT",
"dependencies": {
"@actions/exec": "^3.0.0",
"@actions/http-client": "^4.0.0"
}
},
"node_modules/@actions/exec": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz",
"integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
"license": "MIT",
"dependencies": {
"@actions/io": "^3.0.2"
}
},
"node_modules/@actions/github": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-9.1.1.tgz",
"integrity": "sha512-tL5JbYOBZHc0ngEnCsaDcryUizIUIlQyIMwy1Wkx93H5HzbBJ7TbiPx2PnFjBwZW0Vh05JmfFZhecE6gglYegA==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^3.0.2",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@octokit/request": "^10.0.7",
"@octokit/request-error": "^7.1.0",
"undici": "^6.23.0"
}
},
"node_modules/@actions/github/node_modules/@actions/http-client": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz",
"integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^6.23.0"
}
},
"node_modules/@actions/http-client": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.1.tgz",
"integrity": "sha512-+Nvd1ImaOZBSoPbsUtEhv+1z99H12xzncCkz0a3RuehINE81FZSe2QTj3uvAPTcJX/SCzUQHQ0D1GrPMbrPitg==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^6.23.0"
}
},
"node_modules/@actions/io": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz",
"integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==",
"license": "MIT"
},
"node_modules/@octokit/auth-token": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/core": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
"@octokit/request": "^10.0.6",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"before-after-hook": "^4.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/endpoint": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz",
"integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/graphql": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
"integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^10.0.6",
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/openapi-types": {
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
"integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
"integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
"integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/request": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz",
"integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.3",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"fast-content-type-parse": "^3.0.0",
"json-with-bigint": "^3.5.3",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/request-error": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
"integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^27.0.0"
}
},
"node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
"license": "Apache-2.0"
},
"node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/json-with-bigint": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz",
"integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==",
"license": "MIT"
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"license": "MIT",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
},
"node_modules/undici": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"license": "ISC"
}
}
}

View file

@ -1,12 +0,0 @@
{
"name": "validate-form",
"version": "1.0.0",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"private": true,
"dependencies": {
"@actions/core": "^3.0.1",
"@actions/github": "^9.1.1"
}
}

View file

@ -1,165 +0,0 @@
name: yazi
base: core24
adopt-info: yazi
summary: Blazing fast terminal file manager written in Rust, based on async I/O.
description: |
Yazi 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.
license: MIT
grade: stable
confinement: classic
platforms:
amd64:
arm64:
apps:
yazi:
command: yazi
environment:
PATH: $SNAP/bin:$SNAP/usr/bin:$PATH
ya:
command: ya
environment:
PATH: $SNAP/bin:$SNAP/usr/bin:$PATH
parts:
yazi:
plugin: rust
source: https://github.com/sxyazi/yazi.git
stage-snaps:
- jq
stage-packages:
- 7zip-standalone
- chafa
- fd-find
- ffmpeg
# - fzf
- libglu1-mesa
- libglut3.12
- poppler-utils
- ripgrep
- wl-clipboard
- xclip
- zoxide
override-build: |
craftctl default
craftctl set version=$(git describe --tags --abbrev=0)
build-attributes:
- enable-patchelf
organize:
# Ubuntu's `fd` package installs a binary named `fdfind`. Rename it in the snap.
usr/bin/fdfind: usr/bin/fd
prime:
# Remove unused items bought in by dependency packages
- -usr/bin/fc-*
- -usr/bin/ffplay
- -usr/bin/pdfattach
- -usr/bin/pdfdetach
- -usr/bin/pdffonts
- -usr/bin/pdfimages
- -usr/bin/pdfinfo
- -usr/bin/pdfseparate
- -usr/bin/pdfsig
- -usr/bin/pdftocairo
- -usr/bin/pdftohtml
- -usr/bin/pdftops
- -usr/bin/pdftotext
- -usr/bin/pdfunite
# Remove unused libraries identified by snapcraft's linter
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_bad_*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_civil_time.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_cord*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_examine_stack.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_exponential_biased.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_failure_signal_handler.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_flags*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_hash*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_log_severity.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_periodic_sampler.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_random*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_raw_hash_set.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_scoped_set_env.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_statusor.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_status.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_strerror.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_str_format_internal.so.*
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libcaca++.so.0.99.20
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libcjson_utils.so.1.7.17
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libfftw3_omp.so.3.6.10
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libfftw3_threads.so.3.6.10
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_grapheme_lang.so.2.2
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_grapheme_lex.so.2.2
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_indic_lang.so.2.2
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_indic_lex.so.2.2
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_time_awb.so.2.2
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libfreebl3.so
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libfreeblpriv3.so
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libGLX_mesa.so.0.0.0
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libhwy_contrib.so.1.0.7
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libhwy_test.so.1.0.7
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libicui18n.so.74.2
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libicuio.so.74.2
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libicutest.so.74.2
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libicutu.so.74.2
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libjacknet.so.0.1.0
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libjackserver.so.0.1.0
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libnssckbi.so
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libnssdbm3.so
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libpulse-simple.so.0.1.1
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libsoftokn3.so
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libsphinxad.so.3.0.0
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libssl3.so
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libtheora.so.0.3.10
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-dri2.so.0.0.0
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-glx.so.0.0.0
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-present.so.0.0.0
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-sync.so.1.0.0
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxshmfence.so.1.0.0
- -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libzvbi-chains.so.0.0.0
# The Ubuntu `imagemagick` package does not ship a `magick` binary,
# which `yazi` looks for. Building the package oursleves yields the
# expected binary without much overhead, and works across platforms.
magick:
plugin: autotools
source: https://github.com/ImageMagick/ImageMagick.git
source-type: git
source-tag: 7.1.2-0
source-depth: 1
stage-packages:
- libgomp1
autotools-configure-parameters:
- --prefix=/usr
build-attributes:
- enable-patchelf
organize:
# `libgomp1` installs libgomp into the multiarch directory, but the
# `magick` binary's RPATH only searches `$ORIGIN/../lib` (i.e. usr/lib).
# Move it alongside the Magick libraries so the bundled binary can load
# it at runtime; otherwise magick fails with:
# "libgomp.so.1: cannot open shared object file: No such file or directory"
usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libgomp.so*: usr/lib/
prime:
- usr/bin/magick
- usr/lib/libgomp.so*
- usr/lib/libMagickCore-7.Q16HDRI.so*
- usr/lib/libMagickWand-7.Q16HDRI.so*
cleanup:
after: [yazi, magick]
plugin: nil
build-packages:
- patchelf
build-snaps:
- core24
override-prime: |
# Ubuntu's /usr/bin/7zz is a simple bash wrapper that just exec's
# /usr/lib/7zip/7zz - this just shortcuts that and places the actual
# executable at $SNAP/usr/bin/7zz.
mv $CRAFT_PRIME/usr/lib/7zip/7zz $CRAFT_PRIME/usr/bin/7zz
# Ensure we don't ship duplicates of files that exist in the core24
# snap.
cd /snap/core24/current
find . -type f,l -exec rm -rf "${CRAFT_PRIME}/{}" \;

66
src/config/keymap/exec.rs Normal file
View file

@ -0,0 +1,66 @@
use std::{collections::BTreeMap, fmt};
use serde::{de::{self, Visitor}, Deserializer};
#[derive(Debug, Default)]
pub struct Exec {
pub cmd: String,
pub args: Vec<String>,
pub named: BTreeMap<String, String>,
}
impl From<&str> for Exec {
fn from(value: &str) -> Self {
let mut exec = Self::default();
for x in value.split_whitespace() {
if x.starts_with("--") {
let mut it = x[2..].splitn(2, '=');
let name = it.next().unwrap();
let value = it.next().unwrap_or("");
exec.named.insert(name.to_string(), value.to_string());
} else if exec.cmd.is_empty() {
exec.cmd = x.to_string();
} else {
exec.args.push(x.to_string());
}
}
exec
}
}
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 command 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::from(value.as_str()));
}
Ok(execs)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value.split(';').map(Exec::from).collect())
}
}
deserializer.deserialize_any(ExecVisitor)
}
}

94
src/config/keymap/key.rs Normal file
View file

@ -0,0 +1,94 @@
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 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 {
let shift = if let KeyCode::Char(c) = value.code { c.is_ascii_uppercase() } else { false };
Self {
code: value.code,
shift: shift || value.modifiers.contains(KeyModifiers::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,
"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)
}
}

View file

@ -0,0 +1,50 @@
use serde::{Deserialize, Deserializer};
use super::{Exec, Key};
use crate::config::MERGED_KEYMAP;
#[derive(Debug, Deserialize)]
pub struct Single {
pub on: Vec<Key>,
#[serde(deserialize_with = "Exec::deserialize")]
pub exec: Vec<Exec>,
}
#[derive(Debug)]
pub struct Keymap {
pub manager: Vec<Single>,
pub tasks: Vec<Single>,
pub select: Vec<Single>,
pub input: Vec<Single>,
}
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,
}
#[derive(Deserialize)]
struct Inner {
keymap: Vec<Single>,
}
let shadow = Shadow::deserialize(deserializer)?;
Ok(Self {
manager: shadow.manager.keymap,
tasks: shadow.tasks.keymap,
select: shadow.select.keymap,
input: shadow.input.keymap,
})
}
}
impl Keymap {
pub fn new() -> Self { toml::from_str(&MERGED_KEYMAP).unwrap() }
}

7
src/config/keymap/mod.rs Normal file
View file

@ -0,0 +1,7 @@
mod exec;
mod key;
mod keymap;
pub use exec::*;
pub use key::*;
pub use keymap::*;

View file

@ -0,0 +1,40 @@
use std::{env, fs, path::PathBuf};
use serde::Deserialize;
use super::SortBy;
use crate::config::MERGED_YAZI;
#[derive(Debug, Deserialize)]
pub struct Manager {
#[serde(skip)]
pub cwd: PathBuf,
#[serde(skip)]
pub cache: PathBuf,
// Sorting
pub sort_by: SortBy,
pub sort_reverse: bool,
// Display
pub show_hidden: bool,
}
impl Manager {
pub fn new() -> Self {
#[derive(Deserialize)]
struct Outer {
manager: Manager,
}
let mut manager = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().manager;
manager.cwd = env::current_dir().unwrap_or("/".into());
manager.cache = "/tmp/yazi".into();
if !manager.cache.is_dir() {
fs::create_dir(&manager.cache).unwrap();
}
manager
}
}

View file

@ -0,0 +1,5 @@
mod manager;
mod sorting;
pub use manager::*;
pub use sorting::*;

View file

@ -0,0 +1,25 @@
use anyhow::bail;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(try_from = "String")]
pub enum SortBy {
Alphabetical,
Created,
Modified,
Size,
}
impl TryFrom<String> for SortBy {
type Error = anyhow::Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
Ok(match s.as_str() {
"created" => Self::Created,
"modified" => Self::Modified,
"size" => Self::Size,
"alphabetical" => Self::Alphabetical,
_ => bail!("invalid sort_by value: {}", s),
})
}
}

30
src/config/mod.rs Normal file
View file

@ -0,0 +1,30 @@
use once_cell::sync::Lazy;
pub mod keymap;
pub mod manager;
pub mod open;
mod pattern;
mod preset;
pub mod preview;
pub mod theme;
pub(crate) use pattern::*;
pub(crate) use preset::*;
static MERGED_KEYMAP: Lazy<String> = Lazy::new(|| Preset::keymap());
static MERGED_THEME: Lazy<String> = Lazy::new(|| Preset::theme());
static MERGED_YAZI: Lazy<String> = Lazy::new(|| Preset::yazi());
pub static KEYMAP: Lazy<keymap::Keymap> = Lazy::new(|| keymap::Keymap::new());
pub static MANAGER: Lazy<manager::Manager> = Lazy::new(|| manager::Manager::new());
pub static OPEN: Lazy<open::Open> = Lazy::new(|| open::Open::new());
pub static PREVIEW: Lazy<preview::Preview> = Lazy::new(|| preview::Preview::new());
pub static THEME: Lazy<theme::Theme> = Lazy::new(|| theme::Theme::new());
pub fn init() {
Lazy::force(&KEYMAP);
Lazy::force(&MANAGER);
Lazy::force(&OPEN);
Lazy::force(&PREVIEW);
Lazy::force(&THEME);
}

5
src/config/open/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod open;
mod opener;
pub use open::*;
pub use opener::*;

65
src/config/open/open.rs Normal file
View file

@ -0,0 +1,65 @@
use std::{collections::{BTreeMap, BTreeSet}, path::Path};
use serde::{Deserialize, Deserializer};
use super::Opener;
use crate::config::{Pattern, MERGED_YAZI};
#[derive(Debug)]
pub struct Open {
openers: BTreeMap<String, Vec<Opener>>,
rules: Vec<OpenRule>,
}
#[derive(Debug, Deserialize)]
struct OpenRule {
name: Option<Pattern>,
mime: Option<Pattern>,
#[serde(rename = "use")]
use_: String,
}
impl Open {
pub fn new() -> Self { toml::from_str(&MERGED_YAZI).unwrap() }
pub fn openers(&self, path: impl AsRef<Path>, mime: impl AsRef<str>) -> Option<Vec<&Opener>> {
self.rules.iter().find_map(|rule| {
if rule.name.as_ref().map_or(false, |e| e.match_path(&path, Some(false)))
|| rule.mime.as_ref().map_or(false, |m| m.matches(&mime))
{
self.openers.get(&rule.use_).map(|v| v.iter().collect())
} else {
None
}
})
}
pub fn common_openers<'a>(
&self,
targets: &[(impl AsRef<Path>, impl AsRef<str>)],
) -> Vec<&Opener> {
let grouped = targets.into_iter().filter_map(|(p, m)| self.openers(p, m)).collect::<Vec<_>>();
let flat = grouped.iter().flatten().cloned().collect::<BTreeSet<_>>();
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)?;
Ok(Self { openers: outer.opener, rules: outer.open.rules })
}
}

31
src/config/open/opener.rs Normal file
View file

@ -0,0 +1,31 @@
use serde::{Deserialize, Deserializer};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Opener {
pub cmd: String,
pub args: Vec<String>,
pub block: bool,
pub spread: bool,
}
impl<'de> Deserialize<'de> for Opener {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
pub struct Shadow {
pub cmd: String,
pub args: Vec<String>,
#[serde(default)]
pub block: bool,
#[serde(skip)]
pub spread: bool,
}
let shadow = Shadow::deserialize(deserializer)?;
let spread = shadow.args.contains(&"$*".to_string());
Ok(Self { cmd: shadow.cmd, args: shadow.args, block: shadow.block, spread })
}
}

33
src/config/pattern.rs Normal file
View file

@ -0,0 +1,33 @@
use std::path::Path;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(try_from = "String")]
pub struct Pattern {
inner: glob::Pattern,
is_folder: bool,
}
impl Pattern {
pub fn matches(&self, str: impl AsRef<str>) -> bool { self.inner.matches(str.as_ref()) }
pub fn match_path(&self, path: impl AsRef<Path>, is_folder: Option<bool>) -> bool {
is_folder.map_or(true, |f| f == self.is_folder) && self.inner.matches_path(path.as_ref())
}
}
impl TryFrom<&str> for Pattern {
type Error = anyhow::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let is_folder = s.ends_with('/');
Ok(Self { inner: glob::Pattern::new(s.trim_end_matches('/'))?, is_folder })
}
}
impl TryFrom<String> for Pattern {
type Error = anyhow::Error;
fn try_from(s: String) -> Result<Self, Self::Error> { Self::try_from(s.as_str()) }
}

55
src/config/preset.rs Normal file
View file

@ -0,0 +1,55 @@
use std::fs;
use toml::Table;
use xdg::BaseDirectories;
pub(crate) struct Preset;
impl Preset {
fn merge(a: &mut Table, b: &Table, max: u8) {
for (k, v) in b {
let a = if let Some(a) = a.get_mut(k) {
a
} 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 = BaseDirectories::new().unwrap().get_config_file(user);
let mut user = fs::read_to_string(path).unwrap_or("".to_string()).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("yazi/keymap.toml", include_str!("../../config/keymap.toml"))
}
#[inline]
pub(crate) fn theme() -> String {
Self::merge_str("yazi/theme.toml", include_str!("../../config/theme.toml"))
}
#[inline]
pub(crate) fn yazi() -> String {
Self::merge_str("yazi/yazi.toml", include_str!("../../config/yazi.toml"))
}
}

View file

@ -0,0 +1,3 @@
mod preview;
pub use preview::*;

View file

@ -0,0 +1,23 @@
use serde::Deserialize;
use crate::config::MERGED_YAZI;
#[derive(Debug, Deserialize)]
pub struct Preview {
pub tab_size: u32,
pub max_width: u32,
pub max_height: u32,
}
impl Preview {
pub fn new() -> Self {
#[derive(Deserialize)]
struct Outer {
preview: Preview,
}
let outer: Outer = toml::from_str(&MERGED_YAZI).unwrap();
outer.preview
}
}

47
src/config/theme/color.rs Normal file
View file

@ -0,0 +1,47 @@
use std::ops::Deref;
use anyhow::{bail, Result};
use ratatui::style;
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(try_from = "String")]
pub struct Color(pub(super) style::Color);
impl Default for Color {
fn default() -> Self { Self(style::Color::Reset) }
}
impl TryFrom<String> for Color {
type Error = anyhow::Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
if s.len() < 7 {
bail!("Invalid color: {}", s);
}
Ok(Self(style::Color::Rgb(
u8::from_str_radix(&s[1..3], 16)?,
u8::from_str_radix(&s[3..5], 16)?,
u8::from_str_radix(&s[5..7], 16)?,
)))
}
}
impl Deref for Color {
type Target = style::Color;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Color {
pub fn fg(&self) -> style::Style { style::Style::new().fg(self.0) }
pub fn bg(&self) -> style::Style { style::Style::new().bg(self.0) }
}
#[derive(Deserialize)]
pub struct ColorGroup {
pub normal: Color,
pub select: Color,
pub unselect: Color,
}

View file

@ -0,0 +1,62 @@
use std::path::Path;
use serde::{Deserialize, Deserializer};
use super::Style;
use crate::config::{theme::Color, 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<String>, is_dir: bool) -> bool {
if self.name.as_ref().map_or(false, |e| e.match_path(path, Some(is_dir))) {
return true;
}
if let Some(mime) = mime {
return self.mime.as_ref().map_or(false, |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<FiletypeOuterStyle>,
}
#[derive(Deserialize)]
struct FiletypeOuterStyle {
name: Option<Pattern>,
mime: Option<Pattern>,
fg: Option<Color>,
bg: Option<Color>,
bold: Option<bool>,
underline: Option<bool>,
}
Ok(
FiletypeOuter::deserialize(deserializer)?
.rules
.into_iter()
.map(|r| Filetype {
name: r.name,
mime: r.mime,
style: Style {
fg: r.fg,
bg: r.bg,
bold: r.bold,
underline: r.underline,
},
})
.collect::<Vec<_>>(),
)
}
}

45
src/config/theme/icon.rs Normal file
View file

@ -0,0 +1,45 @@
use std::fmt;
use serde::{de::{self, Visitor}, Deserializer};
use crate::config::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
src/config/theme/mod.rs Normal file
View 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::*;

40
src/config/theme/style.rs Normal file
View file

@ -0,0 +1,40 @@
use ratatui::style::{self, Modifier};
use serde::Deserialize;
use super::Color;
#[derive(Deserialize)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: Option<bool>,
pub underline: Option<bool>,
}
impl Style {
pub fn get(&self) -> style::Style {
let mut style = style::Style::new();
if let Some(fg) = &self.fg {
style = style.fg(fg.0);
}
if let Some(bg) = &self.bg {
style = style.bg(bg.0);
}
if let Some(bold) = self.bold {
if bold {
style = style.add_modifier(Modifier::BOLD);
} else {
style = style.remove_modifier(Modifier::BOLD);
}
}
if let Some(underline) = self.underline {
if underline {
style = style.add_modifier(Modifier::UNDERLINED);
} else {
style = style.remove_modifier(Modifier::UNDERLINED);
}
}
style
}
}

70
src/config/theme/theme.rs Normal file
View file

@ -0,0 +1,70 @@
use std::path::PathBuf;
use serde::Deserialize;
use super::{ColorGroup, Filetype, Icon, Style};
use crate::{config::MERGED_THEME, misc::absolute_path};
#[derive(Deserialize)]
pub struct Tab {
pub active: Style,
pub inactive: Style,
}
#[derive(Deserialize)]
pub struct Status {
pub primary: ColorGroup,
pub secondary: ColorGroup,
pub tertiary: ColorGroup,
pub body: ColorGroup,
pub emphasis: ColorGroup,
pub info: ColorGroup,
pub success: ColorGroup,
pub warning: ColorGroup,
pub danger: ColorGroup,
}
#[derive(Deserialize)]
pub struct Progress {
pub gauge: Style,
pub label: Style,
}
#[derive(Deserialize)]
pub struct Selection {
pub hovered: Style,
}
#[derive(Deserialize)]
pub struct Marker {
pub selecting: Style,
pub selected: Style,
}
#[derive(Deserialize)]
pub struct Preview {
pub hovered: Style,
pub syntect_theme: PathBuf,
}
#[derive(Deserialize)]
pub struct Theme {
pub tab: Tab,
pub status: Status,
pub progress: Progress,
pub selection: Selection,
pub marker: Marker,
pub preview: Preview,
#[serde(rename = "filetype", deserialize_with = "Filetype::deserialize")]
pub filetypes: Vec<Filetype>,
#[serde(deserialize_with = "Icon::deserialize")]
pub icons: Vec<Icon>,
}
impl Theme {
pub fn new() -> Self {
let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap();
theme.preview.syntect_theme = absolute_path(&theme.preview.syntect_theme);
theme
}
}

49
src/core/adapter/kitty.rs Normal file
View file

@ -0,0 +1,49 @@
use std::io::Write;
use anyhow::Result;
use base64::{engine::general_purpose, Engine};
use image::DynamicImage;
pub struct Kitty;
impl Kitty {
pub fn image_show(img: DynamicImage) -> Result<Vec<u8>> {
fn output(raw: Vec<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,
"\x1b_Ga=d\x1b\\\x1b_Ga=T,f={},s={},v={},m={};{}\x1b\\",
format,
size.0,
size.1,
it.peek().is_some() as u8,
first.iter().collect::<String>(),
)?;
}
while let Some(chunk) = it.next() {
write!(
buf,
"\x1b_Gm={};{}\x1b\\",
it.peek().is_some() as u8,
chunk.iter().collect::<String>()
)?;
}
Ok(buf)
}
let size = (img.width(), img.height());
match img {
DynamicImage::ImageRgb8(v) => output(v.into_raw(), 24, size),
DynamicImage::ImageRgba8(v) => output(v.into_raw(), 32, size),
v => output(v.to_rgb8().into_raw(), 24, size),
}
}
#[inline]
pub fn image_hide() -> &'static [u8; 8] { b"\x1b_Ga=d\x1b\\" }
}

3
src/core/adapter/mod.rs Normal file
View file

@ -0,0 +1,3 @@
mod kitty;
pub use kitty::*;

4
src/core/blocker.rs Normal file
View file

@ -0,0 +1,4 @@
use once_cell::sync::Lazy;
use tokio::sync::Semaphore;
pub static BLOCKER: Lazy<Semaphore> = Lazy::new(|| Semaphore::new(1));

111
src/core/event.rs Normal file
View file

@ -0,0 +1,111 @@
use std::{collections::BTreeMap, path::PathBuf};
use anyhow::Result;
use crossterm::event::KeyEvent;
use tokio::sync::{mpsc::Sender, oneshot};
use super::{files::FilesOp, input::InputOpt, manager::PreviewData, select::SelectOpt};
use crate::config::open::Opener;
static mut TX: Option<Sender<Event>> = None;
pub enum Event {
Quit,
Stop(bool, Option<oneshot::Sender<()>>),
Key(KeyEvent),
Render(String),
Resize(u16, u16),
// Manager
Cd(PathBuf),
Refresh,
Files(FilesOp),
Pages(usize),
Mimetype(BTreeMap<PathBuf, String>),
Hover,
Preview(PathBuf, PreviewData),
// Input
Select(SelectOpt, oneshot::Sender<Result<usize>>),
Input(InputOpt, oneshot::Sender<Result<String>>),
// Tasks
Open(Vec<(PathBuf, String)>, Option<Opener>),
Progress(u8, u32),
}
impl Event {
#[inline]
pub fn init(tx: Sender<Event>) {
unsafe {
TX.replace(tx);
}
}
#[inline]
pub fn emit(self) {
let tx = unsafe { TX.as_ref().unwrap() };
tokio::spawn(async {
tx.send(self).await.ok();
});
}
pub async fn wait<T>(self, rx: oneshot::Receiver<T>) -> T {
let tx = unsafe { TX.as_ref().unwrap() };
tx.send(self).await.ok();
rx.await.unwrap()
}
}
#[macro_export]
macro_rules! emit {
(Stop($state:expr)) => {{
let (tx, rx) = tokio::sync::oneshot::channel();
$crate::core::Event::Stop($state, Some(tx)).wait(rx)
}};
(Key($key:expr)) => {
$crate::core::Event::Key($key).emit();
};
(Render) => {
$crate::core::Event::Render(format!("{}:{}", file!(), line!())).emit();
};
(Resize($cols:expr, $rows:expr)) => {
$crate::core::Event::Resize($cols, $rows).emit();
};
(Cd($op:expr)) => {
$crate::core::Event::Cd($op).emit();
};
(Files($op:expr)) => {
$crate::core::Event::Files($op).emit();
};
(Pages($page:expr)) => {
$crate::core::Event::Pages($page).emit();
};
(Mimetype($mimes:expr)) => {
$crate::core::Event::Mimetype($mimes).emit();
};
(Preview($path:expr, $data:expr)) => {
$crate::core::Event::Preview($path, $data).emit();
};
(Select($opt:expr)) => {{
let (tx, rx) = tokio::sync::oneshot::channel();
$crate::core::Event::Select($opt, tx).wait(rx)
}};
(Input($opt:expr)) => {{
let (tx, rx) = tokio::sync::oneshot::channel();
$crate::core::Event::Input($opt, tx).wait(rx)
}};
(Open($targets:expr, $opener:expr)) => {
$crate::core::Event::Open($targets, $opener).emit();
};
(Progress($percent:expr, $tasks:expr)) => {
$crate::core::Event::Progress($percent, $tasks).emit();
};
($event:ident) => {
$crate::core::Event::$event.emit();
};
}

39
src/core/external/fd.rs vendored Normal file
View file

@ -0,0 +1,39 @@
use std::{path::PathBuf, process::Stdio, time::Duration};
use anyhow::Result;
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::UnboundedReceiver};
use crate::misc::DelayedBuffer;
pub struct FdOpt {
pub cwd: PathBuf,
pub hidden: bool,
pub glob: bool,
pub subject: String,
}
pub fn fd(opt: FdOpt) -> Result<UnboundedReceiver<Vec<PathBuf>>> {
let mut child = Command::new("fd")
.arg("--base-directory")
.arg(&opt.cwd)
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
.arg(if opt.glob { "--glob" } else { "--regex" })
.arg(&opt.subject)
.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
drop(child.stderr.take());
let mut it = BufReader::new(child.stdout.take().unwrap()).lines();
let (mut buf, rx) = DelayedBuffer::new(Duration::from_millis(100));
tokio::spawn(async move {
while let Ok(Some(line)) = it.next_line().await {
buf.push(opt.cwd.join(line));
}
child.wait().await.ok();
});
Ok(rx)
}

23
src/core/external/ffmpegthumbnailer.rs vendored Normal file
View file

@ -0,0 +1,23 @@
use std::path::Path;
use anyhow::{bail, Result};
use tokio::process::Command;
use crate::config::PREVIEW;
pub async fn ffmpegthumbnailer(path: &Path, dest: &Path) -> Result<()> {
let output = Command::new("ffmpegthumbnailer")
.arg("-i")
.arg(path)
.arg("-o")
.arg(dest)
.args(["-q", "6", "-c", "jpeg", "-s", &PREVIEW.max_width.to_string()])
.kill_on_drop(true)
.output()
.await?;
if !output.status.success() {
bail!("failed to generate video thumbnail: {}", String::from_utf8_lossy(&output.stderr));
}
Ok(())
}

33
src/core/external/file.rs vendored Normal file
View file

@ -0,0 +1,33 @@
use std::{collections::BTreeMap, ffi::OsStr, path::PathBuf};
use anyhow::{bail, Result};
use tokio::process::Command;
use crate::misc::MimeKind;
pub async fn file(files: &[impl AsRef<OsStr>]) -> Result<BTreeMap<PathBuf, String>> {
if files.is_empty() {
bail!("no files to get mime types for");
}
let output = Command::new("file")
.args(["-bL", "--mime-type"])
.args(files)
.kill_on_drop(true)
.output()
.await?;
let output = String::from_utf8_lossy(&output.stdout);
let mimes = BTreeMap::from_iter(
files
.into_iter()
.zip(output.trim().lines())
.filter(|(_, m)| MimeKind::valid(m))
.map(|(f, m)| (f.as_ref().into(), m.to_string())),
);
if mimes.len() == 0 {
bail!("failed to get mime types");
}
Ok(mimes)
}

26
src/core/external/fzf.rs vendored Normal file
View file

@ -0,0 +1,26 @@
use std::{path::PathBuf, process::Stdio};
use anyhow::Result;
use tokio::{process::Command, sync::oneshot::{self, Receiver}};
pub struct FzfOpt {
pub cwd: PathBuf,
}
pub fn fzf(opt: FzfOpt) -> Result<Receiver<Result<PathBuf>>> {
let child =
Command::new("fzf").current_dir(&opt.cwd).kill_on_drop(true).stdout(Stdio::piped()).spawn()?;
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
if let Ok(output) = child.wait_with_output().await {
let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !selected.is_empty() {
tx.send(Ok(opt.cwd.join(selected))).ok();
return;
}
}
tx.send(Err(anyhow::anyhow!("No match"))).ok();
});
Ok(rx)
}

20
src/core/external/jq.rs vendored Normal file
View file

@ -0,0 +1,20 @@
use std::path::Path;
use anyhow::{bail, Result};
use tokio::process::Command;
use crate::config::PREVIEW;
pub async fn jq(path: &Path) -> Result<String> {
let output = Command::new("jq")
.args(["-C", "--indent", &PREVIEW.tab_size.to_string(), "."])
.arg(path)
.kill_on_drop(true)
.output()
.await?;
if !output.status.success() {
bail!("failed to get json: {}", String::from_utf8_lossy(&output.stderr));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

63
src/core/external/lsar.rs vendored Normal file
View file

@ -0,0 +1,63 @@
use std::path::Path;
use anyhow::{bail, Result};
use serde::Deserialize;
use serde_json::Value;
use tokio::process::Command;
#[derive(Debug)]
pub enum LsarAttr {
Posix(u16),
Windows(u16),
Dos(u8),
}
#[derive(Debug, Deserialize)]
pub struct LsarFile {
#[serde(rename = "XADFileName")]
pub name: String,
#[serde(rename = "XADLastModificationDate")]
pub last_modified: String,
#[serde(rename = "XADFileSize")]
pub size: Option<usize>,
#[serde(rename = "XADCompressedSize")]
pub compressed_size: Option<usize>,
#[serde(rename = "XADCompressionName")]
pub compression_name: Option<String>,
#[serde(skip)]
pub attributes: Option<LsarAttr>,
}
pub async fn lsar(path: &Path) -> Result<Vec<LsarFile>> {
let output =
Command::new("lsar").args(["-j", "-jss"]).arg(path).kill_on_drop(true).output().await?;
if !output.status.success() {
bail!("failed to get json: {}", String::from_utf8_lossy(&output.stderr));
}
#[derive(Deserialize)]
struct Outer {
#[serde(rename = "lsarContents")]
contents: Vec<Value>,
}
let output = String::from_utf8_lossy(&output.stdout);
let contents = serde_json::from_str::<Outer>(output.trim())?.contents;
let mut files = Vec::with_capacity(contents.len());
for content in contents {
let mut file = serde_json::from_value::<LsarFile>(content.clone())?;
if let Some(p) = content.get("XADPosixPermissions").and_then(|p| p.as_u64()) {
file.attributes = Some(LsarAttr::Posix(p as u16));
} else if let Some(a) = content.get("XADWindowsFileAttributes").and_then(|a| a.as_u64()) {
file.attributes = Some(LsarAttr::Windows(a as u16));
} else if let Some(a) = content.get("XADDOSFileAttributes").and_then(|a| a.as_u64()) {
file.attributes = Some(LsarAttr::Dos(a as u8));
}
files.push(file);
}
Ok(files)
}

19
src/core/external/mod.rs vendored Normal file
View file

@ -0,0 +1,19 @@
mod fd;
mod ffmpegthumbnailer;
mod file;
mod fzf;
mod jq;
mod lsar;
mod rg;
mod unar;
mod zoxide;
pub use fd::*;
pub use ffmpegthumbnailer::*;
pub use file::*;
pub use fzf::*;
pub use jq::*;
pub use lsar::*;
pub use rg::*;
pub use unar::*;
pub use zoxide::*;

37
src/core/external/rg.rs vendored Normal file
View file

@ -0,0 +1,37 @@
use std::{path::PathBuf, process::Stdio, time::Duration};
use anyhow::Result;
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::UnboundedReceiver};
use crate::misc::DelayedBuffer;
pub struct RgOpt {
pub cwd: PathBuf,
pub hidden: bool,
pub subject: String,
}
pub fn rg(opt: RgOpt) -> Result<UnboundedReceiver<Vec<PathBuf>>> {
let mut child = Command::new("rg")
.current_dir(&opt.cwd)
.args(&["--color=never", "--files-with-matches", "--smart-case"])
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
.arg(&opt.subject)
.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
drop(child.stderr.take());
let mut it = BufReader::new(child.stdout.take().unwrap()).lines();
let (mut buf, rx) = DelayedBuffer::new(Duration::from_millis(100));
tokio::spawn(async move {
while let Ok(Some(line)) = it.next_line().await {
buf.push(opt.cwd.join(line));
}
child.wait().await.ok();
});
Ok(rx)
}

25
src/core/external/unar.rs vendored Normal file
View file

@ -0,0 +1,25 @@
use std::{path::Path, process::Stdio};
use anyhow::{bail, Result};
use tokio::{io::{AsyncReadExt, BufReader}, process::Command};
pub async fn unar_head(path: &Path, target: &Path) -> Result<Vec<u8>> {
let mut child = Command::new("unar")
.args(&[path, target])
.args(&["-o", "-"])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true)
.spawn()?;
let mut buf = vec![0; 1024];
let mut reader = BufReader::new(child.stdout.take().unwrap());
reader.read(&mut buf).await.ok();
child.kill().await.ok();
if buf.is_empty() {
bail!("failed to get head of unar");
}
Ok(buf)
}

30
src/core/external/zoxide.rs vendored Normal file
View file

@ -0,0 +1,30 @@
use std::{path::PathBuf, process::Stdio};
use anyhow::Result;
use tokio::{process::Command, sync::oneshot::{self, Receiver}};
pub struct ZoxideOpt {
pub cwd: PathBuf,
}
pub fn zoxide(opt: ZoxideOpt) -> Result<Receiver<Result<PathBuf>>> {
let child = Command::new("zoxide")
.args(&["query", "-i", "--exclude"])
.arg(opt.cwd)
.kill_on_drop(true)
.stdout(Stdio::piped())
.spawn()?;
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
if let Ok(output) = child.wait_with_output().await {
let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !selected.is_empty() {
tx.send(Ok(selected.into())).ok();
return;
}
}
tx.send(Err(anyhow::anyhow!("No match"))).ok();
});
Ok(rx)
}

49
src/core/files/file.rs Normal file
View file

@ -0,0 +1,49 @@
use std::{fs::Metadata, path::{Path, PathBuf}};
use anyhow::Result;
use tokio::fs;
#[derive(Clone, Debug)]
pub struct File {
pub path: PathBuf,
pub meta: Metadata,
pub length: Option<u64>,
pub is_link: bool,
pub is_hidden: bool,
pub is_selected: bool,
}
impl File {
#[inline]
pub async fn from(path: &Path) -> Result<File> {
let meta = fs::metadata(path).await?;
Ok(Self::from_meta(path, meta).await)
}
pub async fn from_meta(path: &Path, mut meta: Metadata) -> File {
let is_link = meta.is_symlink();
if is_link {
meta = fs::metadata(&path).await.unwrap_or(meta);
}
let length = if meta.is_dir() { None } else { Some(meta.len()) };
let is_hidden = path.file_name().map(|s| s.to_string_lossy().starts_with('.')).unwrap_or(false);
File { path: path.to_path_buf(), meta, length, is_link, is_hidden, is_selected: false }
}
}
impl File {
#[inline]
pub fn path(&self) -> PathBuf { self.path.clone() }
#[inline]
pub fn set_path(mut self, path: &Path) -> Self {
self.path = path.to_path_buf();
self
}
#[inline]
pub fn name(&self) -> Option<String> {
self.path.file_name().map(|s| s.to_string_lossy().to_string())
}
}

147
src/core/files/files.rs Normal file
View file

@ -0,0 +1,147 @@
use std::{ops::{Deref, DerefMut}, path::{Path, PathBuf}};
use anyhow::Result;
use indexmap::IndexMap;
use tokio::fs;
use super::File;
use crate::config::{manager::SortBy, MANAGER};
#[derive(Default)]
pub struct Files {
items: IndexMap<PathBuf, File>,
sort: FilesSort,
pub show_hidden: bool,
}
impl Files {
pub async fn read(paths: Vec<PathBuf>) -> IndexMap<PathBuf, File> {
let mut items = IndexMap::new();
for path in paths {
if let Ok(file) = File::from(&path).await {
items.insert(path, file);
}
}
items
}
pub async fn read_dir(path: &Path) -> Result<IndexMap<PathBuf, File>> {
let mut it = fs::read_dir(path).await?;
let mut items = IndexMap::new();
while let Ok(Some(item)) = it.next_entry().await {
if let Ok(meta) = item.metadata().await {
let path = item.path();
let file = File::from_meta(&path, meta).await;
items.insert(path, file);
}
}
Ok(items)
}
pub fn sort(&mut self) {
fn cmp<T: Ord>(a: T, b: T, reverse: bool) -> std::cmp::Ordering {
if reverse { b.cmp(&a) } else { a.cmp(&b) }
}
let reverse = self.sort.reverse;
match self.sort.by {
SortBy::Alphabetical => self.items.sort_by(|_, a, _, b| cmp(&a.path, &b.path, reverse)),
SortBy::Created => self.items.sort_by(|_, a, _, b| {
if let (Ok(a), Ok(b)) = (a.meta.created(), b.meta.created()) {
return cmp(a, b, reverse);
}
std::cmp::Ordering::Equal
}),
SortBy::Modified => self.items.sort_by(|_, a, _, b| {
if let (Ok(a), Ok(b)) = (a.meta.modified(), b.meta.modified()) {
return cmp(a, b, reverse);
}
std::cmp::Ordering::Equal
}),
SortBy::Size => {
self.items.sort_by(|_, a, _, b| cmp(a.length.unwrap_or(0), b.length.unwrap_or(0), reverse))
}
}
}
#[inline]
pub fn duplicate(&self, idx: usize) -> Option<File> {
self.items.get_index(idx).map(|(_, file)| file.clone())
}
pub fn update_read(&mut self, mut items: IndexMap<PathBuf, File>) -> bool {
if !self.show_hidden {
items.retain(|_, item| !item.is_hidden);
}
for (path, item) in &mut items {
if let Some(old) = self.items.get(path) {
item.length = old.length;
item.is_selected = old.is_selected;
}
}
self.items = items;
self.sort();
true
}
pub fn update_search(&mut self, items: IndexMap<PathBuf, File>) -> bool {
if !items.is_empty() {
self.items.extend(items);
self.sort();
return true;
}
if !self.items.is_empty() {
self.items.clear();
return true;
}
false
}
}
impl Deref for Files {
type Target = IndexMap<PathBuf, File>;
fn deref(&self) -> &Self::Target { &self.items }
}
impl DerefMut for Files {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items }
}
struct FilesSort {
pub by: SortBy,
pub reverse: bool,
}
impl Default for FilesSort {
fn default() -> Self { Self { by: MANAGER.sort_by, reverse: MANAGER.sort_reverse } }
}
#[derive(Debug)]
pub enum FilesOp {
Read(PathBuf, IndexMap<PathBuf, File>),
IOErr(PathBuf),
Search(PathBuf, IndexMap<PathBuf, File>),
}
impl FilesOp {
#[inline]
pub fn path(&self) -> PathBuf {
match self {
Self::Read(path, _) => path,
Self::IOErr(path) => path,
Self::Search(path, _) => path,
}
.clone()
}
#[inline]
pub fn read_empty(path: &Path) -> Self { Self::Read(path.to_path_buf(), IndexMap::new()) }
#[inline]
pub fn search_empty(path: &Path) -> Self { Self::Search(path.to_path_buf(), IndexMap::new()) }
}

5
src/core/files/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod file;
mod files;
pub use file::*;
pub use files::*;

297
src/core/input/input.rs Normal file
View file

@ -0,0 +1,297 @@
use anyhow::{anyhow, Result};
use ratatui::layout::Rect;
use tokio::sync::oneshot::Sender;
use unicode_width::UnicodeWidthStr;
use crate::{core::Position, misc::CharKind};
#[derive(Default)]
pub struct Input {
title: String,
value: String,
position: (u16, u16),
op: InputOp,
range: Option<(usize, usize)>,
mode: InputMode,
offset: usize,
cursor: usize,
callback: Option<Sender<Result<String>>>,
pub visible: bool,
}
pub struct InputOpt {
pub title: String,
pub value: String,
pub position: Position,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum InputMode {
Normal,
#[default]
Insert,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum InputOp {
#[default]
None,
Delete(bool),
Yank,
}
impl Input {
pub fn show(&mut self, opt: InputOpt, tx: Sender<Result<String>>) {
self.close(false);
self.title = opt.title;
self.value = opt.value;
self.position = match opt.position {
Position::Coords(x, y) => (x, y),
_ => unimplemented!(),
};
self.cursor = self.count();
self.offset = self.value.width().saturating_sub(50);
self.callback = Some(tx);
self.visible = true;
}
pub fn close(&mut self, submit: bool) -> bool {
if let Some(cb) = self.callback.take() {
let _ = cb.send(if submit { Ok(self.value.clone()) } else { Err(anyhow!("canceled")) });
}
self.mode = InputMode::Insert;
self.visible = false;
true
}
pub fn escape(&mut self) -> bool {
match self.mode {
InputMode::Normal => {
self.range = None;
}
InputMode::Insert => {
self.mode = InputMode::Normal;
self.move_(-1);
}
}
true
}
pub fn insert(&mut self, append: bool) -> bool {
if self.mode != InputMode::Normal {
return false;
}
self.mode = InputMode::Insert;
if append {
self.move_(1);
}
true
}
pub fn visual(&mut self) -> bool {
if self.mode != InputMode::Normal {
return false;
}
self.range = Some((self.cursor, self.cursor));
true
}
pub fn move_(&mut self, step: isize) -> bool {
let old = self.cursor;
let mut include = false;
if step <= 0 {
self.cursor = self.cursor.saturating_sub(step.abs() as usize);
} else {
let count = self.count();
self.cursor += step as usize;
if self.cursor >= count {
include = true;
self.cursor = if self.mode == InputMode::Insert { count } else { count.saturating_sub(1) };
}
}
if self.cursor != old {
if self.cursor < self.offset {
self.offset = self.cursor;
} else if self.cursor > self.offset + 50 {
self.offset = self.cursor.saturating_sub(50);
}
}
self.handle_op(include) || self.cursor != old
}
pub fn backward(&mut self) -> bool {
if self.cursor == 0 {
return self.handle_op(false);
}
let idx = self.idx(self.cursor).unwrap_or(self.value.len());
let mut it = self.value[..idx].chars().rev().enumerate();
let mut prev = CharKind::new(it.next().unwrap().1);
for (i, c) in it {
let c = CharKind::new(c);
if prev != CharKind::Space && prev != c {
return self.move_(-(i as isize));
}
prev = c;
}
if prev != CharKind::Space {
return self.move_(-(self.value.len() as isize));
}
false
}
pub fn forward(&mut self, end: bool) -> bool {
if self.value.is_empty() {
return self.handle_op(false);
}
let mut it = self.value.chars().skip(self.cursor).enumerate();
let mut prev = CharKind::new(it.next().unwrap().1);
for (i, c) in it {
let c = CharKind::new(c);
let b = if end {
prev != CharKind::Space && prev != c && i != 1
} else {
c != CharKind::Space && c != prev
};
if b {
return self.move_(if end { i - 1 } else { i } as isize);
}
prev = c;
}
self.move_(self.value.len() as isize)
}
pub fn type_(&mut self, c: char) -> bool {
if self.cursor < 1 {
self.value.insert(0, c);
} else if self.cursor == self.count() {
self.value.push(c);
} else {
self.value.insert(self.idx(self.cursor).unwrap(), c);
}
self.move_(1)
}
pub fn backspace(&mut self) -> bool {
if self.cursor < 1 {
return false;
} else if self.cursor == self.count() {
self.value.pop();
} else {
self.value.remove(self.idx(self.cursor - 1).unwrap());
}
self.move_(-1)
}
pub fn delete(&mut self, insert: bool) -> bool {
match self.op {
InputOp::None => {
self.op = InputOp::Delete(insert);
if self.range.is_some() {
return self.handle_op(true);
}
self.range = Some((self.cursor, self.cursor));
false
}
InputOp::Delete(_) => {
self.move_(-(self.value.len() as isize));
self.value.clear();
self.op = InputOp::None;
self.range = None;
self.mode = if insert { InputMode::Insert } else { InputMode::Normal };
true
}
_ => false,
}
}
fn handle_op(&mut self, include: bool) -> bool {
if let Some(ref mut range) = self.range {
*range = (range.0.min(self.cursor), range.0.max(self.cursor));
}
match self.op {
InputOp::None => return false,
InputOp::Delete(insert) => {
let range = self.range.take().unwrap();
if !self.value.is_empty() {
let (start, end) = (self.idx(range.0), self.idx(range.1 + include as usize));
self.value.drain(start.unwrap()..end.unwrap());
}
self.mode = if insert {
self.cursor = range.0.min(self.count());
InputMode::Insert
} else {
self.cursor = range.0.min(self.count().saturating_sub(1));
InputMode::Normal
};
}
InputOp::Yank => {}
}
self.op = InputOp::None;
true
}
}
impl Input {
#[inline]
pub fn title(&self) -> String { self.title.clone() }
#[inline]
pub fn value(&self) -> String { self.value.clone() }
#[inline]
pub fn area(&self) -> Rect {
Rect { x: self.position.0, y: self.position.1 + 2, width: 50, height: 3 }
}
#[inline]
pub fn mode(&self) -> InputMode { self.mode }
#[inline]
pub fn cursor(&self) -> (u16, u16) {
let width = self
.value
.chars()
.enumerate()
.take_while(|(i, _)| *i < self.cursor)
.map(|(_, c)| c)
.collect::<String>()
.width() as u16;
let area = self.area();
(area.x + width + 1, area.y + 1)
}
#[inline]
fn count(&self) -> usize { self.value.chars().count() }
#[inline]
fn idx(&self, n: usize) -> Option<usize> {
self
.value
.char_indices()
.nth(n)
.map(|(i, _)| i)
.or_else(|| if n == self.count() { Some(self.value.len()) } else { None })
}
}

3
src/core/input/mod.rs Normal file
View file

@ -0,0 +1,3 @@
mod input;
pub use input::*;

202
src/core/manager/folder.rs Normal file
View file

@ -0,0 +1,202 @@
use std::path::{Path, PathBuf};
use indexmap::map::Slice;
use ratatui::layout::Rect;
use super::{ALL_RATIO, CURRENT_RATIO, DIR_PADDING, PARENT_RATIO};
use crate::{core::files::{File, Files, FilesOp}, emit, misc::tty_size};
#[derive(Default)]
pub struct Folder {
pub cwd: PathBuf,
pub files: Files,
offset: usize,
cursor: usize,
pub page: usize,
pub hovered: Option<File>,
pub in_search: bool,
}
impl Folder {
pub fn new(cwd: &Path) -> Self { Self { cwd: cwd.to_path_buf(), ..Default::default() } }
pub fn new_search(cwd: &Path) -> Self {
Self { cwd: cwd.to_path_buf(), in_search: true, ..Default::default() }
}
#[inline]
pub fn limit() -> usize { tty_size().ws_row.saturating_sub(DIR_PADDING) as usize }
pub fn update(&mut self, op: FilesOp) -> bool {
let b = match op {
FilesOp::Read(_, items) => self.files.update_read(items),
FilesOp::Search(_, items) => self.files.update_search(items),
_ => unreachable!(),
};
if !b {
return false;
}
let len = self.files.len();
self.offset = self.offset.min(len);
self.cursor = self.cursor.min(len.saturating_sub(1));
self.set_page(true);
if let Some(h) = self.hovered.as_ref().map(|h| h.path()) {
self.hover(&h);
}
self.hovered = self.files.duplicate(self.cursor);
true
}
pub fn set_page(&mut self, force: bool) -> bool {
let limit = Self::limit();
let new = if limit == 0 { 0 } else { self.cursor / limit };
if !force && self.page == new {
return false;
}
self.page = new;
emit!(Pages(new));
true
}
pub fn next(&mut self, step: usize) -> bool {
let len = self.files.len();
if len == 0 {
return false;
}
let old = self.cursor;
self.cursor = (self.cursor + step).min(len - 1);
self.hovered = self.files.duplicate(self.cursor);
self.set_page(false);
let limit = Self::limit();
if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) {
self.offset = len.saturating_sub(limit).min(self.offset + self.cursor - old);
}
old != self.cursor
}
pub fn prev(&mut self, step: usize) -> bool {
let old = self.cursor;
self.cursor = self.cursor.saturating_sub(step);
self.hovered = self.files.duplicate(self.cursor);
self.set_page(false);
if self.cursor < self.offset + 5 {
self.offset = self.offset.saturating_sub(old - self.cursor);
}
old != self.cursor
}
pub fn hidden(&mut self, show: Option<bool>) -> bool {
if show.is_none() || self.files.show_hidden != show.unwrap() {
self.files.show_hidden = !self.files.show_hidden;
emit!(Refresh);
}
false
}
#[inline]
pub fn window(&self) -> &Slice<PathBuf, File> {
let end = (self.offset + Self::limit()).min(self.files.len());
self.files.get_range(self.offset..end).unwrap()
}
pub fn select(&mut self, idx: Option<usize>, state: Option<bool>) -> bool {
let len = self.files.len();
let mut apply = |idx: usize, state: Option<bool>| -> bool {
if state.is_none() {
self.files[idx].is_selected = !self.files[idx].is_selected;
return true;
}
let state = state.unwrap();
if state != self.files[idx].is_selected {
self.files[idx].is_selected = state;
return true;
}
false
};
if let Some(idx) = idx {
if idx < len {
return apply(idx, state);
}
} else {
let mut applied = false;
for i in 0..len {
if apply(i, state) {
applied = true;
}
}
return applied;
}
false
}
pub fn hover(&mut self, path: &Path) -> bool {
if matches!(self.hovered, Some(ref h) if h.path == path) {
return false;
}
let new = self.position(path).unwrap_or(self.cursor);
if new > self.cursor { self.next(new - self.cursor) } else { self.prev(self.cursor - new) }
}
pub fn hover_force(&mut self, file: File) -> bool {
if !self.hover(&file.path) && self.files.is_empty() {
self.hovered = Some(file);
return true;
}
false
}
}
impl Folder {
#[inline]
pub fn cursor(&self) -> usize { self.cursor }
#[inline]
pub fn position(&self, path: &Path) -> Option<usize> {
self.files.iter().position(|(p, _)| p == path)
}
pub fn paginate(&self) -> &Slice<PathBuf, File> {
let len = self.files.len();
let limit = Self::limit();
let start = (self.page * limit).min(len.saturating_sub(1));
let end = (start + limit).min(len);
self.files.get_range(start..end).unwrap()
}
#[inline]
pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, item)| item.is_selected) }
pub fn selected(&self) -> Option<Vec<&File>> {
let v = self.files.iter().filter(|(_, f)| f.is_selected).map(|(_, f)| f).collect::<Vec<_>>();
if v.is_empty() { None } else { Some(v) }
}
pub fn rect_current(&self, path: &Path) -> Option<Rect> {
let pos = self.position(path)? - self.offset;
let s = tty_size();
Some(Rect {
x: (s.ws_col as u32 * PARENT_RATIO / ALL_RATIO) as u16,
y: pos as u16,
width: (s.ws_col as u32 * CURRENT_RATIO / ALL_RATIO) as u16,
height: 1,
})
}
}

334
src/core/manager/manager.rs Normal file
View file

@ -0,0 +1,334 @@
use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, mem, path::PathBuf};
use tokio::fs;
use super::{PreviewData, Tab, Tabs, Watcher};
use crate::{config::OPEN, core::{external, files::{File, FilesOp}, input::InputOpt, manager::Folder, select::SelectOpt, tasks::Tasks, Position}, emit};
pub struct Manager {
tabs: Tabs,
yanked: (bool, HashSet<PathBuf>),
watcher: Watcher,
pub mimetype: HashMap<PathBuf, String>,
}
impl Manager {
pub fn new() -> Self {
Self {
tabs: Tabs::new(),
yanked: Default::default(),
watcher: Watcher::start(),
mimetype: Default::default(),
}
}
pub fn refresh(&mut self) {
self.watcher.trigger(&self.current().cwd);
if let Some(p) = self.parent() {
self.watcher.trigger(&p.cwd);
}
emit!(Hover);
let mut to_watch = BTreeSet::new();
for tab in self.tabs.iter() {
to_watch.insert(tab.current.cwd.clone());
if let Some(ref p) = tab.parent {
to_watch.insert(p.cwd.clone());
}
if let Some(ref h) = tab.current.hovered {
if h.meta.is_dir() {
to_watch.insert(h.path());
}
}
}
self.watcher.watch(to_watch);
}
pub fn preview(&mut self) -> bool {
let hovered = if let Some(h) = self.hovered() {
h.clone()
} else {
return self.active_mut().preview.reset();
};
if hovered.meta.is_dir() {
self.active_mut().preview.go(&hovered.path, "inode/directory");
if self.active().history(&hovered.path).is_some() {
emit!(Preview(hovered.path, PreviewData::Folder));
}
} else if let Some(mime) = self.mimetype.get(&hovered.path).cloned() {
self.active_mut().preview.go(&hovered.path, &mime);
} else {
tokio::spawn(async move {
if let Ok(mimes) = external::file(&[hovered.path]).await {
emit!(Mimetype(mimes));
}
});
}
false
}
pub fn yank(&mut self, cut: bool) -> bool {
let selected = self.selected().into_iter().map(|f| f.path()).collect::<Vec<_>>();
self.yanked.0 = cut;
self.yanked.1.clear();
self.yanked.1.extend(selected);
false
}
#[inline]
pub fn yanked(&self) -> &(bool, HashSet<PathBuf>) { &self.yanked }
pub fn quit(&self, tasks: &Tasks) -> bool {
let tasks = tasks.len();
if tasks == 0 {
emit!(Quit);
return false;
}
tokio::spawn(async move {
let result = emit!(Input(InputOpt {
title: format!("There are {} tasks running, sure to quit? (y/N)", tasks),
value: "".to_string(),
position: Position::Top,
}))
.await;
if let Ok(choice) = result {
if choice.to_lowercase() == "y" {
emit!(Quit);
}
}
});
false
}
pub fn close(&mut self, tasks: &Tasks) -> bool {
if self.tabs.len() > 1 {
return self.tabs.close(self.tabs.idx());
}
self.quit(tasks)
}
pub fn open(&mut self, select: bool) -> bool {
let mut files = self
.selected()
.into_iter()
.filter(|f| f.meta.is_file())
.map(|f| (f.path(), self.mimetype.get(&f.path).cloned()))
.collect::<Vec<_>>();
if files.is_empty() {
return false;
}
tokio::spawn(async move {
let todo = files.iter().filter(|(_, m)| m.is_none()).map(|(p, _)| p).collect::<Vec<_>>();
if let Ok(mut mimes) = external::file(&todo).await {
files = files
.into_iter()
.map(|(p, m)| {
let mime = m.or_else(|| mimes.remove(&p));
(p, mime)
})
.collect::<Vec<_>>();
}
let files = files.into_iter().filter_map(|(p, m)| m.map(|m| (p, m))).collect::<Vec<_>>();
if !select {
emit!(Open(files, None));
return;
}
let openers = OPEN.common_openers(&files);
if openers.is_empty() {
return;
}
let result = emit!(Select(SelectOpt {
title: "Open with:".to_string(),
items: openers.iter().map(|o| o.cmd.clone()).collect(),
position: Position::Hovered,
}))
.await;
if let Ok(choice) = result {
emit!(Open(files, Some(openers[choice].clone())));
}
});
false
}
pub fn create(&self) -> bool {
let cwd = self.current().cwd.clone();
tokio::spawn(async move {
let result = emit!(Input(InputOpt {
title: "Create:".to_string(),
value: "".to_string(),
position: Position::Top,
}))
.await;
if let Ok(name) = result {
let path = cwd.join(&name);
if name.ends_with('/') {
fs::create_dir_all(path).await.ok();
} else {
fs::create_dir_all(path.parent().unwrap()).await.ok();
fs::File::create(path).await.ok();
}
}
});
false
}
pub fn rename(&self) -> bool {
if self.current().has_selected() {
return self.bulk_rename();
}
let hovered = if let Some(h) = self.hovered() {
h.path.clone()
} else {
return false;
};
tokio::spawn(async move {
let result = emit!(Input(InputOpt {
title: "Rename:".to_string(),
value: hovered.file_name().unwrap().to_string_lossy().to_string(),
position: Position::Hovered,
}))
.await;
if let Ok(new) = result {
let to = hovered.parent().unwrap().join(new);
fs::rename(&hovered, to).await.ok();
}
});
false
}
fn bulk_rename(&self) -> bool { false }
pub fn selected(&self) -> Vec<&File> {
self.current().selected().or_else(|| self.hovered().map(|h| vec![h])).unwrap_or_default()
}
pub fn update_read(&mut self, op: FilesOp) -> bool {
let path = op.path();
let cwd = self.current().cwd.clone();
let hovered = self.hovered().map(|h| h.path());
let mut b = if cwd == path && !self.current().in_search {
self.current_mut().update(op)
} else if matches!(self.parent(), Some(p) if p.cwd == path) {
self.active_mut().parent.as_mut().unwrap().update(op)
} else {
self
.active_mut()
.history
.entry(path.to_path_buf())
.or_insert_with(|| Folder::new(&path))
.update(op);
matches!(self.hovered(), Some(h) if h.path == path)
};
b |= self.active_mut().parent.as_mut().map_or(false, |p| p.hover(&cwd));
b |= hovered.as_ref().map_or(false, |h| self.current_mut().hover(h));
if hovered != self.hovered().map(|h| h.path()) {
emit!(Hover);
}
b
}
pub fn update_ioerr(&mut self, op: FilesOp) -> bool {
let path = op.path();
let op = FilesOp::read_empty(&path);
if path == self.current().cwd {
self.current_mut().update(op);
} else if matches!(self.parent(), Some(p) if p.cwd == path) {
self.active_mut().parent.as_mut().unwrap().update(op);
} else {
return false;
}
self.active_mut().leave();
true
}
pub fn update_search(&mut self, op: FilesOp) -> bool {
let path = op.path();
if self.current().in_search && self.current().cwd == path {
return self.current_mut().update(op);
}
let rep = mem::replace(self.current_mut(), Folder::new_search(&path));
if !rep.in_search {
self.active_mut().history.insert(path, rep);
}
self.current_mut().update(op);
true
}
pub fn update_mimetype(&mut self, mut mimes: BTreeMap<PathBuf, String>, tasks: &Tasks) -> bool {
mimes.retain(|f, m| self.mimetype.get(f) != Some(m));
if mimes.is_empty() {
return false;
}
tasks.precache_image(&mimes);
tasks.precache_video(&mimes);
self.mimetype.extend(mimes);
self.preview();
true
}
pub fn update_preview(&mut self, path: PathBuf, data: PreviewData) -> bool {
let hovered = if let Some(ref h) = self.current().hovered {
h.path()
} else {
return self.active_mut().preview.reset();
};
if hovered != path {
return false;
}
let preview = &mut self.active_mut().preview;
preview.path = path;
preview.data = data;
true
}
}
impl Manager {
#[inline]
pub fn tabs(&self) -> &Tabs { &self.tabs }
#[inline]
pub fn tabs_mut(&mut self) -> &mut Tabs { &mut self.tabs }
#[inline]
pub fn active(&self) -> &Tab { self.tabs.active() }
#[inline]
pub fn active_mut(&mut self) -> &mut Tab { self.tabs.active_mut() }
#[inline]
pub fn current(&self) -> &Folder { &self.tabs.active().current }
#[inline]
pub fn current_mut(&mut self) -> &mut Folder { &mut self.tabs.active_mut().current }
#[inline]
pub fn parent(&self) -> Option<&Folder> { self.tabs.active().parent.as_ref() }
#[inline]
pub fn hovered(&self) -> Option<&File> { self.tabs.active().current.hovered.as_ref() }
}

25
src/core/manager/mod.rs Normal file
View file

@ -0,0 +1,25 @@
mod folder;
mod manager;
mod mode;
mod preview;
mod tab;
mod tabs;
mod watcher;
pub use folder::*;
pub use manager::*;
pub use mode::*;
pub use preview::*;
pub use tab::*;
pub use tabs::*;
pub use watcher::*;
pub const PARENT_RATIO: u32 = 1;
pub const CURRENT_RATIO: u32 = 4;
pub const PREVIEW_RATIO: u32 = 3;
pub const ALL_RATIO: u32 = PARENT_RATIO + CURRENT_RATIO + PREVIEW_RATIO;
pub const DIR_PADDING: u16 = 2;
pub const PREVIEW_BORDER: u16 = 2;
pub const PREVIEW_PADDING: u16 = 2;

41
src/core/manager/mode.rs Normal file
View file

@ -0,0 +1,41 @@
use std::fmt::Display;
use crate::config::theme::{self, ColorGroup};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
#[default]
Normal,
Select(usize),
Unselect(usize),
}
impl Mode {
#[inline]
pub fn color<'a>(&self, group: &'a ColorGroup) -> &'a theme::Color {
match *self {
Mode::Normal => &group.normal,
Mode::Select(_) => &group.select,
Mode::Unselect(_) => &group.unselect,
}
}
#[inline]
pub fn start(&self) -> Option<usize> {
match self {
Mode::Normal => None,
Mode::Select(n) => Some(*n),
Mode::Unselect(n) => Some(*n),
}
}
}
impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Mode::Normal => write!(f, "NORMAL"),
Mode::Select(_) => write!(f, "SELECT"),
Mode::Unselect(_) => write!(f, "UN-SEL"),
}
}
}

170
src/core/manager/preview.rs Normal file
View file

@ -0,0 +1,170 @@
use std::{fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::OnceLock};
use anyhow::{anyhow, Result};
use image::imageops::FilterType;
use syntect::{easy::HighlightFile, highlighting::{Theme, ThemeSet}, parsing::SyntaxSet, util::as_24_bit_terminal_escaped};
use tokio::{fs, task::JoinHandle};
use super::{ALL_RATIO, PREVIEW_BORDER, PREVIEW_PADDING, PREVIEW_RATIO};
use crate::{config::{PREVIEW, THEME}, core::{adapter::Kitty, external, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{tty_ratio, tty_size, MimeKind}};
static SYNTECT_SYNTAX: OnceLock<SyntaxSet> = OnceLock::new();
static SYNTECT_THEME: OnceLock<Theme> = OnceLock::new();
#[derive(Debug)]
pub struct Preview {
pub path: PathBuf,
pub data: PreviewData,
handle: Option<JoinHandle<()>>,
}
#[derive(Debug, Default)]
pub enum PreviewData {
#[default]
None,
Folder,
Text(String),
Image(Vec<u8>),
}
impl Preview {
pub fn new() -> Self {
Self { path: Default::default(), data: Default::default(), handle: Default::default() }
}
fn size() -> (u16, u16) {
let s = tty_size();
let col = (s.ws_col as u32 * PREVIEW_RATIO / ALL_RATIO) as u16;
(col.saturating_sub(PREVIEW_BORDER), s.ws_row.saturating_sub(PREVIEW_PADDING))
}
pub fn go(&mut self, path: &Path, mime: &str) {
if let Some(handle) = self.handle.take() {
handle.abort();
}
let (path, mime) = (path.to_path_buf(), mime.to_owned());
self.handle = Some(tokio::spawn(async move {
let result = match MimeKind::new(&mime) {
MimeKind::Dir => Self::folder(&path).await,
MimeKind::JSON => Self::json(&path).await.map(PreviewData::Text),
MimeKind::Text => Self::highlight(&path).await.map(PreviewData::Text),
MimeKind::Image => Self::image(&path).await.map(PreviewData::Image),
MimeKind::Video => Self::video(&path).await.map(PreviewData::Image),
MimeKind::Archive => Self::archive(&path).await.map(PreviewData::Text),
MimeKind::Others => Err(anyhow!("Unsupported mimetype: {}", mime)),
};
emit!(Preview(path, result.unwrap_or_default()));
}));
}
pub fn reset(&mut self) -> bool {
if self.path == PathBuf::default() {
return false;
}
self.path = Default::default();
self.data = Default::default();
true
}
pub async fn folder(path: &Path) -> Result<PreviewData> {
emit!(Files(match Files::read_dir(&path).await {
Ok(items) => FilesOp::Read(path.to_path_buf(), items),
Err(_) => FilesOp::IOErr(path.to_path_buf()),
}));
Ok(PreviewData::Folder)
}
pub async fn image(mut path: &Path) -> Result<Vec<u8>> {
let cache = Precache::cache(path);
if fs::metadata(&cache).await.is_ok() {
path = cache.as_path();
}
let (w, h) = {
let r = tty_ratio();
let (w, h) = Self::size();
let (w, h) = ((w as f64 * r.0) as u32, (h as f64 * r.1) as u32);
(w.min(PREVIEW.max_width), h.min(PREVIEW.max_height))
};
let file = fs::read(path).await?;
tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
let img = image::load_from_memory(&file)?;
Kitty::image_show(if img.width() > w || img.height() > h {
img.resize(w, h, FilterType::Triangle)
} else {
img
})
})
.await?
}
pub async fn video(path: &Path) -> Result<Vec<u8>> {
let cache = Precache::cache(path);
if fs::metadata(&cache).await.is_err() {
external::ffmpegthumbnailer(path, &cache).await?;
}
Self::image(&cache).await
}
pub async fn json(path: &Path) -> Result<String> {
Ok(
external::jq(path)
.await?
.lines()
.take(Self::size().1 as usize)
.collect::<Vec<_>>()
.join("\n"),
)
}
pub async fn archive(path: &Path) -> Result<String> {
Ok(
external::lsar(path)
.await?
.into_iter()
.take(Self::size().1 as usize)
.map(|f| f.name)
.collect::<Vec<_>>()
.join("\n"),
)
}
pub async fn highlight(path: &Path) -> Result<String> {
let syntax = SYNTECT_SYNTAX.get_or_init(|| SyntaxSet::load_defaults_newlines());
let theme = SYNTECT_THEME.get_or_init(|| {
let from_file = || -> Result<Theme> {
let file = File::open(&THEME.preview.syntect_theme)?;
Ok(ThemeSet::load_from_reader(&mut BufReader::new(file))?)
};
from_file().unwrap_or_else(|_| ThemeSet::load_defaults().themes["base16-ocean.dark"].clone())
});
let path = path.to_path_buf();
let spaces = " ".repeat(PREVIEW.tab_size as usize);
tokio::task::spawn_blocking(move || -> Result<String> {
let mut h = HighlightFile::new(path, syntax, theme)?;
let mut line = String::new();
let mut buf = String::new();
let mut i = Self::size().1 as usize;
while i > 0 && h.reader.read_line(&mut line)? > 0 {
i -= 1;
line = line.replace('\t', &spaces);
let regions = h.highlight_lines.highlight_line(&line, syntax)?;
buf.push_str(&as_24_bit_terminal_escaped(&regions[..], false));
line.clear();
}
buf.push_str("\x1b[0m");
Ok(buf)
})
.await?
}
}

281
src/core/manager/tab.rs Normal file
View file

@ -0,0 +1,281 @@
use std::{collections::BTreeMap, mem, path::{Path, PathBuf}};
use anyhow::{Error, Result};
use tokio::task::JoinHandle;
use super::{Folder, Mode, Preview};
use crate::{core::{external::{self, FzfOpt, ZoxideOpt}, files::{File, Files, FilesOp}, input::InputOpt, Event, Position, BLOCKER}, emit, misc::Defer};
pub struct Tab {
pub(super) current: Folder,
pub(super) parent: Option<Folder>,
search: Option<JoinHandle<Result<()>>>,
pub(super) mode: Mode,
pub(super) history: BTreeMap<PathBuf, Folder>,
pub(super) preview: Preview,
}
impl Tab {
pub fn new(path: &Path) -> Self {
Self {
current: Folder::new(path),
parent: path.parent().map(|p| Folder::new(p)),
search: None,
mode: Default::default(),
history: Default::default(),
preview: Preview::new(),
}
}
pub fn escape(&mut self) -> bool {
if matches!(self.mode, Mode::Select(_) | Mode::Unselect(_)) {
self.mode = Mode::Normal;
return true;
}
if self.select_all(Some(false)) {
return true;
}
self.search_stop()
}
pub fn arrow(&mut self, step: isize) -> bool {
let before = self.current.cursor();
let ok = if step > 0 {
self.current.next(step as usize)
} else {
self.current.prev(step.abs() as usize)
};
if !ok {
return false;
}
// Visual selection
if let Some(start) = self.mode.start() {
let after = self.current.cursor();
if (after > before && before < start) || (after < before && before > start) {
for i in before.min(start)..=start.max(before) {
self.current.select(Some(i), Some(false));
}
}
for i in start.min(after)..=after.max(start) {
self.current.select(Some(i), Some(true));
}
}
emit!(Hover);
true
}
pub async fn cd(&mut self, mut target: PathBuf) -> bool {
let file = if let Ok(f) = File::from(&target).await {
f
} else {
return false;
};
let mut hovered = None;
if !file.meta.is_dir() {
hovered = Some(file);
target = target.parent().unwrap().to_path_buf();
}
if self.current.cwd == target {
if hovered.map(|h| self.current.hover_force(h)).unwrap_or(false) {
emit!(Hover);
}
return false;
}
if let Some(rep) = self.parent.take() {
self.history.insert(rep.cwd.clone(), rep);
}
let rep = self.history_new(&target);
let rep = mem::replace(&mut self.current, rep);
if !rep.in_search {
self.history.insert(rep.cwd.clone(), rep);
}
if let Some(parent) = target.parent() {
self.parent = Some(self.history_new(parent));
}
if let Some(h) = hovered {
self.current.hover_force(h);
}
emit!(Refresh);
true
}
pub fn enter(&mut self) -> bool {
let hovered = if let Some(ref h) = self.current.hovered {
h.clone()
} else {
return false;
};
if !hovered.meta.is_dir() {
return false;
}
let rep = self.history_new(&hovered.path);
let rep = mem::replace(&mut self.current, rep);
if !rep.in_search {
self.history.insert(rep.cwd.clone(), rep);
}
if let Some(rep) = self.parent.take() {
self.history.insert(rep.cwd.clone(), rep);
}
self.parent = Some(self.history_new(hovered.path.parent().unwrap()));
emit!(Refresh);
true
}
pub fn leave(&mut self) -> bool {
let current = self
.current
.hovered
.as_ref()
.and_then(|h| h.path.parent())
.and_then(|p| if p == self.current.cwd { None } else { Some(p) })
.or_else(|| self.current.cwd.parent());
let current = if let Some(c) = current {
c.to_owned()
} else {
return false;
};
if let Some(rep) = self.parent.take() {
self.history.insert(rep.cwd.clone(), rep);
}
if let Some(parent) = current.parent() {
self.parent = Some(self.history_new(parent));
}
let rep = self.history_new(&current);
let rep = mem::replace(&mut self.current, rep);
if !rep.in_search {
self.history.insert(rep.cwd.clone(), rep);
}
emit!(Refresh);
true
}
// TODO
pub fn back(&mut self) -> bool { false }
// TODO
pub fn forward(&mut self) -> bool { false }
pub fn search(&mut self, grep: bool) -> bool {
if let Some(handle) = self.search.take() {
handle.abort();
}
let cwd = self.current.cwd.clone();
let hidden = self.current.files.show_hidden;
self.search = Some(tokio::spawn(async move {
let subject = emit!(Input(InputOpt {
title: "Search:".to_string(),
value: "".to_string(),
position: Position::Top,
}))
.await?;
let mut rx = if grep {
external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject })
} else {
external::fd(external::FdOpt { cwd: cwd.clone(), hidden, glob: false, subject })
}?;
emit!(Files(FilesOp::search_empty(&cwd)));
while let Some(chunk) = rx.recv().await {
if chunk.is_empty() {
break;
}
emit!(Files(FilesOp::Search(cwd.clone(), Files::read(chunk).await)));
}
Ok(())
}));
true
}
pub fn search_stop(&mut self) -> bool {
if let Some(handle) = self.search.take() {
handle.abort();
}
if self.current.in_search {
let cwd = self.current.cwd.clone();
let rep = self.history_new(&cwd);
drop(mem::replace(&mut self.current, rep));
}
emit!(Refresh);
true
}
pub fn jump(&self, global: bool) -> bool {
let cwd = self.current.cwd.clone();
tokio::spawn(async move {
let _guard = BLOCKER.acquire().await.unwrap();
let _defer = Defer::new(|| Event::Stop(false, None).emit());
emit!(Stop(true)).await;
let rx =
if global { external::fzf(FzfOpt { cwd }) } else { external::zoxide(ZoxideOpt { cwd }) }?;
if let Ok(target) = rx.await? {
emit!(Cd(target));
}
Ok::<(), Error>(())
});
false
}
pub fn select(&mut self, state: Option<bool>) -> bool {
let idx = Some(self.current.cursor());
self.current.select(idx, state)
}
pub fn select_all(&mut self, state: Option<bool>) -> bool { self.current.select(None, state) }
pub fn visual_mode(&mut self, unsel: bool) -> bool {
let idx = self.current.cursor();
if unsel {
self.mode = Mode::Unselect(idx);
self.current.select(Some(idx), Some(false));
} else {
self.mode = Mode::Select(idx);
self.current.select(Some(idx), Some(true));
};
true
}
}
impl Tab {
#[inline]
pub fn mode(&self) -> &Mode { &self.mode }
#[inline]
pub fn history(&self, path: &Path) -> Option<&Folder> { self.history.get(path) }
#[inline]
pub fn history_new(&mut self, path: &Path) -> Folder {
self.history.remove(path).unwrap_or_else(|| Folder::new(path))
}
#[inline]
pub fn preview(&self) -> &Preview { &self.preview }
}

98
src/core/manager/tabs.rs Normal file
View file

@ -0,0 +1,98 @@
use std::path::Path;
use super::Tab;
use crate::{config::MANAGER, emit};
const MAX_TABS: usize = 9;
pub struct Tabs {
idx: usize,
items: Vec<Tab>,
}
impl Tabs {
pub fn new() -> Self {
let tabs = Self { idx: 0, items: vec![Tab::new(&MANAGER.cwd)] };
emit!(Refresh);
tabs
}
pub fn create(&mut self, path: &Path) -> bool {
if self.items.len() >= MAX_TABS {
return false;
}
self.items.insert(self.idx + 1, Tab::new(path));
self.set_idx(self.idx + 1);
true
}
pub fn switch(&mut self, idx: isize, rel: bool) -> bool {
let idx = if rel { self.absolute(idx) } else { idx as usize };
if idx == self.idx || idx >= self.items.len() {
return false;
}
self.set_idx(idx);
true
}
pub fn swap(&mut self, rel: isize) -> bool {
let idx = self.absolute(rel);
if idx == self.idx {
return false;
}
self.items.swap(self.idx, idx);
self.set_idx(idx);
true
}
pub fn close(&mut self, idx: usize) -> bool {
let len = self.items.len();
if len < 2 || idx as usize >= len {
return false;
}
self.items.remove(idx);
if idx <= self.idx {
self.set_idx(self.absolute(1));
}
true
}
#[inline]
fn absolute(&self, rel: isize) -> usize {
if rel > 0 {
(self.idx + rel as usize).min(self.items.len() - 1)
} else {
self.idx.saturating_sub(rel.abs() as usize)
}
}
#[inline]
fn set_idx(&mut self, idx: usize) {
self.idx = idx;
emit!(Refresh);
}
}
impl Tabs {
#[inline]
pub fn idx(&self) -> usize { self.idx }
#[inline]
pub fn len(&self) -> usize { self.items.len() }
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &Tab> { self.items.iter() }
#[inline]
pub fn active(&self) -> &Tab { &self.items[self.idx] }
#[inline]
pub(super) fn active_mut(&mut self) -> &mut Tab { &mut self.items[self.idx] }
}

156
src/core/manager/watcher.rs Normal file
View file

@ -0,0 +1,156 @@
use std::{collections::BTreeSet, path::{Path, PathBuf}, sync::Arc};
use indexmap::IndexMap;
use notify::{event::{MetadataKind, ModifyKind}, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _Watcher};
use parking_lot::RwLock;
use tokio::{fs, sync::mpsc::{self, Receiver, Sender}};
use crate::{core::files::{Files, FilesOp}, emit};
pub struct Watcher {
tx: Sender<PathBuf>,
watcher: RecommendedWatcher,
watched: Arc<RwLock<IndexMap<PathBuf, Option<PathBuf>>>>,
}
impl Watcher {
pub(super) fn start() -> Self {
let (tx, rx) = mpsc::channel(50);
let watcher = RecommendedWatcher::new(
{
let tx = tx.clone();
move |res: Result<notify::Event, notify::Error>| {
if res.is_err() {
return;
}
let event = res.unwrap();
let path = if let Some(first) = event.paths.first() {
first.clone()
} else {
return;
};
let parent = path.parent().unwrap_or(&path).to_path_buf();
match event.kind {
EventKind::Create(_) => {
tx.blocking_send(parent).ok();
}
EventKind::Modify(kind) => {
match kind {
ModifyKind::Metadata(kind) => match kind {
MetadataKind::Permissions => {}
MetadataKind::Ownership => {}
MetadataKind::Extended => {}
_ => return,
},
ModifyKind::Name(_) => {}
_ => return,
};
tx.blocking_send(path).ok();
tx.blocking_send(parent).ok();
}
EventKind::Remove(_) => {
tx.blocking_send(path).ok();
tx.blocking_send(parent).ok();
}
_ => return,
}
}
},
notify::Config::default(),
)
.unwrap();
let instance = Self { tx, watcher, watched: Default::default() };
tokio::spawn(Self::changed(rx, instance.watched.clone()));
instance
}
async fn changed(
mut rx: Receiver<PathBuf>,
watched: Arc<RwLock<IndexMap<PathBuf, Option<PathBuf>>>>,
) {
while let Some(path) = rx.recv().await {
let linked = watched
.read()
.iter()
.map_while(|(k, v)| v.as_ref().and_then(|v| path.strip_prefix(v).ok()).map(|v| k.join(v)))
.collect::<Vec<_>>();
let result = Files::read_dir(&path).await;
if linked.is_empty() {
emit!(Files(match result {
Ok(items) => FilesOp::Read(path, items),
Err(_) => FilesOp::IOErr(path),
}));
continue;
}
for ori in linked {
emit!(Files(match &result {
Ok(items) => {
let files = IndexMap::from_iter(items.iter().map(|(p, f)| {
let p = ori.join(p.strip_prefix(&path).unwrap());
let f = f.clone().set_path(&p);
(p, f)
}));
FilesOp::Read(ori, files)
}
Err(_) => FilesOp::IOErr(ori),
}));
}
}
}
pub(super) fn watch(&mut self, mut to_watch: BTreeSet<PathBuf>) {
let keys = self.watched.read().keys().cloned().collect::<BTreeSet<_>>();
for p in keys.difference(&to_watch) {
self.watcher.unwatch(p).ok();
}
for p in to_watch.clone().difference(&keys) {
if self.watcher.watch(&p, RecursiveMode::NonRecursive).is_err() {
to_watch.remove(p);
}
}
let mut todo = Vec::new();
let mut watched = self.watched.write();
*watched = IndexMap::from_iter(to_watch.into_iter().map(|k| {
if let Some(v) = watched.remove(&k) {
(k, v)
} else {
todo.push(k.clone());
(k, None)
}
}));
watched.sort_unstable_by(|_, a, _, b| b.cmp(a));
let watched = self.watched.clone();
tokio::spawn(async move {
let mut ext = IndexMap::new();
for k in todo {
match fs::canonicalize(&k).await {
Ok(v) if v != k => {
ext.insert(k, Some(v));
}
_ => {}
}
}
let mut watched = watched.write();
watched.extend(ext);
watched.sort_unstable_by(|_, a, _, b| b.cmp(a));
});
}
pub(super) fn trigger(&self, path: &Path) {
let tx = self.tx.clone();
let path = path.to_path_buf();
tokio::spawn(async move {
tx.send(path).await.ok();
});
}
}

14
src/core/mod.rs Normal file
View file

@ -0,0 +1,14 @@
pub mod adapter;
mod blocker;
mod event;
pub mod external;
pub mod files;
pub mod input;
pub mod manager;
pub mod position;
pub mod select;
pub mod tasks;
pub use blocker::*;
pub use event::*;
pub use position::*;

5
src/core/position.rs Normal file
View file

@ -0,0 +1,5 @@
pub enum Position {
Top,
Hovered,
Coords(u16, u16),
}

5
src/core/select/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod select;
pub use select::*;
pub const SELECT_PADDING: u16 = 2;

108
src/core/select/select.rs Normal file
View file

@ -0,0 +1,108 @@
use anyhow::{anyhow, Result};
use ratatui::prelude::Rect;
use tokio::sync::oneshot::Sender;
use super::SELECT_PADDING;
use crate::{core::Position, misc::tty_size};
#[derive(Default)]
pub struct Select {
title: String,
items: Vec<String>,
position: (u16, u16),
offset: usize,
cursor: usize,
callback: Option<Sender<Result<usize>>>,
pub visible: bool,
}
pub struct SelectOpt {
pub title: String,
pub items: Vec<String>,
pub position: Position,
}
impl Select {
pub fn show(&mut self, opt: SelectOpt, tx: Sender<Result<usize>>) {
self.close(false);
self.title = opt.title;
self.items = opt.items;
self.position = match opt.position {
Position::Coords(x, y) => (x, y),
_ => unimplemented!(),
};
self.callback = Some(tx);
self.visible = true;
}
pub fn close(&mut self, submit: bool) -> bool {
if let Some(cb) = self.callback.take() {
let _ = cb.send(if submit { Ok(self.cursor) } else { Err(anyhow!("canceled")) });
}
self.cursor = 0;
self.offset = 0;
self.visible = false;
true
}
pub fn next(&mut self, step: usize) -> bool {
let len = self.items.len();
if len == 0 {
return false;
}
let old = self.cursor;
self.cursor = (self.cursor + step).min(len - 1);
let limit = self.limit();
if self.cursor >= len.min(self.offset + limit) {
self.offset = len.saturating_sub(limit).min(self.offset + self.cursor - old);
}
old != self.cursor
}
pub fn prev(&mut self, step: usize) -> bool {
let old = self.cursor;
self.cursor = self.cursor.saturating_sub(step);
if self.cursor < self.offset {
self.offset = self.offset.saturating_sub(old - self.cursor);
}
old != self.cursor
}
#[inline]
pub fn window(&self) -> &[String] {
let end = (self.offset + self.limit()).min(self.items.len());
&self.items[self.offset..end]
}
#[inline]
pub fn limit(&self) -> usize {
self.items.len().min(tty_size().ws_row.saturating_sub(SELECT_PADDING).min(5) as usize)
}
}
impl Select {
#[inline]
pub fn title(&self) -> String { self.title.clone() }
#[inline]
pub fn rel_cursor(&self) -> usize { self.cursor - self.offset }
#[inline]
pub fn area(&self) -> Rect {
Rect {
x: self.position.0,
y: self.position.1 + 2,
width: 50,
height: self.limit() as u16 + SELECT_PADDING,
}
}
}

315
src/core/tasks/file.rs Normal file
View file

@ -0,0 +1,315 @@
use std::{collections::VecDeque, fs::Metadata, path::{Path, PathBuf}};
use anyhow::Result;
use futures::{future::BoxFuture, FutureExt};
use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::{info, trace};
use super::TaskOp;
use crate::misc::{calculate_size, copy_with_progress};
pub(super) struct File {
rx: async_channel::Receiver<FileOp>,
tx: async_channel::Sender<FileOp>,
sch: mpsc::UnboundedSender<TaskOp>,
}
#[derive(Debug)]
pub(super) enum FileOp {
Paste(FileOpPaste),
Link(FileOpLink),
Delete(FileOpDelete),
Trash(FileOpTrash),
}
#[derive(Clone, Debug)]
pub(super) struct FileOpPaste {
pub id: usize,
pub from: PathBuf,
pub to: PathBuf,
pub cut: bool,
pub follow: bool,
pub retry: u8,
}
#[derive(Clone, Debug)]
pub(super) struct FileOpLink {
pub id: usize,
pub from: PathBuf,
pub to: PathBuf,
pub cut: bool,
pub length: u64,
}
#[derive(Clone, Debug)]
pub(super) struct FileOpDelete {
pub id: usize,
pub target: PathBuf,
pub length: u64,
}
#[derive(Clone, Debug)]
pub(super) struct FileOpTrash {
pub id: usize,
pub target: PathBuf,
pub length: u64,
}
impl File {
pub(super) fn new(sch: mpsc::UnboundedSender<TaskOp>) -> Self {
let (tx, rx) = async_channel::unbounded();
Self { tx, rx, sch }
}
#[inline]
pub(super) async fn recv(&self) -> Result<(usize, FileOp)> {
Ok(match self.rx.recv().await? {
FileOp::Paste(t) => (t.id, FileOp::Paste(t)),
FileOp::Link(t) => (t.id, FileOp::Link(t)),
FileOp::Delete(t) => (t.id, FileOp::Delete(t)),
FileOp::Trash(t) => (t.id, FileOp::Trash(t)),
})
}
pub(super) async fn work(&self, task: &mut FileOp) -> Result<()> {
match task {
FileOp::Paste(task) => {
match fs::remove_file(&task.to).await {
Err(e) if e.kind() != NotFound => Err(e)?,
_ => {}
}
let mut it = copy_with_progress(&task.from, &task.to);
while let Some(res) = it.recv().await {
match res {
Ok(0) => {
if task.cut {
fs::remove_file(&task.from).await.ok();
}
break;
}
Ok(n) => {
trace!("Paste task advanced {}: {:?}", n, task);
self.sch.send(TaskOp::Adv(task.id, 0, n))?
}
Err(e) if e.kind() == NotFound => {
trace!("Paste task partially done: {:?}", task);
break;
}
// Operation not permitted (os error 1)
// Attribute not found (os error 93)
Err(e) if task.retry < 3 && matches!(e.raw_os_error(), Some(1) | Some(93)) => {
trace!("Paste task retry: {:?}", task);
task.retry += 1;
return Ok(self.tx.send(FileOp::Paste(task.clone())).await?);
}
Err(e) => Err(e)?,
}
}
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
}
FileOp::Link(task) => {
let src = match fs::read_link(&task.from).await {
Ok(src) => src,
Err(e) if e.kind() == NotFound => {
trace!("Link task partially done: {:?}", task);
return Ok(self.sch.send(TaskOp::Adv(task.id, 1, task.length))?);
}
Err(e) => Err(e)?,
};
match fs::remove_file(&task.to).await {
Err(e) if e.kind() != NotFound => Err(e)?,
_ => fs::symlink(src, &task.to).await?,
}
if task.cut {
fs::remove_file(&task.from).await.ok();
}
self.sch.send(TaskOp::Adv(task.id, 1, task.length))?;
}
FileOp::Delete(task) => {
if let Err(e) = fs::remove_file(&task.target).await {
if e.kind() != NotFound && fs::symlink_metadata(&task.target).await.is_ok() {
info!("Delete task failed: {:?}, {}", task, e);
Err(e)?
}
}
self.sch.send(TaskOp::Adv(task.id, 1, task.length))?
}
FileOp::Trash(task) => {
#[cfg(target_os = "macos")]
{
use trash::{macos::{DeleteMethod, TrashContextExtMacos}, TrashContext};
let mut ctx = TrashContext::default();
ctx.set_delete_method(DeleteMethod::NsFileManager);
ctx.delete(&task.target)?;
}
#[cfg(not(target_os = "macos"))]
{
trash::delete(&task.target)?;
}
self.sch.send(TaskOp::Adv(task.id, 1, task.length))?;
}
}
Ok(())
}
fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) }
pub(super) async fn paste(&self, mut task: FileOpPaste) -> Result<()> {
if task.cut {
match fs::rename(&task.from, &task.to).await {
Ok(_) => return self.done(task.id),
Err(e) if e.kind() == NotFound => return self.done(task.id),
_ => {}
}
}
let meta = Self::metadata(&task.from, task.follow).await?;
if !meta.is_dir() {
let id = task.id;
self.sch.send(TaskOp::New(id, meta.len()))?;
if meta.is_file() {
self.tx.send(FileOp::Paste(task)).await?;
} else if meta.is_symlink() {
self.tx.send(FileOp::Link(task.to_link(meta.len()))).await?;
}
return self.done(id);
}
let root = task.to.clone();
let skip = task.from.components().count();
let mut dirs = VecDeque::from([task.from]);
while let Some(src) = dirs.pop_front() {
let dest = root.join(src.components().skip(skip).collect::<PathBuf>());
match fs::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => {
info!("Create dir failed: {:?}, {}", dest, e);
continue;
}
_ => {}
}
let mut it = match fs::read_dir(&src).await {
Ok(it) => it,
Err(e) => {
info!("Read dir failed: {:?}, {}", src, e);
continue;
}
};
while let Ok(Some(entry)) = it.next_entry().await {
let src = entry.path();
let meta = if let Ok(meta) = Self::metadata(&src, task.follow).await {
meta
} else {
continue;
};
if meta.is_dir() {
dirs.push_back(src);
continue;
}
task.to = dest.join(src.file_name().unwrap());
task.from = src;
self.sch.send(TaskOp::New(task.id, meta.len()))?;
if meta.is_file() {
trace!("Paste: {:?} -> {:?}", task.from, task.to);
self.tx.send(FileOp::Paste(task.clone())).await?;
} else if meta.is_symlink() {
trace!("Link: {:?} -> {:?}", task.from, task.to);
self.tx.send(FileOp::Link(task.to_link(meta.len()))).await?;
}
}
}
self.done(task.id)
}
pub(super) async fn delete(&self, mut task: FileOpDelete) -> Result<()> {
let meta = fs::symlink_metadata(&task.target).await?;
if !meta.is_dir() {
let id = task.id;
task.length = meta.len();
self.sch.send(TaskOp::New(id, meta.len()))?;
self.tx.send(FileOp::Delete(task)).await?;
return self.done(id);
}
let mut dirs = VecDeque::from([task.target]);
while let Some(target) = dirs.pop_front() {
let mut it = match fs::read_dir(target).await {
Ok(it) => it,
Err(_) => continue,
};
while let Ok(Some(entry)) = it.next_entry().await {
let meta = match entry.metadata().await {
Ok(m) => m,
Err(_) => continue,
};
if meta.is_dir() {
dirs.push_front(entry.path());
continue;
}
task.target = entry.path();
task.length = meta.len();
self.sch.send(TaskOp::New(task.id, meta.len()))?;
self.tx.send(FileOp::Delete(task.clone())).await?;
}
}
self.done(task.id)
}
pub(super) async fn trash(&self, mut task: FileOpTrash) -> Result<()> {
let id = task.id;
task.length = calculate_size(&task.target).await;
self.sch.send(TaskOp::New(id, task.length))?;
self.tx.send(FileOp::Trash(task)).await?;
self.done(id)
}
async fn metadata(path: &Path, follow: bool) -> io::Result<Metadata> {
if !follow {
return fs::symlink_metadata(path).await;
}
let meta = fs::metadata(path).await;
if meta.is_ok() { meta } else { fs::symlink_metadata(path).await }
}
pub(super) fn remove_empty_dirs(dir: &Path) -> BoxFuture<()> {
trace!("Remove empty dirs: {:?}", dir);
async move {
let mut it = match fs::read_dir(dir).await {
Ok(it) => it,
Err(_) => return,
};
while let Ok(Some(entry)) = it.next_entry().await {
if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
let path = entry.path();
Self::remove_empty_dirs(&path).await;
fs::remove_dir(path).await.ok();
}
}
fs::remove_dir(dir).await.ok();
}
.boxed()
}
}
impl FileOpPaste {
fn to_link(&self, length: u64) -> FileOpLink {
FileOpLink { id: self.id, from: self.from.clone(), to: self.to.clone(), cut: self.cut, length }
}
}

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