Merge remote-tracking branch 'upstream/main' into improve-rg-search

This commit is contained in:
Jed 2026-07-25 09:00:53 +02:00
commit 2bc1bf126a
412 changed files with 8443 additions and 4086 deletions

View file

@ -19,3 +19,6 @@ If it has already been detailed in the associated issue, please skip this sectio
## 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,4 +1,4 @@
name: Validate Form
name: Validate Issue
on:
issues:
@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: 20
@ -24,7 +24,7 @@ jobs:
cd scripts/validate-form
npm ci
- name: Validate Form
- name: Validate Issue
uses: actions/github-script@v9
with:
script: |

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

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

45
AGENTS.md Normal file
View file

@ -0,0 +1,45 @@
# AGENTS.md
## Rules
- Applies repo-wide. Keep changes scoped; do not create issues or pull requests, or post comments.
## Project
- Rust 2024 Cargo workspace containing all `yazi-*` crates; default members are `yazi-fm` (`yazi`) and `yazi-cli` (`ya`).
## Style
- Follow nearby code and use idiomatic Rust and Lua. Rust uses `snake_case` for modules, functions, and fields and `PascalCase` for types, traits, and variants. Lua uses PascalCase component tables, `local M` plugin modules, `snake_case` methods/locals, and `_name` private fields.
- Preserve established terms and type families: `Url`/`UrlBuf`/`UrlCow`, `PathDyn`/`PathBufDyn`/`PathCow`, `*Ref`, `*Arc`, `*Opt`, `*State`, `*Job`, `*Prog`, `File`, `Folder`, `Tab`, `Mgr`, and `Task`. Use `Url` for logical locations and `Path` for filesystem paths.
- Reuse established plugin and event names (`fetch`, `preload`, `peek`, `seek`, `spot`, `entry`, `setup`, `yank`, `hover`, and `select`) across Rust, Lua, and configuration.
- Use Rust prefixes (`as_`, `to_`, `into_`, `try_`, `is_`, `has_`) according to their usual semantics; prefer descriptive names.
- Name variables, modules, methods, and other symbols simply, elegantly, and expressively. Be creative while keeping names clear, consistent with established terminology, and idiomatic.
- When passing arguments, use the parameter's conversion traits directly (such as `Into<_>` or `AsRef<_>`); avoid eager conversions like `.to_string()`, `.to_owned()`, and `.as_ref()` unless ownership, type inference, or semantics require them.
- Prefer general-purpose traits and conversion APIs already provided by the codebase or its dependencies over manual construction or adapter closures; for example, use `into_lua()` where applicable.
## Code Changes
- Search and reuse first. For new features, extend existing infrastructure or data structures with general, reusable capabilities when that keeps the final code concise.
- For refactors, inspect the whole target module and its callers first. Look for duplicated work, redundant I/O, underpowered return values, one-use wrappers, and reusable cross-platform abstractions; implement high-confidence, behavior-preserving simplifications while preserving error, fallback, and platform semantics.
- Keep diffs minimal and avoid unrelated refactors. Prefer clear code over custom patterns or comments; comment only behavior the code cannot explain.
- Put reusable code in the lowest suitable shared layer; avoid unnecessary dependencies and allocations. Prefer borrowed values and existing wrappers.
- Use stable Rust APIs; nightly is formatting-only. Use only `pub`, `pub(super)`, and `pub(crate)`—never `pub(in ...)`.
- Keep async I/O non-blocking, preserve platform/fork behavior, and follow existing error boundaries with `?`.
- For renames or refactors, update all related variables, functions, parameters, modules, methods, types, derived types, exports, tests, configuration keys, documentation, and Lua bindings.
- Do not add or modify tests unless requested.
## Validation
- Prefer targeted debug checks; use multiple `-p` flags for affected crates before the whole workspace.
```sh
cargo check -p <package>
cargo test -p <package>
cargo clippy -p <package>
find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} +
stylua --color always --check .
```
- Use `cargo check` instead of `cargo build` unless artifacts are needed. Do not use `--release` unless requested; use `scripts/build.sh <target>` for release or cross-target packaging.
- Run relevant existing tests when needed, then inspect `git diff` and verify that only intended files changed.

View file

@ -15,8 +15,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Added
- Drag and drop ([#4005])
- Trash bin ([#4144])
- Bulk create ([#3793])
- Make help menu a command palette ([#4074])
- Input history ([#4104])
- Experimental `%y`, `%Y`, `%t`, `%T`, `%yN`, `%YN`, `%tN`, `%TN` shell formatting parameters ([#4108])
- Custom VFS provider ([#4118])
- Make visual mode support wraparound scrolling ([#4101])
- H/M/L Vim-like motion for moving cursor relative to viewport ([#3970])
- Context-aware icons for inputs ([#4080])
@ -28,7 +32,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Changed
- Rename SFTP sections in `vfs.toml` from `[services.domain]` to `[sftp.domain]` ([#4120]).
- Rename `<BackTab>` to `<S-Tab>` ([#3989])
- Remove `Url.is_archive` - `archive://` is no longer built in and can now be registered by plugins ([#4118])
- Make `mgr::Yanked`, `tab::Selected`, and the `@yank` DDS event return `File` instead of `Url` from `__pairs()` ([#4096])
- Remove `help:filter` action since the filter input is now always available ([#4074])
- `[help]` of `theme.toml`: supersede `on` with `chord`, supersede `run` and `desc` with `action`, remove `footer` ([#4074])
@ -38,6 +44,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Deprecate `backward --far` and `forward --far` in favor of `backward wide` and `forward wide`, respectively ([#4012])
- Deprecate `tab::Mode.is_visual` in favor of the new `tab::Mode.is_normal` ([#4101])
- Deprecate `Url.is_regular`, `Url.is_search`, and `Url.domain` in favor of `Url.spec.is_regular`, `Url.spec.is_search`, and `Url.spec.domain`, respectively ([#4118])
### Fixed
@ -1771,3 +1778,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#4080]: https://github.com/sxyazi/yazi/pull/4080
[#4096]: https://github.com/sxyazi/yazi/pull/4096
[#4101]: https://github.com/sxyazi/yazi/pull/4101
[#4104]: https://github.com/sxyazi/yazi/pull/4104
[#4108]: https://github.com/sxyazi/yazi/pull/4108
[#4118]: https://github.com/sxyazi/yazi/pull/4118
[#4120]: https://github.com/sxyazi/yazi/pull/4120
[#4144]: https://github.com/sxyazi/yazi/pull/4144

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
AGENTS.md

726
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -36,26 +36,28 @@ debug = false
[workspace.dependencies]
ansi-to-tui = "8.0.1"
anyhow = "1.0.103"
anyhow = "1.0.104"
arc-swap = { version = "1.9.2", features = [ "serde" ] }
base64 = "0.22.1"
bitflags = { version = "2.13.0", features = [ "serde" ] }
base64 = "0.23.0"
bitflags = { version = "2.13.1", features = [ "serde" ] }
chrono = "0.4.45"
clap = { version = "4.6.1", features = [ "derive" ] }
clap = { version = "4.6.4", 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" }
either = { version = "1.17.0" }
foldhash = "0.2.0"
futures = "0.3.32"
globset = "0.4.18"
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.0"
mlua = { version = "0.11.6", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] }
libc = "0.2.189"
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"
@ -64,21 +66,21 @@ 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.12.4"
russh = { version = "0.62.1", default-features = false, features = [ "ring", "rsa" ] }
regex = "1.13.1"
russh = { version = "0.62.4", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] }
serde_json = "1.0.150"
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.18"
tokio = { version = "1.52.3", features = [ "full" ] }
tokio-stream = "0.1.18"
tokio-util = "0.7.18"
toml = { version = "1.1.2" }
thiserror = "2.0.19"
tokio = { version = "1.53.1", features = [ "full" ] }
tokio-stream = "0.1.19"
tokio-util = "0.7.19"
toml = { version = "1.1.3" }
tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
typed-path = "0.12.3"
unicode-width = { version = "0.2.2", default-features = false }
uzers = "0.12.2"

View file

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

12
flake.lock generated
View file

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1779877693,
"narHash": "sha256-NOF9NAREhxr50bbBfVcVOq+ArCMSoe8dP79Pk2uyARk=",
"lastModified": 1783279667,
"narHash": "sha256-/NAkDSsve+GNM0Bt6tleJdCGfsTlK89nPjkVOzZMo0s=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4100e830e085863741bc69b156ec4ccd53ab5be0",
"rev": "f205b5574fd0cb7da5b702a2da51507b7f4fdd1b",
"type": "github"
},
"original": {
@ -48,11 +48,11 @@
]
},
"locked": {
"lastModified": 1779851998,
"narHash": "sha256-UkkMh3bX9QW4Luqkm98nUaOqKWrU6i65mUnph3WeSSw=",
"lastModified": 1783320166,
"narHash": "sha256-l7C/OsjcnWDOk2K3ssj+SBduwL67LashjBqis9+t468=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "6cddd512fa2bf7231f098d3a2f92f6e4cff71e0a",
"rev": "20ee15370c9256669d66968b89ee20a4b0a4e673",
"type": "github"
},
"original": {

View file

@ -2,6 +2,15 @@ const LABEL_NAME = "needs info"
const RE_VERSION = /Yazi\s+Version\s*:\s\d+\.\d+\.\d+\s\(/gm
const RE_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)) {
@ -41,6 +50,12 @@ Our maintainers work on Yazi in their free time, this helps them work efficientl
`
}
function skipValidation(context) {
const login = context.payload.issue?.user?.login || context.payload.pull_request?.user?.login
const owner = context.payload.repository?.owner?.login || context.repo.owner
return !!login && login === owner
}
module.exports = async ({ github, context, core }) => {
async function nightlyHash() {
try {
@ -214,19 +229,23 @@ Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.y
}
async function main() {
const hash = await nightlyHash()
if (!hash) return
if (context.eventName === "schedule") {
await closeOldIssues()
return
}
if (skipValidation(context)) {
return
}
if (context.eventName === "issues") {
const id = context.payload.issue.number
const content = context.payload.issue.body || ""
const creator = context.payload.issue.user.login
const hash = await nightlyHash()
if (!hash) return
if (await hasLabel(id, "bug")) {
const body = bugReportBody(creator, content, hash)
await updateLabels(id, !!body, body)
@ -236,6 +255,13 @@ Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.y
} else if (context.payload.action === "opened") {
await closeUnsupportedIssue(id)
}
} else if (context.eventName === "pull_request" || context.eventName === "pull_request_target") {
const content = context.payload.pull_request.body || ""
const body = pullRequestBody(content)
if (body) {
core.setFailed(body)
}
}
}

View file

@ -2,7 +2,7 @@ use std::process;
use anyhow::Result;
use yazi_boot::ARGS;
use yazi_fs::provider::{Provider, local::Local};
use yazi_fs::engine::{Engine, local::Local};
use yazi_parser::app::QuitForm;
use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}};
use yazi_tui::Raterm;

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use mlua::Value;
use mlua::{LuaString, Value};
use ratatui_core::layout::Position;
use tracing::error;
use yazi_actor::lives::Lives;
@ -30,7 +30,7 @@ impl Actor for Reflow {
continue;
};
let id: mlua::String = t.get("_id")?;
let id: LuaString = t.get("_id")?;
match &*id.as_bytes() {
b"current" => layout.current = *t.raw_get::<yazi_binding::elements::Rect>("_area")?,
b"preview" => layout.preview = *t.raw_get::<yazi_binding::elements::Rect>("_area")?,

View file

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

View file

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

View file

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

View file

@ -108,6 +108,9 @@ impl<'a> Ctx<'a> {
#[inline]
pub fn hovered(&self) -> Option<&File> { self.tab().hovered() }
#[inline]
pub fn hovered_url(&self) -> Option<&UrlBuf> { self.tab().hovered_url() }
#[inline]
pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() }

View file

@ -1,4 +1,5 @@
use anyhow::Result;
use yazi_core::input::InputMutGuard;
use yazi_macro::{act, render, succ};
use yazi_parser::{input::CloseForm, spark::SparkKind};
use yazi_shared::{Source, data::Data};
@ -20,11 +21,16 @@ impl Actor for Close {
guard.ticket.next();
if let Some(cb) = guard.cb.take() {
let value = guard.snap().value.clone();
let value = guard.value().to_owned();
cb(if form.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) });
}
drop(guard);
if form.submit
&& let InputMutGuard::Main(input) = guard
{
input.histories.remember(&input.main.history.name, input.main.value());
}
cx.input.main.visible = false;
act!(cmp:close, cx)?;

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
use std::ops::Deref;
use mlua::{AnyUserData, IntoLua, MetaMethod, UserData, UserDataMethods, UserDataRef, Value};
use mlua::{AnyUserData, IntoLua, LuaString, MetaMethod, UserData, UserDataMethods, UserDataRef, Value};
use paste::paste;
use super::{Lives, PtrCell};
@ -43,7 +43,7 @@ impl Core {
impl UserData for Core {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: mlua::String| {
methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: LuaString| {
macro_rules! reuse {
($key:ident, $value:expr) => {
match paste! { &me.[<c_ $key>] } {

View file

@ -4,7 +4,7 @@ use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Range, style::Style};
use yazi_config::THEME;
use yazi_fs::file::FileInventory;
use yazi_shared::{path::AsPath, url::UrlLike};
use yazi_shared::{path::DynPath, url::UrlLike};
use super::{FILE_CACHE, Lives};
use crate::lives::{CoreRef, PtrCell};
@ -73,7 +73,11 @@ impl UserData for File {
Ok(yazi_config::THEME.icon.matches(me, me.is_hovered()))
});
methods.add_method("size", |_, me, ()| {
Ok(if me.is_dir() { me.folder.entries.sizes.get(&me.urn()).copied() } else { Some(me.len) })
Ok(if me.is_dir() {
me.folder.entries.sizes.get(&me.entry_key()).copied()
} else {
Some(me.len)
})
});
methods.add_method("mime", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?;
@ -86,7 +90,7 @@ impl UserData for File {
let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components();
comp.next_back();
Some(lua.create_string(comp.as_path().encoded_bytes())).transpose()
Some(lua.create_string(comp.dyn_path().encoded_bytes())).transpose()
});
methods.add_method("style", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?;
@ -122,7 +126,7 @@ impl UserData for File {
return Ok(None);
};
let Some(idx) = finder.matched_idx(&me.folder, me.urn()) else {
let Some(idx) = finder.matched_idx(&me.folder, me.entry_key()) else {
return Ok(None);
};

View file

@ -4,16 +4,16 @@ use anyhow::{Result, anyhow};
use scopeguard::defer;
use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRuleArc};
use yazi_fs::{FilesOp, Splatter, file::File, provider::{Provider, local::Local}};
use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}};
use yazi_macro::{succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy};
use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, UrlBuf, UrlCow, UrlLike}};
use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt};
use yazi_shared::{data::Data, strand::Strand, url::{UrlBuf, UrlLike}};
use yazi_shim::path::CROSS_SEPARATOR;
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_tty::{TTY, sequence::EraseScreen};
use yazi_vfs::{VfsFile, provider};
use yazi_vfs::engine;
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
@ -31,7 +31,7 @@ impl Actor for BulkCreate {
let cwd = cx.cwd().clone();
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk-create");
provider::create_new(&tmp).await?;
let file = engine::create_new(&tmp).await?.file().await?;
defer! {
let tmp = tmp.clone();
@ -40,13 +40,12 @@ impl Actor for BulkCreate {
});
}
TasksProxy::process_exec(
cwd.clone(),
Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run),
vec![UrlCow::default(), UrlBuf::from(&tmp).into()],
opener.block,
opener.orphan,
)
TasksProxy::process_exec(ShellOpt {
cwd: cwd.clone(),
cmd: Splatter::new(&[file]).splat(&opener.run),
block: opener.block,
orphan: opener.orphan,
})
.await;
let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume());
@ -77,17 +76,17 @@ impl BulkCreate {
};
let result: io::Result<()> = if entry.is_dir {
provider::create_dir_all(&dist).await
engine::create_dir_all(&dist).await
} else if let Some(parent) = dist.parent() {
provider::create_dir_all(parent).await.ok();
provider::create_new(&dist).await.map(|_| ())
engine::create_dir_all(parent).await.ok();
engine::create_new(&dist).await.map(|_| ())
} else {
Err(io::Error::other("No parent directory"))
};
if let Err(e) = result {
failed.push((entry, e.into()));
} else if let Ok(f) = File::new(dist).await {
} else if let Ok(f) = engine::file(dist).await {
succeeded.push(f);
} else {
failed.push((entry, anyhow!("Failed to retrieve file info")));

View file

@ -7,15 +7,15 @@ use tokio::io::AsyncWriteExt;
use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRuleArc};
use yazi_dds::Pubsub;
use yazi_fs::{FilesOp, Splatter, file::File, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}};
use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::Local}, max_common_root, path::skip_url};
use yazi_macro::{err, succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy};
use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlCow, UrlLike}};
use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt};
use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_tty::{TTY, sequence::EraseScreen};
use yazi_vfs::{VfsFile, maybe_exists, provider};
use yazi_vfs::{engine::{self, Demand}, maybe_exists};
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
@ -46,13 +46,8 @@ impl Actor for BulkRename {
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk-rename");
Gate::default()
.write(true)
.create_new(true)
.open(&tmp)
.await?
.write_all(old.join(Strand::Utf8("\n")).encoded_bytes())
.await?;
let mut rw = Demand::default().write(true).create_new(true).open(&tmp).await?;
rw.write_all(old.join(Strand::Utf8("\n")).encoded_bytes()).await?;
defer! {
let tmp = tmp.clone();
@ -63,13 +58,12 @@ impl Actor for BulkRename {
}
batcher.prime(&tmp);
TasksProxy::process_exec(
TasksProxy::process_exec(ShellOpt {
cwd,
Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run),
vec![UrlCow::default(), UrlBuf::from(&tmp).into()],
opener.block,
opener.orphan,
)
cmd: Splatter::new(&[rw.into_file().await?]).splat(&opener.run),
block: opener.block,
orphan: opener.orphan,
})
.await;
let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume());
@ -128,11 +122,11 @@ impl BulkRename {
continue;
};
if maybe_exists(&new).await && !provider::must_identical(&old, &new).await {
if maybe_exists(&new).await && !engine::must_identical(&old, &new).await {
failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = provider::rename(&old, &new).await {
} else if let Err(e) = engine::rename(&old, &new).await {
failed.push((o, n, e.into()));
} else if let Ok(f) = File::new(new).await {
} else if let Ok(f) = engine::file(new).await {
succeeded.insert(old, f);
} else {
failed.push((o, n, anyhow!("Failed to retrieve file info")));

View file

@ -6,12 +6,12 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::YAZI;
use yazi_core::mgr::CdSource;
use yazi_dds::Pubsub;
use yazi_fs::{FilesOp, file::File, path::{clean_url, expand_url}};
use yazi_fs::{FilesOp, path::{clean_url, expand_url}};
use yazi_macro::{act, err, input, render, succ};
use yazi_parser::mgr::CdForm;
use yazi_proxy::{CmpProxy, MgrProxy};
use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, provider};
use yazi_vfs::engine;
use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx};
@ -73,16 +73,16 @@ impl Cd {
match result {
InputEvent::Submit(s) => {
let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return };
let Ok(url) = provider::absolute(&url).await else { return };
let Ok(url) = engine::absolute(&url).await else { return };
let url = clean_url(url);
let Ok(file) = File::new(&url).await else { return };
let Ok(file) = engine::file(&url).await else { return };
if file.is_dir() {
return MgrProxy::cd(&url, CdSource::Cd);
}
if let Some(p) = url.parent() {
FilesOp::Upserting(p.into(), [(url.urn().into(), file)].into()).emit();
if let Some((p, k)) = url.pair2() {
FilesOp::Upserting(p.into(), [(k.into(), file)].into()).emit();
}
MgrProxy::reveal(url);
}

View file

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

View file

@ -9,7 +9,7 @@ use yazi_macro::{input, ok_or_not_found, succ};
use yazi_parser::mgr::CreateForm;
use yazi_proxy::{ConfirmProxy, MgrProxy};
use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, strand::{StrandBuf, StrandLike}, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, provider};
use yazi_vfs::{VfsFile, engine};
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
@ -62,26 +62,26 @@ impl Create {
let _permit = WATCHER.acquire().await.unwrap();
if dir {
provider::create_dir_all(&new).await?;
} else if let Ok(real) = provider::casefold(&new).await
&& let Some((parent, urn)) = real.pair()
engine::create_dir_all(&new).await?;
} else if let Ok(real) = engine::casefold(&new).await
&& let Some((parent, key)) = real.pair2()
{
ok_or_not_found!(provider::remove_file(&new).await);
FilesOp::Deleting(parent.into(), [urn.into()].into()).emit();
provider::create(&new).await?;
ok_or_not_found!(engine::remove_file(&new).await);
FilesOp::Deleting(parent.into(), [key.into()].into()).emit();
engine::create(&new).await?;
} else if let Some(parent) = new.parent() {
provider::create_dir_all(parent).await.ok();
ok_or_not_found!(provider::remove_file(&new).await);
provider::create(&new).await?;
engine::create_dir_all(parent).await.ok();
ok_or_not_found!(engine::remove_file(&new).await);
engine::create(&new).await?;
} else {
bail!("Cannot create file at root");
}
if let Ok(real) = provider::casefold(&new).await
&& let Some((parent, urn)) = real.pair()
if let Ok(real) = engine::casefold(&new).await
&& let Some((parent, key)) = real.pair2()
{
let file = File::new(&real).await?;
FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()).emit();
let file = engine::file(&real).await?;
FilesOp::Upserting(parent.into(), [(key.into(), file)].into()).emit();
MgrProxy::reveal(&real);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ use yazi_macro::{act, err, input, ok_or_not_found, succ};
use yazi_parser::mgr::RenameForm;
use yazi_proxy::{ConfirmProxy, MgrProxy};
use yazi_shared::{data::Data, id::Id, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, provider};
use yazi_vfs::{VfsFile, engine};
use yazi_watcher::WATCHER;
use yazi_widgets::input::InputEvent;
@ -66,30 +66,30 @@ impl Actor for Rename {
impl Rename {
async fn r#do(tab: Id, old: UrlBuf, new: UrlBuf) -> Result<()> {
let Some((old_p, old_n)) = old.pair() else { return Ok(()) };
let Some(_) = new.pair() else { return Ok(()) };
let Some((old_p, old_k)) = old.pair2() else { return Ok(()) };
let Some(_) = new.pair2() else { return Ok(()) };
let _permit = WATCHER.acquire().await.unwrap();
let overwritten = provider::casefold(&new).await;
provider::rename(&old, &new).await?;
let overwritten = engine::casefold(&new).await;
engine::rename(&old, &new).await?;
if let Ok(u) = overwritten
&& u != new
&& let Some((parent, urn)) = u.pair()
&& let Some((parent, key)) = u.pair2()
{
ok_or_not_found!(provider::rename(&u, &new).await);
FilesOp::Deleting(parent.to_owned(), [urn.into()].into()).emit();
ok_or_not_found!(engine::rename(&u, &new).await);
FilesOp::Deleting(parent.to_owned(), [key.into()].into()).emit();
}
let new = provider::casefold(&new).await?;
let Some((new_p, new_n)) = new.pair() else { return Ok(()) };
let new = engine::casefold(&new).await?;
let Some((new_p, new_k)) = new.pair2() else { return Ok(()) };
let file = File::new(&new).await?;
let file = engine::file(&new).await?;
if new_p == old_p {
FilesOp::Upserting(old_p.into(), [(old_n.into(), file)].into()).emit();
FilesOp::Upserting(old_p.into(), [(old_k.into(), file)].into()).emit();
} else {
FilesOp::Deleting(old_p.into(), [old_n.into()].into()).emit();
FilesOp::Upserting(new_p.into(), [(new_n.into(), file)].into()).emit();
FilesOp::Deleting(old_p.into(), [old_k.into()].into()).emit();
FilesOp::Upserting(new_p.into(), [(new_k.into(), file)].into()).emit();
}
MgrProxy::reveal(&new);
@ -103,7 +103,7 @@ impl Rename {
};
Ok(
provider::must_identical(old, new).await
engine::must_identical(old, new).await
|| ConfirmProxy::show(ConfirmCfg::overwrite(&file)).await,
)
}

View file

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

View file

@ -5,7 +5,7 @@ use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::YAZI;
use yazi_core::mgr::{CdSource, SearchVia};
use yazi_fs::{FilesOp, cha::Cha};
use yazi_fs::{FilesOp, cha::ChaType, file::File};
use yazi_macro::{act, input, succ};
use yazi_parser::{VoidForm, mgr::SearchForm};
use yazi_plugin::external;
@ -89,7 +89,7 @@ impl Actor for SearchDo {
while let Some(chunk) = rx.next().await {
FilesOp::Part(cwd.clone(), chunk, ticket).emit();
}
FilesOp::Done(cwd, Cha::default(), ticket).emit();
FilesOp::Done(File::from_dummy(cwd, Some(ChaType::Dir)), ticket).emit();
Ok(())
}));

View file

@ -2,10 +2,12 @@ use std::borrow::Cow;
use anyhow::Result;
use yazi_config::YAZI;
use yazi_core::mgr::MgrSnap;
use yazi_fs::Splatter;
use yazi_macro::{act, input, succ};
use yazi_parser::mgr::ShellForm;
use yazi_proxy::TasksProxy;
use yazi_scheduler::process::ProcessOpt;
use yazi_scheduler::process::ShellOpt;
use yazi_shared::data::Data;
use yazi_widgets::input::InputEvent;
@ -22,7 +24,7 @@ impl Actor for Shell {
act!(mgr:escape_visual, cx)?;
let cwd = form.cwd.take().unwrap_or_else(|| cx.cwd().clone());
let selected: Vec<_> = cx.tab().hovered_and_selected().cloned().map(Into::into).collect();
let snap = MgrSnap::from(&cx.mgr);
let input = if form.interactive {
Some(input!(
@ -44,13 +46,11 @@ impl Actor for Shell {
return;
}
TasksProxy::open_shell_compat(ProcessOpt {
TasksProxy::process_open(ShellOpt {
cwd,
cmd: form.run.to_string().into(),
args: selected,
cmd: Splatter::new(snap).splat(&*form.run),
block: form.block,
orphan: form.orphan,
spread: true,
});
});

View file

@ -4,6 +4,7 @@ use yazi_fs::{FilesSorter, FolderStage};
use yazi_macro::{act, render, render_and, succ};
use yazi_parser::{mgr::SortForm, spark::SparkKind};
use yazi_shared::{Source, data::Data};
use yazi_shim::OptionExt;
use crate::{Actor, Ctx};
@ -24,7 +25,7 @@ impl Actor for Sort {
pref.sort_fallback = form.fallback.unwrap_or(pref.sort_fallback);
let sorter = FilesSorter::from(&*pref);
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let hovered = cx.hovered().map(|f| f.entry_key()).owned();
let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading {
render!();
@ -36,9 +37,7 @@ impl Actor for Sort {
};
// Apply to CWD and parent
if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply))
&& (a | b)
{
if apply(cx.current_mut()) | cx.parent_mut().is_some_and(apply) {
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.entries);
@ -50,7 +49,7 @@ impl Actor for Sort {
{
render!(h.repos(None));
act!(mgr:peek, cx, true)?;
} else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) {
} else if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) {
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}

View file

@ -13,7 +13,7 @@ impl Actor for Stash {
const NAME: &str = "stash";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
if form.target.is_absolute() && form.target.is_internal() {
if form.target.is_absolute() && !form.target.is_search() {
cx.tab_mut().backstack.push(form.target.as_url());
}

View file

@ -57,12 +57,12 @@ impl UpdateFiles {
fn update_parent(cx: &mut Ctx, op: FilesOp) -> Result<Data> {
let tab = cx.tab_mut();
let urn = tab.current.url.urn();
let leave = matches!(op, FilesOp::Deleting(_, ref urns) if urns.contains(&urn));
let key = tab.current.url.entry_key();
let leave = matches!(op, FilesOp::Deleting(_, ref keys) if keys.contains(&key));
if let Some(f) = tab.parent.as_mut() {
render!(f.update_pub(tab.id, op));
render!(f.hover(urn));
render!(f.hover(key));
}
if leave {
@ -97,8 +97,8 @@ impl UpdateFiles {
fn update_history(cx: &mut Ctx, op: FilesOp) -> Result<Data> {
let tab = &mut cx.tab_mut();
let leave = tab.parent.as_ref().and_then(|f| f.url.parent().map(|p| (p, f.url.urn()))).is_some_and(
|(p, n)| matches!(op, FilesOp::Deleting(ref parent, ref urns) if *parent == p && urns.contains(&n)),
let leave = tab.parent.as_ref().and_then(|f| f.url.pair2()).is_some_and(
|(pp, key)| matches!(&op, FilesOp::Deleting(parent, keys) if parent == pp && keys.contains(&key)),
);
tab.history.get_or_insert_with(op.cwd(), |u| Folder::from(u)).update_pub(tab.id, op);

View file

@ -13,7 +13,7 @@ impl Actor for UpdatePeeked {
const NAME: &str = "update_peeked";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some(hovered) = cx.hovered().map(|h| &h.url) else {
let Some(hovered) = cx.hovered_url() else {
succ!(cx.tab_mut().preview.reset());
};

View file

@ -14,7 +14,7 @@ impl Actor for UpdateSpotted {
fn act(cx: &mut Ctx, mut form: Self::Form) -> Result<Data> {
let tab = cx.tab_mut();
let Some(hovered) = tab.hovered().map(|h| &h.url) else {
let Some(hovered) = tab.hovered_url() else {
succ!(tab.spot.reset());
};

View file

@ -15,9 +15,11 @@ impl Actor for Watch {
const NAME: &str = "watch";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let it = iter::once(cx.core.mgr.tabs.active().cwd())
.chain(cx.core.mgr.tabs.parent().map(|p| &p.url))
.chain(cx.core.mgr.tabs.hovered().filter(|h| h.is_dir()).map(|h| &h.url));
let tab = cx.core.mgr.tabs.active();
let it = iter::once(&tab.current.file)
.chain(tab.hovered_folder().map(|h| &h.file).or(tab.hovered().filter(|f| f.is_dir())))
.chain(tab.parent.as_ref().map(|p| &p.file));
cx.core.mgr.watcher.watch(it);
succ!();

View file

@ -1 +1 @@
yazi_macro::mod_flat!(arrow cancel close inspect open_shell_compat process_open show spawn update_succeed);
yazi_macro::mod_flat!(arrow cancel close inspect process_open show spawn update_succeed);

View file

@ -1,19 +0,0 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::tasks::ProcessOpenForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct OpenShellCompat;
// TODO: remove
impl Actor for OpenShellCompat {
type Form = ProcessOpenForm;
const NAME: &str = "open_shell_compat";
fn act(cx: &mut Ctx, Self::Form { opt, .. }: Self::Form) -> Result<Data> {
succ!(cx.tasks.open_shell_compat(opt));
}
}

View file

@ -19,10 +19,10 @@ impl Actor for UpdateSucceed {
if form.track
&& form.id == cx.tasks.scheduler.behavior.first_id()
&& let Some((parent, urn)) = form.urls[0].pair()
&& let Some((parent, key)) = form.urls[0].pair2()
&& parent == *cx.cwd()
{
cx.current_mut().trace = Some(urn.into());
cx.current_mut().trace = Some(key.into());
act!(mgr:hover, cx)?;
}

View file

@ -28,7 +28,7 @@ anyhow = { workspace = true }
base64 = { workspace = true }
image = { workspace = true }
inventory = { workspace = true }
moxcms = "0.8.1"
moxcms = "0.9.0"
palette = { version = "0.7.6", default-features = false }
quantette = { version = "0.6.0", default-features = false }
ratatui-core = { workspace = true }

View file

@ -5,7 +5,7 @@ use image::{DynamicImage, ImageDecoder, ImageError, ImageReader, Limits, codecs:
use ratatui_core::layout::Rect;
use yazi_config::YAZI;
use yazi_emulator::Dimension;
use yazi_fs::provider::{Provider, local::Local};
use yazi_fs::engine::{Engine, local::Local};
use crate::Icc;

View file

@ -1,16 +1,27 @@
use mlua::{ExternalError, FromLua, IntoLua, IntoLuaMulti, UserData, UserDataMethods, Value};
use mlua::{FromLua, IntoLua, IntoLuaMulti, UserData, UserDataMethods, Value};
use yazi_codegen::FromLuaOwned;
use crate::Error;
#[derive(FromLuaOwned)]
pub struct MpscTx<T: FromLua + 'static>(pub tokio::sync::mpsc::Sender<T>);
pub struct MpscTx<T: FromLua + 'static, U: 'static = T> {
tx: tokio::sync::mpsc::Sender<U>,
f: fn(T) -> U,
}
pub struct MpscRx<T: IntoLua + 'static>(pub tokio::sync::mpsc::Receiver<T>);
impl<T: FromLua> UserData for MpscTx<T> {
impl<T: FromLua> MpscTx<T> {
pub fn new(tx: tokio::sync::mpsc::Sender<T>) -> Self { Self { tx, f: |v| v } }
}
impl<T: FromLua, U> MpscTx<T, U> {
pub fn map(tx: tokio::sync::mpsc::Sender<U>, f: fn(T) -> U) -> Self { Self { tx, f } }
}
impl<T: FromLua, U: 'static> UserData for MpscTx<T, U> {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method("send", |lua, me, value: Value| async move {
match me.0.send(T::from_lua(value, &lua)?).await {
match me.tx.send((me.f)(T::from_lua(value, &lua)?)).await {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua),
}
@ -54,16 +65,13 @@ impl<T: IntoLua + 'static> UserData for MpscUnboundedRx<T> {
}
#[derive(FromLuaOwned)]
pub struct OneshotTx<T: FromLua + 'static>(pub Option<tokio::sync::oneshot::Sender<T>>);
pub struct OneshotRx<T: IntoLua + 'static>(pub Option<tokio::sync::oneshot::Receiver<T>>);
pub struct OneshotTx<T: FromLua + 'static>(pub tokio::sync::oneshot::Sender<T>);
pub struct OneshotRx<T: IntoLua + 'static>(pub tokio::sync::oneshot::Receiver<T>);
impl<T: FromLua> UserData for OneshotTx<T> {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method_mut("send", |lua, me, value: Value| {
let Some(tx) = me.0.take() else {
return Err("Oneshot sender already used".into_lua_err());
};
match tx.send(T::from_lua(value, lua)?) {
methods.add_method_once("send", |lua, me, value: Value| {
match me.0.send(T::from_lua(value, lua)?) {
Ok(()) => true.into_lua_multi(lua),
Err(_) => (false, Error::custom("Oneshot receiver closed")).into_lua_multi(lua),
}
@ -73,11 +81,8 @@ impl<T: FromLua> UserData for OneshotTx<T> {
impl<T: IntoLua + 'static> UserData for OneshotRx<T> {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method_mut("recv", |lua, mut me, ()| async move {
let Some(rx) = me.0.take() else {
return Err("Oneshot receiver already used".into_lua_err());
};
match rx.await {
methods.add_async_method_once("recv", |lua, me, ()| async move {
match me.0.await {
Ok(value) => value.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua),
}

View file

@ -1,5 +1,5 @@
use hashbrown::HashMap;
use mlua::{Lua, MetaMethod, UserData, UserDataMethods, Value};
use mlua::{Lua, LuaString, MetaMethod, UserData, UserDataMethods, Value};
pub type ComposerGet = fn(&Lua, &[u8]) -> mlua::Result<Value>;
pub type ComposerSet = fn(&Lua, &[u8], Value) -> mlua::Result<Value>;
@ -24,7 +24,7 @@ where
S: Fn(&Lua, &[u8], Value) -> mlua::Result<Value> + 'static,
{
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: mlua::String| {
methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: LuaString| {
let key = key.as_bytes();
if let Some(v) = me.cache.get(key.as_ref()) {
return Ok(v.clone());
@ -38,7 +38,7 @@ where
methods.add_meta_method_mut(
MetaMethod::NewIndex,
|lua, me, (key, value): (mlua::String, Value)| {
|lua, me, (key, value): (LuaString, Value)| {
let key = key.as_bytes();
let value = (me.set)(lua, key.as_ref(), value)?;

View file

@ -2,7 +2,7 @@ use std::str::FromStr;
use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, Value};
#[derive(Clone, Copy, Default)]
#[derive(Clone, Copy, Default, UserData)]
pub struct Color(pub ratatui_core::style::Color);
impl Color {
@ -32,5 +32,3 @@ impl FromLua for Color {
}))
}
}
impl UserData for Color {}

View file

@ -1,6 +1,6 @@
use mlua::{FromLua, IntoLua, Lua, UserData, Value};
#[derive(Clone, Copy, Default, FromLua)]
#[derive(Clone, Copy, Default, FromLua, UserData)]
pub struct Constraint(pub(super) ratatui_core::layout::Constraint);
impl Constraint {
@ -23,5 +23,3 @@ impl Constraint {
impl From<Constraint> for ratatui_core::layout::Constraint {
fn from(value: Constraint) -> Self { value.0 }
}
impl UserData for Constraint {}

View file

@ -1,7 +1,7 @@
use std::{borrow::Cow, mem, ops::{Deref, DerefMut}};
use ansi_to_tui::IntoText;
use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, Function, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, Function, IntoLua, Lua, LuaString, MetaMethod, Table, UserData, UserDataMethods, Value};
use ratatui_core::widgets::Widget;
use unicode_width::UnicodeWidthChar;
@ -31,7 +31,7 @@ impl Line {
pub fn compose(lua: &Lua) -> mlua::Result<Value> {
let new = lua.create_function(|_, (_, line): (Table, Self)| Ok(line))?;
let parse = lua.create_function(|_, code: mlua::String| {
let parse = lua.create_function(|_, code: LuaString| {
let code = code.as_bytes();
let Some(line) = code.split_inclusive(|&b| b == b'\n').next() else {
return Ok(Self::default());

View file

@ -1,7 +1,7 @@
use std::{any::TypeId, mem};
use ansi_to_tui::IntoText;
use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, LuaString, MetaMethod, Table, UserData, UserDataMethods, Value};
use ratatui_core::widgets::Widget;
use yazi_shim::SStr;
@ -24,7 +24,7 @@ impl Text {
pub fn compose(lua: &Lua) -> mlua::Result<Value> {
let new = lua.create_function(|_, (_, text): (Table, Self)| Ok(text))?;
let parse = lua.create_function(|_, code: mlua::String| {
let parse = lua.create_function(|_, code: LuaString| {
Ok(Self { inner: code.as_bytes().into_text().into_lua_err()?, ..Default::default() })
})?;

View file

@ -1,6 +1,6 @@
use std::{borrow::Cow, fmt::Display};
use std::{borrow::Cow, fmt::Display, io};
use mlua::{ExternalError, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value};
use mlua::{ExternalError, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, Value};
use yazi_codegen::FromLuaOwned;
use yazi_shim::SStr;
@ -12,6 +12,17 @@ pub enum Error {
Custom(SStr),
}
impl From<Error> for io::Error {
fn from(value: Error) -> Self {
match value {
Error::Io(e) => e,
Error::Fs(e) => e.into(),
Error::Serde(e) => Self::other(e),
Error::Custom(s) => Self::other(s.into_owned()),
}
}
}
impl Error {
pub fn install(lua: &Lua) -> mlua::Result<()> {
let custom = lua.create_function(|_, msg: String| Ok(Self::custom(msg)))?;
@ -19,9 +30,9 @@ impl Error {
let fs = lua.create_function(|_, value: Value| {
Ok(Self::Fs(match value {
Value::Table(t) => yazi_shim::fs::Error::custom(
&t.raw_get::<mlua::String>("kind")?.to_str()?,
&t.raw_get::<LuaString>("kind")?.to_str()?,
t.raw_get("code")?,
&t.raw_get::<mlua::String>("message")?.to_str()?,
&t.raw_get::<LuaString>("message")?.to_str()?,
)?,
_ => Err("expected a table".into_lua_err())?,
}))

View file

@ -209,20 +209,18 @@ macro_rules! impl_file_fields {
$fields.add_cached_field("cha", |_, me| Ok(me.cha));
$fields.add_cached_field("url", |_, me| Ok(me.url_owned()));
$fields.add_cached_field("link_to", |_, me| Ok(me.link_to.clone()));
$fields.add_cached_field("link_to", |_, me| Ok(me.extra.link_to().cloned()));
$fields.add_cached_field("name", |lua, me| {
me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose()
});
$fields.add_cached_field("path", |_, me| {
use yazi_fs::FsUrl;
use yazi_shared::{path::PathBufDyn, url::AsUrl};
Ok(PathBufDyn::from(me.url.as_url().unified_path()))
use yazi_shared::path::PathBufDyn;
Ok(PathBufDyn::from(me.content_path()))
});
$fields.add_cached_field("cache", |_, me| {
use yazi_fs::FsUrl;
use yazi_shared::path::PathBufDyn;
Ok(me.url.cache().map(PathBufDyn::from))
Ok(me.cache().map(PathBufDyn::from))
});
};
}
@ -231,8 +229,8 @@ macro_rules! impl_file_fields {
macro_rules! impl_file_methods {
($methods:ident) => {
$methods.add_method("hash", |_, me, ()| {
use yazi_fs::FsHash64;
Ok(me.hash_u64())
use yazi_fs::{FsHash64, file::FileSig};
Ok(FileSig(me).hash_u64())
});
};
}

View file

@ -46,6 +46,6 @@ impl Drop for Permit {
impl UserData for Permit {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method_mut("drop", |_, mut me, ()| async move { Ok(me.dropping().await) });
methods.add_async_method_once("drop", |_, mut me, ()| async move { Ok(me.dropping().await) });
}
}

View file

@ -1,6 +1,6 @@
use std::str::FromStr;
use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataFields, UserDataMethods, Value};
use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, LuaString, MetaMethod, Table, UserData, UserDataFields, UserDataMethods, Value};
use yazi_shim::strum::IntoStr;
use crate::{elements::Pad, position::{Offset, Origin, Position}};
@ -20,7 +20,7 @@ impl TryFrom<Table> for Position {
fn try_from(t: Table) -> Result<Self, Self::Error> {
Ok(Self {
origin: Origin::from_str(&t.raw_get::<mlua::String>(1)?.to_str()?).into_lua_err()?,
origin: Origin::from_str(&t.raw_get::<LuaString>(1)?.to_str()?).into_lua_err()?,
offset: Offset {
x: t.raw_get("x").unwrap_or_default(),
y: t.raw_get("y").unwrap_or_default(),

View file

@ -1,7 +1,7 @@
use std::{ops::DerefMut, process::ExitStatus, time::Duration};
use futures::future::try_join3;
use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, UserDataMethods, Value};
use mlua::{ExternalError, IntoLua, IntoLuaMulti, LuaString, Table, UserData, UserDataMethods, Value};
use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, process::{ChildStderr, ChildStdin, ChildStdout}, select};
use super::Status;
@ -133,7 +133,7 @@ impl UserData for Child {
}
});
methods.add_async_method_mut("write_all", |lua, mut me, src: mlua::String| async move {
methods.add_async_method_mut("write_all", |lua, mut me, src: LuaString| async move {
let Some(stdin) = &mut me.stdin else {
return Err("stdin is not piped".into_lua_err());
};
@ -158,8 +158,8 @@ impl UserData for Child {
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
});
methods.add_async_function("wait_with_output", |lua, ud: AnyUserData| async move {
match ud.take::<Self>()?.wait_with_output().await {
methods.add_async_method_once("wait_with_output", |lua, me, ()| async move {
match me.wait_with_output().await {
Ok(output) => Output::new(output).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}

View file

@ -1,6 +1,6 @@
use std::{any::TypeId, ffi::OsStr, io, process::Stdio};
use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Lua, LuaString, MetaMethod, Table, UserData, UserDataMethods, Value};
use tokio::process::{ChildStderr, ChildStdin, ChildStdout};
use yazi_shim::wtf8::FromWtf8;
@ -152,7 +152,7 @@ impl UserData for Command {
me.inner.arg(OsStr::from_wtf8(&s.as_bytes())?);
}
Value::Table(t) => {
for s in t.sequence_values::<mlua::String>() {
for s in t.sequence_values::<LuaString>() {
me.inner.arg(OsStr::from_wtf8(&s?.as_bytes())?);
}
}
@ -160,19 +160,16 @@ impl UserData for Command {
}
ud.into_lua(lua)
});
methods.add_function("cwd", |_, (ud, dir): (AnyUserData, mlua::String)| {
methods.add_function("cwd", |_, (ud, dir): (AnyUserData, LuaString)| {
ud.borrow_mut::<Self>()?.inner.current_dir(dir.to_str()?.as_ref());
Ok(ud)
});
methods.add_function(
"env",
|_, (ud, key, value): (AnyUserData, mlua::String, mlua::String)| {
ud.borrow_mut::<Self>()?
.inner
.env(OsStr::from_wtf8(&key.as_bytes())?, OsStr::from_wtf8(&value.as_bytes())?);
Ok(ud)
},
);
methods.add_function("env", |_, (ud, key, value): (AnyUserData, LuaString, LuaString)| {
ud.borrow_mut::<Self>()?
.inner
.env(OsStr::from_wtf8(&key.as_bytes())?, OsStr::from_wtf8(&value.as_bytes())?);
Ok(ud)
});
methods.add_function("stdin", |_, (ud, stdio): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.inner.stdin(make_stdio(stdio)?);
Ok(ud)

View file

@ -32,4 +32,4 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
clap = { workspace = true }
clap_complete = "4.6.7"
clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0"
clap_complete_nushell = "4.6.1"

View file

@ -2,7 +2,7 @@ use futures::executor::block_on;
use hashbrown::HashSet;
use yazi_fs::{CWD, path::clean_url};
use yazi_shared::{strand::StrandBuf, url::{UrlBuf, UrlLike}};
use yazi_vfs::provider;
use yazi_vfs::engine;
#[derive(Debug, Default)]
pub struct Boot {
@ -22,7 +22,7 @@ impl Boot {
async fn go(entry: &UrlBuf) -> (UrlBuf, StrandBuf) {
let mut entry = clean_url(entry);
if let Ok(u) = provider::absolute(&entry).await
if let Ok(u) = engine::absolute(&entry).await
&& u.is_owned()
{
entry = u.into_owned();
@ -32,7 +32,7 @@ impl Boot {
return (entry, Default::default());
};
if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) {
if engine::metadata(&entry).await.is_ok_and(|m| m.is_file()) {
(parent.into(), child.into())
} else {
(entry, Default::default())

View file

@ -21,6 +21,7 @@ yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" }
yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.6" }
yazi-version = { path = "../yazi-version", version = "26.5.6" }
@ -44,7 +45,7 @@ anyhow = { workspace = true }
clap = { workspace = true }
clap_complete = "4.6.7"
clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0"
clap_complete_nushell = "4.6.1"
serde = { workspace = true }
serde_json = { workspace = true }

View file

@ -5,6 +5,7 @@ use yazi_config::{THEME, YAZI};
use yazi_emulator::Mux;
use yazi_fs::Xdg;
use yazi_shared::timestamp_us;
use yazi_shim::OptionExt;
use yazi_term::TERM;
use crate::env::Env;
@ -156,7 +157,8 @@ impl Env {
Regex::new(r"\d+\.\d+(\.\d+-\d+|\.\d+|\b)")
.unwrap()
.find(&line)
.map(|m| m.as_str().to_owned())
.map(|m| m.as_str())
.owned()
.unwrap_or(line)
}
Ok(out) => format!("{:?}, {:?}", out.status, String::from_utf8_lossy(&out.stderr)),

View file

@ -1,5 +1,5 @@
use anyhow::{Context, Result};
use yazi_fs::{ok_or_not_found, provider::{Provider, local::Local}};
use yazi_fs::{engine::{Engine, local::Local}, ok_or_not_found};
use yazi_macro::outln;
use super::Dependency;
@ -17,8 +17,14 @@ impl Dependency {
}
self.delete_assets().await?;
self.delete_sources().await?;
Ok(())
if !self.delete_sources().await? {
outln!(
"For safety, user data will be preserved, manually delete them from: {}",
dir.display()
)?;
}
Ok(outln!("Done!")?)
}
pub(super) async fn delete_assets(&self) -> Result<()> {
@ -39,7 +45,7 @@ impl Dependency {
Ok(())
}
pub(super) async fn delete_sources(&self) -> Result<()> {
pub(super) async fn delete_sources(&self) -> Result<bool> {
let dir = self.target();
let files =
if self.is_flavor { Self::flavor_files() } else { Self::plugin_files(&dir).await? };
@ -49,15 +55,6 @@ impl Dependency {
.with_context(|| format!("failed to delete `{}`", path.display()))?;
}
if ok_or_not_found(Local::regular(&dir).remove_dir().await).is_ok() {
outln!("Done!")?;
} else {
outln!(
"Done!
For safety, user data has been preserved, please manually delete them within: {}",
dir.display()
)?;
}
Ok(())
Ok(ok_or_not_found(Local::regular(&dir).remove_dir().await).is_ok())
}
}

View file

@ -1,10 +1,9 @@
use std::{env, io, path::{Path, PathBuf}, str::FromStr};
use anyhow::{Result, bail};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use twox_hash::XxHash3_128;
use yazi_fs::Xdg;
use yazi_macro::ok_or_not_found;
use yazi_shared::BytesExt;
#[derive(Clone, Default)]
@ -65,8 +64,11 @@ impl Dependency {
Ok(())
}
pub(super) async fn plugin_files(dir: &Path) -> io::Result<Vec<String>> {
let mut it = ok_or_not_found!(tokio::fs::read_dir(dir).await, return Ok(vec![]));
pub(super) async fn plugin_files(dir: &Path) -> Result<Vec<String>> {
let mut it = tokio::fs::read_dir(dir)
.await
.with_context(|| format!("failed to read plugin directory `{}`", dir.display()))?;
let mut files: Vec<String> =
["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect();
while let Some(entry) = it.next_entry().await? {

View file

@ -1,7 +1,7 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use yazi_fs::provider::{Provider, local::Local};
use yazi_fs::engine::{Engine, local::Local};
use yazi_macro::outln;
use super::Dependency;
@ -13,6 +13,8 @@ impl Dependency {
self.header("Deploying package `{name}`")?;
self.is_flavor = maybe_exists(&from.join("flavor.toml")).await;
let files =
if self.is_flavor { Self::flavor_files() } else { Self::plugin_files(&from).await? };
let to = self.target();
let exists = maybe_exists(&to).await;
@ -24,18 +26,21 @@ impl Dependency {
self.delete_assets().await?;
let res1 = Self::deploy_assets(from.join("assets"), to.join("assets")).await;
let res2 = Self::deploy_sources(&from, &to, self.is_flavor).await;
let res2 = Self::deploy_sources(&from, &to, files).await;
if !exists && (res2.is_err() || res1.is_err()) {
self.delete_assets().await?;
self.delete_sources().await?;
} else if exists && (res2.is_err() || res1.is_err()) {
self.hash = self.hash().await?;
}
Local::regular(&to).remove_dir_clean().await;
self.hash = self.hash().await?;
res2?;
res1?;
self.hash = self.hash().await?;
outln!("Done!")?;
Ok(())
}
@ -56,8 +61,7 @@ impl Dependency {
Ok(())
}
async fn deploy_sources(from: &Path, to: &Path, is_flavor: bool) -> Result<()> {
let files = if is_flavor { Self::flavor_files() } else { Self::plugin_files(from).await? };
async fn deploy_sources(from: &Path, to: &Path, files: Vec<String>) -> Result<()> {
for file in files {
let (from, to) = (from.join(&file), to.join(&file));
copy_and_seal(&from, &to)

View file

@ -1,6 +1,6 @@
use anyhow::{Context, Result, bail};
use twox_hash::XxHash3_128;
use yazi_fs::provider::local::Local;
use yazi_fs::engine::local::Local;
use yazi_macro::ok_or_not_found;
use super::Dependency;

View file

@ -2,7 +2,7 @@ use std::{path::PathBuf, str::FromStr};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use yazi_fs::{Xdg, provider::{Provider, local::Local}};
use yazi_fs::{Xdg, engine::{Engine, local::Local}};
use yazi_macro::{ok_or_not_found, outln};
use super::Dependency;

View file

@ -1,7 +1,7 @@
use std::{io, path::Path};
use tokio::io::AsyncWriteExt;
use yazi_fs::provider::{FileBuilder, Provider, local::{Gate, Local}};
use yazi_fs::engine::{Engine, FileBuilder, local::{Demand, Local}};
use yazi_macro::ok_or_not_found;
#[inline]
@ -21,7 +21,7 @@ pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> {
let b = Local::regular(from).read().await?;
ok_or_not_found!(remove_sealed(to).await);
let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?;
let mut file = Demand::default().create_new(true).write(true).truncate(true).open(to).await?;
file.write_all(&b).await?;
let mut perm = file.metadata().await?.permissions();

View file

@ -18,5 +18,5 @@ proc-macro = true
[dependencies]
# External dependencies
proc-macro2 = "1"
quote = "1.0.46"
syn = { version = "2.0.118", features = [ "full" ] }
quote = "1.0.47"
syn = { version = "3.0.3", features = [ "full" ] }

View file

@ -54,7 +54,7 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
impl #impl_generics yazi_shim::toml::DeserializeOverWith for #ident #ty_generics #where_clause {
fn deserialize_over_with<'__de, __D: serde::Deserializer<'__de>>(self, de: __D) -> Result<Self, __D::Error> {
use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
use yazi_shared::KebabCasedString;
use yazi_shared::KebabCasedKey;
use yazi_shim::{serde::single_map_entry, toml::{DeserializeOverHook, DeserializeOverSeed, DeserializeOverWith}};
struct V #impl_generics (#ident #ty_generics) #where_clause;
@ -67,7 +67,7 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
}
fn visit_map<__M: MapAccess<'__de>>(mut self, mut map: __M) -> Result<Self::Value, __M::Error> {
while let Some(key) = map.next_key::<KebabCasedString>()? {
while let Some(key) = map.next_key::<KebabCasedKey>()? {
match key.as_ref() {
#(#normal_arms,)*
#flatten_arm
@ -196,21 +196,13 @@ pub fn overlay(input: TokenStream) -> TokenStream {
pub fn from_lua(input: TokenStream) -> TokenStream {
let DeriveInput { ident, generics, .. } = parse_macro_input!(input as DeriveInput);
let ident_str = ident.to_string();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
impl #impl_generics ::mlua::FromLua for #ident #ty_generics #where_clause {
#[inline]
fn from_lua(value: ::mlua::Value, _: &::mlua::Lua) -> ::mlua::Result<Self> {
match value {
::mlua::Value::UserData(ud) => ud.take::<Self>(),
_ => Err(::mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: #ident_str.to_owned(),
message: None,
}),
}
fn from_lua(value: ::mlua::Value, lua: &::mlua::Lua) -> ::mlua::Result<Self> {
<::mlua::UserDataOwned<Self> as ::mlua::FromLua>::from_lua(value, lua).map(|ud| ud.0)
}
}
}

View file

@ -37,5 +37,6 @@ ratatui-widgets = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
strum = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }

View file

@ -128,8 +128,9 @@ keymap = [
# Goto
{ on = [ "g", "h" ], run = "cd ~", desc = "Go home" },
{ on = [ "g", "c" ], run = "cd ~/.config", desc = "Go ~/.config" },
{ on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go ~/Downloads" },
{ on = [ "g", "c" ], run = "cd ~/.config", desc = "Go to ~/.config" },
{ on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go to ~/Downloads" },
{ on = [ "g", "t" ], run = "plugin trash", desc = "Go to trash bin" },
{ on = [ "g", "<Space>" ], run = "cd --interactive", desc = "Jump interactively" },
{ on = [ "g", "f" ], run = "follow", desc = "Follow hovered symlink" },
@ -310,6 +311,14 @@ keymap = [
{ on = "U", run = "casefy upper", desc = "Uppercase" },
{ on = "<C-r>", run = "redo", desc = "Redo the last operation" },
# History
{ on = "k", run = "recall -1", desc = "Recall previous input" },
{ on = "j", run = "recall 1", desc = "Recall next input" },
{ on = "<Up>", run = "recall -1", desc = "Recall previous input" },
{ on = "<Down>", run = "recall 1", desc = "Recall next input" },
{ on = "<C-p>", run = "recall -1", desc = "Recall previous input" },
{ on = "<C-n>", run = "recall 1", desc = "Recall next input" },
# Help
{ on = "~", run = "help", desc = "Open help" },
{ on = "<F1>", run = "help", desc = "Open help" },

View file

@ -1 +1,3 @@
[services]
[trash."*"]
kind = "hub"
run = "trash"

View file

@ -63,9 +63,16 @@ download = [
{ run = "ya emit download --open %S", desc = "Download and open" },
{ run = "ya emit download %S", desc = "Download" },
]
trash = [
{ run = "ya pub trash-restore --list %S", desc = "Restore selected files" },
{ run = "ya pub trash-empty --list %S", desc = "Empty trash bin" },
]
[open]
rules = [
# Trash
{ url = "trash://*", use = [ "open", "trash" ] },
{ url = "trash://*/", use = [ "edit", "trash" ] },
# Folder
{ url = "*/", use = [ "edit", "open", "reveal" ] },
# Text
@ -102,6 +109,7 @@ fetchers = [
# MIME-type
{ url = "*/", run = "mime.dir", prio = "high", group = "mime" },
{ url = "local://*", run = "mime.local", prio = "high", group = "mime" },
{ url = "trash://*", run = "mime.local", prio = "high", group = "mime" },
{ url = "remote://*", run = "mime.remote", prio = "high", group = "mime" },
]
spotters = [

View file

@ -19,9 +19,14 @@ pub struct Key {
impl Key {
pub fn plain(&self) -> Option<char> {
match self.code {
KeyCode::Char(c) if !self.ctrl && !self.alt && !self.super_ => Some(c),
_ => None,
if self.ctrl || self.alt || self.super_ {
None
} else if self.shift && !self.code.implies_shift() {
None
} else if let KeyCode::Char(c) = self.code {
Some(c)
} else {
None
}
}
}
@ -130,7 +135,7 @@ impl Display for Key {
if self.alt {
write!(f, "A-")?;
}
if self.shift && !matches!(self.code, KeyCode::Char(_)) {
if self.shift && !self.code.implies_shift() {
write!(f, "S-")?;
}

View file

@ -1,6 +1,6 @@
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which);
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme vfs which);
yazi_macro::mod_flat!(icon inject layout mixing pattern platform preset priority selectable selector yazi);
yazi_macro::mod_flat!(icon inject layout mixing pattern platform preset priority selectable selector tests yazi);
use std::io::{Read, Write};
@ -11,6 +11,7 @@ use yazi_tty::{TTY, sequence::SetSgr};
pub static YAZI: RoCell<yazi::Yazi> = RoCell::new();
pub static KEYMAP: RoCell<keymap::Keymap> = RoCell::new();
pub static THEME: RoCell<theme::Theme> = RoCell::new();
pub static VFS: RoCell<vfs::Vfs> = RoCell::new();
pub static LAYOUT: SyncCell<Layout> = SyncCell::new(Layout::default());
pub fn init() -> anyhow::Result<()> {
@ -24,14 +25,17 @@ pub fn init() -> anyhow::Result<()> {
fn try_init(merge: bool) -> anyhow::Result<()> {
let mut yazi = Preset::yazi()?;
let mut keymap = Preset::keymap()?;
let mut vfs = Preset::vfs()?;
if merge {
yazi = yazi.deserialize_over(&yazi::Yazi::read()?)?;
keymap = keymap.deserialize_over(&keymap::Keymap::read()?)?;
vfs = vfs.deserialize_over(&vfs::Vfs::read()?)?;
}
YAZI.init(yazi);
KEYMAP.init(keymap);
VFS.init(vfs);
Ok(())
}

View file

@ -2,7 +2,7 @@ use std::{mem, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use hashbrown::HashMap;
use mlua::{ExternalError, FromLua, IntoLua, IntoLuaMulti, MetaMethod, UserData, UserDataMethods, Value};
use mlua::{ExternalError, FromLua, IntoLua, IntoLuaMulti, LuaString, MetaMethod, UserData, UserDataMethods, Value};
use serde::{Deserialize, Deserializer};
use yazi_shim::{arc_swap::IntoPointee, toml::{DeserializeOverHook, DeserializeOverWith}};
@ -86,7 +86,7 @@ impl DeserializeOverWith for Opener {
impl UserData for &'static Opener {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(MetaMethod::Index, |lua, &me, key: mlua::String| {
methods.add_meta_method(MetaMethod::Index, |lua, &me, key: LuaString| {
let key = key.to_str()?;
match me.load().get(&*key) {
Some(rules) => rules.clone().into_lua(lua),
@ -94,22 +94,19 @@ impl UserData for &'static Opener {
}
});
methods.add_meta_method(
MetaMethod::NewIndex,
|lua, &me, (key, value): (mlua::String, Value)| {
let key = key.to_str()?;
match value {
t @ Value::Table(_) => {
me.insert(&key, &OpenerRulesArc::from_lua(t, lua)?);
}
Value::Nil => {
me.remove(&key);
}
_ => return Err("expected a table or nil".into_lua_err()),
methods.add_meta_method(MetaMethod::NewIndex, |lua, &me, (key, value): (LuaString, Value)| {
let key = key.to_str()?;
match value {
t @ Value::Table(_) => {
me.insert(&key, &OpenerRulesArc::from_lua(t, lua)?);
}
Ok(())
},
);
Value::Nil => {
me.remove(&key);
}
_ => return Err("expected a table or nil".into_lua_err()),
}
Ok(())
});
methods.add_meta_method(MetaMethod::Pairs, |lua, &me, ()| {
let mut matcher = OpenerRulesMatcher::from(me);

View file

@ -32,15 +32,5 @@ impl OpenerRule {
}
}
pub fn fill(&mut self) {
#[cfg(unix)]
{
self.spread =
Splatter::<()>::spread(&self.run) || self.run.contains("$@") || self.run.contains("$*");
}
#[cfg(windows)]
{
self.spread = Splatter::<()>::spread(&self.run) || self.run.contains("%*");
}
}
pub fn fill(&mut self) { self.spread = Splatter::<()>::spread(&self.run); }
}

View file

@ -2,13 +2,13 @@ use std::{fmt::Debug, str::FromStr};
use anyhow::{Result, bail};
use globset::{Candidate, GlobBuilder};
use serde::Deserialize;
use yazi_shared::{scheme::SchemeKind, url::AsUrl};
use serde_with::DeserializeFromStr;
use strum::EnumIs;
use yazi_shared::{auth::Auth, url::AsUrl};
use crate::Mixable;
#[derive(Clone, Deserialize)]
#[serde(try_from = "String")]
#[derive(Clone, DeserializeFromStr)]
pub struct Pattern {
inner: globset::GlobMatcher,
scheme: PatternScheme,
@ -35,7 +35,7 @@ impl Pattern {
if is_dir != self.is_dir {
return false;
} else if !self.scheme.matches(url.kind()) {
} else if !self.scheme.matches(url.auth()) {
return false;
} else if self.is_star {
return true;
@ -98,73 +98,50 @@ impl FromStr for Pattern {
}
}
// FIXME: remove
impl TryFrom<String> for Pattern {
type Error = anyhow::Error;
fn try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(s.as_str()) }
}
impl Mixable for Pattern {
fn any_file(&self) -> bool { self.is_star && !self.is_dir }
fn any_file(&self) -> bool { self.is_star && !self.is_dir && self.scheme.is_any() }
fn any_dir(&self) -> bool { self.is_star && self.is_dir }
fn any_dir(&self) -> bool { self.is_star && self.is_dir && self.scheme.is_any() }
}
// --- Scheme
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug, EnumIs)]
enum PatternScheme {
Any,
Local,
Remote,
Virtual,
Regular,
Search,
Archive,
Sftp,
Custom(String),
}
impl PatternScheme {
fn parse(s: &str) -> Result<(Self, usize)> {
let Some((protocol, _)) = s.split_once("://") else {
let Some((s, _)) = s.split_once("://") else {
return Ok((Self::Any, 0));
};
let scheme = match protocol {
let scheme = match s {
"*" => Self::Any,
"local" => Self::Local,
"remote" => Self::Remote,
"virtual" => Self::Virtual,
"regular" => Self::Regular,
"search" => Self::Search,
"archive" => Self::Archive,
"sftp" => Self::Sftp,
"" => bail!("Invalid URL pattern: protocol is empty"),
_ => bail!("Unknown protocol in URL pattern: {protocol}"),
"" => bail!("Invalid URL pattern: scheme is empty"),
other => Self::Custom(other.to_owned()),
};
Ok((scheme, protocol.len() + 3))
Ok((scheme, s.len() + 3))
}
#[inline]
fn matches(self, kind: SchemeKind) -> bool {
use SchemeKind as K;
match (self, kind) {
(Self::Any, _) => true,
(Self::Local, s) => s.is_local(),
(Self::Remote, s) => s.is_remote(),
(Self::Virtual, s) => s.is_virtual(),
(Self::Regular, K::Regular) => true,
(Self::Search, K::Search) => true,
(Self::Archive, K::Archive) => true,
(Self::Sftp, K::Sftp) => true,
_ => false,
fn matches(&self, auth: &Auth) -> bool {
match self {
Self::Any => true,
Self::Local => auth.kind.is_local(),
Self::Remote => auth.kind.is_remote(),
Self::Virtual => auth.kind.is_virtual(),
Self::Custom(name) => auth.scheme == name,
}
}
}
@ -183,6 +160,8 @@ mod tests {
#[cfg(unix)]
#[test]
fn test_unix() {
yazi_shared::init_tests();
// Wildcard
assert!(matches("*", "/foo"));
assert!(matches("*", "/foo/bar"));
@ -217,6 +196,8 @@ mod tests {
#[cfg(windows)]
#[test]
fn test_windows() {
yazi_shared::init_tests();
// Wildcard
assert!(matches("*", r#"C:\foo"#));
assert!(matches("*", r#"C:\foo\bar"#));

View file

@ -1,7 +1,7 @@
use serde::Deserialize;
use yazi_binding::position::{Offset, Origin, Position};
use yazi_codegen::{DeserializeOver, DeserializeOver2};
use yazi_shared::{scheme::Encode as EncodeScheme, url::Url};
use yazi_shared::{spec::EncodeSpec, url::Url};
use yazi_widgets::input::InputOpt;
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
@ -49,7 +49,8 @@ impl Input {
InputOpt {
name: "cd".to_owned(),
title: self.cd_title.clone(),
value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() },
value: if cwd.kind().is_local() { String::new() } else { EncodeSpec(cwd).to_string() },
history: "shared".to_owned(),
position: Position::new(self.cd_origin, self.cd_offset),
completion: true,
..Default::default()
@ -60,6 +61,7 @@ impl Input {
InputOpt {
name: format!("create-{}", if dir { "dir" } else { "file" }),
title: self.create_title[dir as usize].clone(),
history: "shared".to_owned(),
position: Position::new(self.create_origin, self.create_offset),
..Default::default()
}
@ -69,6 +71,7 @@ impl Input {
InputOpt {
name: format!("rename-{}", if is_dir { "dir" } else { "file" }),
title: self.rename_title.clone(),
history: "shared".to_owned(),
position: Position::new(self.rename_origin, self.rename_offset),
..Default::default()
}
@ -78,6 +81,7 @@ impl Input {
InputOpt {
name: "filter".to_owned(),
title: self.filter_title.clone(),
history: "shared".to_owned(),
position: Position::new(self.filter_origin, self.filter_offset),
realtime: true,
..Default::default()
@ -88,6 +92,7 @@ impl Input {
InputOpt {
name: "find".to_owned(),
title: self.find_title[prev as usize].clone(),
history: "shared".to_owned(),
position: Position::new(self.find_origin, self.find_offset),
realtime: true,
..Default::default()
@ -98,6 +103,7 @@ impl Input {
InputOpt {
name: "search".to_owned(),
title: self.search_title.replace("{n}", name),
history: "shared".to_owned(),
position: Position::new(self.search_origin, self.search_offset),
..Default::default()
}
@ -107,6 +113,7 @@ impl Input {
InputOpt {
name: "shell".to_owned(),
title: self.shell_title[block as usize].clone(),
history: "shared".to_owned(),
position: Position::new(self.shell_origin, self.shell_offset),
..Default::default()
}
@ -116,6 +123,7 @@ impl Input {
InputOpt {
name: "tab-rename".to_owned(),
title: "Rename tab:".to_owned(),
history: "shared".to_owned(),
position: Position::new(Origin::TopCenter, Offset {
x: 0,
y: 2,

View file

@ -1,4 +1,4 @@
use crate::{Yazi, keymap::Keymap, theme::Theme};
use crate::{Yazi, keymap::Keymap, theme::Theme, vfs::Vfs};
pub(crate) struct Preset;
@ -11,6 +11,10 @@ impl Preset {
toml::from_str(&yazi_macro::config_preset!("keymap"))
}
pub(super) fn vfs() -> Result<Vfs, toml::de::Error> {
toml::from_str(&yazi_macro::config_preset!("vfs"))
}
pub(super) fn theme(light: bool) -> Result<Theme, toml::de::Error> {
toml::from_str(&if light {
yazi_macro::theme_preset!("light")

9
yazi-config/src/tests.rs Normal file
View file

@ -0,0 +1,9 @@
use std::sync::OnceLock;
use crate::{Preset, VFS};
pub fn init_tests() {
static INIT: OnceLock<()> = OnceLock::new();
INIT.get_or_init(|| VFS.init(Preset::vfs().unwrap()));
}

View file

@ -4,7 +4,7 @@ use arc_swap::ArcSwap;
use hashbrown::{HashMap, hash_map};
use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}};
use yazi_codegen::{DeserializeOver, Overlay};
use yazi_shared::{KebabCasedString, SnakeCasedString};
use yazi_shared::{KebabCasedKey, SnakeCasedString};
use yazi_shim::{arc_swap::IntoPointee, toml::DeserializeOverWith};
use crate::theme::CustomSection;
@ -39,7 +39,7 @@ impl<'de> Deserialize<'de> for Custom {
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut sections = HashMap::with_capacity(map.size_hint().unwrap_or(0));
while let Some(key) = map.next_key::<KebabCasedString>()? {
while let Some(key) = map.next_key::<KebabCasedKey>()? {
let section = map.next_value::<CustomSection>()?;
if !section.load().is_empty() {
sections.insert(key.into_snake_cased(), section);
@ -64,7 +64,7 @@ impl DeserializeOverWith for Custom {
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Custom, M::Error> {
let mut sections = self.0.unwrap_unchecked();
while let Some(key) = map.next_key::<KebabCasedString>()? {
while let Some(key) = map.next_key::<KebabCasedKey>()? {
let (key, new) = (key.into_snake_cased(), map.next_value::<CustomSection>()?);
match sections.entry(key) {
hash_map::Entry::Occupied(mut oe) => {

View file

@ -1,7 +1,7 @@
use std::{ops::Deref, sync::Arc};
use hashbrown::HashMap;
use mlua::{MetaMethod, UserData, UserDataMethods};
use mlua::{LuaString, MetaMethod, UserData, UserDataMethods};
use yazi_shared::SnakeCasedString;
use crate::theme::{CustomField, CustomSection};
@ -20,7 +20,7 @@ impl From<&CustomSection> for CustomSectionArc {
impl UserData for CustomSectionArc {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(MetaMethod::Index, |_, me, key: mlua::String| {
methods.add_meta_method(MetaMethod::Index, |_, me, key: LuaString| {
Ok(me.get(&*key.to_str()?).cloned())
});
}

View file

@ -0,0 +1,60 @@
use std::sync::Arc;
use hashbrown::HashMap;
use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}};
use yazi_shared::auth::{Auth, Domain, Scheme};
use yazi_shim::toml::DeserializeOverWith;
use super::{DomainSeed, Domains};
use crate::vfs::Service;
pub struct Authorities(HashMap<Scheme, Domains>);
impl Authorities {
pub fn service(&self, scheme: &Scheme, domain: &Domain<'_>) -> Option<&Service> {
self.0.get(scheme)?.get(domain)
}
pub fn auth(&self, scheme: &Scheme, domain: &Domain<'_>) -> Option<Arc<Auth>> {
let service = self.service(scheme, domain)?;
if service.auth().domain.is_catchall() {
Some(Auth::new(service.kind(), scheme.clone(), domain.clone()))
} else {
Some(service.auth().clone())
}
}
}
impl<'de> Deserialize<'de> for Authorities {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Authorities;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a map of VFS schemes")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut authorities = HashMap::new();
while let Some(scheme) = map.next_key()? {
let domains = map.next_value_seed(DomainSeed(&scheme))?;
authorities.insert(scheme, domains);
}
Ok(Authorities(authorities))
}
}
deserializer.deserialize_map(V)
}
}
impl DeserializeOverWith for Authorities {
fn deserialize_over_with<'de, D: Deserializer<'de>>(mut self, de: D) -> Result<Self, D::Error> {
for (scheme, domains) in Self::deserialize(de)?.0 {
self.0.entry(scheme).or_default().extend(domains);
}
Ok(self)
}
}

View file

@ -0,0 +1,96 @@
use std::sync::Arc;
use hashbrown::HashMap;
use serde::{Deserialize, Deserializer, de::{self, DeserializeSeed, Error}};
use yazi_shared::auth::{AuthKind, Domain, Scheme};
use super::{Service, ServiceSftp};
#[derive(Default)]
pub struct Domains {
exact: HashMap<Domain<'static>, Service>,
catchall: Option<Service>,
}
impl Domains {
pub fn get(&self, domain: &Domain<'_>) -> Option<&Service> {
self.exact.get(domain.as_ref()).or(self.catchall.as_ref())
}
pub fn extend(&mut self, other: Self) {
self.exact.extend(other.exact);
if other.catchall.is_some() {
self.catchall = other.catchall;
}
}
fn init(&mut self, scheme: &Scheme) {
for (domain, service) in &mut self.exact {
let kind = service.kind();
let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc");
auth.kind = kind;
auth.scheme = scheme.clone();
auth.domain = domain.clone();
}
if let Some(service) = &mut self.catchall {
let kind = service.kind();
let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc");
auth.kind = kind;
auth.scheme = scheme.clone();
auth.domain = Domain::CATCHALL;
}
}
fn from_map<E>(map: HashMap<Domain<'static>, Service>) -> Result<Self, E>
where
E: de::Error,
{
let mut domains = Self::default();
for (domain, service) in map {
if domain.is_catchall() {
domains.catchall = Some(service);
continue;
}
if service.kind() == AuthKind::Hub {
return Err(E::custom("Hub services require a `*` catch-all domain"));
}
domains.exact.insert(domain, service);
}
Ok(domains)
}
}
// --- DomainSeed
pub(super) struct DomainSeed<'a>(pub &'a Scheme);
impl<'de> DeserializeSeed<'de> for DomainSeed<'_> {
type Value = Domains;
fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
let mut domains = match self.0 {
Scheme::Regular | Scheme::Search => {
return Err(D::Error::custom("scheme cannot be configured"));
}
Scheme::Sftp => {
let map = HashMap::<Domain<'static>, ServiceSftp>::deserialize(deserializer)?;
Domains::from_map(
map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect(),
)?
}
Scheme::Custom(_) => {
let map = HashMap::<Domain<'static>, Service>::deserialize(deserializer)?;
if map.values().any(|service| matches!(service, Service::Sftp(_))) {
return Err(D::Error::custom("SFTP services must use the `sftp` scheme"));
}
Domains::from_map(map)?
}
};
domains.init(self.0);
Ok(domains)
}
}

View file

@ -0,0 +1,17 @@
use std::{ops::Deref, sync::Arc};
use serde::Deserialize;
use yazi_shared::{auth::Auth, event::Cmd};
#[derive(Deserialize)]
pub struct ServiceLua {
#[serde(skip, default)]
pub auth: Arc<Auth>,
pub run: Cmd,
}
impl Deref for ServiceLua {
type Target = Auth;
fn deref(&self) -> &Self::Target { &self.auth }
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(authorities domains lua service sftp vfs);

View file

@ -0,0 +1,68 @@
use std::sync::Arc;
use serde::Deserialize;
use yazi_shared::auth::{Auth, AuthKind};
use super::{ServiceLua, ServiceSftp};
#[derive(Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum Service {
Sftp(ServiceSftp),
Mount(ServiceLua),
Hub(ServiceLua),
Scope(ServiceLua),
}
impl TryFrom<&'static Service> for &'static ServiceSftp {
type Error = &'static str;
fn try_from(value: &'static Service) -> Result<Self, Self::Error> {
match value {
Service::Sftp(p) => Ok(p),
Service::Mount(_) | Service::Hub(_) | Service::Scope(_) => {
Err("expected an SFTP service, got a custom VFS service")
}
}
}
}
impl TryFrom<&'static Service> for &'static ServiceLua {
type Error = &'static str;
fn try_from(value: &'static Service) -> Result<Self, Self::Error> {
match value {
Service::Sftp(_) => Err("expected a custom VFS service, got an SFTP service"),
Service::Mount(lua) | Service::Hub(lua) | Service::Scope(lua) => Ok(lua),
}
}
}
impl Service {
pub fn kind(&self) -> AuthKind {
match self {
Self::Sftp(_) => AuthKind::Sftp,
Self::Mount(_) => AuthKind::Mount,
Self::Hub(_) => AuthKind::Hub,
Self::Scope(_) => AuthKind::Scope,
}
}
pub fn auth(&self) -> &Arc<Auth> {
match self {
Self::Sftp(sftp) => &sftp.auth,
Self::Mount(lua) => &lua.auth,
Self::Hub(lua) => &lua.auth,
Self::Scope(lua) => &lua.auth,
}
}
pub fn auth_mut(&mut self) -> &mut Arc<Auth> {
match self {
Self::Sftp(sftp) => &mut sftp.auth,
Self::Mount(lua) => &mut lua.auth,
Self::Hub(lua) => &mut lua.auth,
Self::Scope(lua) => &mut lua.auth,
}
}
}

View file

@ -1,10 +1,13 @@
use std::{env, path::PathBuf};
use std::{env, ops::Deref, path::PathBuf, sync::Arc};
use serde::{Deserialize, Deserializer, Serialize, de};
use yazi_fs::path::sanitize_path;
use yazi_shared::auth::Auth;
#[derive(Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ServiceSftp {
#[serde(skip, default)]
pub auth: Arc<Auth>,
pub host: String,
pub user: String,
pub port: u16,
@ -20,6 +23,12 @@ pub struct ServiceSftp {
pub identity_agent: PathBuf,
}
impl Deref for ServiceSftp {
type Target = Auth;
fn deref(&self) -> &Self::Target { &self.auth }
}
fn deserialize_path<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: Deserializer<'de>,

View file

@ -0,0 +1,52 @@
use std::io;
use anyhow::{Context, Result};
use serde::{Deserialize, Deserializer};
use yazi_codegen::DeserializeOver;
use yazi_fs::{Xdg, ok_or_not_found};
use yazi_shared::auth::{Auth, AuthInventory};
use yazi_shim::toml::DeserializeOverWith;
use super::{Authorities, Service};
use crate::VFS;
#[derive(Deserialize, DeserializeOver)]
pub struct Vfs {
#[serde(flatten)]
pub authorities: Authorities,
}
impl Vfs {
pub fn service<P>(auth: &Auth) -> io::Result<P>
where
P: TryFrom<&'static Service, Error = &'static str>,
{
let Some(value) = VFS.authorities.service(&auth.scheme, &auth.domain) else {
return Err(io::Error::other(format!("No such VFS service: {auth}")));
};
match value.try_into() {
Ok(p) => Ok(p),
Err(e) => Err(io::Error::other(format!("VFS service `{auth}` has wrong kind: {e}"))),
}
}
pub(crate) fn read() -> Result<String> {
let p = Xdg::config_dir().join("vfs.toml");
ok_or_not_found(std::fs::read_to_string(&p))
.with_context(|| format!("Failed to read config {p:?}"))
}
}
impl DeserializeOverWith for Vfs {
fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result<Self, D::Error> {
Ok(Self { authorities: self.authorities.deserialize_over_with(de)? })
}
}
// --- Inject
inventory::submit! {
AuthInventory {
get: |scheme, domain| VFS.authorities.auth(scheme, domain),
}
}

View file

@ -25,7 +25,7 @@ pub struct Help {
}
impl Help {
pub fn r#type(&mut self, key: KeyEvent) -> Result<bool> {
pub fn r#type(&mut self, key: &KeyEvent) -> Result<bool> {
if !self.input.r#type(key)? {
return Ok(false);
}

View file

@ -1,6 +1,8 @@
use std::ops::{Deref, DerefMut};
use parking_lot::MutexGuard;
use parking_lot::{ArcMutexGuard, MutexGuard, RawMutex};
use crate::input::Input;
// --- InputGuard
pub enum InputGuard<'a> {
@ -21,8 +23,8 @@ impl Deref for InputGuard<'_> {
// --- InputMutGuard
pub enum InputMutGuard<'a> {
Main(&'a mut yazi_widgets::input::Input),
Alt(MutexGuard<'a, yazi_widgets::input::Input>),
Main(&'a mut Input),
Alt(&'a mut Input, ArcMutexGuard<RawMutex, yazi_widgets::input::Input>),
}
impl Deref for InputMutGuard<'_> {
@ -30,8 +32,8 @@ impl Deref for InputMutGuard<'_> {
fn deref(&self) -> &Self::Target {
match self {
Self::Main(main) => main,
Self::Alt(alt) => alt,
Self::Main(input) => &input.main.inner,
Self::Alt(_, guard) => guard,
}
}
}
@ -39,8 +41,8 @@ impl Deref for InputMutGuard<'_> {
impl DerefMut for InputMutGuard<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Main(main) => main,
Self::Alt(alt) => alt,
Self::Main(input) => &mut input.main.inner,
Self::Alt(_, guard) => guard,
}
}
}

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