feat!: custom VFS provider (#4118)

This commit is contained in:
三咲雅 misaki masa 2026-07-13 02:01:37 +08:00 committed by GitHub
parent dbb0cc0d55
commit 6d84921e70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
173 changed files with 2665 additions and 1852 deletions

View file

@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- 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])
@ -31,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Changed
- Rename `<BackTab>` to `<S-Tab>` ([#3989])
- Rename `type` field in `vfs.toml` to `kind`, leaving `type` available for future custom VFS parameters ([#4118])
- Remove `Url.is_archive` so `archive://` can be registered as a custom scheme ([#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])
@ -40,6 +43,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
@ -1775,3 +1779,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#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

7
Cargo.lock generated
View file

@ -5197,6 +5197,7 @@ dependencies = [
"hashbrown 0.17.1",
"mlua",
"parking_lot",
"strum",
"thiserror 2.0.18",
"tokio",
"tokio-util",
@ -5381,15 +5382,14 @@ dependencies = [
"mlua",
"parking_lot",
"russh",
"serde",
"tokio",
"toml",
"tracing",
"typed-path",
"yazi-binding",
"yazi-codegen",
"yazi-config",
"yazi-fs",
"yazi-macro",
"yazi-runner",
"yazi-sftp",
"yazi-shared",
"yazi-shim",
@ -5436,7 +5436,6 @@ dependencies = [
"tracing",
"unicode-width",
"yazi-binding",
"yazi-dds",
"yazi-macro",
"yazi-shared",
"yazi-shim",

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

@ -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::{AsPath, 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,14 +66,14 @@ 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 `~/`
}
@ -81,11 +81,11 @@ impl Trigger {
let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.as_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

@ -4,7 +4,7 @@ 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}, file::File};
use yazi_macro::{succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
@ -13,7 +13,7 @@ use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, 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::{VfsFile, 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?;
engine::create_new(&tmp).await?;
defer! {
let tmp = tmp.clone();
@ -76,10 +76,10 @@ 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"))
};

View file

@ -7,7 +7,7 @@ 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::{Demand, Local}}, file::File, max_common_root, path::skip_url};
use yazi_macro::{err, succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
@ -15,7 +15,7 @@ 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::{VfsFile, engine, maybe_exists};
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
@ -46,7 +46,7 @@ impl Actor for BulkRename {
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk-rename");
Gate::default()
Demand::default()
.write(true)
.create_new(true)
.open(&tmp)
@ -127,9 +127,9 @@ 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 {
succeeded.insert(old, f);

View file

@ -11,7 +11,7 @@ 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::{VfsFile, engine};
use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx};
@ -73,7 +73,7 @@ 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 };

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,22 +62,22 @@ 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
engine::create_dir_all(&new).await?;
} else if let Ok(real) = engine::casefold(&new).await
&& let Some((parent, urn)) = real.pair()
{
ok_or_not_found!(provider::remove_file(&new).await);
ok_or_not_found!(engine::remove_file(&new).await);
FilesOp::Deleting(parent.into(), [urn.into()].into()).emit();
provider::create(&new).await?;
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
if let Ok(real) = engine::casefold(&new).await
&& let Some((parent, urn)) = real.pair()
{
let file = File::new(&real).await?;

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,7 +4,7 @@ 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::{FsSpec, engine::{Engine, local::Local}, file::File};
use yazi_macro::succ;
use yazi_parser::mgr::DownloadForm;
use yazi_proxy::MgrProxy;
@ -74,7 +74,7 @@ impl Actor for Download {
impl Download {
async fn prepare(urls: &[UrlBuf]) {
let roots: HashSet<_> = urls.iter().filter_map(|u| u.scheme().cache()).collect();
let roots: HashSet<_> = urls.iter().filter_map(|u| u.auth().cache()).collect();
for mut root in roots {
root.push("%lock");
Local::regular(&root).create_dir_all().await.ok();

View file

@ -52,11 +52,8 @@ impl Refresh {
}
}
let futs: Vec<_> = folders
.iter()
.filter(|&f| f.url.is_absolute() && f.url.is_internal())
.map(|&f| go(f.url.clone(), f.cha))
.collect();
let futs: Vec<_> =
folders.iter().filter(|&f| f.url.is_absolute()).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;
@ -70,18 +70,18 @@ impl Rename {
let Some(_) = new.pair() 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()
{
ok_or_not_found!(provider::rename(&u, &new).await);
ok_or_not_found!(engine::rename(&u, &new).await);
FilesOp::Deleting(parent.to_owned(), [urn.into()].into()).emit();
}
let new = provider::casefold(&new).await?;
let new = engine::casefold(&new).await?;
let Some((new_p, new_n)) = new.pair() else { return Ok(()) };
let file = File::new(&new).await?;
@ -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

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

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

@ -4,13 +4,24 @@ 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),
}

View file

@ -1,4 +1,4 @@
use std::{borrow::Cow, fmt::Display};
use std::{borrow::Cow, fmt::Display, io};
use mlua::{ExternalError, Lua, LuaString, MetaMethod, UserData, UserDataFields, UserDataMethods, Value};
use yazi_codegen::FromLuaOwned;
@ -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)))?;

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

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

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;

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

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

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

@ -3,7 +3,7 @@ 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 yazi_shared::{auth::Auth, url::AsUrl};
use crate::Mixable;
@ -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;
@ -112,59 +112,43 @@ impl Mixable for Pattern {
}
// --- Scheme
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
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 +167,8 @@ mod tests {
#[cfg(unix)]
#[test]
fn test_unix() {
yazi_shared::init_tests();
// Wildcard
assert!(matches("*", "/foo"));
assert!(matches("*", "/foo/bar"));
@ -217,6 +203,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::Encode as EncodeSpec, url::Url};
use yazi_widgets::input::InputOpt;
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
@ -49,7 +49,7 @@ 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,

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

@ -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(flatten)]
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!(lua service services sftp vfs);

View file

@ -0,0 +1,64 @@
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),
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::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::Scope(lua) => Ok(lua),
}
}
}
impl Service {
pub fn kind(&self) -> AuthKind {
match self {
Self::Sftp(_) => AuthKind::Sftp,
Self::Mount(_) => AuthKind::Mount,
Self::Scope(_) => AuthKind::Scope,
}
}
pub fn auth(&self) -> &Arc<Auth> {
match self {
Self::Sftp(sftp) => &sftp.auth,
Self::Mount(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::Scope(lua) => &mut lua.auth,
}
}
}

View file

@ -0,0 +1,47 @@
use std::{ops::{Deref, DerefMut}, sync::Arc};
use hashbrown::HashMap;
use serde::{Deserialize, Deserializer};
use yazi_shared::KebabCasedKey;
use yazi_shim::toml::{DeserializeOverHook, DeserializeOverWith};
use super::Service;
pub struct Services(HashMap<KebabCasedKey, Service>);
impl Deref for Services {
type Target = HashMap<KebabCasedKey, Service>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl DerefMut for Services {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl<'de> Deserialize<'de> for Services {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Self(HashMap::deserialize(deserializer)?)
.deserialize_over_hook()
.map_err(serde::de::Error::custom)
}
}
impl DeserializeOverWith for Services {
fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result<Self, D::Error> {
Ok(Self(self.0.deserialize_over_with(de)?))
}
}
impl DeserializeOverHook for Services {
fn deserialize_over_hook(mut self) -> Result<Self, toml::de::Error> {
for (domain, service) in &mut self.0 {
let kind = service.kind();
let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc");
auth.kind = kind;
auth.domain = domain.to_string().into();
}
Ok(self)
}
}

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, AuthKind, Scheme};
#[derive(Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ServiceSftp {
#[serde(skip, default = "default_auth")]
pub auth: Arc<Auth>,
pub host: String,
pub user: String,
pub port: u16,
@ -20,6 +23,14 @@ pub struct ServiceSftp {
pub identity_agent: PathBuf,
}
impl Deref for ServiceSftp {
type Target = Auth;
fn deref(&self) -> &Self::Target { &self.auth }
}
fn default_auth() -> Arc<Auth> { Auth::new(AuthKind::Sftp, Scheme::Sftp, "") }
fn deserialize_path<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: Deserializer<'de>,

View file

@ -0,0 +1,53 @@
use std::io;
use anyhow::{Context, Result};
use serde::Deserialize;
use yazi_codegen::{DeserializeOver, DeserializeOver1};
use yazi_fs::{Xdg, ok_or_not_found};
use yazi_shared::auth::AuthInventory;
use super::{Service, Services};
use crate::VFS;
#[derive(Deserialize, DeserializeOver, DeserializeOver1)]
pub struct Vfs {
pub services: Services,
}
impl Vfs {
pub fn service<P>(domain: &str) -> io::Result<P>
where
P: TryFrom<&'static Service, Error = &'static str>,
{
let Some((key, value)) = VFS.services.get_key_value(domain) else {
return Err(io::Error::other(format!("No such VFS service: {domain}")));
};
match value.try_into() {
Ok(p) => Ok(p),
Err(e) => Err(io::Error::other(format!("VFS service `{key}` 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:?}"))
}
}
// --- Inject
inventory::submit! {
AuthInventory {
get: |scheme, domain| {
VFS.services.get(domain).and_then(|service| {
let auth = service.auth();
if auth.scheme == scheme {
Some(auth.clone())
} else {
None
}
})
},
}
}

View file

@ -28,7 +28,7 @@ impl Splatable for MgrSnap {
.checked_sub(1)
.and_then(|tab| self.tabs.get(tab))
.map_or_else(|| &[][..], |s| &s.selected)
.into_iter()
.iter()
.skip(idx.unwrap_or(0))
.take(if idx.is_some() { 1 } else { usize::MAX })
.map(AsUrl::as_url)

View file

@ -7,7 +7,7 @@ use yazi_config::{LAYOUT, YAZI};
use yazi_fs::{Entries, FilesOp, cha::Cha, file::File};
use yazi_macro::render;
use yazi_runner::{RUNNER, previewer::{PeekError, PeekJob}};
use yazi_shared::{pool::Symbol, url::{UrlBuf, UrlLike}};
use yazi_shared::{pool::Symbol, url::UrlBuf};
use yazi_vfs::{VfsEntries, VfsFilesOp};
use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock};
@ -48,9 +48,7 @@ impl Preview {
}
pub fn go_folder(&mut self, file: File, dir: Option<Cha>, mime: Symbol<str>, force: bool) {
if !file.url.is_internal() {
return self.go(file, mime, force);
} else if self.folder_lock.as_ref() == Some(&file.url) {
if self.folder_lock.as_ref() == Some(&file.url) {
return self.go(file, mime, force);
}

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use hashbrown::HashMap;
use parking_lot::RwLock;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
use yazi_fs::{Xdg, provider::{FileBuilder, Provider, local::{Gate, Local}}};
use yazi_fs::{Xdg, engine::{Engine, FileBuilder, local::{Demand, Local}}};
use yazi_shared::timestamp_us;
use yazi_shim::cell::RoCell;
@ -63,7 +63,12 @@ impl State {
Local::regular(&state_dir).create_dir_all().await?;
let mut buf = BufWriter::new(
Gate::default().write(true).create(true).truncate(true).open(state_dir.join(".dds")).await?,
Demand::default()
.write(true)
.create(true)
.truncate(true)
.open(state_dir.join(".dds"))
.await?,
);
let mut state = inner.into_iter().collect::<Vec<_>>();

View file

@ -1,7 +1,7 @@
use std::{io, path::PathBuf};
use tokio::{io::{AsyncBufReadExt, BufReader, Lines, ReadHalf, WriteHalf}, sync::OnceCell};
use yazi_fs::{Xdg, create_owned_dir, provider::{Provider, local::Local}};
use yazi_fs::{Xdg, create_owned_dir, engine::{Engine, local::Local}};
pub struct Stream;

View file

@ -16,12 +16,14 @@ async fn main() -> anyhow::Result<()> {
yazi_fs::init();
yazi_vfs::init();
yazi_tty::init();
yazi_config::init()?;
yazi_vfs::init();
yazi_runner::init(yazi_plugin::slim_lua);
yazi_boot::init();
yazi_term::init()?;
@ -34,8 +36,6 @@ async fn main() -> anyhow::Result<()> {
yazi_watcher::init();
yazi_runner::init(yazi_plugin::slim_lua);
yazi_plugin::init()?;
yazi_dds::serve();

View file

@ -2,6 +2,7 @@ use std::ops::Deref;
use anyhow::{anyhow, bail};
use bitflags::bitflags;
use mlua::{IntoLua, Lua, Value};
use serde::{Deserialize, Serialize};
use crate::cha::ChaType;
@ -158,3 +159,7 @@ impl ChaMode {
#[inline]
pub const fn is_sticky(self) -> bool { self.contains(Self::S_STICKY) }
}
impl IntoLua for ChaMode {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { self.bits().into_lua(lua) }
}

View file

@ -1,5 +1,7 @@
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use mlua::{IntoLua, Lua, Value};
use crate::cha::{Cha, ChaMode};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@ -63,6 +65,8 @@ impl TryFrom<Attrs> for std::fs::Permissions {
}
impl Attrs {
pub fn mode(mode: ChaMode) -> Self { Self { mode: Some(mode), ..Default::default() } }
pub fn has_times(self) -> bool {
self.atime.is_some() || self.btime.is_some() || self.mtime.is_some()
}
@ -73,3 +77,16 @@ impl Attrs {
pub fn mtime_dur(self) -> Option<Duration> { self.mtime?.duration_since(UNIX_EPOCH).ok() }
}
impl IntoLua for Attrs {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
lua
.create_table_from([
("mode", self.mode.map(|m| m.bits()).into_lua(lua)?),
("atime", self.atime_dur().map(|d| d.as_secs_f64()).into_lua(lua)?),
("btime", self.btime_dur().map(|d| d.as_secs_f64()).into_lua(lua)?),
("mtime", self.mtime_dur().map(|d| d.as_secs_f64()).into_lua(lua)?),
])?
.into_lua(lua)
}
}

View file

@ -0,0 +1,22 @@
use mlua::{FromLua, Lua, Table, Value};
#[derive(Clone, Copy, Debug, Default)]
pub struct Capabilities {
pub symlink: bool,
pub hard_link: bool,
pub trash: bool,
pub copy_progressive: bool,
}
impl FromLua for Capabilities {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let t = Table::from_lua(value, lua)?;
Ok(Self {
symlink: t.raw_get("symlink")?,
hard_link: t.raw_get("hard_link")?,
trash: t.raw_get("trash")?,
copy_progressive: t.raw_get("copy_progressive")?,
})
}
}

View file

@ -0,0 +1,55 @@
use mlua::{IntoLua, Lua, Value};
use super::{Attrs, FileBuilder};
#[derive(Clone, Copy, Default)]
pub struct Demand {
pub attrs: Attrs,
pub append: bool,
pub create: bool,
pub create_new: bool,
pub read: bool,
pub truncate: bool,
pub write: bool,
}
impl Demand {
pub fn build<T: FileBuilder>(self) -> T {
let mut demand = T::default();
demand.attrs(self.attrs);
if self.append {
demand.append(true);
}
if self.create {
demand.create(true);
}
if self.create_new {
demand.create_new(true);
}
if self.read {
demand.read(true);
}
if self.truncate {
demand.truncate(true);
}
if self.write {
demand.write(true);
}
demand
}
}
impl IntoLua for Demand {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
let t = lua.create_table_from([
("append", self.append),
("create", self.create),
("create_new", self.create_new),
("read", self.read),
("truncate", self.truncate),
("write", self.write),
])?;
t.into_lua(lua)
}
}

View file

@ -1,6 +1,6 @@
use std::path::PathBuf;
use yazi_shared::{loc::LocBuf, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}};
use yazi_shared::{loc::LocBuf, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}};
use crate::CWD;
@ -50,7 +50,7 @@ fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option<UrlCow<'a>> {
Some(match url.as_url() {
Url::Regular(_) => UrlBuf::Regular(loc).into(),
Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }.into(),
Url::Archive { .. } | Url::Sftp { .. } => None?,
Url::Search { auth, .. } => UrlBuf::Search { loc, auth: auth.clone() }.into(),
Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => None?,
})
}

View file

@ -2,7 +2,7 @@ use std::{io, path::PathBuf};
use tokio::{select, sync::{mpsc, oneshot}};
use crate::provider::Attrs;
use crate::engine::Attrs;
pub(super) async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::Result<u64> {
#[cfg(any(target_os = "linux", target_os = "android"))]
@ -48,7 +48,7 @@ pub(super) async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::R
}
}
pub(super) fn copy_with_progress_impl(
pub(super) fn copy_progressive_impl(
from: PathBuf,
to: PathBuf,
attrs: Attrs,

View file

@ -2,12 +2,12 @@ use std::io;
use yazi_shared::url::AsUrl;
use crate::provider::{Attrs, FileBuilder};
use crate::engine::{Attrs, FileBuilder};
#[derive(Default)]
pub struct Gate(tokio::fs::OpenOptions);
pub struct Demand(tokio::fs::OpenOptions);
impl FileBuilder for Gate {
impl FileBuilder for Demand {
type File = tokio::fs::File;
fn append(&mut self, append: bool) -> &mut Self {

View file

@ -2,7 +2,7 @@ use std::{io, sync::Arc};
use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}};
use crate::{cha::{Cha, ChaType}, provider::FileHolder};
use crate::{cha::{Cha, ChaType}, engine::FileHolder};
pub enum DirEntry {
Regular(tokio::fs::DirEntry),

View file

@ -1,9 +1,9 @@
use std::{io, path::Path, sync::Arc};
use std::{fs::FileTimes, io, path::Path, sync::Arc};
use tokio::sync::mpsc;
use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow}};
use yazi_shared::{auth::AuthKind, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}};
use crate::{cha::{Cha, ChaMode}, provider::{Attrs, Capabilities, Provider}};
use crate::{cha::{Cha, ChaMode}, engine::{Attrs, Capabilities, Engine}};
#[derive(Clone)]
pub struct Local<'a> {
@ -11,9 +11,9 @@ pub struct Local<'a> {
path: &'a Path,
}
impl<'a> Provider for Local<'a> {
impl<'a> Engine for Local<'a> {
type Demand = super::Demand;
type File = tokio::fs::File;
type Gate = super::Gate;
type Me<'b> = Local<'b>;
type ReadDir = super::ReadDir;
type UrlCow = UrlCow<'a>;
@ -29,7 +29,14 @@ impl<'a> Provider for Local<'a> {
}
#[inline]
fn capabilities(&self) -> Capabilities { Capabilities { symlink: true } }
async fn capabilities(&self) -> io::Result<Capabilities> {
Ok(Capabilities {
symlink: true,
hard_link: true,
trash: true,
copy_progressive: true,
})
}
async fn casefold(&self) -> io::Result<UrlBuf> {
super::casefold(self.path).await.map(Into::into)
@ -45,14 +52,14 @@ impl<'a> Provider for Local<'a> {
super::copy_impl(from, to, attrs).await
}
fn copy_with_progress<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
where
P: AsPath,
A: Into<Attrs>,
{
let to = to.as_path().to_os_owned()?;
let from = self.path.to_owned();
Ok(super::copy_with_progress_impl(from, to, attrs.into()))
Ok(super::copy_progressive_impl(from, to, attrs.into()))
}
#[inline]
@ -80,7 +87,7 @@ impl<'a> Provider for Local<'a> {
async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> {
match url {
Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_inner() }),
Url::Archive { .. } | Url::Sftp { .. } => {
Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => {
Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}")))
}
}
@ -89,12 +96,12 @@ impl<'a> Provider for Local<'a> {
#[inline]
async fn read_dir(self) -> io::Result<Self::ReadDir> {
Ok(match self.url.kind() {
SchemeKind::Regular => Self::ReadDir::Regular(tokio::fs::read_dir(self.path).await?),
SchemeKind::Search => Self::ReadDir::Others {
AuthKind::Regular => Self::ReadDir::Regular(tokio::fs::read_dir(self.path).await?),
AuthKind::Search => Self::ReadDir::Others {
reader: tokio::fs::read_dir(self.path).await?,
dir: Arc::new(self.url.to_owned()),
},
SchemeKind::Archive | SchemeKind::Sftp => Err(io::Error::new(
AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Not a local URL: {:?}", self.url),
))?,
@ -125,24 +132,19 @@ impl<'a> Provider for Local<'a> {
tokio::fs::rename(self.path, to).await
}
async fn set_mode(&self, mode: ChaMode) -> io::Result<()> {
#[cfg(unix)]
{
return tokio::fs::set_permissions(self.path, mode.into()).await;
async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> {
let (mode, times) = (attrs.mode, attrs.try_into());
if mode.is_none() && times.is_err() {
return Ok(());
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
let path: Vec<u16> = self.path.as_os_str().encode_wide().chain(Some(0)).collect();
let perm = if mode.contains(ChaMode::U_WRITE) { libc::S_IWRITE } else { libc::S_IREAD };
return tokio::task::spawn_blocking(move || {
let result = unsafe { libc::wchmod(path.as_ptr(), perm) };
if result == 0 { Ok(()) } else { Err(io::Error::last_os_error()) }
let path = self.path.to_owned();
tokio::task::spawn_blocking(move || {
let a = mode.map_or(Ok(()), |mode| Self::set_mode(&path, mode));
let b = times.map_or(Ok(()), |times| Self::set_times(&path, times));
a.and(b)
})
.await?;
}
.await?
}
#[inline]
@ -256,4 +258,26 @@ impl<'a> Local<'a> {
{
Self { url: Url::regular(path), path: path.as_ref() }
}
fn set_mode(path: &Path, mode: ChaMode) -> io::Result<()> {
#[cfg(unix)]
{
std::fs::set_permissions(path, mode.into())
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
let path: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let perm = if mode.contains(ChaMode::U_WRITE) { libc::S_IWRITE } else { libc::S_IREAD };
let result = unsafe { libc::wchmod(path.as_ptr(), perm) };
if result == 0 { Ok(()) } else { Err(io::Error::last_os_error()) }
}
}
fn set_times(path: &Path, times: FileTimes) -> io::Result<()> {
std::fs::File::open(path)?.set_times(times)
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(absolute calculator casefold copier dir_entry gate identical local read_dir);
yazi_macro::mod_flat!(absolute calculator casefold copier dir_entry demand identical local read_dir);

View file

@ -2,7 +2,7 @@ use std::{io, sync::Arc};
use yazi_shared::url::UrlBuf;
use crate::provider::DirReader;
use crate::engine::DirReader;
pub enum ReadDir {
Regular(tokio::fs::ReadDir),

View file

@ -0,0 +1,3 @@
yazi_macro::mod_pub!(local);
yazi_macro::mod_flat!(attrs capabilities demand traits);

View file

@ -4,20 +4,20 @@ use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc};
use yazi_macro::ok_or_not_found;
use yazi_shared::{path::{AsPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}};
use crate::{cha::{Cha, ChaMode, ChaType}, provider::{Attrs, Capabilities}};
use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}};
pub trait Provider: Sized {
pub trait Engine: Sized {
type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin;
type Gate: FileBuilder<File = Self::File>;
type Demand: FileBuilder<File = Self::File>;
type ReadDir: DirReader + 'static;
type UrlCow;
type Me<'a>: Provider;
type Me<'a>: Engine;
fn absolute(&self) -> impl Future<Output = io::Result<Self::UrlCow>>;
fn canonicalize(&self) -> impl Future<Output = io::Result<UrlBuf>>;
fn capabilities(&self) -> Capabilities;
fn capabilities(&self) -> impl Future<Output = io::Result<Capabilities>>;
fn casefold(&self) -> impl Future<Output = io::Result<UrlBuf>>;
@ -25,17 +25,13 @@ pub trait Provider: Sized {
where
P: AsPath;
fn copy_with_progress<P, A>(
&self,
to: P,
attrs: A,
) -> io::Result<mpsc::Receiver<io::Result<u64>>>
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
where
P: AsPath,
A: Into<Attrs>;
fn create(&self) -> impl Future<Output = io::Result<Self::File>> {
async move { self.gate().write(true).create(true).truncate(true).open(self.url()).await }
async move { self.demand().write(true).create(true).truncate(true).open(self.url()).await }
}
fn create_dir(&self) -> impl Future<Output = io::Result<()>>;
@ -77,10 +73,10 @@ pub trait Provider: Sized {
}
fn create_new(&self) -> impl Future<Output = io::Result<Self::File>> {
async move { self.gate().write(true).create_new(true).open(self.url()).await }
async move { self.demand().write(true).create_new(true).open(self.url()).await }
}
fn gate(&self) -> Self::Gate { Self::Gate::default() }
fn demand(&self) -> Self::Demand { Self::Demand::default() }
fn hard_link<P>(&self, to: P) -> impl Future<Output = io::Result<()>>
where
@ -91,7 +87,7 @@ pub trait Provider: Sized {
fn new<'a>(url: Url<'a>) -> impl Future<Output = io::Result<Self::Me<'a>>>;
fn open(&self) -> impl Future<Output = io::Result<Self::File>> {
async move { self.gate().read(true).open(self.url()).await }
async move { self.demand().read(true).open(self.url()).await }
}
fn read_dir(self) -> impl Future<Output = io::Result<Self::ReadDir>>;
@ -103,7 +99,7 @@ pub trait Provider: Sized {
fn remove_dir_all(&self) -> impl Future<Output = io::Result<()>> {
async fn remove_dir_all_impl<P>(url: Url<'_>) -> io::Result<()>
where
P: Provider,
P: Engine,
{
let mut it = ok_or_not_found!(P::new(url).await?.read_dir().await, return Ok(()));
while let Some(child) = it.next().await? {
@ -138,13 +134,13 @@ pub trait Provider: Sized {
let mut result = Ok(());
while let Some((dir, visited)) = stack.pop() {
let Ok(provider) = Self::new(dir.as_url()).await else {
let Ok(engine) = Self::new(dir.as_url()).await else {
continue;
};
if visited {
result = provider.remove_dir().await;
} else if let Ok(mut it) = provider.read_dir().await {
result = engine.remove_dir().await;
} else if let Ok(mut it) = engine.read_dir().await {
stack.push((dir, true));
while let Ok(Some(ent)) = it.next().await {
if ent.file_type().await.is_ok_and(|t| t.is_dir()) {
@ -163,7 +159,7 @@ pub trait Provider: Sized {
where
P: AsPath;
fn set_mode(&self, mode: ChaMode) -> impl Future<Output = io::Result<()>>;
fn set_attrs(&self, attrs: Attrs) -> impl Future<Output = io::Result<()>>;
fn symlink<S, F>(&self, original: S, _is_dir: F) -> impl Future<Output = io::Result<()>>
where

View file

@ -1,5 +1,6 @@
use std::ops::{Deref, DerefMut};
use mlua::{FromLua, Lua, Value};
use yazi_shared::url::UrlBuf;
use crate::file::File;
@ -28,3 +29,9 @@ impl From<Files> for Vec<File> {
impl From<Files> for Vec<UrlBuf> {
fn from(value: Files) -> Self { value.0.into_iter().map(|f| f.url).collect() }
}
impl FromLua for Files {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
Vec::<File>::from_lua(value, lua).map(Self)
}
}

View file

@ -36,7 +36,7 @@ pub fn max_common_root(urls: &[UrlBuf]) -> usize {
.components()
.zip(parent.components())
.take_while(|(a, b)| match (a, b) {
(Component::Scheme(a), Component::Scheme(b)) => a.covariant(*b),
(Component::Auth(a), Component::Auth(b)) => a.covariant(b),
(a, b) => a == b,
})
.count()
@ -53,6 +53,8 @@ pub fn max_common_root(urls: &[UrlBuf]) -> usize {
#[cfg(unix)]
#[test]
fn test_max_common_root() {
yazi_shared::init_tests();
fn assert(input: &[&str], expected: &str) {
use std::{ffi::OsStr, str::FromStr};
let urls: Vec<_> =

View file

@ -1,8 +1,8 @@
extern crate self as yazi_fs;
yazi_macro::mod_pub!(cha file mounts path provider);
yazi_macro::mod_pub!(cha file mounts path engine);
yazi_macro::mod_flat!(cwd entries filter fns hash op scheme sorter sorting splatter stage url xdg);
yazi_macro::mod_flat!(cwd spec entries filter fns hash op sorter sorting splatter stage url xdg);
pub fn init() {
CWD.init(<_>::default());

View file

@ -8,8 +8,8 @@ pub fn clean_url<'a>(url: impl Into<UrlCow<'a>>) -> UrlBuf {
cow.trail().components().count() - 1,
);
let scheme = cow.into_scheme().into_owned().with_ports(uri, urn);
(scheme, path).try_into().expect("UrlBuf from cleaned path")
let spec = cow.into_spec().with_ports(uri, urn);
(spec, path).try_into().expect("UrlBuf from cleaned path")
}
fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usize, usize) {
@ -76,19 +76,25 @@ mod tests {
yazi_shared::init_tests();
let cases = [
// CurDir
("archive://:3//./tmp/test.zip/foo/bar", "archive://:3//tmp/test.zip/foo/bar"),
("archive://:3//tmp/./test.zip/foo/bar", "archive://:3//tmp/test.zip/foo/bar"),
("archive://:3//tmp/./test.zip/./foo/bar", "archive://:3//tmp/test.zip/foo/bar"),
("archive://:3//tmp/./test.zip/./foo/./bar/.", "archive://:3//tmp/test.zip/foo/bar"),
("test-mount://7z:3//./tmp/test.zip/foo/bar", "test-mount://7z:3//tmp/test.zip/foo/bar"),
("test-mount://7z:3//tmp/./test.zip/foo/bar", "test-mount://7z:3//tmp/test.zip/foo/bar"),
("test-mount://7z:3//tmp/./test.zip/./foo/bar", "test-mount://7z:3//tmp/test.zip/foo/bar"),
(
"test-mount://7z:3//tmp/./test.zip/./foo/./bar/.",
"test-mount://7z:3//tmp/test.zip/foo/bar",
),
// ParentDir
("archive://:3:2//../../tmp/test.zip/foo/bar", "archive://:3:2//tmp/test.zip/foo/bar"),
("archive://:3:2//tmp/../../test.zip/foo/bar", "archive://:3:2//test.zip/foo/bar"),
("archive://:4:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"),
("archive://:5:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"),
("archive://:4:4//tmp/test.zip/foo/bar/../../", "archive:////tmp/test.zip"),
("archive://:5:4//tmp/test.zip/foo/bar/../../", "archive://:1//tmp/test.zip"),
("archive://:4:4//tmp/test.zip/foo/bar/../../../", "archive:////tmp"),
("sftp://test//root/.config/yazi/../../Downloads", "sftp://test//root/Downloads"),
(
"test-mount://7z:3:2//../../tmp/test.zip/foo/bar",
"test-mount://7z:3:2//tmp/test.zip/foo/bar",
),
("test-mount://7z:3:2//tmp/../../test.zip/foo/bar", "test-mount://7z:3:2//test.zip/foo/bar"),
("test-mount://7z:4:2//tmp/test.zip/../../foo/bar", "test-mount://7z:2:2//foo/bar"),
("test-mount://7z:5:2//tmp/test.zip/../../foo/bar", "test-mount://7z:2:2//foo/bar"),
("test-mount://7z:4:4//tmp/test.zip/foo/bar/../../", "test-mount://7z//tmp/test.zip"),
("test-mount://7z:5:4//tmp/test.zip/foo/bar/../../", "test-mount://7z:1//tmp/test.zip"),
("test-mount://7z:4:4//tmp/test.zip/foo/bar/../../../", "test-mount://7z//tmp"),
("sftp://vps//root/.config/yazi/../../Downloads", "sftp://vps//root/Downloads"),
];
for (input, expected) in cases {

View file

@ -1,6 +1,6 @@
use std::borrow::Cow;
use yazi_shared::{loc::LocBuf, path::{PathBufDyn, PathCow, PathKind, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}};
use yazi_shared::{loc::LocBuf, path::{PathBufDyn, PathCow, PathKind, PathLike}, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}};
use yazi_shim::wtf8::FromWtf8Vec;
#[inline]
@ -32,17 +32,21 @@ fn expand_url_impl(url: UrlCow) -> UrlCow {
Url::Regular(_) => UrlBuf::Regular(
LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
),
Url::Search { domain, .. } => UrlBuf::Search {
Url::Search { auth, .. } => UrlBuf::Search {
loc: LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),
auth: auth.clone(),
},
Url::Archive { domain, .. } => UrlBuf::Archive {
Url::Mount { auth, .. } => UrlBuf::Mount {
loc: LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),
auth: auth.clone(),
},
Url::Sftp { domain, .. } => UrlBuf::Sftp {
Url::Scope { auth, .. } => UrlBuf::Scope {
loc: LocBuf::<typed_path::UnixPathBuf>::with(path.into_unix().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),
auth: auth.clone(),
},
Url::Sftp { auth, .. } => UrlBuf::Sftp {
loc: LocBuf::<typed_path::UnixPathBuf>::with(path.into_unix().unwrap(), uri, urn).unwrap(),
auth: auth.clone(),
},
}
.into()
@ -94,39 +98,48 @@ mod tests {
let cases = [
// Zero extra component expanded
("archive:////tmp/test.zip/$FOO/bar", "archive:////tmp/test.zip/foo/bar"),
("archive://:1//tmp/test.zip/$FOO/bar", "archive://:1//tmp/test.zip/foo/bar"),
("archive://:2//tmp/test.zip/bar/$FOO", "archive://:2//tmp/test.zip/bar/foo"),
("archive://:3//tmp/test.zip/$FOO/bar", "archive://:3//tmp/test.zip/foo/bar"),
("archive://:3:1//tmp/test.zip/bar/$FOO", "archive://:3:1//tmp/test.zip/bar/foo"),
("archive://:3:2//tmp/test.zip/$FOO/bar", "archive://:3:2//tmp/test.zip/foo/bar"),
("archive://:3:3//tmp/test.zip/bar/$FOO", "archive://:3:3//tmp/test.zip/bar/foo"),
("test-mount://7z//tmp/test.zip/$FOO/bar", "test-mount://7z//tmp/test.zip/foo/bar"),
("test-mount://7z:1//tmp/test.zip/$FOO/bar", "test-mount://7z:1//tmp/test.zip/foo/bar"),
("test-mount://7z:2//tmp/test.zip/bar/$FOO", "test-mount://7z:2//tmp/test.zip/bar/foo"),
("test-mount://7z:3//tmp/test.zip/$FOO/bar", "test-mount://7z:3//tmp/test.zip/foo/bar"),
("test-mount://7z:3:1//tmp/test.zip/bar/$FOO", "test-mount://7z:3:1//tmp/test.zip/bar/foo"),
("test-mount://7z:3:2//tmp/test.zip/$FOO/bar", "test-mount://7z:3:2//tmp/test.zip/foo/bar"),
("test-mount://7z:3:3//tmp/test.zip/bar/$FOO", "test-mount://7z:3:3//tmp/test.zip/bar/foo"),
// +1 component
("archive:////tmp/test.zip/$BAR_BAZ", "archive:////tmp/test.zip/bar/baz"),
("archive://:1//tmp/test.zip/$BAR_BAZ", "archive://:2//tmp/test.zip/bar/baz"),
("archive://:2//$BAR_BAZ/tmp/test.zip", "archive://:2//bar/baz/tmp/test.zip"),
("archive://:2:1//tmp/test.zip/$BAR_BAZ", "archive://:3:2//tmp/test.zip/bar/baz"),
("archive://:2:2//tmp/$BAR_BAZ/test.zip", "archive://:3:3//tmp/bar/baz/test.zip"),
("archive://:2:2//$BAR_BAZ/tmp/test.zip", "archive://:2:2//bar/baz/tmp/test.zip"),
("test-mount://7z//tmp/test.zip/$BAR_BAZ", "test-mount://7z//tmp/test.zip/bar/baz"),
("test-mount://7z:1//tmp/test.zip/$BAR_BAZ", "test-mount://7z:2//tmp/test.zip/bar/baz"),
("test-mount://7z:2//$BAR_BAZ/tmp/test.zip", "test-mount://7z:2//bar/baz/tmp/test.zip"),
("test-mount://7z:2:1//tmp/test.zip/$BAR_BAZ", "test-mount://7z:3:2//tmp/test.zip/bar/baz"),
("test-mount://7z:2:2//tmp/$BAR_BAZ/test.zip", "test-mount://7z:3:3//tmp/bar/baz/test.zip"),
("test-mount://7z:2:2//$BAR_BAZ/tmp/test.zip", "test-mount://7z:2:2//bar/baz/tmp/test.zip"),
// -1 component
("archive:////tmp/test.zip/${BAR/BAZ}", "archive:////tmp/test.zip/bar_baz"),
("archive://:1//tmp/test.zip/${BAR/BAZ}", "archive://:1//tmp/test.zip/${BAR/BAZ}"),
("archive://:1//tmp/${BAR/BAZ}/test.zip", "archive://:1//tmp/bar_baz/test.zip"),
("archive://:2//tmp/test.zip/${BAR/BAZ}", "archive://:1//tmp/test.zip/bar_baz"),
("archive://:2//tmp/${BAR/BAZ}/test.zip", "archive://:2//tmp/${BAR/BAZ}/test.zip"),
("archive://:2:1//tmp/test.zip/${BAR/BAZ}", "archive://:2:1//tmp/test.zip/${BAR/BAZ}"),
("archive://:2:1//tmp/${BAR/BAZ}/test.zip", "archive://:2:1//tmp/${BAR/BAZ}/test.zip"),
("archive://:2:1//${BAR/BAZ}/tmp/test.zip", "archive://:2:1//bar_baz/tmp/test.zip"),
("archive://:3:2//tmp/test.zip/${BAR/BAZ}", "archive://:2:1//tmp/test.zip/bar_baz"),
("archive://:3:2//tmp/${BAR/BAZ}/test.zip", "archive://:3:2//tmp/${BAR/BAZ}/test.zip"),
("archive://:3:3//tmp/test.zip/${BAR/BAZ}", "archive://:2:2//tmp/test.zip/bar_baz"),
("archive://:3:3//tmp/${BAR/BAZ}/test.zip", "archive://:2:2//tmp/bar_baz/test.zip"),
("test-mount://7z//tmp/test.zip/${BAR/BAZ}", "test-mount://7z//tmp/test.zip/bar_baz"),
("test-mount://7z:1//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:1//tmp/test.zip/${BAR/BAZ}"),
("test-mount://7z:1//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:1//tmp/bar_baz/test.zip"),
("test-mount://7z:2//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:1//tmp/test.zip/bar_baz"),
("test-mount://7z:2//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:2//tmp/${BAR/BAZ}/test.zip"),
(
"test-mount://7z:2:1//tmp/test.zip/${BAR/BAZ}",
"test-mount://7z:2:1//tmp/test.zip/${BAR/BAZ}",
),
(
"test-mount://7z:2:1//tmp/${BAR/BAZ}/test.zip",
"test-mount://7z:2:1//tmp/${BAR/BAZ}/test.zip",
),
("test-mount://7z:2:1//${BAR/BAZ}/tmp/test.zip", "test-mount://7z:2:1//bar_baz/tmp/test.zip"),
("test-mount://7z:3:2//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:2:1//tmp/test.zip/bar_baz"),
(
"test-mount://7z:3:2//tmp/${BAR/BAZ}/test.zip",
"test-mount://7z:3:2//tmp/${BAR/BAZ}/test.zip",
),
("test-mount://7z:3:3//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:2:2//tmp/test.zip/bar_baz"),
("test-mount://7z:3:3//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:2:2//tmp/bar_baz/test.zip"),
// Zeros all components
("archive:////${EM/PT/Y}", "archive:////"),
("archive://:1//${EM/PT/Y}", "archive://:1//${EM/PT/Y}"),
("archive://:2//${EM/PT/Y}", "archive://:2//${EM/PT/Y}"),
("archive://:3//${EM/PT/Y}", "archive:////"),
("archive://:4//${EM/PT/Y}", "archive://:1//"),
("test-mount://7z//${EM/PT/Y}", "test-mount://7z//"),
("test-mount://7z:1//${EM/PT/Y}", "test-mount://7z:1//${EM/PT/Y}"),
("test-mount://7z:2//${EM/PT/Y}", "test-mount://7z:2//${EM/PT/Y}"),
("test-mount://7z:3//${EM/PT/Y}", "test-mount://7z//"),
("test-mount://7z:4//${EM/PT/Y}", "test-mount://7z:1//"),
];
for (input, expected) in cases {

View file

@ -5,7 +5,7 @@ use yazi_shared::url::UrlBuf;
use crate::path::{clean_url, expand_url};
pub fn sanitize_path(path: PathBuf) -> Option<PathBuf> {
clean_url(yazi_fs::provider::local::try_absolute(expand_url(UrlBuf::from(path)))?)
clean_url(yazi_fs::engine::local::try_absolute(expand_url(UrlBuf::from(path)))?)
.into_loc()
.into_os()
.ok()

View file

@ -1,4 +0,0 @@
#[derive(Clone, Copy, Debug)]
pub struct Capabilities {
pub symlink: bool,
}

View file

@ -1,3 +0,0 @@
yazi_macro::mod_pub!(local);
yazi_macro::mod_flat!(attrs capabilities traits);

View file

@ -1,37 +0,0 @@
use std::path::PathBuf;
use yazi_shared::{path::PathBufDyn, scheme::{AsScheme, Scheme, SchemeInventory, SchemeRef}};
use yazi_shim::mlua::UserDataFieldsExt;
use crate::Xdg;
pub trait FsScheme {
fn cache(&self) -> Option<PathBuf>;
}
impl FsScheme for SchemeRef<'_> {
fn cache(&self) -> Option<PathBuf> {
match self {
Self::Regular { .. } | Self::Search { .. } => None,
Self::Archive { domain, .. } => Some(
Xdg::temp_dir().join(format!("archive-{}", yazi_shared::scheme::Encode::domain(domain))),
),
Self::Sftp { domain, .. } => {
Some(Xdg::temp_dir().join(format!("sftp-{}", yazi_shared::scheme::Encode::domain(domain))))
}
}
}
}
impl FsScheme for Scheme {
fn cache(&self) -> Option<PathBuf> { self.as_scheme().cache() }
}
// --- Inject
inventory::submit! {
SchemeInventory {
register: |registry| {
registry.add_cached_field("cache", |_, me| Ok(me.cache().map(PathBufDyn::from)));
}
}
}

33
yazi-fs/src/spec.rs Normal file
View file

@ -0,0 +1,33 @@
use std::path::PathBuf;
use yazi_shared::{auth::{Auth, AuthKind, Encode}, path::PathBufDyn, spec::SpecInventory};
use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr};
use crate::Xdg;
pub trait FsSpec {
fn cache(&self) -> Option<PathBuf>;
}
impl FsSpec for Auth {
fn cache(&self) -> Option<PathBuf> {
match self.kind {
AuthKind::Regular | AuthKind::Search => None,
AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Some(Xdg::temp_dir().join(format!(
"{}_{}_{}",
self.kind.into_str(),
self.scheme,
Encode::domain(&self.domain)
))),
}
}
}
// --- Inject
inventory::submit! {
SpecInventory {
register: |registry| {
registry.add_cached_field("cache", |_, me| Ok(me.cache().map(PathBufDyn::from)));
}
}
}

View file

@ -1,9 +1,10 @@
use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}};
use yazi_shared::{path::{AsPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow}};
use mlua::UserDataFields;
use yazi_shared::{path::{AsPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}};
use yazi_shim::mlua::UserDataFieldsExt;
use crate::{FsHash128, FsScheme, path::PercentEncoding};
use crate::{FsHash128, FsSpec, path::PercentEncoding};
pub trait FsUrl<'a> {
fn cache(&self) -> Option<PathBuf>;
@ -36,11 +37,11 @@ impl<'a> FsUrl<'a> for Url<'a> {
root
}
self.scheme().cache().map(|root| with_loc(self.loc(), root))
self.auth().cache().map(|root| with_loc(self.loc(), root))
}
fn cache_lock(&self) -> Option<PathBuf> {
self.scheme().cache().map(|mut root| {
self.auth().cache().map(|mut root| {
root.push("%lock");
root.push(format!("{:x}", self.hash_u128()));
root
@ -50,7 +51,7 @@ impl<'a> FsUrl<'a> for Url<'a> {
fn unified_path(self) -> Cow<'a, Path> {
match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(),
Self::Archive { .. } | Self::Sftp { .. } => {
Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
self.cache().expect("non-local URL should have a cache path").into()
}
}
@ -65,7 +66,7 @@ impl FsUrl<'_> for UrlBuf {
fn unified_path(self) -> Cow<'static, Path> {
match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(),
Self::Archive { .. } | Self::Sftp { .. } => {
Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
self.cache().expect("non-local URL should have a cache path").into()
}
}
@ -80,7 +81,7 @@ impl<'a> FsUrl<'a> for UrlCow<'a> {
fn unified_path(self) -> Cow<'a, Path> {
match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(),
Self::Archive { .. } | Self::Sftp { .. } => {
Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
self.cache().expect("non-local URL should have a cache path").into()
}
}
@ -92,6 +93,22 @@ inventory::submit! {
UrlBufInventory {
register: |registry| {
registry.add_cached_field("cache", |_, me| Ok(me.cache()));
registry.add_cached_field("domain", |lua, me| {
yazi_binding::deprecate!(lua, "{}: `Url.domain` is deprecated, use `Url.spec.domain` instead.");
lua.create_string(&*me.spec().domain)
});
registry.add_field_method_get("is_regular", |lua, me| {
yazi_binding::deprecate!(lua, "{}: `Url.is_regular` is deprecated, use `Url.spec.is_regular` instead.");
Ok(me.is_regular())
});
registry.add_field_method_get("is_search", |lua, me| {
yazi_binding::deprecate!(lua, "{}: `Url.is_search` is deprecated, use `Url.spec.is_search` instead.");
Ok(me.is_search())
});
registry.add_cached_field("scheme", |lua, me| {
yazi_binding::deprecate!(lua, "{}: `Url.scheme` is deprecated, use `Url.spec` instead.");
Ok(me.spec())
});
}
}
}

View file

@ -3,7 +3,7 @@ use serde::Deserialize;
use yazi_core::mgr::CdSource;
use yazi_fs::path::{clean_url, expand_url};
use yazi_shared::{event::ActionCow, url::{Url, UrlBuf}};
use yazi_vfs::provider;
use yazi_vfs::engine;
#[derive(Debug, Deserialize)]
pub struct CdForm {
@ -27,7 +27,7 @@ impl TryFrom<ActionCow> for CdForm {
me.target = expand_url(me.target).into_owned();
}
if let Some(u) = provider::try_absolute(&me.target)
if let Some(u) = engine::try_absolute(&me.target)
&& u.is_owned()
{
me.target = u.into_owned();

View file

@ -3,7 +3,7 @@ use serde::Deserialize;
use yazi_core::mgr::CdSource;
use yazi_fs::path::{clean_url, expand_url};
use yazi_shared::{event::ActionCow, url::UrlBuf};
use yazi_vfs::provider;
use yazi_vfs::engine;
#[derive(Debug, Deserialize)]
pub struct RevealForm {
@ -27,7 +27,7 @@ impl TryFrom<ActionCow> for RevealForm {
me.target = expand_url(me.target).into_owned();
}
if let Some(u) = provider::try_absolute(&me.target)
if let Some(u) = engine::try_absolute(&me.target)
&& u.is_owned()
{
me.target = u.into_owned();

View file

@ -3,7 +3,7 @@ use serde::Deserialize;
use yazi_boot::BOOT;
use yazi_fs::path::{clean_url, expand_url};
use yazi_shared::{event::ActionCow, url::UrlBuf};
use yazi_vfs::provider;
use yazi_vfs::engine;
#[derive(Debug, Deserialize)]
pub struct TabCreateForm {
@ -30,7 +30,7 @@ impl TryFrom<ActionCow> for TabCreateForm {
target = expand_url(target).into_owned();
}
if let Some(u) = provider::try_absolute(&target)
if let Some(u) = engine::try_absolute(&target)
&& u.is_owned()
{
target = u.into_owned();

View file

@ -37,8 +37,8 @@ function Header:flags()
local finder = self._tab.finder
local t = {}
if cwd.is_search then
t[#t + 1] = string.format("search: %s", cwd.domain)
if cwd.spec.is_search then
t[#t + 1] = string.format("search: %s", cwd.spec.domain)
end
if filter then
t[#t + 1] = string.format("filter: %s", filter)

View file

@ -12,7 +12,7 @@ function M:entry()
ya.emit("escape", { visual = true })
local cwd, selected = state()
if cwd.scheme.is_virtual then
if cwd.spec.is_virtual then
return ya.notify { title = "Fzf", content = "Not supported under virtual filesystems", timeout = 5, level = "warn" }
end

View file

@ -1,7 +1,7 @@
local function fetch(_, job)
local updates = {}
for _, file in ipairs(job.files) do
if file.url.scheme.is_virtual then
if file.url.spec.is_virtual then
updates[file.url] = "folder/remote"
else
updates[file.url] = "folder/local"

View file

@ -2,7 +2,7 @@ local M = {}
local function stale_cache(file)
local url = file.url
local lock = url.scheme.cache:join(string.format("%%lock/%s", url:hash(true)))
local lock = url.spec.cache:join(string.format("%%lock/%s", url:hash(true)))
local fd = fs.access():read(true):open(Url(lock))
if not fd then

View file

@ -2,8 +2,8 @@ use mlua::{IntoLuaMulti, UserData, UserDataFields, UserDataMethods, Value};
use yazi_binding::Error;
pub enum SizeCalculator {
Local(yazi_fs::provider::local::SizeCalculator),
Remote(yazi_vfs::provider::SizeCalculator),
Local(yazi_fs::engine::local::SizeCalculator),
Remote(yazi_vfs::engine::SizeCalculator),
}
impl UserData for SizeCalculator {

View file

@ -3,9 +3,9 @@ use std::str::FromStr;
use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, LuaString, Table, Value};
use yazi_binding::{Composer, ComposerGet, ComposerSet, Error};
use yazi_config::Pattern;
use yazi_fs::{file::File, mounts::PARTITIONS, provider::{Attrs, DirReader, FileHolder}};
use yazi_fs::{engine::{Attrs, DirReader, FileHolder}, file::File, mounts::PARTITIONS};
use yazi_shared::url::{UrlBuf, UrlCow, UrlLike, UrlRef};
use yazi_vfs::{VfsFile, provider};
use yazi_vfs::{VfsFile, engine};
use crate::fs::SizeCalculator;
@ -38,15 +38,15 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
}
fn access(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, ()| Ok(yazi_vfs::provider::Gate::default()))
lua.create_function(|_, ()| Ok(yazi_vfs::engine::Demand::default()))
}
fn calc_size(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, url: UrlRef| async move {
let it = if let Some(path) = url.as_local() {
yazi_fs::provider::local::SizeCalculator::new(path).await.map(SizeCalculator::Local)
yazi_fs::engine::local::SizeCalculator::new(path).await.map(SizeCalculator::Local)
} else {
yazi_vfs::provider::SizeCalculator::new(&*url).await.map(SizeCalculator::Remote)
yazi_vfs::engine::SizeCalculator::new(&*url).await.map(SizeCalculator::Remote)
};
match it {
@ -59,9 +59,9 @@ fn calc_size(lua: &Lua) -> mlua::Result<Function> {
fn cha(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, follow): (UrlRef, Option<bool>)| async move {
let cha = if follow.unwrap_or(false) {
provider::metadata(&*url).await
engine::metadata(&*url).await
} else {
provider::symlink_metadata(&*url).await
engine::symlink_metadata(&*url).await
};
match cha {
@ -73,7 +73,7 @@ fn cha(lua: &Lua) -> mlua::Result<Function> {
fn copy(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (from, to): (UrlRef, UrlRef)| async move {
match provider::copy(&*from, &*to, Attrs::default()).await {
match engine::copy(&*from, &*to, Attrs::default()).await {
Ok(len) => len.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
@ -83,8 +83,8 @@ fn copy(lua: &Lua) -> mlua::Result<Function> {
fn create(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (r#type, url): (LuaString, UrlRef)| async move {
let result = match &*r#type.as_bytes() {
b"dir" => provider::create_dir(&*url).await,
b"dir_all" => provider::create_dir_all(&*url).await,
b"dir" => engine::create_dir(&*url).await,
b"dir_all" => engine::create_dir_all(&*url).await,
_ => Err("Creation type must be 'dir' or 'dir_all'".into_lua_err())?,
};
@ -171,7 +171,7 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
let limit = options.raw_get("limit").unwrap_or(usize::MAX);
let resolve = options.raw_get::<bool>("resolve")?;
let mut it = match provider::read_dir(&*dir).await {
let mut it = match engine::read_dir(&*dir).await {
Ok(it) => it,
Err(e) => return (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
};
@ -204,10 +204,10 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
fn remove(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (r#type, url): (LuaString, UrlRef)| async move {
let result = match &*r#type.as_bytes() {
b"file" => provider::remove_file(&*url).await,
b"dir" => provider::remove_dir(&*url).await,
b"dir_all" => provider::remove_dir_all(&*url).await,
b"dir_clean" => provider::remove_dir_clean(&*url).await,
b"file" => engine::remove_file(&*url).await,
b"dir" => engine::remove_dir(&*url).await,
b"dir_all" => engine::remove_dir_all(&*url).await,
b"dir_clean" => engine::remove_dir_clean(&*url).await,
_ => Err("Removal type must be 'file', 'dir', 'dir_all', or 'dir_clean'".into_lua_err())?,
};
@ -220,7 +220,7 @@ fn remove(lua: &Lua) -> mlua::Result<Function> {
fn rename(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (from, to): (UrlRef, UrlRef)| async move {
match provider::rename(&*from, &*to).await {
match engine::rename(&*from, &*to).await {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
}
@ -244,7 +244,7 @@ fn unique(lua: &Lua) -> mlua::Result<Function> {
fn write(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, data): (UrlRef, LuaString)| async move {
match provider::write(&*url, data.as_bytes()).await {
match engine::write(&*url, data.as_bytes()).await {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
}

View file

@ -1,7 +1,10 @@
use mlua::{IntoLua, Lua, Value};
use yazi_binding::{Composer, ComposerGet, ComposerSet, elements::{Align, Bar, Border, Color, Constraint, Edge, Fill, Gauge, Layout, Line, List, Pad, Rect, Row, Span, Table, Text, Wrap}, position::Position, style::Style};
use yazi_config::THEME;
use yazi_widgets::{clear::Clear, input::InputArc};
use yazi_dds::Pubsub;
use yazi_macro::err;
use yazi_shim::strum::IntoStr;
use yazi_widgets::{clear::Clear, input::{InputArc, InputEvent}};
pub fn compose() -> Composer<ComposerGet, ComposerSet> {
fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
@ -16,7 +19,7 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
b"Edge" => Edge::compose(lua)?,
b"Fill" => Fill::compose(lua)?,
b"Gauge" => Gauge::compose(lua)?,
b"Input" => InputArc::compose(lua, (&THEME.input).into())?,
b"Input" => InputArc::compose(lua, (&THEME.input).into(), publish_input)?,
b"Layout" => Layout::compose(lua)?,
b"Line" => Line::compose(lua)?,
b"List" => List::compose(lua)?,
@ -48,3 +51,7 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
Composer::new(get, set)
}
fn publish_input(event: InputEvent) {
err!(Pubsub::pub_after_input((&event).into_str(), event.value()));
}

View file

@ -2,7 +2,7 @@ use std::any::TypeId;
use mlua::{AnyUserData, ExternalError, Function, Lua, LuaString};
use tokio::process::{ChildStderr, ChildStdin, ChildStdout};
use yazi_vfs::provider::RwFile;
use yazi_vfs::engine::RwFile;
use super::Utils;

View file

@ -95,7 +95,7 @@ impl Utils {
}
(b"mpsc", Some(buffer)) => {
let (tx, rx) = tokio::sync::mpsc::channel::<Value>(buffer);
(MpscTx(tx), MpscRx(rx)).into_lua_multi(lua)
(MpscTx::new(tx), MpscRx(rx)).into_lua_multi(lua)
}
(b"mpsc", None) => {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<Value>();

View file

@ -30,6 +30,7 @@ anyhow = { workspace = true }
hashbrown = { workspace = true }
mlua = { workspace = true }
parking_lot = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tokio-util = { workspace = true }

View file

@ -1,4 +1,4 @@
yazi_macro::mod_pub!(entry fetcher loader preloader previewer);
yazi_macro::mod_pub!(entry fetcher loader preloader previewer provider);
yazi_macro::mod_flat!(runner spot);
@ -6,6 +6,5 @@ pub static RUNNER: yazi_shim::cell::RoCell<Runner> = yazi_shim::cell::RoCell::ne
pub fn init(setter: fn(&mlua::Lua) -> mlua::Result<()>) {
crate::loader::init();
RUNNER.init(Runner { setter });
}

View file

@ -4,7 +4,7 @@ use anyhow::{Context, Result, bail, ensure};
use hashbrown::HashMap;
use mlua::{ExternalError, Lua, Table, chunk::ChunkMode};
use parking_lot::RwLock;
use yazi_fs::{Xdg, provider::local::Local};
use yazi_fs::{Xdg, engine::local::Local};
use yazi_macro::plugin_preset as preset;
use yazi_shared::{BytesExt, LOG_LEVEL};
use yazi_shim::cell::RoCell;

View file

@ -0,0 +1,162 @@
use std::io;
use mlua::{IntoLua, Lua, Value};
use strum::AsRefStr;
use yazi_binding::MpscTx;
use yazi_fs::engine::{Attrs, Demand};
use yazi_shared::{path::PathBufDyn, url::UrlBuf};
#[derive(AsRefStr)]
#[strum(serialize_all = "PascalCase")]
pub enum ProviderJob {
Capabilities,
Absolute {
url: UrlBuf,
},
Canonicalize {
url: UrlBuf,
},
Casefold {
url: UrlBuf,
},
SymlinkMetadata {
url: UrlBuf,
},
Metadata {
url: UrlBuf,
},
ReadDir {
url: UrlBuf,
},
Open {
url: UrlBuf,
attrs: Attrs,
demand: Demand,
},
CreateDir {
url: UrlBuf,
},
HardLink {
from: UrlBuf,
to: PathBufDyn,
},
ReadLink {
url: UrlBuf,
},
RemoveDir {
url: UrlBuf,
},
RemoveFile {
url: UrlBuf,
},
Rename {
from: UrlBuf,
to: PathBufDyn,
},
Symlink {
original: Vec<u8>,
url: UrlBuf,
is_dir: bool,
},
Trash {
url: UrlBuf,
},
Read {
url: UrlBuf,
offset: u64,
len: usize,
},
Write {
url: UrlBuf,
offset: u64,
bytes: Vec<u8>,
},
Copy {
from: UrlBuf,
to: PathBufDyn,
attrs: Attrs,
},
CopyProgressive {
from: UrlBuf,
to: PathBufDyn,
attrs: Attrs,
tx: MpscTx<u64, io::Result<u64>>,
},
SetLen {
url: UrlBuf,
size: u64,
},
SetAttrs {
url: UrlBuf,
attrs: Attrs,
},
}
impl IntoLua for ProviderJob {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
let t = lua.create_table()?;
t.raw_set("op", self.as_ref())?;
match self {
Self::Capabilities => {}
Self::Absolute { url }
| Self::Canonicalize { url }
| Self::Casefold { url }
| Self::SymlinkMetadata { url }
| Self::Metadata { url }
| Self::ReadDir { url }
| Self::CreateDir { url }
| Self::ReadLink { url }
| Self::RemoveDir { url }
| Self::RemoveFile { url }
| Self::Trash { url } => t.raw_set("url", url)?,
Self::Open { url, attrs, demand } => {
t.raw_set("url", url)?;
t.raw_set("attrs", attrs)?;
t.raw_set("demand", demand)?;
}
Self::HardLink { from, to } | Self::Rename { from, to } => {
t.raw_set("from", from)?;
t.raw_set("to", to)?;
}
Self::Symlink { original, url, is_dir } => {
t.raw_set("original", lua.create_external_string(original)?)?;
t.raw_set("url", url)?;
t.raw_set("is_dir", is_dir)?;
}
Self::Read { url, offset, len } => {
t.raw_set("url", url)?;
t.raw_set("offset", offset)?;
t.raw_set("len", len)?;
}
Self::Write { url, offset, bytes } => {
t.raw_set("url", url)?;
t.raw_set("offset", offset)?;
t.raw_set("bytes", lua.create_external_string(bytes)?)?;
}
Self::Copy { from, to, attrs } => {
t.raw_set("from", from)?;
t.raw_set("to", to)?;
t.raw_set("attrs", attrs)?;
}
Self::CopyProgressive { from, to, attrs, tx } => {
t.raw_set("from", from)?;
t.raw_set("to", to)?;
t.raw_set("attrs", attrs)?;
t.raw_set("tx", tx)?;
}
Self::SetLen { url, size } => {
t.raw_set("url", url)?;
t.raw_set("size", size)?;
}
Self::SetAttrs { url, attrs } => {
t.raw_set("url", url)?;
t.raw_set("attrs", attrs)?;
}
}
t.into_lua(lua)
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(job result provider);

View file

@ -0,0 +1,42 @@
use mlua::{ExternalError, FromLua, FromLuaMulti, IntoLua, ObjectLike, Value};
use tokio::runtime::Handle;
use yazi_shared::{data::Sendable, event::Cmd};
use crate::{Runner, loader::LOADER, provider::{ProvideResult, ProviderJob}};
impl Runner {
pub async fn provide<T>(&'static self, run: &'static Cmd, job: ProviderJob) -> ProvideResult<T>
where
T: FromLua + Send + 'static,
{
match LOADER.ensure(&run.name, |_| ()).await {
Ok(()) => self.provide_do(run, job).await,
Err(e) => yazi_binding::Error::custom(e.to_string()).into(),
}
}
async fn provide_do<T>(&'static self, run: &'static Cmd, job: ProviderJob) -> ProvideResult<T>
where
T: FromLua + Send + 'static,
{
match tokio::task::spawn_blocking(move || {
let lua = self.spawn(&run.name)?;
Handle::current().block_on(async {
let Value::Table(job) = job.into_lua(&lua)? else {
return Err("ProviderJob should be a table".into_lua_err());
};
job.raw_set("args", Sendable::args_to_table_ref(&lua, &run.args)?)?;
let values = LOADER.load(&lua, &run.name).await?.call_async_method("provide", job).await?;
ProvideResult::from_lua_multi(values, &lua)
})
})
.await
{
Ok(Ok(result)) => result,
Ok(Err(error)) => error.into(),
Err(error) => error.into(),
}
}
}

View file

@ -0,0 +1,43 @@
use mlua::{FromLua, FromLuaMulti, Lua, MultiValue, Value};
pub struct ProvideResult<T>(pub Result<T, yazi_binding::Error>);
impl<T> From<yazi_binding::Error> for ProvideResult<T> {
fn from(value: yazi_binding::Error) -> Self { Self(Err(value)) }
}
impl<T> From<mlua::Error> for ProvideResult<T> {
fn from(value: mlua::Error) -> Self { yazi_binding::Error::custom(value.to_string()).into() }
}
impl<T> From<tokio::task::JoinError> for ProvideResult<T> {
fn from(value: tokio::task::JoinError) -> Self {
yazi_binding::Error::custom(value.to_string()).into()
}
}
impl<T: FromLua> FromLuaMulti for ProvideResult<T> {
fn from_lua_multi(mut values: MultiValue, lua: &Lua) -> mlua::Result<Self> {
let value = values.pop_front().unwrap_or(Value::Nil);
let error = values.pop_front().unwrap_or(Value::Nil);
Ok(Self(if error.is_nil() {
T::from_lua(value, lua).map_err(|e| yazi_binding::Error::custom(e.to_string()))
} else {
Err(
yazi_binding::Error::from_lua(error, lua)
.unwrap_or_else(|e| yazi_binding::Error::custom(e.to_string())),
)
}))
}
}
impl ProvideResult<bool> {
pub fn ok(self) -> Result<(), yazi_binding::Error> {
if self.0? {
Ok(())
} else {
Err(yazi_binding::Error::custom("Lua VFS returned false without an Error"))
}
}
}

View file

@ -4,9 +4,9 @@ use anyhow::{Context, Result, anyhow};
use tokio::{io::{self, ErrorKind::NotFound}, sync::mpsc};
use tracing::warn;
use yazi_config::YAZI;
use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::path_relative_to, provider::{Attrs, FileHolder, Provider, local::Local}};
use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, engine::{Attrs, Engine, FileHolder, local::Local}, ok_or_not_found, path::path_relative_to};
use yazi_shared::{path::PathCow, url::{AsUrl, UrlCow, UrlLike}};
use yazi_vfs::{VfsCha, maybe_exists, provider::{self, DirEntry}, unique_file};
use yazi_vfs::{VfsCha, engine::{self, DirEntry}, maybe_exists, unique_file};
use super::{FileInCopy, FileInDelete, FileInHardlink, FileInLink, FileInTrash};
use crate::{LOW, NORMAL, TaskOp, TaskOps, TasksProxy, ctx, file::{FileIn, FileInCut, FileInDownload, FileInUpload, FileOutCopy, FileOutCopyDo, FileOutCut, FileOutCutDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutTrash, FileOutUpload, FileOutUploadDo, Transaction, Traverse}, hook::{HookInOutCopy, HookInOutCut, HookInOutHardlink, HookInOutLink}, ok_or_not_found};
@ -38,7 +38,7 @@ impl File {
super::traverse::<FileOutCopy, _, _, _, _, _>(
task,
async |dir| match provider::create_dir(dir).await {
async |dir| match engine::create_dir(dir).await {
Err(e) if e.kind() != io::ErrorKind::AlreadyExists => Err(e)?,
_ => Ok(()),
},
@ -63,7 +63,7 @@ impl File {
pub(crate) async fn copy_do(&self, mut task: FileInCopy) -> Result<(), FileOutCopyDo> {
ok_or_not_found!(task, Transaction::unlink(&task.to).await);
let mut rx =
ctx!(task, provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await)?;
ctx!(task, engine::copy_progressive(&task.from, &task.to, task.cha.unwrap()).await)?;
loop {
match rx.recv().await.unwrap_or(Ok(0)) {
@ -101,16 +101,16 @@ impl File {
self.ops.out(id, HookInOutCut::new(&task.from, &task.to));
TasksProxy::update_succeed(id, [&task.to], true);
if !task.follow && ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok() {
if !task.follow && ok_or_not_found(engine::rename(&task.from, &task.to).await).is_ok() {
return Ok(self.ops.out(id, FileOutCut::Succ));
}
let (mut links, mut files) = (vec![], vec![]);
let reorder = task.follow && ctx!(task, provider::capabilities(&task.from).await)?.symlink;
let reorder = task.follow && ctx!(task, engine::capabilities(&task.from).await)?.symlink;
super::traverse::<FileOutCut, _, _, _, _, _>(
task,
async |dir| match provider::create_dir(dir).await {
async |dir| match engine::create_dir(dir).await {
Err(e) if e.kind() != io::ErrorKind::AlreadyExists => Err(e)?,
_ => Ok(()),
},
@ -155,12 +155,12 @@ impl File {
pub(crate) async fn cut_do(&self, mut task: FileInCut) -> Result<(), FileOutCutDo> {
ok_or_not_found!(task, Transaction::unlink(&task.to).await);
let mut rx =
ctx!(task, provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await)?;
ctx!(task, engine::copy_progressive(&task.from, &task.to, task.cha.unwrap()).await)?;
loop {
match rx.recv().await.unwrap_or(Ok(0)) {
Ok(0) => {
provider::remove_file(&task.from).await.ok();
engine::remove_file(&task.from).await.ok();
break;
}
Ok(n) => self.ops.out(task.id, FileOutCutDo::Adv(n)),
@ -199,7 +199,7 @@ impl File {
let mut src: PathCow = if task.resolve {
ok_or_not_found!(
task,
provider::read_link(&task.from).await,
engine::read_link(&task.from).await,
return Ok(self.ops.out(task.id, FileOutLink::Succ))
)
.into()
@ -208,14 +208,14 @@ impl File {
};
if task.relative {
let canon = ctx!(task, provider::canonicalize(task.to.parent().unwrap()).await)?;
let canon = ctx!(task, engine::canonicalize(task.to.parent().unwrap()).await)?;
src = ctx!(task, path_relative_to(canon.loc(), src))?;
}
ok_or_not_found!(task, provider::remove_file(&task.to).await);
ok_or_not_found!(task, engine::remove_file(&task.to).await);
ctx!(
task,
provider::symlink(&task.to, src, async || {
engine::symlink(&task.to, src, async || {
Ok(match task.cha {
Some(cha) => cha.is_dir(),
None => Self::cha(&task.from, task.resolve, None).await?.is_dir(),
@ -225,7 +225,7 @@ impl File {
)?;
if task.delete {
provider::remove_file(&task.from).await.ok();
engine::remove_file(&task.from).await.ok();
}
Ok(self.ops.out(task.id, FileOutLink::Succ))
}
@ -241,7 +241,7 @@ impl File {
self.ops.out(task.id, HookInOutHardlink::new(&task.from, &task.to));
super::traverse::<FileOutHardlink, _, _, _, _, _>(
task,
async |dir| match provider::create_dir(dir).await {
async |dir| match engine::create_dir(dir).await {
Err(e) if e.kind() != io::ErrorKind::AlreadyExists => Err(e)?,
_ => Ok(()),
},
@ -261,14 +261,14 @@ impl File {
pub(crate) async fn hardlink_do(&self, task: FileInHardlink) -> Result<(), FileOutHardlinkDo> {
let src = if !task.follow {
UrlCow::from(&task.from)
} else if let Ok(p) = provider::canonicalize(&task.from).await {
} else if let Ok(p) = engine::canonicalize(&task.from).await {
UrlCow::from(p)
} else {
UrlCow::from(&task.from)
};
ok_or_not_found!(task, provider::remove_file(&task.to).await);
ok_or_not_found!(task, provider::hard_link(&src, &task.to).await);
ok_or_not_found!(task, engine::remove_file(&task.to).await);
ok_or_not_found!(task, engine::hard_link(&src, &task.to).await);
Ok(self.ops.out(task.id, FileOutHardlinkDo::Succ))
}
@ -291,7 +291,7 @@ impl File {
}
pub(crate) async fn delete_do(&self, task: FileInDelete) -> Result<(), FileOutDeleteDo> {
match provider::remove_file(&task.target).await {
match engine::remove_file(&task.target).await {
Ok(()) => {}
Err(e) if e.kind() == NotFound => {}
Err(_) if !maybe_exists(&task.target).await => {}
@ -305,7 +305,7 @@ impl File {
}
pub(crate) async fn trash_do(&self, task: FileInTrash) -> Result<(), FileOutTrash> {
ctx!(task, provider::trash(&task.target).await)?;
ctx!(task, engine::trash(&task.target).await)?;
Ok(self.ops.out(task.id, FileOutTrash::Succ))
}
@ -345,12 +345,12 @@ impl File {
let cache = ctx!(task, task.target.cache(), "Cannot determine cache path")?;
let cache_tmp = ctx!(task, Transaction::tmp(&cache).await, "Cannot determine download cache")?;
let mut rx = ctx!(task, provider::copy_with_progress(&task.target, &cache_tmp, cha).await)?;
let mut rx = ctx!(task, engine::copy_progressive(&task.target, &cache_tmp, cha).await)?;
loop {
match rx.recv().await.unwrap_or(Ok(0)) {
Ok(0) => {
Local::regular(&cache).remove_dir_all().await.ok();
ctx!(task, provider::rename(cache_tmp, cache).await, "Cannot persist downloaded file")?;
ctx!(task, engine::rename(cache_tmp, cache).await, "Cannot persist downloaded file")?;
let lock = ctx!(task, task.target.cache_lock(), "Cannot determine cache lock")?;
let hash = format!("{:x}", cha.hash_u128());
@ -422,7 +422,7 @@ impl File {
ctx!(task, Transaction::tmp(&task.target).await, "Cannot determine temporary upload path")?;
let mut rx = ctx!(
task,
provider::copy_with_progress(cache, &tmp, Attrs {
engine::copy_progressive(cache, &tmp, Attrs {
mode: Some(cha.mode),
atime: None,
btime: None,
@ -440,7 +440,7 @@ impl File {
Err(anyhow!("Failed to work on: {task:?}: remote file has changed during upload"))?;
}
ctx!(task, provider::rename(&tmp, &task.target).await, "Cannot persist uploaded file")?;
ctx!(task, engine::rename(&tmp, &task.target).await, "Cannot persist uploaded file")?;
let cha =
ctx!(task, Self::cha(&task.target, true, None).await, "Cannot stat uploaded file")?;
@ -463,7 +463,7 @@ impl File {
let cha = if let Some(entry) = entry {
entry.metadata().await?
} else {
provider::symlink_metadata(url.as_url()).await?
engine::symlink_metadata(url.as_url()).await?
};
Ok(if follow { Cha::from_follow(url, cha).await } else { cha })
}

View file

@ -206,7 +206,7 @@ impl Drop for FileInCut {
impl FileInCut {
pub fn new(from: UrlBuf, to: UrlBuf, force: bool) -> Self {
Self {
follow: !from.scheme().covariant(to.scheme()),
follow: !from.auth().covariant(to.auth()),
id: Id::ZERO,
from,
to,

View file

@ -1,9 +1,9 @@
use std::{hash::{BuildHasher, Hash, Hasher}, io};
use yazi_fs::cha::ChaMode;
use yazi_fs::{cha::ChaMode, engine::Attrs};
use yazi_macro::ok_or_not_found;
use yazi_shared::{timestamp_us, url::{AsUrl, Url, UrlBuf}};
use yazi_vfs::{provider, unique_file};
use yazi_vfs::{engine, unique_file};
pub(super) struct Transaction;
@ -33,11 +33,11 @@ impl Transaction {
{
let url = url.as_url();
let cha = ok_or_not_found!(provider::symlink_metadata(url).await, return Ok(()));
let cha = ok_or_not_found!(engine::symlink_metadata(url).await, return Ok(()));
if cha.is_link() {
provider::rename(Self::tmp(url).await?, url).await?;
engine::rename(Self::tmp(url).await?, url).await?;
} else if !cha.contains(ChaMode::U_WRITE) {
provider::set_mode(url, cha.mode | ChaMode::U_WRITE).await?;
engine::set_attrs(url, Attrs::mode(cha.mode | ChaMode::U_WRITE)).await?;
}
Ok(())

View file

@ -1,8 +1,8 @@
use std::{collections::VecDeque, fmt::Debug};
use yazi_fs::{FsUrl, cha::Cha, path::skip_url, provider::{DirReader, FileHolder}};
use yazi_fs::{FsUrl, cha::Cha, engine::{DirReader, FileHolder}, path::skip_url};
use yazi_shared::{strand::StrandLike, url::{AsUrl, Url, UrlBuf, UrlLike}};
use yazi_vfs::provider::{self};
use yazi_vfs::engine::{self};
use crate::{ctx, file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInUpload}};
@ -180,7 +180,7 @@ where
}
while let Some(src) = dirs.pop_front() {
let mut it = err!(provider::read_dir(&src).await, "Cannot read directory {src:?}");
let mut it = err!(engine::read_dir(&src).await, "Cannot read directory {src:?}");
let dest = if let Some(root) = root {
let s = skip_url(&src, skip);

View file

@ -4,7 +4,7 @@ use parking_lot::Mutex;
use tokio::sync::mpsc;
use yazi_dds::Pump;
use yazi_fs::ok_or_not_found;
use yazi_vfs::provider;
use yazi_vfs::engine;
use crate::{Ongoing, TaskOp, TaskOps, TasksProxy, file::{FileOutCopy, FileOutCut, FileOutDelete, FileOutDownload, FileOutHardlink, FileOutLink, FileOutTrash, FileOutUpload}, hook::{HookIn, HookInDelete, HookInDownload, HookInOutCopy, HookInOutCut, HookInOutHardlink, HookInOutLink, HookInPreload, HookInTrash, HookInUpload}, preload::{Preload, PreloadOut}};
@ -31,7 +31,7 @@ impl Hook {
return self.ops.out(task.id, FileOutCut::Clean(Ok(())));
}
let result = ok_or_not_found(provider::remove_dir_clean(&task.from).await);
let result = ok_or_not_found(engine::remove_dir_clean(&task.from).await);
TasksProxy::update_succeed(task.id, [&task.to, &task.from], true);
Pump::push_move(task.from, task.to);
@ -52,7 +52,7 @@ impl Hook {
return self.ops.out(task.id, FileOutDelete::Clean(Ok(())));
}
let result = ok_or_not_found(provider::remove_dir_all(&task.target).await);
let result = ok_or_not_found(engine::remove_dir_all(&task.target).await);
TasksProxy::update_succeed(task.id, [&task.target], false);
Pump::push_delete(task.target);

View file

@ -78,7 +78,7 @@ impl Scheduler {
}
pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) {
let follow = follow || !from.scheme().covariant(to.scheme());
let follow = follow || !from.auth().covariant(to.auth());
let mut r#in = FileInCopy { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0 };
self.add(&mut r#in, |_| ());
@ -109,7 +109,7 @@ impl Scheduler {
let mut r#in = FileInHardlink { id: Id::ZERO, from, to, force, cha: None, follow };
self.add(&mut r#in, |_| ());
if !r#in.from.scheme().covariant(r#in.to.scheme()) {
if !r#in.from.auth().covariant(r#in.to.auth()) {
return self
.ops
.out(r#in.id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned()));

View file

@ -4,7 +4,7 @@ use parking_lot::RwLock;
use tokio::sync::mpsc;
use yazi_fs::FilesOp;
use yazi_shared::url::{UrlBuf, UrlLike};
use yazi_vfs::provider;
use yazi_vfs::engine;
use super::SizeIn;
use crate::{TaskOp, TaskOps, size::SizeOut};
@ -25,7 +25,7 @@ impl Size {
}
pub(crate) async fn size(&self, task: SizeIn) -> Result<(), SizeOut> {
let length = provider::calculate(&task.target).await.unwrap_or(0);
let length = engine::calculate(&task.target).await.unwrap_or(0);
task.throttle.done((task.target, length), |buf| {
{
let mut loading = self.sizing.write();

View file

@ -0,0 +1,65 @@
use std::{fmt, sync::Arc};
use anyhow::{Result, anyhow};
use percent_encoding::percent_decode_str;
use serde::Deserialize;
use yazi_shim::{SStr, cell::RoCell};
use crate::auth::{AuthInventory, AuthKind, Encode, Scheme};
pub(super) static DEFAULT_ARC: RoCell<Arc<Auth>> = RoCell::new();
#[derive(Debug, Deserialize, Eq, Hash, PartialEq)]
pub struct Auth {
#[serde(default)]
pub kind: AuthKind,
pub scheme: Scheme,
#[serde(default)]
pub domain: SStr,
}
impl Default for Auth {
fn default() -> Self { Self::DEFAULT }
}
impl fmt::Display for Auth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Encode(self, false).fmt(f) }
}
impl Auth {
pub const DEFAULT: Self =
Self { kind: AuthKind::Regular, scheme: Scheme::Regular, domain: SStr::Borrowed("") };
pub fn default_arc() -> Arc<Self> { DEFAULT_ARC.clone() }
pub fn new(kind: AuthKind, scheme: Scheme, domain: impl Into<SStr>) -> Arc<Self> {
Arc::new(Self { kind, scheme, domain: domain.into() })
}
pub fn get(scheme: &Scheme, domain: &str) -> Option<Arc<Self>> {
match scheme {
Scheme::Regular => Some(Self::default_arc()),
Scheme::Search => Some(Self::search(domain)),
_ => inventory::iter::<AuthInventory>().find_map(|entry| (entry.get)(scheme, domain)),
}
}
pub fn search(query: impl Into<String>) -> Arc<Self> {
Self::new(AuthKind::Search, Scheme::Search, query.into())
}
pub fn covariant(&self, other: &Self) -> bool {
!self.kind.is_virtual() && !other.kind.is_virtual() || self == other
}
pub fn parse_cache(cache: &str) -> Result<Arc<Self>> {
let (kind, rest) = cache.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?;
let (scheme, domain) = rest.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?;
Ok(Arc::new(Self {
kind: kind.parse()?,
scheme: scheme.parse()?,
domain: percent_decode_str(domain).decode_utf8()?.into_owned().into(),
}))
}
}

View file

@ -0,0 +1,26 @@
use std::fmt;
use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode};
use super::Auth;
pub struct Encode<'a>(pub &'a Auth, pub bool);
impl Encode<'_> {
pub fn domain(s: &str) -> PercentEncode<'_> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':');
percent_encode(s.as_bytes(), SET)
}
}
impl fmt::Display for Encode<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}://{}",
self.0.scheme,
if self.1 { "~" } else { "" },
Self::domain(&self.0.domain)
)
}
}

View file

@ -0,0 +1,9 @@
use std::sync::Arc;
use crate::auth::{Auth, Scheme};
pub struct AuthInventory {
pub get: fn(&Scheme, &str) -> Option<Arc<Auth>>,
}
inventory::collect!(AuthInventory);

View file

@ -0,0 +1,42 @@
use serde::Deserialize;
use strum::{EnumString, IntoStaticStr};
#[derive(
Clone, Copy, Debug, Default, Deserialize, EnumString, Eq, Hash, IntoStaticStr, PartialEq,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum AuthKind {
#[default]
Regular,
Search,
Mount,
Scope,
Sftp,
}
impl AuthKind {
#[inline]
pub fn is_local(self) -> bool {
match self {
Self::Regular | Self::Search => true,
Self::Mount | Self::Scope | Self::Sftp => false,
}
}
#[inline]
pub fn is_remote(self) -> bool {
match self {
Self::Regular | Self::Search | Self::Mount => false,
Self::Scope | Self::Sftp => true,
}
}
#[inline]
pub fn is_virtual(self) -> bool {
match self {
Self::Regular | Self::Search => false,
Self::Mount | Self::Scope | Self::Sftp => true,
}
}
}

View file

@ -0,0 +1,3 @@
yazi_macro::mod_flat!(auth encode inventory kind scheme);
pub(super) fn init() { DEFAULT_ARC.with(Default::default); }

View file

@ -0,0 +1,63 @@
use std::{fmt, str::FromStr};
use anyhow::{Result, bail};
use serde_with::DeserializeFromStr;
use crate::{BytesExt, pool::{InternStr, Symbol}};
#[derive(Clone, Debug, DeserializeFromStr, Eq, Hash, PartialEq)]
pub enum Scheme {
Regular,
Search,
Sftp,
Custom(Symbol<str>),
}
impl AsRef<str> for Scheme {
fn as_ref(&self) -> &str { self.as_str() }
}
impl PartialEq<str> for Scheme {
fn eq(&self, other: &str) -> bool { self.as_str() == other }
}
impl PartialEq<&str> for Scheme {
fn eq(&self, other: &&str) -> bool { self == *other }
}
impl PartialEq<&String> for Scheme {
fn eq(&self, other: &&String) -> bool { self == other.as_str() }
}
impl PartialEq<&Self> for Scheme {
fn eq(&self, other: &&Self) -> bool { self == *other }
}
impl fmt::Display for Scheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_str().fmt(f) }
}
impl FromStr for Scheme {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
Ok(match s {
"regular" => Self::Regular,
"search" => Self::Search,
"sftp" => Self::Sftp,
_ if !s.is_empty() && s.as_bytes().kebab_cased() => Self::Custom(s.intern()),
_ => bail!("scheme must be kebab-case and non-empty, got: {s}"),
})
}
}
impl Scheme {
pub fn as_str(&self) -> &str {
match self {
Self::Regular => "regular",
Self::Search => "search",
Self::Sftp => "sftp",
Self::Custom(s) => s,
}
}
}

View file

@ -6,10 +6,12 @@ use crate::{BytesExt, SnakeCasedString};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct KebabCasedString(String);
pub struct KebabCasedKey(String);
impl KebabCasedString {
pub fn new(s: String) -> Option<Self> { s.as_bytes().kebab_cased().then_some(Self(s)) }
impl KebabCasedKey {
pub fn new(s: String) -> Option<Self> {
(!s.is_empty() && s.len() < 20 && s.as_bytes().kebab_cased()).then_some(Self(s))
}
pub fn into_snake_cased(self) -> SnakeCasedString {
let mut b = self.0.into_bytes();
@ -22,50 +24,52 @@ impl KebabCasedString {
}
}
impl Deref for KebabCasedString {
impl Deref for KebabCasedKey {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Borrow<str> for KebabCasedString {
impl Borrow<str> for KebabCasedKey {
#[inline]
fn borrow(&self) -> &str { &self.0 }
}
impl Borrow<String> for KebabCasedString {
impl Borrow<String> for KebabCasedKey {
#[inline]
fn borrow(&self) -> &String { &self.0 }
}
impl AsRef<str> for KebabCasedString {
impl AsRef<str> for KebabCasedKey {
#[inline]
fn as_ref(&self) -> &str { &self.0 }
}
impl AsRef<OsStr> for KebabCasedString {
impl AsRef<OsStr> for KebabCasedKey {
#[inline]
fn as_ref(&self) -> &OsStr { self.0.as_ref() }
}
impl Display for KebabCasedString {
impl Display for KebabCasedKey {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(&self.0, f) }
}
impl From<KebabCasedString> for String {
impl From<KebabCasedKey> for String {
#[inline]
fn from(value: KebabCasedString) -> Self { value.0 }
fn from(value: KebabCasedKey) -> Self { value.0 }
}
impl From<KebabCasedString> for Cow<'_, str> {
impl From<KebabCasedKey> for Cow<'_, str> {
#[inline]
fn from(value: KebabCasedString) -> Self { Cow::Owned(value.0) }
fn from(value: KebabCasedKey) -> Self { Cow::Owned(value.0) }
}
impl<'de> Deserialize<'de> for KebabCasedString {
impl<'de> Deserialize<'de> for KebabCasedKey {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::new(value).ok_or_else(|| serde::de::Error::custom("must be a kebab-cased string"))
Self::new(value).ok_or_else(|| {
serde::de::Error::custom("must be a non-empty kebab-cased key shorter than 20 characters")
})
}
}

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