feat: virtual file system (#3034)

This commit is contained in:
三咲雅 misaki masa 2025-08-05 02:23:57 +08:00 committed by GitHub
parent da97e5a8b4
commit d5f225bc4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 908 additions and 447 deletions

1
Cargo.lock generated
View file

@ -3556,6 +3556,7 @@ dependencies = [
"clap_complete", "clap_complete",
"clap_complete_fig", "clap_complete_fig",
"clap_complete_nushell", "clap_complete_nushell",
"futures",
"regex", "regex",
"serde", "serde",
"vergen-gitcl", "vergen-gitcl",

View file

@ -1,7 +1,7 @@
use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR}; use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR};
use anyhow::Result; use anyhow::Result;
use yazi_fs::{CWD, expand_url, services::Local}; use yazi_fs::{CWD, expand_url, provider};
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
use yazi_proxy::CmpProxy; use yazi_proxy::CmpProxy;
@ -36,8 +36,7 @@ impl Actor for Trigger {
let ticket = cmp.ticket; let ticket = cmp.ticket;
tokio::spawn(async move { tokio::spawn(async move {
// TODO: support VFS let mut dir = provider::read_dir(&parent).await?;
let mut dir = Local::read_dir(&parent).await?;
let mut cache = vec![]; let mut cache = vec![];
// "/" is both a directory separator and the root directory per se // "/" is both a directory separator and the root directory per se
@ -68,7 +67,7 @@ impl Actor for Trigger {
impl Trigger { impl Trigger {
fn split_url(s: &str) -> Option<(Url, UrnBuf)> { fn split_url(s: &str) -> Option<(Url, UrnBuf)> {
let (scheme, path) = Url::parse(s.as_bytes()).ok()?; let (scheme, path, _) = Url::parse(s.as_bytes()).ok()?;
if !scheme.is_virtual() && path.as_os_str() == "~" { if !scheme.is_virtual() && path.as_os_str() == "~" {
return None; // We don't autocomplete a `~`, but `~/` return None; // We don't autocomplete a `~`, but `~/`
@ -96,7 +95,7 @@ mod tests {
fn compare(s: &str, parent: &str, child: &str) { fn compare(s: &str, parent: &str, child: &str) {
let (p, c) = Trigger::split_url(s).unwrap(); let (p, c) = Trigger::split_url(s).unwrap();
let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(p); let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(p);
assert_eq!((p, c.as_urn()), (parent.try_into().unwrap(), Urn::new(child))); assert_eq!((p, c.as_urn()), (parent.parse().unwrap(), Urn::new(child)));
} }
#[cfg(unix)] #[cfg(unix)]

View file

@ -145,7 +145,7 @@ impl UserData for File {
return None; return None;
} }
let h = finder.filter.highlighted(me.name())?; let h = finder.filter.highlighted(me.url.file_name()?)?;
Some(h.into_iter().map(Range::from).collect::<Vec<_>>()) Some(h.into_iter().map(Range::from).collect::<Vec<_>>())
}) })
}); });

View file

@ -1,12 +1,12 @@
use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, hash::Hash, io::{Read, Write}, ops::Deref}; use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, hash::Hash, io::{Read, Write}, ops::Deref, path::Path};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use crossterm::{execute, style::Print}; use crossterm::{execute, style::Print};
use scopeguard::defer; use scopeguard::defer;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use yazi_config::YAZI; use yazi_config::{YAZI, opener::OpenerRule};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file, services::{self, Local}, skip_url}; use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file, provider::{self, local::{Gate, Local}}, skip_url};
use yazi_macro::{err, succ}; use yazi_macro::{err, succ};
use yazi_parser::VoidOpt; use yazi_parser::VoidOpt;
use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER}; use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER};
@ -23,7 +23,7 @@ impl Actor for BulkRename {
const NAME: &str = "bulk_rename"; const NAME: &str = "bulk_rename";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let Some(opener) = YAZI.opener.block(YAZI.open.all("bulk-rename.txt", "text/plain")) else { let Some(opener) = Self::opener() else {
succ!(AppProxy::notify_warn("Bulk rename", "No text opener found")); succ!(AppProxy::notify_warn("Bulk rename", "No text opener found"));
}; };
@ -39,8 +39,7 @@ impl Actor for BulkRename {
let cwd = cx.cwd().clone(); let cwd = cx.cwd().clone();
tokio::spawn(async move { tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk"); let tmp = YAZI.preview.tmpfile("bulk");
// TODO: pull `OpenOptions` into `yazi_fs` Gate::default()
tokio::fs::OpenOptions::new()
.write(true) .write(true)
.create_new(true) .create_new(true)
.open(&tmp) .open(&tmp)
@ -116,7 +115,7 @@ impl BulkRename {
if maybe_exists(&new).await && !paths_to_same_file(&old, &new).await { if maybe_exists(&new).await && !paths_to_same_file(&old, &new).await {
failed.push((o, n, anyhow!("Destination already exists"))); failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = services::rename(&old, &new).await { } else if let Err(e) = provider::rename(&old, &new).await {
failed.push((o, n, e.into())); failed.push((o, n, e.into()));
} else if let Ok(f) = File::new(new).await { } else if let Ok(f) = File::new(new).await {
succeeded.insert(old, f); succeeded.insert(old, f);
@ -138,6 +137,10 @@ impl BulkRename {
Ok(()) Ok(())
} }
fn opener() -> Option<&'static OpenerRule> {
YAZI.opener.block(YAZI.open.all(Url::from(Path::new("bulk-rename.txt")), "text/plain"))
}
async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout(); let mut stdout = TTY.lockout();
terminal_clear(&mut *stdout)?; terminal_clear(&mut *stdout)?;

View file

@ -5,7 +5,7 @@ use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::popup::InputCfg; use yazi_config::popup::InputCfg;
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, expand_path}; use yazi_fs::{File, FilesOp, expand_url};
use yazi_macro::{act, err, render, succ}; use yazi_macro::{act, err, render, succ};
use yazi_parser::mgr::CdOpt; use yazi_parser::mgr::CdOpt;
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; use yazi_proxy::{CmpProxy, InputProxy, MgrProxy};
@ -76,9 +76,9 @@ impl Cd {
while let Some(result) = rx.next().await { while let Some(result) = rx.next().await {
match result { match result {
Ok(s) => { Ok(s) => {
let url = Url::from(expand_path(s)); let Ok(url) = Url::try_from(s).map(expand_url) else { return };
let Ok(file) = File::new(url.clone()).await else { return }; let Ok(file) = File::new(url.as_ref().clone()).await else { return };
if file.is_dir() { if file.is_dir() {
return MgrProxy::cd(&url); return MgrProxy::cd(&url);
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, realname, services}; use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
use yazi_macro::succ; use yazi_macro::succ;
use yazi_parser::mgr::CreateOpt; use yazi_parser::mgr::CreateOpt;
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER}; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
@ -45,15 +45,15 @@ impl Create {
let _permit = WATCHER.acquire().await.unwrap(); let _permit = WATCHER.acquire().await.unwrap();
if dir { if dir {
services::create_dir_all(&new).await?; provider::create_dir_all(&new).await?;
} else if let Some(real) = realname(&new).await { } else if let Some(real) = realname(&new).await {
ok_or_not_found(services::remove_file(&new).await)?; ok_or_not_found(provider::remove_file(&new).await)?;
FilesOp::Deleting(parent.clone(), [UrnBuf::from(real)].into()).emit(); FilesOp::Deleting(parent.clone(), [UrnBuf::from(real)].into()).emit();
services::create(&new).await?; provider::create(&new).await?;
} else { } else {
services::create_dir_all(&parent).await.ok(); provider::create_dir_all(&parent).await.ok();
ok_or_not_found(services::remove_file(&new).await)?; ok_or_not_found(provider::remove_file(&new).await)?;
services::create(&new).await?; provider::create(&new).await?;
} }
if let Ok(f) = File::new(new.clone()).await { if let Ok(f) = File::new(new.clone()).await {

View file

@ -75,7 +75,7 @@ impl Quit {
} }
let paths = selected.fold(OsString::new(), |mut s, u| { let paths = selected.fold(OsString::new(), |mut s, u| {
s.push(u.as_os_str()); s.push(u.os_str());
s.push("\n"); s.push("\n");
s s
}); });

View file

@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, paths_to_same_file, realname, services}; use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, paths_to_same_file, provider, realname};
use yazi_macro::{act, err, succ}; use yazi_macro::{act, err, succ};
use yazi_parser::mgr::RenameOpt; use yazi_parser::mgr::RenameOpt;
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER}; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
@ -65,10 +65,10 @@ impl Rename {
let _permit = WATCHER.acquire().await.unwrap(); let _permit = WATCHER.acquire().await.unwrap();
let overwritten = realname(&new).await; let overwritten = realname(&new).await;
services::rename(&old, &new).await?; provider::rename(&old, &new).await?;
if let Some(o) = overwritten { if let Some(o) = overwritten {
ok_or_not_found(services::rename(p_new.join(&o), &new).await)?; ok_or_not_found(provider::rename(p_new.join(&o), &new).await)?;
FilesOp::Deleting(p_new.clone(), [UrnBuf::from(o)].into()).emit(); FilesOp::Deleting(p_new.clone(), [UrnBuf::from(o)].into()).emit();
} }

View file

@ -1,10 +1,8 @@
use std::io::BufReader;
use anyhow::Result; use anyhow::Result;
use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation};
use ratatui::layout::Rect; use ratatui::layout::Rect;
use yazi_config::YAZI; use yazi_config::YAZI;
use yazi_fs::services; use yazi_fs::provider;
use yazi_shared::url::Url; use yazi_shared::url::Url;
use crate::Dimension; use crate::Dimension;
@ -40,7 +38,7 @@ impl Image {
}) })
.await??; .await??;
Ok(services::write(cache, buf).await?) Ok(provider::write(cache, buf).await?)
} }
pub(super) async fn downscale(url: &Url, rect: Rect) -> Result<DynamicImage> { pub(super) async fn downscale(url: &Url, rect: Rect) -> Result<DynamicImage> {
@ -110,7 +108,7 @@ impl Image {
limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32); limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32);
} }
let mut reader = ImageReader::new(BufReader::new(services::open(&url).await?.into_std().await)); let mut reader = ImageReader::new(provider::open(&url).await?.reader_sync().await);
if let Ok(format) = ImageFormat::from_path(url) { if let Ok(format) = ImageFormat::from_path(url) {
reader.set_format(format); reader.set_format(format);
} }

View file

@ -1,9 +1,9 @@
use std::{borrow::Cow, ops::Deref}; use std::{borrow::Cow, ops::Deref};
use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value};
use yazi_shared::url::{CovUrl, Scheme}; use yazi_shared::url::CovUrl;
use crate::{Urn, cached_field}; use crate::{Urn, cached_field, deprecate};
pub type UrlRef = UserDataRef<Url>; pub type UrlRef = UserDataRef<Url>;
@ -16,7 +16,7 @@ pub struct Url {
v_urn: Option<Value>, v_urn: Option<Value>,
v_base: Option<Value>, v_base: Option<Value>,
v_parent: Option<Value>, v_parent: Option<Value>,
v_frag: Option<Value>, v_domain: Option<Value>,
} }
impl Deref for Url { impl Deref for Url {
@ -64,7 +64,7 @@ impl Url {
v_urn: None, v_urn: None,
v_base: None, v_base: None,
v_parent: None, v_parent: None,
v_frag: None, v_domain: None,
} }
} }
@ -94,10 +94,7 @@ impl FromLua for Url {
impl UserData for Url { impl UserData for Url {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, name, |lua, me| { cached_field!(fields, name, |lua, me| {
Some(me.name()) me.file_name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
.filter(|&s| !s.is_empty())
.map(|s| lua.create_string(s.as_encoded_bytes()))
.transpose()
}); });
cached_field!(fields, stem, |lua, me| { cached_field!(fields, stem, |lua, me| {
me.file_stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() me.file_stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
@ -110,13 +107,13 @@ impl UserData for Url {
cached_field!(fields, base, |_, me| { cached_field!(fields, base, |_, me| {
Ok(if me.base().as_os_str().is_empty() { None } else { Some(Self::new(me.base())) }) Ok(if me.base().as_os_str().is_empty() { None } else { Some(Self::new(me.base())) })
}); });
// TODO: remove cached_field!(fields, domain, |lua, me| {
cached_field!(fields, frag, |lua, me| { me.scheme.domain().map(|s| lua.create_string(s)).transpose()
if let Scheme::Search(kw) = &me.scheme { });
Some(lua.create_string(kw)).transpose()
} else { fields.add_field_method_get("frag", |lua, me| {
Ok(None) deprecate!(lua, "`frag` property of Url is deprecated and renamed to `domain`, please use the new name instead, in your {}");
} me.scheme.domain().map(|s| lua.create_string(s)).transpose()
}); });
fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular()));
@ -157,8 +154,8 @@ impl UserData for Url {
Ok(url.map(Self::new)) Ok(url.map(Self::new))
}); });
methods.add_function_mut("into_search", |_, (ud, frag): (AnyUserData, mlua::String)| { methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| {
Ok(Self::new(ud.take::<Self>()?.inner.into_search(frag.to_str()?))) Ok(Self::new(ud.take::<Self>()?.inner.into_search(domain.to_str()?)))
}); });
methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner)); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner));

View file

@ -16,9 +16,10 @@ yazi-macro = { path = "../yazi-macro", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" } yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
# External dependencies # External dependencies
clap = { workspace = true } clap = { workspace = true }
regex = { workspace = true } futures = { workspace = true }
serde = { workspace = true } regex = { workspace = true }
serde = { workspace = true }
[build-dependencies] [build-dependencies]
yazi-shared = { path = "../yazi-shared", version = "25.6.11" } yazi-shared = { path = "../yazi-shared", version = "25.6.11" }

View file

@ -1,8 +1,9 @@
use std::{env, ffi::OsStr, fmt::Write}; use std::{env, ffi::OsStr, fmt::Write, path::Path};
use regex::Regex; use regex::Regex;
use yazi_adapter::Mux; use yazi_adapter::Mux;
use yazi_shared::timestamp_us; use yazi_config::YAZI;
use yazi_shared::{timestamp_us, url::Url};
use super::Actions; use super::Actions;
@ -57,17 +58,17 @@ impl Actions {
writeln!( writeln!(
s, s,
" default : {:?}", " default : {:?}",
yazi_config::YAZI.opener.first(yazi_config::YAZI.open.all("f75a.txt", "text/plain")) YAZI.opener.first(YAZI.open.all(Url::from(Path::new("f75a.txt")), "text/plain"))
)?; )?;
writeln!( writeln!(
s, s,
" block-create: {:?}", " block-create: {:?}",
yazi_config::YAZI.opener.block(yazi_config::YAZI.open.all("bulk-create.txt", "text/plain")) YAZI.opener.block(YAZI.open.all(Url::from(Path::new("bulk-create.txt")), "text/plain"))
)?; )?;
writeln!( writeln!(
s, s,
" block-rename: {:?}", " block-rename: {:?}",
yazi_config::YAZI.opener.block(yazi_config::YAZI.open.all("bulk-rename.txt", "text/plain")) YAZI.opener.block(YAZI.open.all(Url::from(Path::new("bulk-rename.txt")), "text/plain"))
)?; )?;
writeln!(s, "\nMultiplexers")?; writeln!(s, "\nMultiplexers")?;

View file

@ -1,14 +1,14 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Parser, command}; use clap::{Parser, command};
use yazi_shared::Id; use yazi_shared::{Id, url::Url};
#[derive(Debug, Default, Parser)] #[derive(Debug, Default, Parser)]
#[command(name = "yazi")] #[command(name = "yazi")]
pub struct Args { pub struct Args {
/// Set the current working entry /// Set the current working entry
#[arg(index = 1, num_args = 1..=9)] #[arg(index = 1, num_args = 1..=9)]
pub entries: Vec<PathBuf>, pub entries: Vec<Url>,
/// Write the cwd on exit to this file /// Write the cwd on exit to this file
#[arg(long)] #[arg(long)]

View file

@ -1,12 +1,14 @@
use std::{collections::HashSet, ffi::OsString, path::PathBuf}; use std::{borrow::Cow, collections::HashSet, path::PathBuf};
use futures::executor::block_on;
use serde::Serialize; use serde::Serialize;
use yazi_fs::{CWD, Xdg, expand_path}; use yazi_fs::{CWD, Xdg, expand_url, provider};
use yazi_shared::url::{Url, UrnBuf};
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
pub struct Boot { pub struct Boot {
pub cwds: Vec<PathBuf>, pub cwds: Vec<Url>,
pub files: Vec<OsString>, pub files: Vec<UrnBuf>,
pub local_events: HashSet<String>, pub local_events: HashSet<String>,
pub remote_events: HashSet<String>, pub remote_events: HashSet<String>,
@ -18,31 +20,31 @@ pub struct Boot {
} }
impl Boot { impl Boot {
fn parse_entries(entries: &[PathBuf]) -> (Vec<PathBuf>, Vec<OsString>) { async fn parse_entries(entries: &[Url]) -> (Vec<Url>, Vec<UrnBuf>) {
if entries.is_empty() { if entries.is_empty() {
return (vec![CWD.load().to_path_buf()], vec![OsString::new()]); return (vec![CWD.load().as_ref().clone()], vec![UrnBuf::default()]);
} }
let mut cwds = Vec::with_capacity(entries.len()); async fn go<'a>(entry: Cow<'a, Url>) -> (Url, UrnBuf) {
let mut files = Vec::with_capacity(entries.len()); let Some((parent, child)) = entry.pair() else {
for entry in entries.iter().map(expand_path) { return (entry.into_owned(), UrnBuf::default());
if let Some(p) = entry.parent().filter(|_| !entry.is_dir()) { };
cwds.push(p.to_owned());
files.push(entry.file_name().unwrap().to_owned()); if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) {
(parent, child)
} else { } else {
cwds.push(entry); (entry.into_owned(), UrnBuf::default())
files.push(OsString::new());
} }
} }
(cwds, files) futures::future::join_all(entries.iter().map(expand_url).map(go)).await.into_iter().unzip()
} }
} }
impl From<&crate::Args> for Boot { impl From<&crate::Args> for Boot {
fn from(args: &crate::Args) -> Self { fn from(args: &crate::Args) -> Self {
let config_dir = Xdg::config_dir(); let config_dir = Xdg::config_dir();
let (cwds, files) = Self::parse_entries(&args.entries); let (cwds, files) = block_on(Self::parse_entries(&args.entries));
let local_events = args let local_events = args
.local_events .local_events

View file

@ -1,5 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use yazi_fs::{ok_or_not_found, remove_dir_clean, services::Local}; use yazi_fs::{ok_or_not_found, provider::local::Local, remove_dir_clean};
use yazi_macro::outln; use yazi_macro::outln;
use super::Dependency; use super::Dependency;

View file

@ -1,7 +1,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use yazi_fs::{remove_dir_clean, services::Local}; use yazi_fs::{provider::local::Local, remove_dir_clean};
use yazi_macro::outln; use yazi_macro::outln;
use super::Dependency; use super::Dependency;

View file

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

View file

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

View file

@ -1,7 +1,7 @@
use std::{io, path::Path}; use std::{io, path::Path};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use yazi_fs::{ok_or_not_found, services::Local}; use yazi_fs::{ok_or_not_found, provider::local::{Gate, Local}};
#[inline] #[inline]
pub async fn must_exists(path: impl AsRef<Path>) -> bool { pub async fn must_exists(path: impl AsRef<Path>) -> bool {
@ -16,13 +16,11 @@ pub async fn maybe_exists(path: impl AsRef<Path>) -> bool {
} }
} }
// TODO: use `yazi_fs` instead of `tokio::fs`
pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> {
let b = Local::read(from).await?; let b = Local::read(from).await?;
ok_or_not_found(remove_sealed(to).await)?; ok_or_not_found(remove_sealed(to).await)?;
let mut file = let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?;
tokio::fs::OpenOptions::new().create_new(true).write(true).truncate(true).open(to).await?;
file.write_all(&b).await?; file.write_all(&b).await?;
let mut perm = file.metadata().await?.permissions(); let mut perm = file.metadata().await?.permissions();

View file

@ -239,16 +239,16 @@ rules = [
# { mime = "inode/empty", fg = "red" }, # { mime = "inode/empty", fg = "red" },
# Special files # Special files
{ name = "*", is = "orphan", bg = "red" }, { url = "*", is = "orphan", bg = "red" },
{ name = "*", is = "exec" , fg = "green" }, { url = "*", is = "exec" , fg = "green" },
# Dummy files # Dummy files
{ name = "*", is = "dummy", bg = "red" }, { url = "*", is = "dummy", bg = "red" },
{ name = "*/", is = "dummy", bg = "red" }, { url = "*/", is = "dummy", bg = "red" },
# Fallback # Fallback
# { name = "*", fg = "white" }, # { url = "*", fg = "white" },
{ name = "*/", fg = "blue" } { url = "*/", fg = "blue" }
] ]
# : }}} # : }}}

View file

@ -239,16 +239,16 @@ rules = [
# { mime = "inode/empty", fg = "red" }, # { mime = "inode/empty", fg = "red" },
# Special files # Special files
{ name = "*", is = "orphan", bg = "red" }, { url = "*", is = "orphan", bg = "red" },
{ name = "*", is = "exec" , fg = "green" }, { url = "*", is = "exec" , fg = "green" },
# Dummy files # Dummy files
{ name = "*", is = "dummy", bg = "red" }, { url = "*", is = "dummy", bg = "red" },
{ name = "*/", is = "dummy", bg = "red" }, { url = "*/", is = "dummy", bg = "red" },
# Fallback # Fallback
# { name = "*", fg = "white" }, # { url = "*", fg = "white" },
{ name = "*/", fg = "blue" } { url = "*/", fg = "blue" }
] ]
# : }}} # : }}}

View file

@ -63,7 +63,7 @@ play = [
[open] [open]
rules = [ rules = [
# Folder # Folder
{ name = "*/", use = [ "edit", "open", "reveal" ] }, { url = "*/", use = [ "edit", "open", "reveal" ] },
# Text # Text
{ mime = "text/*", use = [ "edit", "reveal" ] }, { mime = "text/*", use = [ "edit", "reveal" ] },
# Image # Image
@ -78,7 +78,7 @@ rules = [
# Empty file # Empty file
{ mime = "inode/empty", use = [ "edit", "reveal" ] }, { mime = "inode/empty", use = [ "edit", "reveal" ] },
# Fallback # Fallback
{ name = "*", use = [ "open", "reveal" ] }, { url = "*", use = [ "open", "reveal" ] },
] ]
[tasks] [tasks]
@ -92,10 +92,10 @@ suppress_preload = false
[plugin] [plugin]
fetchers = [ fetchers = [
# Mimetype # Mimetype
{ id = "mime", name = "*", run = "mime", prio = "high" }, { id = "mime", url = "*", run = "mime", prio = "high" },
] ]
spotters = [ spotters = [
{ name = "*/", run = "folder" }, { url = "*/", run = "folder" },
# Code # Code
{ mime = "text/*", run = "code" }, { mime = "text/*", run = "code" },
{ mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" }, { mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" },
@ -106,7 +106,7 @@ spotters = [
# Video # Video
{ mime = "video/*", run = "video" }, { mime = "video/*", run = "video" },
# Fallback # Fallback
{ name = "*", run = "file" }, { url = "*", run = "file" },
] ]
preloaders = [ preloaders = [
# Image # Image
@ -122,7 +122,7 @@ preloaders = [
{ mime = "application/ms-opentype", run = "font" }, { mime = "application/ms-opentype", run = "font" },
] ]
previewers = [ previewers = [
{ name = "*/", run = "folder" }, { url = "*/", run = "folder" },
# Code # Code
{ mime = "text/*", run = "code" }, { mime = "text/*", run = "code" },
{ mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" }, { mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" },
@ -139,18 +139,18 @@ previewers = [
# Archive # Archive
{ mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", run = "archive" }, { mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", run = "archive" },
{ mime = "application/{debian*-package,redhat-package-manager,rpm,android.package-archive}", run = "archive" }, { mime = "application/{debian*-package,redhat-package-manager,rpm,android.package-archive}", run = "archive" },
{ name = "*.{AppImage,appimage}", run = "archive" }, { url = "*.{AppImage,appimage}", run = "archive" },
# Virtual Disk / Disk Image # Virtual Disk / Disk Image
{ mime = "application/{iso9660-image,qemu-disk,ms-wim,apple-diskimage}", run = "archive" }, { mime = "application/{iso9660-image,qemu-disk,ms-wim,apple-diskimage}", run = "archive" },
{ mime = "application/virtualbox-{vhd,vhdx}", run = "archive" }, { mime = "application/virtualbox-{vhd,vhdx}", run = "archive" },
{ name = "*.{img,fat,ext,ext2,ext3,ext4,squashfs,ntfs,hfs,hfsx}", run = "archive" }, { url = "*.{img,fat,ext,ext2,ext3,ext4,squashfs,ntfs,hfs,hfsx}", run = "archive" },
# Font # Font
{ mime = "font/*", run = "font" }, { mime = "font/*", run = "font" },
{ mime = "application/ms-opentype", run = "font" }, { mime = "application/ms-opentype", run = "font" },
# Empty file # Empty file
{ mime = "inode/empty", run = "empty" }, { mime = "inode/empty", run = "empty" },
# Fallback # Fallback
{ name = "*", run = "file" }, { url = "*", run = "file" },
] ]
[input] [input]

View file

@ -1,10 +1,10 @@
use std::{ops::Deref, path::Path}; use std::ops::Deref;
use anyhow::Result; use anyhow::Result;
use indexmap::IndexSet; use indexmap::IndexSet;
use serde::Deserialize; use serde::Deserialize;
use yazi_codegen::DeserializeOver2; use yazi_codegen::DeserializeOver2;
use yazi_shared::MIME_DIR; use yazi_shared::{MIME_DIR, url::Url};
use crate::{Preset, open::OpenRule}; use crate::{Preset, open::OpenRule};
@ -24,10 +24,10 @@ impl Deref for Open {
} }
impl Open { impl Open {
pub fn all<'a, 'b, P, M>(&'a self, path: P, mime: M) -> impl Iterator<Item = &'a str> + 'b pub fn all<'a, 'b, P, M>(&'a self, url: P, mime: M) -> impl Iterator<Item = &'a str> + 'b
where where
'a: 'b, 'a: 'b,
P: AsRef<Path> + 'b, P: AsRef<Url> + 'b,
M: AsRef<str> + 'b, M: AsRef<str> + 'b,
{ {
let is_dir = mime.as_ref() == MIME_DIR; let is_dir = mime.as_ref() == MIME_DIR;
@ -36,19 +36,16 @@ impl Open {
.iter() .iter()
.filter(move |&r| { .filter(move |&r| {
r.mime.as_ref().is_some_and(|p| p.match_mime(&mime)) r.mime.as_ref().is_some_and(|p| p.match_mime(&mime))
|| r.name.as_ref().is_some_and(|p| p.match_path(&path, is_dir)) || r.url.as_ref().is_some_and(|p| p.match_url(&url, is_dir))
}) })
.flat_map(|r| &r.r#use) .flat_map(|r| &r.r#use)
.map(String::as_str) .map(String::as_str)
} }
pub fn common<'a>( pub fn common<'a>(&'a self, targets: &[(impl AsRef<Url>, impl AsRef<str>)]) -> IndexSet<&'a str> {
&'a self,
targets: &[(impl AsRef<Path>, impl AsRef<str>)],
) -> IndexSet<&'a str> {
let each: Vec<IndexSet<&str>> = targets let each: Vec<IndexSet<&str>> = targets
.iter() .iter()
.map(|(p, m)| self.all(p, m).collect::<IndexSet<_>>()) .map(|(u, m)| self.all(u, m).collect::<IndexSet<_>>())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.collect(); .collect();

View file

@ -6,7 +6,7 @@ use crate::pattern::Pattern;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct OpenRule { pub struct OpenRule {
pub name: Option<Pattern>, pub url: Option<Pattern>,
pub mime: Option<Pattern>, pub mime: Option<Pattern>,
#[serde(deserialize_with = "OpenRule::deserialize")] #[serde(deserialize_with = "OpenRule::deserialize")]
pub r#use: Vec<String>, pub r#use: Vec<String>,
@ -14,10 +14,10 @@ pub struct OpenRule {
impl OpenRule { impl OpenRule {
#[inline] #[inline]
pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
#[inline] #[inline]
pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
} }
impl OpenRule { impl OpenRule {

View file

@ -1,12 +1,15 @@
use std::{path::Path, str::FromStr}; use std::str::FromStr;
use anyhow::Result;
use globset::GlobBuilder; use globset::GlobBuilder;
use serde::Deserialize; use serde::Deserialize;
use yazi_shared::url::{Scheme, Url};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(try_from = "String")] #[serde(try_from = "String")]
pub struct Pattern { pub struct Pattern {
inner: globset::GlobMatcher, inner: globset::GlobMatcher,
scheme: PatternScheme,
is_dir: bool, is_dir: bool,
is_star: bool, is_star: bool,
#[cfg(windows)] #[cfg(windows)]
@ -15,28 +18,35 @@ pub struct Pattern {
impl Pattern { impl Pattern {
#[inline] #[inline]
pub fn match_mime(&self, mime: impl AsRef<str>) -> bool { pub fn match_url(&self, url: impl AsRef<Url>, is_dir: bool) -> bool {
self.is_star || (!mime.as_ref().is_empty() && self.inner.is_match(mime.as_ref())) let url = url.as_ref();
}
#[inline]
pub fn match_path(&self, path: impl AsRef<Path>, is_dir: bool) -> bool {
if is_dir != self.is_dir { if is_dir != self.is_dir {
return false; return false;
} else if self.is_star { } else if self.is_star {
return true; return true;
} else if !self.scheme.matches(&url.scheme) {
return false;
} }
#[cfg(unix)]
let path = &url.loc;
#[cfg(windows)] #[cfg(windows)]
let path = if self.sep_lit { let path = if self.sep_lit {
yazi_fs::backslash_to_slash(path.as_ref()) yazi_fs::backslash_to_slash(&url.loc)
} else { } else {
std::borrow::Cow::Borrowed(path.as_ref()) std::borrow::Cow::Borrowed(url.loc.as_path())
}; };
self.inner.is_match(path) self.inner.is_match(path)
} }
#[inline]
pub fn match_mime(&self, mime: impl AsRef<str>) -> bool {
self.is_star || (!mime.as_ref().is_empty() && self.inner.is_match(mime.as_ref()))
}
#[inline] #[inline]
pub fn any_file(&self) -> bool { self.is_star && !self.is_dir } pub fn any_file(&self) -> bool { self.is_star && !self.is_dir }
@ -45,14 +55,23 @@ impl Pattern {
} }
impl FromStr for Pattern { impl FromStr for Pattern {
type Err = globset::Error; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let a = s.trim_start_matches("\\s"); // Trim leading case-sensitive indicator
let b = a.trim_end_matches('/'); let a = s.trim_start_matches(r"\s");
let sep_lit = b.contains('/');
let inner = GlobBuilder::new(b) // Parse the URL scheme if present
let (scheme, skip) = PatternScheme::parse(a)?;
let b = &a[skip..];
// Trim the ending slash which indicates a directory
let c = b.trim_end_matches('/');
// Check whether it's a filename pattern or a full path pattern
let sep_lit = c.contains('/');
let inner = GlobBuilder::new(c)
.case_insensitive(a.len() == s.len()) .case_insensitive(a.len() == s.len())
.literal_separator(sep_lit) .literal_separator(sep_lit)
.backslash_escape(false) .backslash_escape(false)
@ -62,8 +81,9 @@ impl FromStr for Pattern {
Ok(Self { Ok(Self {
inner, inner,
is_dir: b.len() < a.len(), scheme,
is_star: b == "*", is_dir: c.len() < b.len(),
is_star: c == "*",
#[cfg(windows)] #[cfg(windows)]
sep_lit, sep_lit,
}) })
@ -71,17 +91,40 @@ impl FromStr for Pattern {
} }
impl TryFrom<String> for Pattern { impl TryFrom<String> for Pattern {
type Error = globset::Error; type Error = anyhow::Error;
fn try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(s.as_str()) } fn try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(s.as_str()) }
} }
// --- Scheme
#[derive(Debug)]
struct PatternScheme(Option<&'static str>);
impl PatternScheme {
fn parse(s: &str) -> Result<(Self, usize)> {
let mut me = Self(None);
let Some((protocol, _)) = s.split_once("://") else {
return Ok((me, 0));
};
if protocol != "*" {
me.0 = Some(Scheme::parse_kind(protocol.as_bytes())?);
}
Ok((me, protocol.len() + 3))
}
#[inline]
fn matches(&self, scheme: &Scheme) -> bool { self.0.is_none_or(|s| s == scheme.kind()) }
}
// --- Tests
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
fn matches(glob: &str, path: &str) -> bool { fn matches(glob: &str, url: &str) -> bool {
Pattern::from_str(glob).unwrap().match_path(path, false) Pattern::from_str(glob).unwrap().match_url(Url::from_str(url).unwrap(), false)
} }
#[cfg(unix)] #[cfg(unix)]

View file

@ -1,7 +1,5 @@
use std::path::Path;
use serde::Deserialize; use serde::Deserialize;
use yazi_shared::{MIME_DIR, event::Cmd}; use yazi_shared::{MIME_DIR, event::Cmd, url::Url};
use crate::{Pattern, Priority}; use crate::{Pattern, Priority};
@ -11,7 +9,7 @@ pub struct Fetcher {
pub idx: u8, pub idx: u8,
pub id: String, pub id: String,
pub name: Option<Pattern>, pub url: Option<Pattern>,
pub mime: Option<Pattern>, pub mime: Option<Pattern>,
pub run: Cmd, pub run: Cmd,
#[serde(default)] #[serde(default)]
@ -20,8 +18,8 @@ pub struct Fetcher {
impl Fetcher { impl Fetcher {
#[inline] #[inline]
pub fn matches(&self, path: &Path, mime: &str) -> bool { pub fn matches(&self, url: &Url, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)) || self.url.as_ref().is_some_and(|p| p.match_url(url, mime == MIME_DIR))
} }
} }

View file

@ -1,10 +1,11 @@
use std::{collections::HashSet, path::Path}; use std::collections::HashSet;
use anyhow::Result; use anyhow::Result;
use serde::Deserialize; use serde::Deserialize;
use tracing::warn; use tracing::warn;
use yazi_codegen::DeserializeOver2; use yazi_codegen::DeserializeOver2;
use yazi_fs::File; use yazi_fs::File;
use yazi_shared::url::Url;
use super::{Fetcher, Preloader, Previewer, Spotter}; use super::{Fetcher, Preloader, Previewer, Spotter};
use crate::{Preset, plugin::MAX_PREWORKERS}; use crate::{Preset, plugin::MAX_PREWORKERS};
@ -39,12 +40,12 @@ pub struct Plugin {
impl Plugin { impl Plugin {
pub fn fetchers<'a, 'b: 'a>( pub fn fetchers<'a, 'b: 'a>(
&'b self, &'b self,
path: &'a Path, url: &'a Url,
mime: &'a str, mime: &'a str,
) -> impl Iterator<Item = &'b Fetcher> + 'a { ) -> impl Iterator<Item = &'b Fetcher> + 'a {
let mut seen = HashSet::new(); let mut seen = HashSet::new();
self.fetchers.iter().filter(move |&f| { self.fetchers.iter().filter(move |&f| {
if seen.contains(&f.id) || !f.matches(path, mime) { if seen.contains(&f.id) || !f.matches(url, mime) {
return false; return false;
} }
seen.insert(&f.id); seen.insert(&f.id);
@ -68,18 +69,18 @@ impl Plugin {
}) })
} }
pub fn spotter(&self, path: &Path, mime: &str) -> Option<&Spotter> { pub fn spotter(&self, url: &Url, mime: &str) -> Option<&Spotter> {
self.spotters.iter().find(|&p| p.matches(path, mime)) self.spotters.iter().find(|&p| p.matches(url, mime))
} }
pub fn preloaders<'a, 'b: 'a>( pub fn preloaders<'a, 'b: 'a>(
&'b self, &'b self,
path: &'a Path, url: &'a Url,
mime: &'a str, mime: &'a str,
) -> impl Iterator<Item = &'b Preloader> + 'a { ) -> impl Iterator<Item = &'b Preloader> + 'a {
let mut next = true; let mut next = true;
self.preloaders.iter().filter(move |&p| { self.preloaders.iter().filter(move |&p| {
if !next || !p.matches(path, mime) { if !next || !p.matches(url, mime) {
return false; return false;
} }
next = p.next; next = p.next;
@ -87,8 +88,8 @@ impl Plugin {
}) })
} }
pub fn previewer(&self, path: &Path, mime: &str) -> Option<&Previewer> { pub fn previewer(&self, url: &Url, mime: &str) -> Option<&Previewer> {
self.previewers.iter().find(|&p| p.matches(path, mime)) self.previewers.iter().find(|&p| p.matches(url, mime))
} }
} }

View file

@ -1,7 +1,5 @@
use std::path::Path;
use serde::Deserialize; use serde::Deserialize;
use yazi_shared::{MIME_DIR, event::Cmd}; use yazi_shared::{MIME_DIR, event::Cmd, url::Url};
use crate::{Pattern, Priority}; use crate::{Pattern, Priority};
@ -10,7 +8,7 @@ pub struct Preloader {
#[serde(skip)] #[serde(skip)]
pub idx: u8, pub idx: u8,
pub name: Option<Pattern>, pub url: Option<Pattern>,
pub mime: Option<Pattern>, pub mime: Option<Pattern>,
pub run: Cmd, pub run: Cmd,
#[serde(default)] #[serde(default)]
@ -21,8 +19,8 @@ pub struct Preloader {
impl Preloader { impl Preloader {
#[inline] #[inline]
pub fn matches(&self, path: &Path, mime: &str) -> bool { pub fn matches(&self, url: &Url, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)) || self.url.as_ref().is_some_and(|p| p.match_url(url, mime == MIME_DIR))
} }
} }

View file

@ -1,27 +1,25 @@
use std::path::Path;
use serde::Deserialize; use serde::Deserialize;
use yazi_shared::{MIME_DIR, event::Cmd}; use yazi_shared::{MIME_DIR, event::Cmd, url::Url};
use crate::Pattern; use crate::Pattern;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Previewer { pub struct Previewer {
pub name: Option<Pattern>, pub url: Option<Pattern>,
pub mime: Option<Pattern>, pub mime: Option<Pattern>,
pub run: Cmd, pub run: Cmd,
} }
impl Previewer { impl Previewer {
#[inline] #[inline]
pub fn matches(&self, path: &Path, mime: &str) -> bool { pub fn matches(&self, url: &Url, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)) || self.url.as_ref().is_some_and(|p| p.match_url(url, mime == MIME_DIR))
} }
#[inline] #[inline]
pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
#[inline] #[inline]
pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
} }

View file

@ -1,27 +1,25 @@
use std::path::Path;
use serde::Deserialize; use serde::Deserialize;
use yazi_shared::{MIME_DIR, event::Cmd}; use yazi_shared::{MIME_DIR, event::Cmd, url::Url};
use crate::Pattern; use crate::Pattern;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Spotter { pub struct Spotter {
pub name: Option<Pattern>, pub url: Option<Pattern>,
pub mime: Option<Pattern>, pub mime: Option<Pattern>,
pub run: Cmd, pub run: Cmd,
} }
impl Spotter { impl Spotter {
#[inline] #[inline]
pub fn matches(&self, path: &Path, mime: &str) -> bool { pub fn matches(&self, url: &Url, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)) || self.url.as_ref().is_some_and(|p| p.match_url(url, mime == MIME_DIR))
} }
#[inline] #[inline]
pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
#[inline] #[inline]
pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
} }

View file

@ -22,7 +22,7 @@ impl Deref for Filetype {
pub struct FiletypeRule { pub struct FiletypeRule {
#[serde(default)] #[serde(default)]
is: Is, is: Is,
name: Option<Pattern>, url: Option<Pattern>,
mime: Option<Pattern>, mime: Option<Pattern>,
#[serde(flatten)] #[serde(flatten)]
pub style: Style, pub style: Style,
@ -35,6 +35,6 @@ impl FiletypeRule {
} }
self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.name.as_ref().is_some_and(|n| n.match_path(&file.url, file.is_dir())) || self.url.as_ref().is_some_and(|n| n.match_url(&file.url, file.is_dir()))
} }
} }

View file

@ -70,7 +70,7 @@ impl Icon {
#[inline] #[inline]
fn match_by_glob(&self, file: &File) -> Option<&I> { fn match_by_glob(&self, file: &File) -> Option<&I> {
self.globs.iter().find(|(p, _)| p.match_path(&file.url, file.is_dir())).map(|(_, i)| i) self.globs.iter().find(|(p, _)| p.match_url(&file.url, file.is_dir())).map(|(_, i)| i)
} }
#[inline] #[inline]

View file

@ -6,7 +6,7 @@ use parking_lot::RwLock;
use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}}; use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use tracing::error; use tracing::error;
use yazi_fs::{File, Files, FilesOp, cha::Cha, realname_unchecked, services}; use yazi_fs::{File, Files, FilesOp, cha::Cha, provider, realname_unchecked};
use yazi_proxy::WATCHER; use yazi_proxy::WATCHER;
use yazi_shared::{RoCell, url::Url}; use yazi_shared::{RoCell, url::Url};
@ -139,7 +139,7 @@ impl Watcher {
}; };
let u = &file.url; let u = &file.url;
let eq = (!file.is_link() && services::canonicalize(u).await.is_ok_and(|c| c == *u)) let eq = (!file.is_link() && provider::canonicalize(u).await.is_ok_and(|c| c == *u))
|| realname_unchecked(u, &mut cached).await.is_ok_and(|s| urn.as_urn() == s); || realname_unchecked(u, &mut cached).await.is_ok_and(|s| urn.as_urn() == s);
if !eq { if !eq {
@ -194,7 +194,7 @@ impl Watcher {
async fn go(todo: HashSet<Url>) { async fn go(todo: HashSet<Url>) {
for from in todo { for from in todo {
let Ok(to) = services::canonicalize(&from).await else { continue }; let Ok(to) = provider::canonicalize(&from).await else { continue };
if to != from && WATCHED.read().contains(&from) { if to != from && WATCHED.read().contains(&from) {
LINKED.write().insert(from, to); LINKED.write().insert(from, to);

View file

@ -126,7 +126,7 @@ impl Selected {
mod tests { mod tests {
use super::*; use super::*;
fn url(s: &str) -> Url { Url::try_from(s).unwrap() } fn url(s: &str) -> Url { s.parse().unwrap() }
#[test] #[test]
fn test_insert_non_conflicting() { fn test_insert_non_conflicting() {

View file

@ -2,9 +2,9 @@ use std::{collections::HashMap, mem, ops::Deref, sync::atomic::{AtomicU64, Order
use anyhow::Result; use anyhow::Result;
use parking_lot::RwLock; use parking_lot::RwLock;
use tokio::{fs::OpenOptions, io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufWriter};
use yazi_boot::BOOT; use yazi_boot::BOOT;
use yazi_fs::services::Local; use yazi_fs::provider::local::{Gate, Local};
use yazi_shared::{RoCell, timestamp_us}; use yazi_shared::{RoCell, timestamp_us};
use crate::CLIENTS; use crate::CLIENTS;
@ -59,8 +59,7 @@ impl State {
Local::create_dir_all(&BOOT.state_dir).await?; Local::create_dir_all(&BOOT.state_dir).await?;
let mut buf = BufWriter::new( let mut buf = BufWriter::new(
// TODO: VFS Gate::default()
OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.truncate(true) .truncate(true)
@ -79,7 +78,7 @@ impl State {
} }
async fn load(&self) -> Result<()> { async fn load(&self) -> Result<()> {
let mut file = BufReader::new(Local::open(BOOT.state_dir.join(".dds")).await?); let mut file = Local::open(BOOT.state_dir.join(".dds")).await?.reader();
let mut buf = String::new(); let mut buf = String::new();
let mut inner = HashMap::new(); let mut inner = HashMap::new();

View file

@ -38,7 +38,7 @@ impl Stream {
pub(super) async fn bind() -> std::io::Result<ServerListener> { pub(super) async fn bind() -> std::io::Result<ServerListener> {
let p = Self::socket_file(); let p = Self::socket_file();
yazi_fs::services::Local::remove_file(&p).await.ok(); yazi_fs::provider::local::Local::remove_file(&p).await.ok();
tokio::net::UnixListener::bind(p) tokio::net::UnixListener::bind(p)
} }

View file

@ -9,7 +9,7 @@ fn main() -> Result<(), Box<dyn Error>> {
// C:\Users\Ika\AppData\Local\Temp\cargo-installTFU8cj\release\build\ // C:\Users\Ika\AppData\Local\Temp\cargo-installTFU8cj\release\build\
// yazi-fm-45dffef2500eecd0\out // yazi-fm-45dffef2500eecd0\out
if dir.contains("\\release\\build\\yazi-fm-") { if dir.contains(r"\release\build\yazi-fm-") {
panic!( panic!(
"Unwinding must be enabled for Windows. Please use `cargo build --profile release-windows --locked` instead to build Yazi." "Unwinding must be enabled for Windows. Please use `cargo build --profile release-windows --locked` instead to build Yazi."
); );

View file

@ -3,7 +3,7 @@ use yazi_actor::Ctx;
use yazi_boot::BOOT; use yazi_boot::BOOT;
use yazi_macro::act; use yazi_macro::act;
use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_parser::{VoidOpt, mgr::CdSource};
use yazi_shared::{event::Data, url::Url}; use yazi_shared::event::Data;
use crate::app::App; use crate::app::App;
@ -18,10 +18,10 @@ impl App {
let cx = &mut Ctx::active(&mut self.core); let cx = &mut Ctx::active(&mut self.core);
cx.tab = i; cx.tab = i;
if file.is_empty() { if file.as_os_str().is_empty() {
act!(mgr:cd, cx, (Url::from(&BOOT.cwds[i]), CdSource::Tab))?; act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?;
} else { } else {
act!(mgr:reveal, cx, (Url::from(BOOT.cwds[i].join(file)), CdSource::Tab))?; act!(mgr:reveal, cx, (BOOT.cwds[i].join(file), CdSource::Tab))?;
} }
} }

View file

@ -1,7 +1,7 @@
use std::ffi::OsString; use std::ffi::OsString;
use yazi_boot::ARGS; use yazi_boot::ARGS;
use yazi_fs::services::Local; use yazi_fs::provider::local::Local;
use yazi_shared::event::EventQuit; use yazi_shared::event::EventQuit;
use crate::{Term, app::App}; use crate::{Term, app::App};
@ -25,7 +25,7 @@ impl App {
async fn cwd_to_file(&self, no: bool) { async fn cwd_to_file(&self, no: bool) {
if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) { if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) {
let cwd = self.core.mgr.cwd().as_os_str(); let cwd = self.core.mgr.cwd().os_str();
Local::write(p, cwd.as_encoded_bytes()).await.ok(); Local::write(p, cwd.as_encoded_bytes()).await.ok();
} }
} }

View file

@ -1,11 +1,11 @@
use std::{collections::VecDeque, fs::ReadDir, future::poll_fn, io, mem, pin::Pin, task::{Poll, ready}, time::{Duration, Instant}}; use std::{collections::VecDeque, future::poll_fn, io, mem, pin::Pin, task::{Poll, ready}, time::{Duration, Instant}};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use yazi_shared::{Either, url::Url}; use yazi_shared::{Either, url::Url};
use crate::services; use crate::provider::{self, ReadDirSync};
type Task = Either<Url, ReadDir>; type Task = Either<Url, ReadDirSync>;
pub enum SizeCalculator { pub enum SizeCalculator {
Idle((VecDeque<Task>, Option<u64>)), Idle((VecDeque<Task>, Option<u64>)),
@ -16,12 +16,12 @@ impl SizeCalculator {
pub async fn new(url: &Url) -> io::Result<Self> { pub async fn new(url: &Url) -> io::Result<Self> {
let u = url.to_owned(); let u = url.to_owned();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let meta = services::symlink_metadata_sync(&u)?; let meta = provider::symlink_metadata_sync(&u)?;
if !meta.is_dir() { if !meta.is_dir() {
return Ok(Self::Idle((VecDeque::new(), Some(meta.len())))); return Ok(Self::Idle((VecDeque::new(), Some(meta.len()))));
} }
let mut buf = VecDeque::from([Either::Right(services::read_dir_sync(u)?)]); let mut buf = VecDeque::from([Either::Right(provider::read_dir_sync(u)?)]);
let size = Self::next_chunk(&mut buf); let size = Self::next_chunk(&mut buf);
Ok(Self::Idle((buf, size))) Ok(Self::Idle((buf, size)))
}) })
@ -63,7 +63,7 @@ impl SizeCalculator {
.await .await
} }
fn next_chunk(buf: &mut VecDeque<Either<Url, ReadDir>>) -> Option<u64> { fn next_chunk(buf: &mut VecDeque<Either<Url, ReadDirSync>>) -> Option<u64> {
let (mut i, mut size, now) = (0, 0, Instant::now()); let (mut i, mut size, now) = (0, 0, Instant::now());
macro_rules! pop_and_continue { macro_rules! pop_and_continue {
() => {{ () => {{
@ -80,7 +80,7 @@ impl SizeCalculator {
let front = buf.front_mut()?; let front = buf.front_mut()?;
if let Either::Left(u) = front { if let Either::Left(u) = front {
*front = match services::read_dir_sync(u) { *front = match provider::read_dir_sync(u) {
Ok(it) => Either::Right(it), Ok(it) => Either::Right(it),
Err(_) => pop_and_continue!(), Err(_) => pop_and_continue!(),
}; };
@ -93,7 +93,7 @@ impl SizeCalculator {
let Ok(ent) = next else { continue }; let Ok(ent) = next else { continue };
let Ok(ft) = ent.file_type() else { continue }; let Ok(ft) = ent.file_type() else { continue };
if ft.is_dir() { if ft.is_dir() {
buf.push_back(Either::Left(ent.path().into())); buf.push_back(Either::Left(ent.url()));
} else if let Ok(meta) = ent.metadata() { } else if let Ok(meta) = ent.metadata() {
size += meta.len(); size += meta.len();
} }

View file

@ -4,7 +4,7 @@ use yazi_macro::{unix_either, win_either};
use yazi_shared::url::Url; use yazi_shared::url::Url;
use super::ChaKind; use super::ChaKind;
use crate::services; use crate::provider;
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cha { pub struct Cha {
@ -59,14 +59,14 @@ impl Cha {
#[inline] #[inline]
pub async fn from_url(url: &Url) -> std::io::Result<Self> { pub async fn from_url(url: &Url) -> std::io::Result<Self> {
Ok(Self::from_follow(url, services::symlink_metadata(url).await?).await) Ok(Self::from_follow(url, provider::symlink_metadata(url).await?).await)
} }
pub async fn from_follow(url: &Url, mut meta: Metadata) -> Self { pub async fn from_follow(url: &Url, mut meta: Metadata) -> Self {
let mut attached = ChaKind::hidden(url, &meta); let mut attached = ChaKind::hidden(url, &meta);
if meta.is_symlink() { if meta.is_symlink() {
attached |= ChaKind::LINK; attached |= ChaKind::LINK;
meta = services::metadata(url).await.unwrap_or(meta); meta = provider::metadata(url).await.unwrap_or(meta);
} }
if meta.is_symlink() { if meta.is_symlink() {
attached |= ChaKind::ORPHAN; attached |= ChaKind::ORPHAN;

View file

@ -3,7 +3,7 @@ use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher
use anyhow::Result; use anyhow::Result;
use yazi_shared::url::{Url, Urn, UrnBuf}; use yazi_shared::url::{Url, Urn, UrnBuf};
use crate::{cha::Cha, services}; use crate::{cha::Cha, provider};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct File { pub struct File {
@ -22,13 +22,13 @@ impl Deref for File {
impl File { impl File {
#[inline] #[inline]
pub async fn new(url: Url) -> Result<Self> { pub async fn new(url: Url) -> Result<Self> {
let meta = services::symlink_metadata(&url).await?; let meta = provider::symlink_metadata(&url).await?;
Ok(Self::from_follow(url, meta).await) Ok(Self::from_follow(url, meta).await)
} }
#[inline] #[inline]
pub async fn from_follow(url: Url, meta: Metadata) -> Self { pub async fn from_follow(url: Url, meta: Metadata) -> Self {
let link_to = if meta.is_symlink() { services::read_link(&url).await.ok() } else { None }; let link_to = if meta.is_symlink() { provider::read_link(&url).await.ok() } else { None };
let cha = Cha::from_follow(&url, meta).await; let cha = Cha::from_follow(&url, meta).await;

View file

@ -4,7 +4,7 @@ use tokio::{select, sync::mpsc::{self, UnboundedReceiver}};
use yazi_shared::{Id, url::{Url, Urn, UrnBuf}}; use yazi_shared::{Id, url::{Url, Urn, UrnBuf}};
use super::{FilesSorter, Filter}; use super::{FilesSorter, Filter};
use crate::{FILES_TICKET, File, FilesOp, SortBy, cha::Cha, mounts::PARTITIONS, services}; use crate::{FILES_TICKET, File, FilesOp, SortBy, cha::Cha, mounts::PARTITIONS, provider::{self, DirEntry}};
#[derive(Default)] #[derive(Default)]
pub struct Files { pub struct Files {
@ -31,7 +31,7 @@ impl Files {
pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } } pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } }
pub async fn from_dir(dir: &Url) -> std::io::Result<UnboundedReceiver<File>> { pub async fn from_dir(dir: &Url) -> std::io::Result<UnboundedReceiver<File>> {
let mut it = services::read_dir(dir).await?; let mut it = provider::read_dir(dir).await?;
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::unbounded_channel();
tokio::spawn(async move { tokio::spawn(async move {
@ -39,7 +39,7 @@ impl Files {
select! { select! {
_ = tx.closed() => break, _ = tx.closed() => break,
result = item.metadata() => { result = item.metadata() => {
let url = Url::from(item.path()); let url = item.url();
_ = tx.send(match result { _ = tx.send(match result {
Ok(meta) => File::from_follow(url, meta).await, Ok(meta) => File::from_follow(url, meta).await,
Err(_) => File::from_dummy(url, item.file_type().await.ok()) Err(_) => File::from_dummy(url, item.file_type().await.ok())
@ -52,7 +52,7 @@ impl Files {
} }
pub async fn from_dir_bulk(dir: &Url) -> std::io::Result<Vec<File>> { pub async fn from_dir_bulk(dir: &Url) -> std::io::Result<Vec<File>> {
let mut it = services::read_dir(dir).await?; let mut it = provider::read_dir(dir).await?;
let mut entries = Vec::with_capacity(5000); let mut entries = Vec::with_capacity(5000);
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(entry)) = it.next_entry().await {
entries.push(entry); entries.push(entry);
@ -60,10 +60,10 @@ impl Files {
let (first, rest) = entries.split_at(entries.len() / 3); let (first, rest) = entries.split_at(entries.len() / 3);
let (second, third) = rest.split_at(entries.len() / 3); let (second, third) = rest.split_at(entries.len() / 3);
async fn go(entries: &[tokio::fs::DirEntry]) -> Vec<File> { async fn go(entries: &[DirEntry]) -> Vec<File> {
let mut files = Vec::with_capacity(entries.len()); let mut files = Vec::with_capacity(entries.len());
for entry in entries { for entry in entries {
let url = Url::from(entry.path()); let url = entry.url();
files.push(match entry.metadata().await { files.push(match entry.metadata().await {
Ok(meta) => File::from_follow(url, meta).await, Ok(meta) => File::from_follow(url, meta).await,
Err(_) => File::from_dummy(url, entry.file_type().await.ok()), Err(_) => File::from_dummy(url, entry.file_type().await.ok()),

View file

@ -6,11 +6,11 @@ use anyhow::{Result, bail};
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
use yazi_shared::url::{Component, Url}; use yazi_shared::url::{Component, Url};
use crate::{cha::Cha, services}; use crate::{cha::Cha, provider};
#[inline] #[inline]
pub async fn maybe_exists(u: impl AsRef<Url>) -> bool { pub async fn maybe_exists(u: impl AsRef<Url>) -> bool {
match services::symlink_metadata(u).await { match provider::symlink_metadata(u).await {
Ok(_) => true, Ok(_) => true,
Err(e) => e.kind() != io::ErrorKind::NotFound, Err(e) => e.kind() != io::ErrorKind::NotFound,
} }
@ -18,7 +18,7 @@ pub async fn maybe_exists(u: impl AsRef<Url>) -> bool {
#[inline] #[inline]
pub async fn must_be_dir(u: impl AsRef<Url>) -> bool { pub async fn must_be_dir(u: impl AsRef<Url>) -> bool {
services::metadata(u).await.is_ok_and(|m| m.is_dir()) provider::metadata(u).await.is_ok_and(|m| m.is_dir())
} }
#[inline] #[inline]
@ -85,7 +85,7 @@ async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result<bool> {
pub async fn realname(u: &Url) -> Option<OsString> { pub async fn realname(u: &Url) -> Option<OsString> {
let name = u.file_name()?; let name = u.file_name()?;
if *u == services::canonicalize(u).await.ok()? { if *u == provider::canonicalize(u).await.ok()? {
return None; return None;
} }
@ -98,16 +98,16 @@ pub async fn realname(u: &Url) -> Option<OsString> {
#[cfg(unix)] #[cfg(unix)]
#[tokio::test] #[tokio::test]
async fn test_realname_unchecked() { async fn test_realname_unchecked() -> Result<()> {
use crate::services::Local; use crate::provider::local::Local;
Local::remove_dir_all("/tmp/issue-1173").await.ok(); Local::remove_dir_all("/tmp/issue-1173").await.ok();
Local::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap(); Local::create_dir_all("/tmp/issue-1173/real-dir").await?;
Local::create("/tmp/issue-1173/A").await.unwrap(); Local::create("/tmp/issue-1173/A").await?;
Local::create("/tmp/issue-1173/b").await.unwrap(); Local::create("/tmp/issue-1173/b").await?;
Local::create("/tmp/issue-1173/real-dir/C").await.unwrap(); Local::create("/tmp/issue-1173/real-dir/C").await?;
Local::symlink_file("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap(); Local::symlink_file("/tmp/issue-1173/b", "/tmp/issue-1173/D").await?;
Local::symlink_dir("real-dir", "/tmp/issue-1173/link-dir").await.unwrap(); Local::symlink_dir("real-dir", "/tmp/issue-1173/link-dir").await?;
let c = &mut HashMap::new(); let c = &mut HashMap::new();
async fn check(a: &str, b: &str, c: &mut HashMap<PathBuf, HashSet<OsString>>) { async fn check(a: &str, b: &str, c: &mut HashMap<PathBuf, HashSet<OsString>>) {
@ -125,6 +125,7 @@ async fn test_realname_unchecked() {
check("/tmp/issue-1173/d", "D", c).await; check("/tmp/issue-1173/d", "D", c).await;
check("/tmp/issue-1173/D", "D", c).await; check("/tmp/issue-1173/D", "D", c).await;
Ok(())
} }
// realpath(3) without resolving symlinks. This is useful for case-insensitive // realpath(3) without resolving symlinks. This is useful for case-insensitive
@ -199,7 +200,7 @@ pub fn copy_with_progress(
None => {} None => {}
} }
let len = services::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0); let len = provider::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0);
if len > last { if len > last {
tx.send(Ok(len - last)).await.ok(); tx.send(Ok(len - last)).await.ok();
last = len; last = len;
@ -260,17 +261,17 @@ async fn _copy_with_progress(from: Url, to: Url, cha: Cha) -> io::Result<u64> {
} }
pub async fn remove_dir_clean(dir: &Url) { pub async fn remove_dir_clean(dir: &Url) {
let Ok(mut it) = services::read_dir(dir).await else { return }; let Ok(mut it) = provider::read_dir(dir).await else { return };
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(entry)) = it.next_entry().await {
if entry.file_type().await.is_ok_and(|t| t.is_dir()) { if entry.file_type().await.is_ok_and(|t| t.is_dir()) {
let path = entry.path().into(); let url = entry.url();
Box::pin(remove_dir_clean(&path)).await; Box::pin(remove_dir_clean(&url)).await;
services::remove_dir(path).await.ok(); provider::remove_dir(url).await.ok();
} }
} }
services::remove_dir(dir).await.ok(); provider::remove_dir(dir).await.ok();
} }
// Convert a file mode to a string representation // Convert a file mode to a string representation
@ -369,7 +370,8 @@ pub fn max_common_root(urls: &[Url]) -> usize {
#[test] #[test]
fn test_max_common_root() { fn test_max_common_root() {
fn assert(input: &[&str], expected: &str) { fn assert(input: &[&str], expected: &str) {
let urls: Vec<_> = input.iter().copied().map(Url::try_from).collect::<Result<_>>().unwrap(); use std::str::FromStr;
let urls: Vec<_> = input.iter().copied().map(Url::from_str).collect::<Result<_>>().unwrap();
let mut comp = urls[0].components(); let mut comp = urls[0].components();
for _ in 0..comp.clone().count() - max_common_root(&urls) { for _ in 0..comp.clone().count() - max_common_root(&urls) {

View file

@ -1,6 +1,6 @@
#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn)] #![allow(clippy::if_same_then_else, clippy::option_map_unit_fn)]
yazi_macro::mod_pub!(cha mounts services); yazi_macro::mod_pub!(cha mounts provider);
yazi_macro::mod_flat!(calculator cwd file files filter fns op path sorter sorting stage xdg); yazi_macro::mod_flat!(calculator cwd file files filter fns op path sorter sorting stage xdg);

View file

@ -3,7 +3,7 @@ use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{P
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use yazi_shared::url::{Loc, Url}; use yazi_shared::url::{Loc, Url};
use crate::{CWD, services}; use crate::{CWD, provider};
pub fn clean_url<'a>(url: impl Into<Cow<'a, Url>>) -> Cow<'a, Url> { pub fn clean_url<'a>(url: impl Into<Cow<'a, Url>>) -> Cow<'a, Url> {
let url = url.into(); let url = url.into();
@ -12,7 +12,7 @@ pub fn clean_url<'a>(url: impl Into<Cow<'a, Url>>) -> Cow<'a, Url> {
if path.as_os_str() == url.loc.as_os_str() { if path.as_os_str() == url.loc.as_os_str() {
url url
} else { } else {
url.with(Loc::with(&clean_path(url.loc.base()), path)).into() url.with(Loc::with_lossy(&clean_path(url.loc.base()), path)).into()
} }
} }
@ -106,7 +106,7 @@ pub async fn unique_name<F>(u: Url, append: F) -> io::Result<Url>
where where
F: Future<Output = bool>, F: Future<Output = bool>,
{ {
match services::symlink_metadata(&u).await { match provider::symlink_metadata(&u).await {
Ok(_) => _unique_name(u, append.await).await, Ok(_) => _unique_name(u, append.await).await,
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(u), Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(u),
Err(e) => Err(e), Err(e) => Err(e),
@ -142,7 +142,7 @@ async fn _unique_name(mut url: Url, append: bool) -> io::Result<Url> {
} }
url.set_name(&name); url.set_name(&name);
match services::symlink_metadata(&url).await { match provider::symlink_metadata(&url).await {
Ok(_) => i += 1, Ok(_) => i += 1,
Err(e) if e.kind() == io::ErrorKind::NotFound => break, Err(e) if e.kind() == io::ErrorKind::NotFound => break,
Err(e) => return Err(e), Err(e) => return Err(e),
@ -214,16 +214,14 @@ pub fn backslash_to_slash(p: &Path) -> Cow<'_, Path> {
mod tests { mod tests {
use std::borrow::Cow; use std::borrow::Cow;
use yazi_shared::url::Url;
use super::url_relative_to; use super::url_relative_to;
#[test] #[test]
fn test_path_relative_to() { fn test_path_relative_to() {
fn assert(from: &str, to: &str, ret: &str) { fn assert(from: &str, to: &str, ret: &str) {
assert_eq!( assert_eq!(
url_relative_to(&Url::try_from(from).unwrap(), &Url::try_from(to).unwrap()).unwrap(), url_relative_to(&from.parse().unwrap(), &to.parse().unwrap()).unwrap(),
Cow::Owned(Url::try_from(ret).unwrap()) Cow::Owned(ret.parse().unwrap())
); );
} }

View file

@ -0,0 +1,9 @@
// --- BufRead
pub trait BufRead: tokio::io::AsyncRead + Send {}
impl<T: tokio::io::AsyncRead + Send> BufRead for T {}
// --- BufReadSync
pub trait BufReadSync: std::io::BufRead + std::io::Seek + Send {}
impl<T: std::io::BufRead + std::io::Seek + Send> BufReadSync for T {}

View file

@ -0,0 +1,68 @@
use std::{ffi::OsString, io};
use yazi_shared::url::Url;
pub enum DirEntry {
Local(super::local::DirEntry),
}
impl DirEntry {
#[must_use]
pub fn url(&self) -> Url {
match self {
DirEntry::Local(local) => local.url(),
}
}
#[must_use]
pub fn file_name(&self) -> OsString {
match self {
DirEntry::Local(local) => local.file_name(),
}
}
pub async fn metadata(&self) -> io::Result<std::fs::Metadata> {
match self {
DirEntry::Local(local) => local.metadata().await,
}
}
pub async fn file_type(&self) -> io::Result<std::fs::FileType> {
match self {
DirEntry::Local(local) => local.file_type().await,
}
}
}
// --- DirEntrySync
pub enum DirEntrySync {
Local(super::local::DirEntrySync),
}
impl DirEntrySync {
#[must_use]
pub fn url(&self) -> Url {
match self {
DirEntrySync::Local(local) => local.url(),
}
}
#[must_use]
pub fn file_name(&self) -> OsString {
match self {
DirEntrySync::Local(local) => local.file_name(),
}
}
pub fn metadata(&self) -> io::Result<std::fs::Metadata> {
match self {
DirEntrySync::Local(local) => local.metadata(),
}
}
pub fn file_type(&self) -> io::Result<std::fs::FileType> {
match self {
DirEntrySync::Local(local) => local.file_type(),
}
}
}

View file

@ -0,0 +1,46 @@
use std::ops::Deref;
use yazi_shared::url::Url;
pub struct DirEntry(tokio::fs::DirEntry);
impl Deref for DirEntry {
type Target = tokio::fs::DirEntry;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<tokio::fs::DirEntry> for DirEntry {
fn from(value: tokio::fs::DirEntry) -> Self { Self(value) }
}
impl From<DirEntry> for crate::provider::DirEntry {
fn from(value: DirEntry) -> Self { crate::provider::DirEntry::Local(value) }
}
impl DirEntry {
#[must_use]
pub fn url(&self) -> Url { self.0.path().into() }
}
// --- DirEntrySync
pub struct DirEntrySync(std::fs::DirEntry);
impl Deref for DirEntrySync {
type Target = std::fs::DirEntry;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<std::fs::DirEntry> for DirEntrySync {
fn from(value: std::fs::DirEntry) -> Self { Self(value) }
}
impl From<DirEntrySync> for crate::provider::DirEntrySync {
fn from(value: DirEntrySync) -> Self { crate::provider::DirEntrySync::Local(value) }
}
impl DirEntrySync {
#[must_use]
pub fn url(&self) -> Url { self.0.path().into() }
}

View file

@ -0,0 +1,14 @@
use std::ops::{Deref, DerefMut};
#[derive(Default)]
pub struct Gate(tokio::fs::OpenOptions);
impl Deref for Gate {
type Target = tokio::fs::OpenOptions;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl DerefMut for Gate {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}

View file

@ -1,5 +1,7 @@
use std::{io, path::{Path, PathBuf}}; use std::{io, path::{Path, PathBuf}};
use crate::provider::local::{Gate, ReadDir, ReadDirSync, RwFile};
pub struct Local; pub struct Local;
impl Local { impl Local {
@ -9,8 +11,8 @@ impl Local {
} }
#[inline] #[inline]
pub async fn create(path: impl AsRef<Path>) -> io::Result<tokio::fs::File> { pub async fn create(path: impl AsRef<Path>) -> io::Result<RwFile> {
tokio::fs::File::create(path).await Gate::default().write(true).create(true).truncate(true).open(path).await.map(Into::into)
} }
#[inline] #[inline]
@ -34,21 +36,21 @@ impl Local {
} }
#[inline] #[inline]
pub async fn open(path: impl AsRef<Path>) -> io::Result<tokio::fs::File> { pub async fn open(path: impl AsRef<Path>) -> io::Result<RwFile> {
tokio::fs::File::open(path).await Gate::default().read(true).open(path).await.map(Into::into)
} }
#[inline] #[inline]
pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> { tokio::fs::read(path).await } pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> { tokio::fs::read(path).await }
#[inline] #[inline]
pub async fn read_dir(path: impl AsRef<Path>) -> io::Result<tokio::fs::ReadDir> { pub async fn read_dir(path: impl AsRef<Path>) -> io::Result<ReadDir> {
tokio::fs::read_dir(path).await tokio::fs::read_dir(path).await.map(Into::into)
} }
#[inline] #[inline]
pub fn read_dir_sync(path: impl AsRef<Path>) -> io::Result<std::fs::ReadDir> { pub fn read_dir_sync(path: impl AsRef<Path>) -> io::Result<ReadDirSync> {
std::fs::read_dir(path) std::fs::read_dir(path).map(Into::into)
} }
#[inline] #[inline]

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(dir_entry gate local read_dir rw_file);

View file

@ -0,0 +1,38 @@
use std::io;
use super::{DirEntry, DirEntrySync};
pub struct ReadDir(tokio::fs::ReadDir);
impl From<tokio::fs::ReadDir> for ReadDir {
fn from(value: tokio::fs::ReadDir) -> Self { Self(value) }
}
impl From<ReadDir> for crate::provider::ReadDir {
fn from(value: ReadDir) -> Self { crate::provider::ReadDir::Local(value) }
}
impl ReadDir {
pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> {
self.0.next_entry().await.map(|entry| entry.map(Into::into))
}
}
// --- ReadDirSync
pub struct ReadDirSync(std::fs::ReadDir);
impl From<std::fs::ReadDir> for ReadDirSync {
fn from(value: std::fs::ReadDir) -> Self { Self(value) }
}
impl From<ReadDirSync> for crate::provider::ReadDirSync {
fn from(value: ReadDirSync) -> Self { crate::provider::ReadDirSync::Local(value) }
}
impl Iterator for ReadDirSync {
type Item = io::Result<DirEntrySync>;
fn next(&mut self) -> Option<io::Result<DirEntrySync>> {
self.0.next().map(|result| result.map(Into::into))
}
}

View file

@ -0,0 +1,23 @@
pub struct RwFile(tokio::fs::File);
impl From<tokio::fs::File> for RwFile {
fn from(value: tokio::fs::File) -> Self { Self(value) }
}
impl From<RwFile> for crate::provider::RwFile {
fn from(value: RwFile) -> Self { crate::provider::RwFile::Local(value) }
}
impl From<tokio::fs::File> for crate::provider::RwFile {
fn from(value: tokio::fs::File) -> Self { RwFile(value).into() }
}
impl RwFile {
#[inline]
pub fn reader(self) -> tokio::io::BufReader<tokio::fs::File> { tokio::io::BufReader::new(self.0) }
#[inline]
pub async fn reader_sync(self) -> std::io::BufReader<std::fs::File> {
std::io::BufReader::new(self.0.into_std().await)
}
}

View file

@ -0,0 +1,3 @@
yazi_macro::mod_pub!(local);
yazi_macro::mod_flat!(buffer dir_entry provider read_dir rw_file);

View file

@ -2,7 +2,7 @@ use std::io;
use yazi_shared::url::Url; use yazi_shared::url::Url;
use crate::services::Local; use crate::provider::{ReadDir, ReadDirSync, RwFile, local::Local};
#[inline] #[inline]
pub async fn canonicalize(url: impl AsRef<Url>) -> io::Result<Url> { pub async fn canonicalize(url: impl AsRef<Url>) -> io::Result<Url> {
@ -14,9 +14,9 @@ pub async fn canonicalize(url: impl AsRef<Url>) -> io::Result<Url> {
} }
#[inline] #[inline]
pub async fn create(url: impl AsRef<Url>) -> io::Result<tokio::fs::File> { pub async fn create(url: impl AsRef<Url>) -> io::Result<RwFile> {
if let Some(path) = url.as_ref().as_path() { if let Some(path) = url.as_ref().as_path() {
Local::create(path).await Local::create(path).await.map(Into::into)
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -59,27 +59,27 @@ pub async fn metadata(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
} }
#[inline] #[inline]
pub async fn open(url: impl AsRef<Url>) -> io::Result<tokio::fs::File> { pub async fn open(url: impl AsRef<Url>) -> io::Result<RwFile> {
if let Some(path) = url.as_ref().as_path() { if let Some(path) = url.as_ref().as_path() {
Local::open(path).await Local::open(path).await.map(Into::into)
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
} }
#[inline] #[inline]
pub async fn read_dir(url: impl AsRef<Url>) -> io::Result<tokio::fs::ReadDir> { pub async fn read_dir(url: impl AsRef<Url>) -> io::Result<ReadDir> {
if let Some(path) = url.as_ref().as_path() { if let Some(path) = url.as_ref().as_path() {
Local::read_dir(path).await Local::read_dir(path).await.map(Into::into)
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
} }
#[inline] #[inline]
pub fn read_dir_sync(url: impl AsRef<Url>) -> io::Result<std::fs::ReadDir> { pub fn read_dir_sync(url: impl AsRef<Url>) -> io::Result<ReadDirSync> {
if let Some(path) = url.as_ref().as_path() { if let Some(path) = url.as_ref().as_path() {
Local::read_dir_sync(path) Local::read_dir_sync(path).map(Into::into)
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }

View file

@ -0,0 +1,30 @@
use std::io;
use super::{DirEntry, DirEntrySync};
pub enum ReadDir {
Local(super::local::ReadDir),
}
impl ReadDir {
pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> {
match self {
ReadDir::Local(local) => local.next_entry().await.map(|entry| entry.map(Into::into)),
}
}
}
// --- ReadDirSync
pub enum ReadDirSync {
Local(super::local::ReadDirSync),
}
impl Iterator for ReadDirSync {
type Item = io::Result<DirEntrySync>;
fn next(&mut self) -> Option<io::Result<DirEntrySync>> {
match self {
ReadDirSync::Local(local) => local.next().map(|result| result.map(Into::into)),
}
}
}

View file

@ -0,0 +1,21 @@
use crate::provider::{BufRead, BufReadSync};
pub enum RwFile {
Local(super::local::RwFile),
}
impl RwFile {
#[inline]
pub fn reader(self) -> Box<dyn BufRead> {
match self {
RwFile::Local(local) => Box::new(local.reader()),
}
}
#[inline]
pub async fn reader_sync(self) -> Box<dyn BufReadSync> {
match self {
RwFile::Local(local) => Box::new(local.reader_sync().await),
}
}
}

View file

@ -1 +0,0 @@
yazi_macro::mod_flat!(local services);

View file

@ -16,7 +16,7 @@ impl From<CmdCow> for TabCreateOpt {
return Self { wd: None }; return Self { wd: None };
} }
let Some(mut wd) = c.take_first_url() else { let Some(mut wd) = c.take_first_url() else {
return Self { wd: Some(Url::from(&BOOT.cwds[0])) }; return Self { wd: Some(BOOT.cwds[0].clone()) };
}; };
if !c.bool("raw") if !c.bool("raw")
&& let Cow::Owned(u) = expand_url(&wd) && let Cow::Owned(u) = expand_url(&wd)

View file

@ -38,7 +38,7 @@ function Header:flags()
local t = {} local t = {}
if cwd.is_search then if cwd.is_search then
t[#t + 1] = string.format("search: %s", cwd.frag) t[#t + 1] = string.format("search: %s", cwd.domain)
end end
if filter then if filter then
t[#t + 1] = string.format("filter: %s", filter) t[#t + 1] = string.format("filter: %s", filter)

View file

@ -3,9 +3,9 @@ use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}, sync::OnceLock};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use ratatui::{layout::Size, text::{Line, Span, Text}}; use ratatui::{layout::Size, text::{Line, Span, Text}};
use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}}; use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}};
use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::io::AsyncBufReadExt;
use yazi_config::{THEME, YAZI, preview::PreviewWrap}; use yazi_config::{THEME, YAZI, preview::PreviewWrap};
use yazi_fs::services::Local; use yazi_fs::provider::local::Local;
use yazi_shared::{Ids, errors::PeekError, replace_to_printable}; use yazi_shared::{Ids, errors::PeekError, replace_to_printable};
static INCR: Ids = Ids::new(); static INCR: Ids = Ids::new();
@ -39,7 +39,7 @@ impl Highlighter {
pub fn abort() { INCR.next(); } pub fn abort() { INCR.next(); }
pub async fn highlight(&self, skip: usize, size: Size) -> Result<Text<'static>, PeekError> { pub async fn highlight(&self, skip: usize, size: Size) -> Result<Text<'static>, PeekError> {
let mut reader = BufReader::new(Local::open(&self.path).await?); let mut reader = Local::open(&self.path).await?.reader();
let syntax = Self::find_syntax(&self.path).await; let syntax = Self::find_syntax(&self.path).await;
let mut plain = syntax.is_err(); let mut plain = syntax.is_err();
@ -143,7 +143,7 @@ impl Highlighter {
} }
let mut line = String::new(); let mut line = String::new();
let mut reader = BufReader::new(Local::open(&path).await?); let mut reader = Local::open(&path).await?.reader();
reader.read_line(&mut line).await?; reader.read_line(&mut line).await?;
syntaxes.find_syntax_by_first_line(&line).ok_or_else(|| anyhow!("No syntax found")) syntaxes.find_syntax_by_first_line(&line).ok_or_else(|| anyhow!("No syntax found"))
} }

View file

@ -3,7 +3,7 @@ use std::borrow::Cow;
use globset::GlobBuilder; use globset::GlobBuilder;
use mlua::{ExternalError, ExternalResult, Function, IntoLua, IntoLuaMulti, Lua, Table, Value}; use mlua::{ExternalError, ExternalResult, Function, IntoLua, IntoLuaMulti, Lua, Table, Value};
use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef}; use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef};
use yazi_fs::{mounts::PARTITIONS, remove_dir_clean, services}; use yazi_fs::{mounts::PARTITIONS, provider, remove_dir_clean};
use crate::bindings::SizeCalculator; use crate::bindings::SizeCalculator;
@ -50,9 +50,9 @@ fn cwd(lua: &Lua) -> mlua::Result<Function> {
fn cha(lua: &Lua) -> mlua::Result<Function> { fn cha(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, follow): (UrlRef, Option<bool>)| async move { lua.create_async_function(|lua, (url, follow): (UrlRef, Option<bool>)| async move {
let meta = if follow.unwrap_or(false) { let meta = if follow.unwrap_or(false) {
services::metadata(&*url).await provider::metadata(&*url).await
} else { } else {
services::symlink_metadata(&*url).await provider::symlink_metadata(&*url).await
}; };
match meta { match meta {
@ -64,7 +64,7 @@ fn cha(lua: &Lua) -> mlua::Result<Function> {
fn write(lua: &Lua) -> mlua::Result<Function> { fn write(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move { lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move {
match services::write(&*url, data.as_bytes()).await { match provider::write(&*url, data.as_bytes()).await {
Ok(()) => true.into_lua_multi(&lua), Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
} }
@ -74,8 +74,8 @@ fn write(lua: &Lua) -> mlua::Result<Function> {
fn create(lua: &Lua) -> mlua::Result<Function> { fn create(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move { lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move {
let result = match r#type.as_bytes().as_ref() { let result = match r#type.as_bytes().as_ref() {
b"dir" => services::create_dir(&*url).await, b"dir" => provider::create_dir(&*url).await,
b"dir_all" => services::create_dir_all(&*url).await, b"dir_all" => provider::create_dir_all(&*url).await,
_ => Err("Creation type must be 'dir' or 'dir_all'".into_lua_err())?, _ => Err("Creation type must be 'dir' or 'dir_all'".into_lua_err())?,
}; };
@ -89,9 +89,9 @@ fn create(lua: &Lua) -> mlua::Result<Function> {
fn remove(lua: &Lua) -> mlua::Result<Function> { fn remove(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move { lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move {
let result = match r#type.as_bytes().as_ref() { let result = match r#type.as_bytes().as_ref() {
b"file" => services::remove_file(&*url).await, b"file" => provider::remove_file(&*url).await,
b"dir" => services::remove_dir(&*url).await, b"dir" => provider::remove_dir(&*url).await,
b"dir_all" => services::remove_dir_all(&*url).await, b"dir_all" => provider::remove_dir_all(&*url).await,
b"dir_clean" => Ok(remove_dir_clean(&url).await), b"dir_clean" => Ok(remove_dir_clean(&url).await),
_ => Err("Removal type must be 'file', 'dir', 'dir_all', or 'dir_clean'".into_lua_err())?, _ => Err("Removal type must be 'file', 'dir', 'dir_all', or 'dir_clean'".into_lua_err())?,
}; };
@ -105,7 +105,9 @@ fn remove(lua: &Lua) -> mlua::Result<Function> {
fn read_dir(lua: &Lua) -> mlua::Result<Function> { fn read_dir(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (dir, options): (UrlRef, Table)| async move { lua.create_async_function(|lua, (dir, options): (UrlRef, Table)| async move {
// FIXME: VFS
let glob = if let Ok(s) = options.raw_get::<mlua::String>("glob") { let glob = if let Ok(s) = options.raw_get::<mlua::String>("glob") {
return Err("`glob` is disabled temporarily".into_lua_err());
Some( Some(
GlobBuilder::new(&s.to_str()?) GlobBuilder::new(&s.to_str()?)
.case_insensitive(true) .case_insensitive(true)
@ -123,7 +125,7 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
let limit = options.raw_get("limit").unwrap_or(usize::MAX); let limit = options.raw_get("limit").unwrap_or(usize::MAX);
let resolve = options.raw_get("resolve").unwrap_or(false); let resolve = options.raw_get("resolve").unwrap_or(false);
let mut it = match services::read_dir(&*dir).await { let mut it = match provider::read_dir(&*dir).await {
Ok(it) => it, Ok(it) => it,
Err(e) => return (Value::Nil, Error::Io(e)).into_lua_multi(&lua), Err(e) => return (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}; };
@ -134,12 +136,11 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
break; break;
} }
let path = next.path(); let url = next.url();
if glob.as_ref().is_some_and(|g| !g.is_match(&path)) { // if glob.as_ref().is_some_and(|g| !g.is_match(&path)) {
continue; // continue;
} // }
let url = yazi_shared::url::Url::from(path);
let file = if !resolve { let file = if !resolve {
yazi_fs::File::from_dummy(url, next.file_type().await.ok()) yazi_fs::File::from_dummy(url, next.file_type().await.ok())
} else if let Ok(meta) = next.metadata().await { } else if let Ok(meta) = next.metadata().await {

View file

@ -4,7 +4,7 @@ use anyhow::{Context, Result, bail};
use mlua::{ChunkMode, ExternalError, Lua, Table}; use mlua::{ChunkMode, ExternalError, Lua, Table};
use parking_lot::RwLock; use parking_lot::RwLock;
use yazi_boot::BOOT; use yazi_boot::BOOT;
use yazi_fs::services::Local; use yazi_fs::provider::local::Local;
use yazi_macro::plugin_preset as preset; use yazi_macro::plugin_preset as preset;
use yazi_shared::{LOG_LEVEL, RoCell}; use yazi_shared::{LOG_LEVEL, RoCell};

View file

@ -1,10 +1,10 @@
use std::{borrow::Cow, collections::VecDeque}; use std::{borrow::Cow, collections::VecDeque};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use tokio::{fs::DirEntry, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::warn; use tracing::warn;
use yazi_config::YAZI; use yazi_config::YAZI;
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, services, skip_url, url_relative_to}; use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, provider::{self, DirEntry}, skip_url, url_relative_to};
use yazi_shared::{Id, url::Url}; use yazi_shared::{Id, url::Url};
use super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; use super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash};
@ -26,14 +26,14 @@ impl File {
pub async fn work(&self, r#in: FileIn) -> Result<()> { pub async fn work(&self, r#in: FileIn) -> Result<()> {
match r#in { match r#in {
FileIn::Paste(mut task) => { FileIn::Paste(mut task) => {
ok_or_not_found(services::remove_file(&task.to).await)?; ok_or_not_found(provider::remove_file(&task.to).await)?;
let mut it = copy_with_progress(&task.from, &task.to, task.cha.unwrap()); let mut it = copy_with_progress(&task.from, &task.to, task.cha.unwrap());
while let Some(res) = it.recv().await { while let Some(res) = it.recv().await {
match res { match res {
Ok(0) => { Ok(0) => {
if task.cut { if task.cut {
services::remove_file(&task.from).await.ok(); provider::remove_file(&task.from).await.ok();
} }
break; break;
} }
@ -62,7 +62,7 @@ impl File {
let cha = task.cha.unwrap(); let cha = task.cha.unwrap();
let src = if task.resolve { let src = if task.resolve {
match services::read_link(&task.from).await { match provider::read_link(&task.from).await {
Ok(u) => Cow::Owned(u), Ok(u) => Cow::Owned(u),
Err(e) if e.kind() == NotFound => { Err(e) if e.kind() == NotFound => {
warn!("Link task partially done: {task:?}"); warn!("Link task partially done: {task:?}");
@ -75,20 +75,20 @@ impl File {
}; };
let src = if task.relative { let src = if task.relative {
url_relative_to(&services::canonicalize(task.to.parent_url().unwrap()).await?, &src)? url_relative_to(&provider::canonicalize(task.to.parent_url().unwrap()).await?, &src)?
} else { } else {
src src
}; };
ok_or_not_found(services::remove_file(&task.to).await)?; ok_or_not_found(provider::remove_file(&task.to).await)?;
if cha.is_dir() { if cha.is_dir() {
services::symlink_dir(src, &task.to).await?; provider::symlink_dir(src, &task.to).await?;
} else { } else {
services::symlink_file(src, &task.to).await?; provider::symlink_file(src, &task.to).await?;
} }
if task.delete { if task.delete {
services::remove_file(&task.from).await.ok(); provider::remove_file(&task.from).await.ok();
} }
self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?; self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?;
} }
@ -96,14 +96,14 @@ impl File {
let cha = task.cha.unwrap(); let cha = task.cha.unwrap();
let src = if !task.follow { let src = if !task.follow {
Cow::Borrowed(&task.from) Cow::Borrowed(&task.from)
} else if let Ok(p) = services::canonicalize(&task.from).await { } else if let Ok(p) = provider::canonicalize(&task.from).await {
Cow::Owned(p) Cow::Owned(p)
} else { } else {
Cow::Borrowed(&task.from) Cow::Borrowed(&task.from)
}; };
ok_or_not_found(services::remove_file(&task.to).await)?; ok_or_not_found(provider::remove_file(&task.to).await)?;
match services::hard_link(src, &task.to).await { match provider::hard_link(src, &task.to).await {
Err(e) if e.kind() == NotFound => { Err(e) if e.kind() == NotFound => {
warn!("Hardlink task partially done: {task:?}"); warn!("Hardlink task partially done: {task:?}");
} }
@ -113,7 +113,7 @@ impl File {
self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?; self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?;
} }
FileIn::Delete(task) => { FileIn::Delete(task) => {
if let Err(e) = services::remove_file(&task.target).await if let Err(e) = provider::remove_file(&task.target).await
&& e.kind() != NotFound && e.kind() != NotFound
&& maybe_exists(&task.target).await && maybe_exists(&task.target).await
{ {
@ -145,7 +145,7 @@ impl File {
} }
pub async fn paste(&self, mut task: FileInPaste) -> Result<()> { pub async fn paste(&self, mut task: FileInPaste) -> Result<()> {
if task.cut && ok_or_not_found(services::rename(&task.from, &task.to).await).is_ok() { if task.cut && ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok() {
return self.succ(task.id); return self.succ(task.id);
} }
@ -185,14 +185,14 @@ impl File {
while let Some(src) = dirs.pop_front() { while let Some(src) = dirs.pop_front() {
let dest = root.join(skip_url(&src, skip)); let dest = root.join(skip_url(&src, skip));
continue_unless_ok!(match services::create_dir(&dest).await { continue_unless_ok!(match provider::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => Err(e), Err(e) if e.kind() != AlreadyExists => Err(e),
_ => Ok(()), _ => Ok(()),
}); });
let mut it = continue_unless_ok!(services::read_dir(&src).await); let mut it = continue_unless_ok!(provider::read_dir(&src).await);
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(entry)) = it.next_entry().await {
let from = Url::from(entry.path()); let from = entry.url();
let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await); let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await);
if cha.is_dir() { if cha.is_dir() {
@ -256,14 +256,14 @@ impl File {
while let Some(src) = dirs.pop_front() { while let Some(src) = dirs.pop_front() {
let dest = root.join(skip_url(&src, skip)); let dest = root.join(skip_url(&src, skip));
continue_unless_ok!(match services::create_dir(&dest).await { continue_unless_ok!(match provider::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => Err(e), Err(e) if e.kind() != AlreadyExists => Err(e),
_ => Ok(()), _ => Ok(()),
}); });
let mut it = continue_unless_ok!(services::read_dir(&src).await); let mut it = continue_unless_ok!(provider::read_dir(&src).await);
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(entry)) = it.next_entry().await {
let from = Url::from(entry.path()); let from = entry.url();
let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await); let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await);
if cha.is_dir() { if cha.is_dir() {
@ -280,7 +280,7 @@ impl File {
} }
pub async fn delete(&self, mut task: FileInDelete) -> Result<()> { pub async fn delete(&self, mut task: FileInDelete) -> Result<()> {
let meta = services::symlink_metadata(&task.target).await?; let meta = provider::symlink_metadata(&task.target).await?;
if !meta.is_dir() { if !meta.is_dir() {
let id = task.id; let id = task.id;
task.length = meta.len(); task.length = meta.len();
@ -291,17 +291,17 @@ impl File {
let mut dirs = VecDeque::from([task.target]); let mut dirs = VecDeque::from([task.target]);
while let Some(target) = dirs.pop_front() { while let Some(target) = dirs.pop_front() {
let Ok(mut it) = services::read_dir(target).await else { continue }; let Ok(mut it) = provider::read_dir(target).await else { continue };
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(entry)) = it.next_entry().await {
let Ok(meta) = entry.metadata().await else { continue }; let Ok(meta) = entry.metadata().await else { continue };
if meta.is_dir() { if meta.is_dir() {
dirs.push_front(Url::from(entry.path())); dirs.push_front(entry.url());
continue; continue;
} }
task.target = Url::from(entry.path()); task.target = entry.url();
task.length = meta.len(); task.length = meta.len();
self.prog.send(TaskProg::New(task.id, meta.len()))?; self.prog.send(TaskProg::New(task.id, meta.len()))?;
self.queue(FileIn::Delete(task.clone()), NORMAL).await?; self.queue(FileIn::Delete(task.clone()), NORMAL).await?;
@ -321,7 +321,7 @@ impl File {
#[inline] #[inline]
async fn cha(url: &Url, follow: bool) -> io::Result<Cha> { async fn cha(url: &Url, follow: bool) -> io::Result<Cha> {
let meta = services::symlink_metadata(url).await?; let meta = provider::symlink_metadata(url).await?;
Ok(if follow { Cha::from_follow(url, meta).await } else { Cha::new(url, meta) }) Ok(if follow { Cha::from_follow(url, meta).await } else { Cha::new(url, meta) })
} }

View file

@ -6,7 +6,7 @@ use parking_lot::Mutex;
use tokio::{select, sync::mpsc::{self, UnboundedReceiver}, task::JoinHandle}; use tokio::{select, sync::mpsc::{self, UnboundedReceiver}, task::JoinHandle};
use yazi_config::{YAZI, plugin::{Fetcher, Preloader}}; use yazi_config::{YAZI, plugin::{Fetcher, Preloader}};
use yazi_dds::Pump; use yazi_dds::Pump;
use yazi_fs::{must_be_dir, remove_dir_clean, services, unique_name}; use yazi_fs::{must_be_dir, provider, remove_dir_clean, unique_name};
use yazi_parser::{app::PluginOpt, tasks::ProcessExecOpt}; use yazi_parser::{app::PluginOpt, tasks::ProcessExecOpt};
use yazi_proxy::MgrProxy; use yazi_proxy::MgrProxy;
use yazi_shared::{Id, Throttle, url::Url}; use yazi_shared::{Id, Throttle, url::Url};
@ -175,7 +175,7 @@ impl Scheduler {
move |canceled: bool| { move |canceled: bool| {
async move { async move {
if !canceled { if !canceled {
services::remove_dir_all(&target).await.ok(); provider::remove_dir_all(&target).await.ok();
MgrProxy::update_tasks(&target); MgrProxy::update_tasks(&target);
Pump::push_delete(target); Pump::push_delete(target);
} }

View file

@ -66,9 +66,9 @@ impl Data {
#[inline] #[inline]
pub fn into_url(self) -> Option<Url> { pub fn into_url(self) -> Option<Url> {
match self { match self {
Self::String(s) => Url::try_from(s.as_ref()).ok(), Self::String(s) => s.parse().ok(),
Self::Url(u) => Some(u), Self::Url(u) => Some(u),
Self::Bytes(b) => Url::try_from(b.as_slice()).ok(), Self::Bytes(b) => b.as_slice().try_into().ok(),
_ => None, _ => None,
} }
} }
@ -96,9 +96,9 @@ impl Data {
#[inline] #[inline]
pub fn to_url(&self) -> Option<Url> { pub fn to_url(&self) -> Option<Url> {
match self { match self {
Self::String(s) => Url::try_from(s.as_ref()).ok(), Self::String(s) => s.parse().ok(),
Self::Url(u) => Some(u.clone()), Self::Url(u) => Some(u.clone()),
Self::Bytes(b) => Url::try_from(b.as_slice()).ok(), Self::Bytes(b) => b.as_slice().try_into().ok(),
_ => None, _ => None,
} }
} }
@ -186,9 +186,9 @@ impl DataKey {
#[inline] #[inline]
pub fn into_url(self) -> Option<Url> { pub fn into_url(self) -> Option<Url> {
match self { match self {
Self::String(s) => Url::try_from(s.as_ref()).ok(), Self::String(s) => s.parse().ok(),
Self::Url(u) => Some(u), Self::Url(u) => Some(u),
Self::Bytes(b) => Url::try_from(b.as_slice()).ok(), Self::Bytes(b) => b.as_slice().try_into().ok(),
_ => None, _ => None,
} }
} }

View file

@ -1,6 +1,6 @@
use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, path::{self, PathBuf, PrefixComponent}}; use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, path::{self, PathBuf, PrefixComponent}};
use crate::url::{Scheme, Url}; use crate::url::{Encode, Loc, Scheme, Url};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Component<'a> { pub enum Component<'a> {
@ -60,6 +60,7 @@ impl<'a> FromIterator<Component<'a>> for PathBuf {
#[derive(Clone)] #[derive(Clone)]
pub struct Components<'a> { pub struct Components<'a> {
inner: path::Components<'a>, inner: path::Components<'a>,
loc: &'a Loc,
scheme: &'a Scheme, scheme: &'a Scheme,
scheme_yielded: bool, scheme_yielded: bool,
} }
@ -68,6 +69,7 @@ impl<'a> Components<'a> {
pub fn new(url: &'a Url) -> Self { pub fn new(url: &'a Url) -> Self {
Self { Self {
inner: url.loc.components(), inner: url.loc.components(),
loc: &url.loc,
scheme: &url.scheme, scheme: &url.scheme,
scheme_yielded: false, scheme_yielded: false,
} }
@ -79,7 +81,7 @@ impl<'a> Components<'a> {
return path.as_os_str().into(); return path.as_os_str().into();
} }
let mut s = OsString::from(format!("{}", self.scheme)); let mut s = OsString::from(Encode::new(self.loc, self.scheme).to_string());
s.reserve_exact(path.as_os_str().len()); s.reserve_exact(path.as_os_str().len());
s.push(path); s.push(path);
s.into() s.into()
@ -147,14 +149,16 @@ mod tests {
#[test] #[test]
fn test_collect() { fn test_collect() {
let search = Url::try_from("search://keyword//root/projects/yazi").unwrap(); let search: Url = "search://keyword//root/projects/yazi".parse().unwrap();
assert_eq!(search.loc.urn().as_os_str(), OsStr::new(""));
assert_eq!(search.scheme, Scheme::Search("keyword".to_owned())); assert_eq!(search.scheme, Scheme::Search("keyword".to_owned()));
let item = search.join("main.rs"); let item = search.join("main.rs");
assert_eq!(item.scheme, Scheme::SearchItem); assert_eq!(item.loc.urn().as_os_str(), OsStr::new("main.rs"));
assert_eq!(item.scheme, Scheme::Search("keyword".to_owned()));
let u: Url = item.components().take(4).collect(); let u: Url = item.components().take(4).collect();
assert_eq!(u.scheme, Scheme::SearchItem); assert_eq!(u.scheme, Scheme::Search("keyword".to_owned()));
assert_eq!(u.loc.as_path(), Path::new("/root/projects")); assert_eq!(u.loc.as_path(), Path::new("/root/projects"));
let u: Url = item let u: Url = item
@ -162,7 +166,7 @@ mod tests {
.take(5) .take(5)
.chain([Component::Normal(OsStr::new("target/release/yazi"))]) .chain([Component::Normal(OsStr::new("target/release/yazi"))])
.collect(); .collect();
assert_eq!(u.scheme, Scheme::SearchItem); assert_eq!(u.scheme, Scheme::Search("keyword".to_owned()));
assert_eq!(u.loc.as_path(), Path::new("/root/projects/yazi/target/release/yazi")); assert_eq!(u.loc.as_path(), Path::new("/root/projects/yazi/target/release/yazi"));
} }
} }

View file

@ -1,4 +1,4 @@
use crate::url::Url; use crate::url::{Encode, Url};
pub struct Display<'a> { pub struct Display<'a> {
inner: &'a Url, inner: &'a Url,
@ -13,7 +13,7 @@ impl<'a> std::fmt::Display for Display<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Url { loc, scheme } = self.inner; let Url { loc, scheme } = self.inner;
if scheme.is_virtual() { if scheme.is_virtual() {
scheme.fmt(f)?; Encode::from(self.inner).fmt(f)?;
} }
loc.display().fmt(f) loc.display().fmt(f)
} }

View file

@ -0,0 +1,76 @@
use std::fmt::{self, Display};
use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode};
use crate::url::{Loc, Scheme, Url};
pub struct Encode<'a> {
loc: &'a Loc,
scheme: &'a Scheme,
}
impl<'a> From<&'a Url> for Encode<'a> {
fn from(url: &'a Url) -> Self { Self::new(&url.loc, &url.scheme) }
}
impl<'a> Encode<'a> {
#[inline]
pub(super) fn new(loc: &'a Loc, scheme: &'a Scheme) -> Self { Self { loc, scheme } }
#[inline]
fn domain<'s>(s: &'s str) -> PercentEncode<'s> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':');
percent_encode(s.as_bytes(), SET)
}
#[inline]
fn urn(loc: &'a Loc) -> impl Display {
struct D(usize);
impl Display for D {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0 != 0 {
write!(f, ":{}", self.0)?;
}
Ok(())
}
}
D(loc.urn().components().count())
}
}
impl Display for Encode<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.scheme {
Scheme::Regular => write!(f, "regular://"),
Scheme::Search(d) => write!(f, "search://{}{}/", Self::domain(d), Self::urn(self.loc)),
Scheme::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), Self::urn(self.loc)),
Scheme::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), Self::urn(self.loc)),
}
}
}
// --- Tilded
pub struct EncodeTilded<'a> {
loc: &'a Loc,
scheme: &'a Scheme,
}
impl<'a> From<&'a Url> for EncodeTilded<'a> {
fn from(url: &'a Url) -> Self { Self { loc: &url.loc, scheme: &url.scheme } }
}
impl Display for EncodeTilded<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Encode as E;
let loc = percent_encode(self.loc.as_os_str().as_encoded_bytes(), CONTROLS);
match self.scheme {
Scheme::Regular => write!(f, "regular~://{loc}"),
Scheme::Search(d) => write!(f, "search~://{}{}/{loc}", E::domain(d), E::urn(self.loc)),
Scheme::Archive(d) => write!(f, "archive~://{}{}/{loc}", E::domain(d), E::urn(self.loc)),
Scheme::Sftp(d) => write!(f, "sftp~://{}{}/{loc}", E::domain(d), E::urn(self.loc)),
}
}
}

View file

@ -1,12 +1,13 @@
use std::{borrow::Cow, cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; use std::{borrow::Cow, cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use anyhow::{Result, bail};
use crate::url::{Urn, UrnBuf}; use crate::url::{Urn, UrnBuf};
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct Loc { pub struct Loc {
inner: PathBuf, inner: PathBuf,
urn: usize, urn: usize,
name: usize,
} }
impl Deref for Loc { impl Deref for Loc {
@ -39,11 +40,7 @@ impl Hash for Loc {
impl Debug for Loc { impl Debug for Loc {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("Loc") f.debug_struct("Loc").field("path", &self.inner).field("urn", &self.urn()).finish()
.field("path", &self.inner)
.field("urn", &self.urn())
.field("name", &self.name())
.finish()
} }
} }
@ -59,20 +56,22 @@ impl From<PathBuf> for Loc {
fn from(path: PathBuf) -> Self { fn from(path: PathBuf) -> Self {
let Some(name) = path.file_name() else { let Some(name) = path.file_name() else {
let urn = path.as_os_str().len(); let urn = path.as_os_str().len();
return Self { inner: path, urn, name: 0 }; return Self { inner: path, urn };
}; };
let name_len = name.len(); let name_len = name.len();
let prefix_len = unsafe { let prefix_len = unsafe {
name.as_encoded_bytes().as_ptr().offset_from(path.as_os_str().as_encoded_bytes().as_ptr()) name
.as_encoded_bytes()
.as_ptr()
.offset_from_unsigned(path.as_os_str().as_encoded_bytes().as_ptr())
}; };
let mut bytes = path.into_os_string().into_encoded_bytes(); let mut bytes = path.into_os_string().into_encoded_bytes();
bytes.truncate(name_len + prefix_len as usize); bytes.truncate(prefix_len + name_len);
Self { Self {
inner: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), inner: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }),
urn: name_len, urn: name_len,
name: name_len,
} }
} }
} }
@ -86,7 +85,26 @@ impl<T: ?Sized + AsRef<OsStr>> From<&T> for Loc {
} }
impl Loc { impl Loc {
pub fn with(base: &Path, path: PathBuf) -> Self { pub fn zeroed(path: PathBuf) -> Self {
let mut loc = Self::from(path);
loc.urn = 0;
loc
}
pub fn with(urn: usize, path: PathBuf) -> Result<Self> {
let mut loc = Self::from(path);
let mut it = loc.inner.components();
for _ in 0..urn {
if it.next_back().is_none() {
bail!("URN exceeds its entire URL");
}
}
loc.urn = loc.strip_prefix(it).unwrap().as_os_str().len();
Ok(loc)
}
pub fn with_lossy(base: &Path, path: PathBuf) -> Self {
let mut loc = Self::from(path); let mut loc = Self::from(path);
loc.urn = loc.inner.strip_prefix(base).unwrap_or(&loc.inner).as_os_str().len(); loc.urn = loc.inner.strip_prefix(base).unwrap_or(&loc.inner).as_os_str().len();
loc loc
@ -105,28 +123,20 @@ impl Loc {
pub fn urn_owned(&self) -> UrnBuf { self.urn().to_owned() } pub fn urn_owned(&self) -> UrnBuf { self.urn().to_owned() }
#[inline] #[inline]
pub fn name(&self) -> &OsStr { pub fn name(&self) -> &OsStr { self.inner.file_name().unwrap_or(OsStr::new("")) }
unsafe {
OsStr::from_encoded_bytes_unchecked(
self.bytes().get_unchecked(self.bytes().len() - self.name..),
)
}
}
pub fn set_name(&mut self, name: impl AsRef<OsStr>) { pub fn set_name(&mut self, name: impl AsRef<OsStr>) {
let name = name.as_ref(); let (old, new) = (self.name(), name.as_ref());
if name == self.name() { if old == new {
return; return;
} }
if self.name > name.len() { if old.len() > new.len() {
self.urn -= self.name - name.len(); self.urn -= old.len() - new.len();
} else { } else {
self.urn += name.len() - self.name; self.urn += new.len() - old.len();
} }
self.inner.set_file_name(new);
self.name = name.len();
self.inner.set_file_name(name);
} }
#[inline] #[inline]
@ -143,13 +153,16 @@ impl Loc {
#[inline] #[inline]
pub fn rebase(&self, parent: &Path) -> Self { pub fn rebase(&self, parent: &Path) -> Self {
debug_assert!(self.urn == self.name); debug_assert!(self.urn == self.name().len());
let path = parent.join(self.name()); let path = parent.join(self.name());
debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name)); debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name().len()));
Self { inner: path, urn: self.name, name: self.name } Self { inner: path, urn: self.urn }
} }
#[inline]
pub fn to_path(&self) -> PathBuf { self.inner.clone() }
#[inline] #[inline]
pub fn into_path(self) -> PathBuf { self.inner } pub fn into_path(self) -> PathBuf { self.inner }
@ -159,8 +172,6 @@ impl Loc {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::MAIN_SEPARATOR;
use super::*; use super::*;
#[test] #[test]
@ -182,18 +193,37 @@ mod tests {
} }
#[test] #[test]
fn test_from() { fn test_with() -> Result<()> {
let loc = Loc::with(Path::new("/"), "/".into()); let loc = Loc::with(0, "/".into())?;
assert_eq!(loc.urn().as_os_str(), OsStr::new("")); assert_eq!(loc.urn().as_os_str(), OsStr::new(""));
assert_eq!(loc.name(), OsStr::new("")); assert_eq!(loc.name(), OsStr::new(""));
assert_eq!(loc.base().as_os_str(), OsStr::new("/")); assert_eq!(loc.base().as_os_str(), OsStr::new("/"));
let loc = Loc::with(Path::new("/root/"), "/root/code/".into()); let loc = Loc::with(1, "/root/code/".into())?;
assert_eq!(loc.urn().as_os_str(), OsStr::new("code")); assert_eq!(loc.urn().as_os_str(), OsStr::new("code"));
assert_eq!(loc.name(), OsStr::new("code")); assert_eq!(loc.name(), OsStr::new("code"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
let loc = Loc::with(Path::new("/root//"), "/root/code/foo//".into()); let loc = Loc::with(2, "/root/code/foo//".into())?;
assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo"));
assert_eq!(loc.name(), OsStr::new("foo"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
Ok(())
}
#[test]
fn test_with_lossy() {
let loc = Loc::with_lossy(Path::new("/"), "/".into());
assert_eq!(loc.urn().as_os_str(), OsStr::new(""));
assert_eq!(loc.name(), OsStr::new(""));
assert_eq!(loc.base().as_os_str(), OsStr::new("/"));
let loc = Loc::with_lossy(Path::new("/root/"), "/root/code/".into());
assert_eq!(loc.urn().as_os_str(), OsStr::new("code"));
assert_eq!(loc.name(), OsStr::new("code"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
let loc = Loc::with_lossy(Path::new("/root//"), "/root/code/foo//".into());
assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo"));
assert_eq!(loc.name(), OsStr::new("foo")); assert_eq!(loc.name(), OsStr::new("foo"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
@ -201,18 +231,20 @@ mod tests {
#[test] #[test]
fn test_set_name() { fn test_set_name() {
let mut loc = Loc::with(Path::new("/root"), "/root/code/foo/".into()); const S: char = std::path::MAIN_SEPARATOR;
let mut loc = Loc::with_lossy(Path::new("/root"), "/root/code/foo/".into());
assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo"));
assert_eq!(loc.name(), OsStr::new("foo")); assert_eq!(loc.name(), OsStr::new("foo"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
loc.set_name("bar.txt"); loc.set_name("bar.txt");
assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{MAIN_SEPARATOR}bar.txt"))); assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{S}bar.txt")));
assert_eq!(loc.name(), OsStr::new("bar.txt")); assert_eq!(loc.name(), OsStr::new("bar.txt"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
loc.set_name("baz"); loc.set_name("baz");
assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{MAIN_SEPARATOR}baz"))); assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{S}baz")));
assert_eq!(loc.name(), OsStr::new("baz")); assert_eq!(loc.name(), OsStr::new("baz"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
} }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(component cov display loc scheme url urn); yazi_macro::mod_flat!(component cov display encode loc scheme url urn);

View file

@ -1,9 +1,9 @@
use std::{borrow::Cow, fmt::Display}; use std::borrow::Cow;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_decode, percent_encode}; use percent_encoding::percent_decode;
use crate::{BytesExt, url::Loc}; use crate::BytesExt;
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Scheme { pub enum Scheme {
@ -11,7 +11,6 @@ pub enum Scheme {
Regular, Regular,
Search(String), Search(String),
SearchItem,
Archive(String), Archive(String),
@ -19,32 +18,67 @@ pub enum Scheme {
} }
impl Scheme { impl Scheme {
pub(super) fn parse(bytes: &[u8]) -> Result<(Self, usize, bool)> { #[inline]
pub const fn kind(&self) -> &'static str {
match self {
Self::Regular => "regular",
Self::Search(_) => "search",
Self::Archive(_) => "archive",
Self::Sftp(_) => "sftp",
}
}
#[inline]
pub fn domain(&self) -> Option<&str> {
match self {
Self::Regular => None,
Self::Search(s) | Self::Archive(s) | Self::Sftp(s) => Some(s),
}
}
pub(super) fn parse(bytes: &[u8], skip: &mut usize) -> Result<(Self, bool, Option<usize>)> {
let Some((mut protocol, rest)) = bytes.split_by_seq(b"://") else { let Some((mut protocol, rest)) = bytes.split_by_seq(b"://") else {
return Ok((Self::Regular, 0, false)); return Ok((Self::Regular, false, None));
}; };
// Advance to the beginning of the path
*skip += 3 + protocol.len();
// Tilded schemes
let tilde = protocol.ends_with(b"~"); let tilde = protocol.ends_with(b"~");
if tilde { if tilde {
protocol = &protocol[..protocol.len() - 1]; protocol = &protocol[..protocol.len() - 1];
} }
Ok(match protocol { let (scheme, port) = match protocol {
b"regular" => (Self::Regular, 10, tilde), b"regular" => (Self::Regular, None),
b"search" => { b"search" => {
let (name, skip) = Self::decode_param(rest)?; let (domain, port) = Self::decode_param(rest, skip)?;
(Self::Search(name), 9 + skip, tilde) (Self::Search(domain), Some(port))
} }
b"archive" => { b"archive" => {
let (name, skip) = Self::decode_param(rest)?; let (domain, port) = Self::decode_param(rest, skip)?;
(Self::Archive(name), 10 + skip, tilde) (Self::Archive(domain), Some(port))
} }
b"sftp" => { b"sftp" => {
let (name, skip) = Self::decode_param(rest)?; let (domain, port) = Self::decode_param(rest, skip)?;
(Self::Sftp(name), 7 + skip, tilde) (Self::Sftp(domain), Some(port))
} }
_ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)), _ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)),
}) };
Ok((scheme, tilde, port))
}
#[inline]
pub fn parse_kind(bytes: &[u8]) -> Result<&'static str> {
match bytes {
b"regular" => Ok("regular"),
b"search" => Ok("search"),
b"archive" => Ok("archive"),
b"sftp" => Ok("sftp"),
_ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)),
}
} }
#[inline] #[inline]
@ -55,49 +89,30 @@ impl Scheme {
#[inline] #[inline]
pub fn is_virtual(&self) -> bool { pub fn is_virtual(&self) -> bool {
match self { match self {
Self::Regular | Self::Search(_) | Self::SearchItem => false, Self::Regular | Self::Search(_) => false,
Self::Archive(_) | Self::Sftp(_) => true, Self::Archive(_) | Self::Sftp(_) => true,
} }
} }
fn decode_param(bytes: &[u8]) -> Result<(String, usize)> { fn decode_param(bytes: &[u8], skip: &mut usize) -> Result<(String, usize)> {
let len = bytes.iter().copied().take_while(|&b| b != b'/').count(); let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count();
let slash = bytes.get(len).is_some_and(|&b| b == b'/');
*skip += len + slash as usize;
let s = match Cow::from(percent_decode(&bytes[..len])) { let port = Self::decode_port(&bytes[..len], &mut len)?;
let domain = match Cow::from(percent_decode(&bytes[..len])) {
Cow::Borrowed(b) => str::from_utf8(b)?.to_owned(), Cow::Borrowed(b) => str::from_utf8(b)?.to_owned(),
Cow::Owned(b) => String::from_utf8(b)?, Cow::Owned(b) => String::from_utf8(b)?,
}; };
let slash = bytes.get(len).is_some_and(|&b| b == b'/') as usize; Ok((domain, port))
Ok((s, len + slash))
} }
#[inline] fn decode_port(bytes: &[u8], skip: &mut usize) -> Result<usize> {
fn encode_param<'a>(s: &'a str) -> PercentEncode<'a> { let Some(idx) = bytes.iter().rposition(|&b| b == b':') else { return Ok(0) };
const SET: AsciiSet = CONTROLS.add(b'/'); let len = bytes.len() - idx;
percent_encode(s.as_bytes(), &SET)
}
pub fn encode_tilded(&self, loc: &Loc) -> String { *skip -= len;
let loc = percent_encode(loc.as_os_str().as_encoded_bytes(), CONTROLS); Ok(if len == 1 { 0 } else { str::from_utf8(&bytes[idx + 1..])?.parse()? })
match self {
Self::Regular => format!("regular~://{loc}"),
Self::Search(kw) => format!("search~://{}/{loc}", Self::encode_param(kw)),
Self::SearchItem => format!("search-item~://{loc}"),
Self::Archive(id) => format!("archive~://{}/{loc}", Self::encode_param(id)),
Self::Sftp(id) => format!("sftp~://{}/{loc}", Self::encode_param(id)),
}
}
}
impl Display for Scheme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Regular => write!(f, "regular://"),
Self::Search(kw) => write!(f, "search://{}/", Self::encode_param(kw)),
Self::SearchItem => write!(f, "search-item://"),
Self::Archive(id) => write!(f, "archive://{}/", Self::encode_param(id)),
Self::Sftp(id) => write!(f, "sftp://{}/", Self::encode_param(id)),
}
} }
} }

View file

@ -1,11 +1,11 @@
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}}; use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}, str::FromStr};
use anyhow::Result; use anyhow::Result;
use percent_encoding::percent_decode; use percent_encoding::percent_decode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::UrnBuf; use super::UrnBuf;
use crate::{IntoOsStr, url::{Components, Display, Loc, Scheme}}; use crate::{IntoOsStr, url::{Components, Display, Encode, EncodeTilded, Loc, Scheme, Urn}};
#[derive(Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)] #[derive(Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)]
pub struct Url { pub struct Url {
@ -27,6 +27,10 @@ impl From<PathBuf> for Url {
fn from(path: PathBuf) -> Self { Loc::from(path).into() } fn from(path: PathBuf) -> Self { Loc::from(path).into() }
} }
impl From<&Url> for Url {
fn from(url: &Url) -> Self { url.clone() }
}
impl From<&PathBuf> for Url { impl From<&PathBuf> for Url {
fn from(path: &PathBuf) -> Self { path.to_owned().into() } fn from(path: &PathBuf) -> Self { path.to_owned().into() }
} }
@ -39,15 +43,18 @@ impl TryFrom<&[u8]> for Url {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let (scheme, path) = Self::parse(bytes)?; let (scheme, path, port) = Self::parse(bytes)?;
Ok(Self { loc: path.into(), scheme })
let loc = if let Some(urn) = port { Loc::with(urn, path)? } else { Loc::from(path) };
Ok(Self { loc, scheme })
} }
} }
impl TryFrom<&str> for Url { impl FromStr for Url {
type Error = anyhow::Error; type Err = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> { value.as_bytes().try_into() } fn from_str(s: &str) -> Result<Self, Self::Err> { s.as_bytes().try_into() }
} }
impl TryFrom<String> for Url { impl TryFrom<String> for Url {
@ -82,7 +89,7 @@ impl Url {
pub fn with(&self, loc: impl Into<Loc>) -> Self { pub fn with(&self, loc: impl Into<Loc>) -> Self {
let loc: Loc = loc.into(); let loc: Loc = loc.into();
// FIXME: simplify this // FIXME: simplify this
Self { loc: Loc::with(self.loc.base(), loc.into_path()), scheme: self.scheme.clone() } Self { loc: Loc::with_lossy(self.loc.base(), loc.into_path()), scheme: self.scheme.clone() }
} }
#[inline] #[inline]
@ -91,7 +98,6 @@ impl Url {
match &self.scheme { match &self.scheme {
Scheme::Regular => Self { loc, scheme: Scheme::Regular }, Scheme::Regular => Self { loc, scheme: Scheme::Regular },
Scheme::Search(_) => self.with(loc), Scheme::Search(_) => self.with(loc),
Scheme::SearchItem => Self { loc, scheme: Scheme::Search(String::new()) },
Scheme::Archive(_) => self.with(loc), Scheme::Archive(_) => self.with(loc),
Scheme::Sftp(_) => self.with(loc), Scheme::Sftp(_) => self.with(loc),
} }
@ -100,12 +106,7 @@ impl Url {
pub fn join(&self, path: impl AsRef<Path>) -> Self { pub fn join(&self, path: impl AsRef<Path>) -> Self {
match self.scheme { match self.scheme {
Scheme::Regular => Self { loc: self.loc.join(path).into(), scheme: Scheme::Regular }, Scheme::Regular => Self { loc: self.loc.join(path).into(), scheme: Scheme::Regular },
Scheme::Search(_) => { Scheme::Search(_) => self.with(self.loc.join(path)),
Self { loc: Loc::with(&self.loc, self.loc.join(path)), scheme: Scheme::SearchItem }
}
Scheme::SearchItem => {
Self { loc: Loc::with(self.loc.base(), self.loc.join(path)), scheme: Scheme::SearchItem }
}
Scheme::Archive(_) => self.with(self.loc.join(path)), Scheme::Archive(_) => self.with(self.loc.join(path)),
Scheme::Sftp(_) => self.with(self.loc.join(path)), Scheme::Sftp(_) => self.with(self.loc.join(path)),
} }
@ -127,17 +128,14 @@ impl Url {
pub fn parent_url(&self) -> Option<Url> { pub fn parent_url(&self) -> Option<Url> {
let parent = self.loc.parent()?; let parent = self.loc.parent()?;
let base = self.loc.base(); let urn = self.loc.urn();
Some(match &self.scheme { Some(match &self.scheme {
Scheme::Regular => Self { loc: parent.into(), scheme: Scheme::Regular }, Scheme::Regular => Self { loc: parent.into(), scheme: Scheme::Regular },
Scheme::Search(_) => Self { loc: parent.into(), scheme: Scheme::Regular }, Scheme::Search(_) if urn == Urn::new("") => {
Scheme::SearchItem if parent == base => { Self { loc: parent.into(), scheme: Scheme::Regular }
Self { loc: parent.into(), scheme: Scheme::Search(String::new()) }
}
Scheme::SearchItem => {
Self { loc: Loc::with(base, parent.to_owned()), scheme: Scheme::SearchItem }
} }
Scheme::Search(_) => self.with(parent),
Scheme::Archive(_) => self.with(parent), Scheme::Archive(_) => self.with(parent),
Scheme::Sftp(_) => self.with(parent), Scheme::Sftp(_) => self.with(parent),
}) })
@ -172,17 +170,22 @@ impl Url {
self.loc.rebase(parent).into() self.loc.rebase(parent).into()
} }
pub fn parse(bytes: &[u8]) -> Result<(Scheme, Cow<'_, Path>)> { pub fn parse(bytes: &[u8]) -> Result<(Scheme, PathBuf, Option<usize>)> {
let (scheme, skip, tilde) = Scheme::parse(bytes)?; let mut skip = 0;
let (scheme, tilde, port) = Scheme::parse(bytes, &mut skip)?;
let rest = &bytes[skip + tilde as usize..]; let rest = if tilde {
let rest = Cow::from(percent_decode(&bytes[skip..])).into_os_str()?
if tilde { Cow::from(percent_decode(rest)).into_os_str()? } else { rest.into_os_str()? }; } else {
bytes[skip..].into_os_str()?
};
Ok((scheme, match rest { let path = match rest {
Cow::Borrowed(s) => Path::new(s).into(), Cow::Borrowed(s) => Path::new(s).to_owned(),
Cow::Owned(s) => PathBuf::from(s).into(), Cow::Owned(s) => PathBuf::from(s),
})) };
Ok((scheme, path, port))
} }
} }
@ -192,10 +195,13 @@ impl Url {
pub fn is_regular(&self) -> bool { self.scheme == Scheme::Regular } pub fn is_regular(&self) -> bool { self.scheme == Scheme::Regular }
#[inline] #[inline]
pub fn to_regular(&self) -> Self { Self { loc: self.loc.clone(), scheme: Scheme::Regular } } pub fn to_regular(&self) -> Self {
Self { loc: self.loc.to_path().into(), scheme: Scheme::Regular }
}
#[inline] #[inline]
pub fn into_regular(mut self) -> Self { pub fn into_regular(mut self) -> Self {
self.loc = self.loc.into_path().into();
self.scheme = Scheme::Regular; self.scheme = Scheme::Regular;
self self
} }
@ -205,13 +211,17 @@ impl Url {
pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) } pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) }
#[inline] #[inline]
pub fn to_search(&self, frag: impl AsRef<str>) -> Self { pub fn to_search(&self, domain: impl AsRef<str>) -> Self {
Self { loc: self.loc.clone(), scheme: Scheme::Search(frag.as_ref().to_owned()) } Self {
loc: Loc::zeroed(self.loc.to_path()),
scheme: Scheme::Search(domain.as_ref().to_owned()),
}
} }
#[inline] #[inline]
pub fn into_search(mut self, frag: impl AsRef<str>) -> Self { pub fn into_search(mut self, domain: impl AsRef<str>) -> Self {
self.scheme = Scheme::Search(frag.as_ref().to_owned()); self.loc = Loc::zeroed(self.loc.into_path());
self.scheme = Scheme::Search(domain.as_ref().to_owned());
self self
} }
@ -226,7 +236,7 @@ impl Url {
impl Debug for Url { impl Debug for Url {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.scheme, self.loc.display()) write!(f, "{}{}", Encode::from(self), self.loc.display())
} }
} }
@ -235,8 +245,8 @@ impl Serialize for Url {
let Url { scheme, loc } = self; let Url { scheme, loc } = self;
match (scheme.is_virtual(), loc.to_str()) { match (scheme.is_virtual(), loc.to_str()) {
(false, Some(s)) => serializer.serialize_str(s), (false, Some(s)) => serializer.serialize_str(s),
(true, Some(s)) => serializer.serialize_str(&format!("{scheme}{s}")), (true, Some(s)) => serializer.serialize_str(&format!("{}{s}", Encode::from(self))),
(_, None) => serializer.serialize_str(&scheme.encode_tilded(loc)), (_, None) => serializer.collect_str(&EncodeTilded::from(self)),
} }
} }
} }
@ -250,3 +260,40 @@ impl<'de> Deserialize<'de> for Url {
Self::try_from(s).map_err(serde::de::Error::custom) Self::try_from(s).map_err(serde::de::Error::custom)
} }
} }
// --- Tests
#[cfg(test)]
mod tests {
use anyhow::Result;
use super::*;
#[test]
fn test_search() -> Result<()> {
const S: char = std::path::MAIN_SEPARATOR;
let u: Url = "/root/project".parse()?;
assert_eq!(format!("{u:?}"), "regular:///root/project");
let u = u.into_search("readme");
assert_eq!(format!("{u:?}"), "search://readme//root/project");
assert_eq!(format!("{:?}", u.parent_url().unwrap()), "regular:///root");
let u = u.join("examples");
assert_eq!(format!("{u:?}"), format!("search://readme:1//root/project{S}examples"));
let u = u.join("README.md");
assert_eq!(format!("{u:?}"), format!("search://readme:2//root/project{S}examples{S}README.md"));
let u = u.parent_url().unwrap();
assert_eq!(format!("{u:?}"), format!("search://readme:1//root/project{S}examples"));
let u = u.parent_url().unwrap();
assert_eq!(format!("{u:?}"), "search://readme//root/project");
let u = u.parent_url().unwrap();
assert_eq!(format!("{u:?}"), "regular:///root");
Ok(())
}
}