mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: virtual file system (#3034)
This commit is contained in:
parent
da97e5a8b4
commit
d5f225bc4a
74 changed files with 908 additions and 447 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3556,6 +3556,7 @@ dependencies = [
|
|||
"clap_complete",
|
||||
"clap_complete_fig",
|
||||
"clap_complete_nushell",
|
||||
"futures",
|
||||
"regex",
|
||||
"serde",
|
||||
"vergen-gitcl",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR};
|
||||
|
||||
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_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
|
||||
use yazi_proxy::CmpProxy;
|
||||
|
|
@ -36,8 +36,7 @@ impl Actor for Trigger {
|
|||
|
||||
let ticket = cmp.ticket;
|
||||
tokio::spawn(async move {
|
||||
// TODO: support VFS
|
||||
let mut dir = Local::read_dir(&parent).await?;
|
||||
let mut dir = provider::read_dir(&parent).await?;
|
||||
let mut cache = vec![];
|
||||
|
||||
// "/" is both a directory separator and the root directory per se
|
||||
|
|
@ -68,7 +67,7 @@ impl Actor for Trigger {
|
|||
|
||||
impl Trigger {
|
||||
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() == "~" {
|
||||
return None; // We don't autocomplete a `~`, but `~/`
|
||||
|
|
@ -96,7 +95,7 @@ mod tests {
|
|||
fn compare(s: &str, parent: &str, child: &str) {
|
||||
let (p, c) = Trigger::split_url(s).unwrap();
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ impl UserData for File {
|
|||
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<_>>())
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 crossterm::{execute, style::Print};
|
||||
use scopeguard::defer;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use yazi_config::YAZI;
|
||||
use yazi_config::{YAZI, opener::OpenerRule};
|
||||
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_parser::VoidOpt;
|
||||
use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER};
|
||||
|
|
@ -23,7 +23,7 @@ impl Actor for BulkRename {
|
|||
const NAME: &str = "bulk_rename";
|
||||
|
||||
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"));
|
||||
};
|
||||
|
||||
|
|
@ -39,8 +39,7 @@ impl Actor for BulkRename {
|
|||
let cwd = cx.cwd().clone();
|
||||
tokio::spawn(async move {
|
||||
let tmp = YAZI.preview.tmpfile("bulk");
|
||||
// TODO: pull `OpenOptions` into `yazi_fs`
|
||||
tokio::fs::OpenOptions::new()
|
||||
Gate::default()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&tmp)
|
||||
|
|
@ -116,7 +115,7 @@ impl BulkRename {
|
|||
|
||||
if maybe_exists(&new).await && !paths_to_same_file(&old, &new).await {
|
||||
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()));
|
||||
} else if let Ok(f) = File::new(new).await {
|
||||
succeeded.insert(old, f);
|
||||
|
|
@ -138,6 +137,10 @@ impl BulkRename {
|
|||
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<()> {
|
||||
let mut stdout = TTY.lockout();
|
||||
terminal_clear(&mut *stdout)?;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use tokio::pin;
|
|||
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
||||
use yazi_config::popup::InputCfg;
|
||||
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_parser::mgr::CdOpt;
|
||||
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy};
|
||||
|
|
@ -76,9 +76,9 @@ impl Cd {
|
|||
while let Some(result) = rx.next().await {
|
||||
match result {
|
||||
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() {
|
||||
return MgrProxy::cd(&url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use anyhow::Result;
|
||||
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_parser::mgr::CreateOpt;
|
||||
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
|
||||
|
|
@ -45,15 +45,15 @@ impl Create {
|
|||
let _permit = WATCHER.acquire().await.unwrap();
|
||||
|
||||
if dir {
|
||||
services::create_dir_all(&new).await?;
|
||||
provider::create_dir_all(&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();
|
||||
services::create(&new).await?;
|
||||
provider::create(&new).await?;
|
||||
} else {
|
||||
services::create_dir_all(&parent).await.ok();
|
||||
ok_or_not_found(services::remove_file(&new).await)?;
|
||||
services::create(&new).await?;
|
||||
provider::create_dir_all(&parent).await.ok();
|
||||
ok_or_not_found(provider::remove_file(&new).await)?;
|
||||
provider::create(&new).await?;
|
||||
}
|
||||
|
||||
if let Ok(f) = File::new(new.clone()).await {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ impl Quit {
|
|||
}
|
||||
|
||||
let paths = selected.fold(OsString::new(), |mut s, u| {
|
||||
s.push(u.as_os_str());
|
||||
s.push(u.os_str());
|
||||
s.push("\n");
|
||||
s
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use yazi_config::popup::{ConfirmCfg, InputCfg};
|
||||
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_parser::mgr::RenameOpt;
|
||||
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
|
||||
|
|
@ -65,10 +65,10 @@ impl Rename {
|
|||
let _permit = WATCHER.acquire().await.unwrap();
|
||||
|
||||
let overwritten = realname(&new).await;
|
||||
services::rename(&old, &new).await?;
|
||||
provider::rename(&old, &new).await?;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
use std::io::BufReader;
|
||||
|
||||
use anyhow::Result;
|
||||
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 yazi_config::YAZI;
|
||||
use yazi_fs::services;
|
||||
use yazi_fs::provider;
|
||||
use yazi_shared::url::Url;
|
||||
|
||||
use crate::Dimension;
|
||||
|
|
@ -40,7 +38,7 @@ impl Image {
|
|||
})
|
||||
.await??;
|
||||
|
||||
Ok(services::write(cache, buf).await?)
|
||||
Ok(provider::write(cache, buf).await?)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
reader.set_format(format);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use std::{borrow::Cow, ops::Deref};
|
||||
|
||||
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>;
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ pub struct Url {
|
|||
v_urn: Option<Value>,
|
||||
v_base: Option<Value>,
|
||||
v_parent: Option<Value>,
|
||||
v_frag: Option<Value>,
|
||||
v_domain: Option<Value>,
|
||||
}
|
||||
|
||||
impl Deref for Url {
|
||||
|
|
@ -64,7 +64,7 @@ impl Url {
|
|||
v_urn: None,
|
||||
v_base: None,
|
||||
v_parent: None,
|
||||
v_frag: None,
|
||||
v_domain: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,10 +94,7 @@ impl FromLua for Url {
|
|||
impl UserData for Url {
|
||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||
cached_field!(fields, name, |lua, me| {
|
||||
Some(me.name())
|
||||
.filter(|&s| !s.is_empty())
|
||||
.map(|s| lua.create_string(s.as_encoded_bytes()))
|
||||
.transpose()
|
||||
me.file_name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
|
||||
});
|
||||
cached_field!(fields, stem, |lua, me| {
|
||||
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| {
|
||||
Ok(if me.base().as_os_str().is_empty() { None } else { Some(Self::new(me.base())) })
|
||||
});
|
||||
// TODO: remove
|
||||
cached_field!(fields, frag, |lua, me| {
|
||||
if let Scheme::Search(kw) = &me.scheme {
|
||||
Some(lua.create_string(kw)).transpose()
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
cached_field!(fields, domain, |lua, me| {
|
||||
me.scheme.domain().map(|s| lua.create_string(s)).transpose()
|
||||
});
|
||||
|
||||
fields.add_field_method_get("frag", |lua, me| {
|
||||
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()));
|
||||
|
|
@ -157,8 +154,8 @@ impl UserData for Url {
|
|||
Ok(url.map(Self::new))
|
||||
});
|
||||
|
||||
methods.add_function_mut("into_search", |_, (ud, frag): (AnyUserData, mlua::String)| {
|
||||
Ok(Self::new(ud.take::<Self>()?.inner.into_search(frag.to_str()?)))
|
||||
methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| {
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@ yazi-macro = { path = "../yazi-macro", version = "25.6.11" }
|
|||
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
|
||||
|
||||
# External dependencies
|
||||
clap = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use std::{env, ffi::OsStr, fmt::Write};
|
||||
use std::{env, ffi::OsStr, fmt::Write, path::Path};
|
||||
|
||||
use regex::Regex;
|
||||
use yazi_adapter::Mux;
|
||||
use yazi_shared::timestamp_us;
|
||||
use yazi_config::YAZI;
|
||||
use yazi_shared::{timestamp_us, url::Url};
|
||||
|
||||
use super::Actions;
|
||||
|
||||
|
|
@ -57,17 +58,17 @@ impl Actions {
|
|||
writeln!(
|
||||
s,
|
||||
" 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!(
|
||||
s,
|
||||
" 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!(
|
||||
s,
|
||||
" 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")?;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, command};
|
||||
use yazi_shared::Id;
|
||||
use yazi_shared::{Id, url::Url};
|
||||
|
||||
#[derive(Debug, Default, Parser)]
|
||||
#[command(name = "yazi")]
|
||||
pub struct Args {
|
||||
/// Set the current working entry
|
||||
#[arg(index = 1, num_args = 1..=9)]
|
||||
pub entries: Vec<PathBuf>,
|
||||
pub entries: Vec<Url>,
|
||||
|
||||
/// Write the cwd on exit to this file
|
||||
#[arg(long)]
|
||||
|
|
|
|||
|
|
@ -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 yazi_fs::{CWD, Xdg, expand_path};
|
||||
use yazi_fs::{CWD, Xdg, expand_url, provider};
|
||||
use yazi_shared::url::{Url, UrnBuf};
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Boot {
|
||||
pub cwds: Vec<PathBuf>,
|
||||
pub files: Vec<OsString>,
|
||||
pub cwds: Vec<Url>,
|
||||
pub files: Vec<UrnBuf>,
|
||||
|
||||
pub local_events: HashSet<String>,
|
||||
pub remote_events: HashSet<String>,
|
||||
|
|
@ -18,31 +20,31 @@ pub struct 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() {
|
||||
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());
|
||||
let mut files = Vec::with_capacity(entries.len());
|
||||
for entry in entries.iter().map(expand_path) {
|
||||
if let Some(p) = entry.parent().filter(|_| !entry.is_dir()) {
|
||||
cwds.push(p.to_owned());
|
||||
files.push(entry.file_name().unwrap().to_owned());
|
||||
async fn go<'a>(entry: Cow<'a, Url>) -> (Url, UrnBuf) {
|
||||
let Some((parent, child)) = entry.pair() else {
|
||||
return (entry.into_owned(), UrnBuf::default());
|
||||
};
|
||||
|
||||
if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) {
|
||||
(parent, child)
|
||||
} else {
|
||||
cwds.push(entry);
|
||||
files.push(OsString::new());
|
||||
(entry.into_owned(), UrnBuf::default())
|
||||
}
|
||||
}
|
||||
|
||||
(cwds, files)
|
||||
futures::future::join_all(entries.iter().map(expand_url).map(go)).await.into_iter().unzip()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&crate::Args> for Boot {
|
||||
fn from(args: &crate::Args) -> Self {
|
||||
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
|
||||
.local_events
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 super::Dependency;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
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 super::Dependency;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::{path::PathBuf, str::FromStr};
|
|||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use yazi_fs::{Xdg, services::Local};
|
||||
use yazi_fs::{Xdg, provider::local::Local};
|
||||
use yazi_macro::outln;
|
||||
|
||||
use super::Dependency;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{io, path::Path};
|
||||
|
||||
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]
|
||||
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<()> {
|
||||
let b = Local::read(from).await?;
|
||||
ok_or_not_found(remove_sealed(to).await)?;
|
||||
|
||||
let mut file =
|
||||
tokio::fs::OpenOptions::new().create_new(true).write(true).truncate(true).open(to).await?;
|
||||
let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?;
|
||||
file.write_all(&b).await?;
|
||||
|
||||
let mut perm = file.metadata().await?.permissions();
|
||||
|
|
|
|||
|
|
@ -239,16 +239,16 @@ rules = [
|
|||
# { mime = "inode/empty", fg = "red" },
|
||||
|
||||
# Special files
|
||||
{ name = "*", is = "orphan", bg = "red" },
|
||||
{ name = "*", is = "exec" , fg = "green" },
|
||||
{ url = "*", is = "orphan", bg = "red" },
|
||||
{ url = "*", is = "exec" , fg = "green" },
|
||||
|
||||
# Dummy files
|
||||
{ name = "*", is = "dummy", bg = "red" },
|
||||
{ name = "*/", is = "dummy", bg = "red" },
|
||||
{ url = "*", is = "dummy", bg = "red" },
|
||||
{ url = "*/", is = "dummy", bg = "red" },
|
||||
|
||||
# Fallback
|
||||
# { name = "*", fg = "white" },
|
||||
{ name = "*/", fg = "blue" }
|
||||
# { url = "*", fg = "white" },
|
||||
{ url = "*/", fg = "blue" }
|
||||
]
|
||||
|
||||
# : }}}
|
||||
|
|
|
|||
|
|
@ -239,16 +239,16 @@ rules = [
|
|||
# { mime = "inode/empty", fg = "red" },
|
||||
|
||||
# Special files
|
||||
{ name = "*", is = "orphan", bg = "red" },
|
||||
{ name = "*", is = "exec" , fg = "green" },
|
||||
{ url = "*", is = "orphan", bg = "red" },
|
||||
{ url = "*", is = "exec" , fg = "green" },
|
||||
|
||||
# Dummy files
|
||||
{ name = "*", is = "dummy", bg = "red" },
|
||||
{ name = "*/", is = "dummy", bg = "red" },
|
||||
{ url = "*", is = "dummy", bg = "red" },
|
||||
{ url = "*/", is = "dummy", bg = "red" },
|
||||
|
||||
# Fallback
|
||||
# { name = "*", fg = "white" },
|
||||
{ name = "*/", fg = "blue" }
|
||||
# { url = "*", fg = "white" },
|
||||
{ url = "*/", fg = "blue" }
|
||||
]
|
||||
|
||||
# : }}}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ play = [
|
|||
[open]
|
||||
rules = [
|
||||
# Folder
|
||||
{ name = "*/", use = [ "edit", "open", "reveal" ] },
|
||||
{ url = "*/", use = [ "edit", "open", "reveal" ] },
|
||||
# Text
|
||||
{ mime = "text/*", use = [ "edit", "reveal" ] },
|
||||
# Image
|
||||
|
|
@ -78,7 +78,7 @@ rules = [
|
|||
# Empty file
|
||||
{ mime = "inode/empty", use = [ "edit", "reveal" ] },
|
||||
# Fallback
|
||||
{ name = "*", use = [ "open", "reveal" ] },
|
||||
{ url = "*", use = [ "open", "reveal" ] },
|
||||
]
|
||||
|
||||
[tasks]
|
||||
|
|
@ -92,10 +92,10 @@ suppress_preload = false
|
|||
[plugin]
|
||||
fetchers = [
|
||||
# Mimetype
|
||||
{ id = "mime", name = "*", run = "mime", prio = "high" },
|
||||
{ id = "mime", url = "*", run = "mime", prio = "high" },
|
||||
]
|
||||
spotters = [
|
||||
{ name = "*/", run = "folder" },
|
||||
{ url = "*/", run = "folder" },
|
||||
# Code
|
||||
{ mime = "text/*", run = "code" },
|
||||
{ mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" },
|
||||
|
|
@ -106,7 +106,7 @@ spotters = [
|
|||
# Video
|
||||
{ mime = "video/*", run = "video" },
|
||||
# Fallback
|
||||
{ name = "*", run = "file" },
|
||||
{ url = "*", run = "file" },
|
||||
]
|
||||
preloaders = [
|
||||
# Image
|
||||
|
|
@ -122,7 +122,7 @@ preloaders = [
|
|||
{ mime = "application/ms-opentype", run = "font" },
|
||||
]
|
||||
previewers = [
|
||||
{ name = "*/", run = "folder" },
|
||||
{ url = "*/", run = "folder" },
|
||||
# Code
|
||||
{ mime = "text/*", run = "code" },
|
||||
{ mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" },
|
||||
|
|
@ -139,18 +139,18 @@ previewers = [
|
|||
# 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" },
|
||||
{ name = "*.{AppImage,appimage}", run = "archive" },
|
||||
{ url = "*.{AppImage,appimage}", run = "archive" },
|
||||
# Virtual Disk / Disk Image
|
||||
{ mime = "application/{iso9660-image,qemu-disk,ms-wim,apple-diskimage}", 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
|
||||
{ mime = "font/*", run = "font" },
|
||||
{ mime = "application/ms-opentype", run = "font" },
|
||||
# Empty file
|
||||
{ mime = "inode/empty", run = "empty" },
|
||||
# Fallback
|
||||
{ name = "*", run = "file" },
|
||||
{ url = "*", run = "file" },
|
||||
]
|
||||
|
||||
[input]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use std::{ops::Deref, path::Path};
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::Result;
|
||||
use indexmap::IndexSet;
|
||||
use serde::Deserialize;
|
||||
use yazi_codegen::DeserializeOver2;
|
||||
use yazi_shared::MIME_DIR;
|
||||
use yazi_shared::{MIME_DIR, url::Url};
|
||||
|
||||
use crate::{Preset, open::OpenRule};
|
||||
|
||||
|
|
@ -24,10 +24,10 @@ impl Deref for 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
|
||||
'a: 'b,
|
||||
P: AsRef<Path> + 'b,
|
||||
P: AsRef<Url> + 'b,
|
||||
M: AsRef<str> + 'b,
|
||||
{
|
||||
let is_dir = mime.as_ref() == MIME_DIR;
|
||||
|
|
@ -36,19 +36,16 @@ impl Open {
|
|||
.iter()
|
||||
.filter(move |&r| {
|
||||
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)
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn common<'a>(
|
||||
&'a self,
|
||||
targets: &[(impl AsRef<Path>, impl AsRef<str>)],
|
||||
) -> IndexSet<&'a str> {
|
||||
pub fn common<'a>(&'a self, targets: &[(impl AsRef<Url>, impl AsRef<str>)]) -> IndexSet<&'a str> {
|
||||
let each: Vec<IndexSet<&str>> = targets
|
||||
.iter()
|
||||
.map(|(p, m)| self.all(p, m).collect::<IndexSet<_>>())
|
||||
.map(|(u, m)| self.all(u, m).collect::<IndexSet<_>>())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::pattern::Pattern;
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OpenRule {
|
||||
pub name: Option<Pattern>,
|
||||
pub url: Option<Pattern>,
|
||||
pub mime: Option<Pattern>,
|
||||
#[serde(deserialize_with = "OpenRule::deserialize")]
|
||||
pub r#use: Vec<String>,
|
||||
|
|
@ -14,10 +14,10 @@ pub struct OpenRule {
|
|||
|
||||
impl OpenRule {
|
||||
#[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]
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
use std::{path::Path, str::FromStr};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use globset::GlobBuilder;
|
||||
use serde::Deserialize;
|
||||
use yazi_shared::url::{Scheme, Url};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(try_from = "String")]
|
||||
pub struct Pattern {
|
||||
inner: globset::GlobMatcher,
|
||||
scheme: PatternScheme,
|
||||
is_dir: bool,
|
||||
is_star: bool,
|
||||
#[cfg(windows)]
|
||||
|
|
@ -15,28 +18,35 @@ pub struct Pattern {
|
|||
|
||||
impl Pattern {
|
||||
#[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()))
|
||||
}
|
||||
pub fn match_url(&self, url: impl AsRef<Url>, is_dir: bool) -> bool {
|
||||
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 {
|
||||
return false;
|
||||
} else if self.is_star {
|
||||
return true;
|
||||
} else if !self.scheme.matches(&url.scheme) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
let path = &url.loc;
|
||||
|
||||
#[cfg(windows)]
|
||||
let path = if self.sep_lit {
|
||||
yazi_fs::backslash_to_slash(path.as_ref())
|
||||
yazi_fs::backslash_to_slash(&url.loc)
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(path.as_ref())
|
||||
std::borrow::Cow::Borrowed(url.loc.as_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]
|
||||
pub fn any_file(&self) -> bool { self.is_star && !self.is_dir }
|
||||
|
||||
|
|
@ -45,14 +55,23 @@ impl Pattern {
|
|||
}
|
||||
|
||||
impl FromStr for Pattern {
|
||||
type Err = globset::Error;
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let a = s.trim_start_matches("\\s");
|
||||
let b = a.trim_end_matches('/');
|
||||
let sep_lit = b.contains('/');
|
||||
// Trim leading case-sensitive indicator
|
||||
let a = s.trim_start_matches(r"\s");
|
||||
|
||||
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())
|
||||
.literal_separator(sep_lit)
|
||||
.backslash_escape(false)
|
||||
|
|
@ -62,8 +81,9 @@ impl FromStr for Pattern {
|
|||
|
||||
Ok(Self {
|
||||
inner,
|
||||
is_dir: b.len() < a.len(),
|
||||
is_star: b == "*",
|
||||
scheme,
|
||||
is_dir: c.len() < b.len(),
|
||||
is_star: c == "*",
|
||||
#[cfg(windows)]
|
||||
sep_lit,
|
||||
})
|
||||
|
|
@ -71,17 +91,40 @@ impl FromStr 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()) }
|
||||
}
|
||||
|
||||
// --- 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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn matches(glob: &str, path: &str) -> bool {
|
||||
Pattern::from_str(glob).unwrap().match_path(path, false)
|
||||
fn matches(glob: &str, url: &str) -> bool {
|
||||
Pattern::from_str(glob).unwrap().match_url(Url::from_str(url).unwrap(), false)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
use yazi_shared::{MIME_DIR, event::Cmd};
|
||||
use yazi_shared::{MIME_DIR, event::Cmd, url::Url};
|
||||
|
||||
use crate::{Pattern, Priority};
|
||||
|
||||
|
|
@ -11,7 +9,7 @@ pub struct Fetcher {
|
|||
pub idx: u8,
|
||||
|
||||
pub id: String,
|
||||
pub name: Option<Pattern>,
|
||||
pub url: Option<Pattern>,
|
||||
pub mime: Option<Pattern>,
|
||||
pub run: Cmd,
|
||||
#[serde(default)]
|
||||
|
|
@ -20,8 +18,8 @@ pub struct Fetcher {
|
|||
|
||||
impl Fetcher {
|
||||
#[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.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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use std::{collections::HashSet, path::Path};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
use yazi_codegen::DeserializeOver2;
|
||||
use yazi_fs::File;
|
||||
use yazi_shared::url::Url;
|
||||
|
||||
use super::{Fetcher, Preloader, Previewer, Spotter};
|
||||
use crate::{Preset, plugin::MAX_PREWORKERS};
|
||||
|
|
@ -39,12 +40,12 @@ pub struct Plugin {
|
|||
impl Plugin {
|
||||
pub fn fetchers<'a, 'b: 'a>(
|
||||
&'b self,
|
||||
path: &'a Path,
|
||||
url: &'a Url,
|
||||
mime: &'a str,
|
||||
) -> impl Iterator<Item = &'b Fetcher> + 'a {
|
||||
let mut seen = HashSet::new();
|
||||
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;
|
||||
}
|
||||
seen.insert(&f.id);
|
||||
|
|
@ -68,18 +69,18 @@ impl Plugin {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn spotter(&self, path: &Path, mime: &str) -> Option<&Spotter> {
|
||||
self.spotters.iter().find(|&p| p.matches(path, mime))
|
||||
pub fn spotter(&self, url: &Url, mime: &str) -> Option<&Spotter> {
|
||||
self.spotters.iter().find(|&p| p.matches(url, mime))
|
||||
}
|
||||
|
||||
pub fn preloaders<'a, 'b: 'a>(
|
||||
&'b self,
|
||||
path: &'a Path,
|
||||
url: &'a Url,
|
||||
mime: &'a str,
|
||||
) -> impl Iterator<Item = &'b Preloader> + 'a {
|
||||
let mut next = true;
|
||||
self.preloaders.iter().filter(move |&p| {
|
||||
if !next || !p.matches(path, mime) {
|
||||
if !next || !p.matches(url, mime) {
|
||||
return false;
|
||||
}
|
||||
next = p.next;
|
||||
|
|
@ -87,8 +88,8 @@ impl Plugin {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn previewer(&self, path: &Path, mime: &str) -> Option<&Previewer> {
|
||||
self.previewers.iter().find(|&p| p.matches(path, mime))
|
||||
pub fn previewer(&self, url: &Url, mime: &str) -> Option<&Previewer> {
|
||||
self.previewers.iter().find(|&p| p.matches(url, mime))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
use yazi_shared::{MIME_DIR, event::Cmd};
|
||||
use yazi_shared::{MIME_DIR, event::Cmd, url::Url};
|
||||
|
||||
use crate::{Pattern, Priority};
|
||||
|
||||
|
|
@ -10,7 +8,7 @@ pub struct Preloader {
|
|||
#[serde(skip)]
|
||||
pub idx: u8,
|
||||
|
||||
pub name: Option<Pattern>,
|
||||
pub url: Option<Pattern>,
|
||||
pub mime: Option<Pattern>,
|
||||
pub run: Cmd,
|
||||
#[serde(default)]
|
||||
|
|
@ -21,8 +19,8 @@ pub struct Preloader {
|
|||
|
||||
impl Preloader {
|
||||
#[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.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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,25 @@
|
|||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
use yazi_shared::{MIME_DIR, event::Cmd};
|
||||
use yazi_shared::{MIME_DIR, event::Cmd, url::Url};
|
||||
|
||||
use crate::Pattern;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Previewer {
|
||||
pub name: Option<Pattern>,
|
||||
pub url: Option<Pattern>,
|
||||
pub mime: Option<Pattern>,
|
||||
pub run: Cmd,
|
||||
}
|
||||
|
||||
impl Previewer {
|
||||
#[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.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]
|
||||
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]
|
||||
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()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,25 @@
|
|||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
use yazi_shared::{MIME_DIR, event::Cmd};
|
||||
use yazi_shared::{MIME_DIR, event::Cmd, url::Url};
|
||||
|
||||
use crate::Pattern;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Spotter {
|
||||
pub name: Option<Pattern>,
|
||||
pub url: Option<Pattern>,
|
||||
pub mime: Option<Pattern>,
|
||||
pub run: Cmd,
|
||||
}
|
||||
|
||||
impl Spotter {
|
||||
#[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.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]
|
||||
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]
|
||||
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()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ impl Deref for Filetype {
|
|||
pub struct FiletypeRule {
|
||||
#[serde(default)]
|
||||
is: Is,
|
||||
name: Option<Pattern>,
|
||||
url: Option<Pattern>,
|
||||
mime: Option<Pattern>,
|
||||
#[serde(flatten)]
|
||||
pub style: Style,
|
||||
|
|
@ -35,6 +35,6 @@ impl FiletypeRule {
|
|||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ impl Icon {
|
|||
|
||||
#[inline]
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use parking_lot::RwLock;
|
|||
use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}};
|
||||
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
||||
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_shared::{RoCell, url::Url};
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ impl Watcher {
|
|||
};
|
||||
|
||||
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);
|
||||
|
||||
if !eq {
|
||||
|
|
@ -194,7 +194,7 @@ impl Watcher {
|
|||
|
||||
async fn go(todo: HashSet<Url>) {
|
||||
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) {
|
||||
LINKED.write().insert(from, to);
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ impl Selected {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn url(s: &str) -> Url { Url::try_from(s).unwrap() }
|
||||
fn url(s: &str) -> Url { s.parse().unwrap() }
|
||||
|
||||
#[test]
|
||||
fn test_insert_non_conflicting() {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use std::{collections::HashMap, mem, ops::Deref, sync::atomic::{AtomicU64, Order
|
|||
|
||||
use anyhow::Result;
|
||||
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_fs::services::Local;
|
||||
use yazi_fs::provider::local::{Gate, Local};
|
||||
use yazi_shared::{RoCell, timestamp_us};
|
||||
|
||||
use crate::CLIENTS;
|
||||
|
|
@ -59,8 +59,7 @@ impl State {
|
|||
|
||||
Local::create_dir_all(&BOOT.state_dir).await?;
|
||||
let mut buf = BufWriter::new(
|
||||
// TODO: VFS
|
||||
OpenOptions::new()
|
||||
Gate::default()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
|
|
@ -79,7 +78,7 @@ impl State {
|
|||
}
|
||||
|
||||
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 inner = HashMap::new();
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ impl Stream {
|
|||
pub(super) async fn bind() -> std::io::Result<ServerListener> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
// C:\Users\Ika\AppData\Local\Temp\cargo-installTFU8cj\release\build\
|
||||
// yazi-fm-45dffef2500eecd0\out
|
||||
|
||||
if dir.contains("\\release\\build\\yazi-fm-") {
|
||||
if dir.contains(r"\release\build\yazi-fm-") {
|
||||
panic!(
|
||||
"Unwinding must be enabled for Windows. Please use `cargo build --profile release-windows --locked` instead to build Yazi."
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use yazi_actor::Ctx;
|
|||
use yazi_boot::BOOT;
|
||||
use yazi_macro::act;
|
||||
use yazi_parser::{VoidOpt, mgr::CdSource};
|
||||
use yazi_shared::{event::Data, url::Url};
|
||||
use yazi_shared::event::Data;
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
|
|
@ -18,10 +18,10 @@ impl App {
|
|||
let cx = &mut Ctx::active(&mut self.core);
|
||||
cx.tab = i;
|
||||
|
||||
if file.is_empty() {
|
||||
act!(mgr:cd, cx, (Url::from(&BOOT.cwds[i]), CdSource::Tab))?;
|
||||
if file.as_os_str().is_empty() {
|
||||
act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?;
|
||||
} 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))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::ffi::OsString;
|
||||
|
||||
use yazi_boot::ARGS;
|
||||
use yazi_fs::services::Local;
|
||||
use yazi_fs::provider::local::Local;
|
||||
use yazi_shared::event::EventQuit;
|
||||
|
||||
use crate::{Term, app::App};
|
||||
|
|
@ -25,7 +25,7 @@ impl App {
|
|||
|
||||
async fn cwd_to_file(&self, no: bool) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 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 {
|
||||
Idle((VecDeque<Task>, Option<u64>)),
|
||||
|
|
@ -16,12 +16,12 @@ impl SizeCalculator {
|
|||
pub async fn new(url: &Url) -> io::Result<Self> {
|
||||
let u = url.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let meta = services::symlink_metadata_sync(&u)?;
|
||||
let meta = provider::symlink_metadata_sync(&u)?;
|
||||
if !meta.is_dir() {
|
||||
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);
|
||||
Ok(Self::Idle((buf, size)))
|
||||
})
|
||||
|
|
@ -63,7 +63,7 @@ impl SizeCalculator {
|
|||
.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());
|
||||
macro_rules! pop_and_continue {
|
||||
() => {{
|
||||
|
|
@ -80,7 +80,7 @@ impl SizeCalculator {
|
|||
let front = buf.front_mut()?;
|
||||
|
||||
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),
|
||||
Err(_) => pop_and_continue!(),
|
||||
};
|
||||
|
|
@ -93,7 +93,7 @@ impl SizeCalculator {
|
|||
let Ok(ent) = next else { continue };
|
||||
let Ok(ft) = ent.file_type() else { continue };
|
||||
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() {
|
||||
size += meta.len();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use yazi_macro::{unix_either, win_either};
|
|||
use yazi_shared::url::Url;
|
||||
|
||||
use super::ChaKind;
|
||||
use crate::services;
|
||||
use crate::provider;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Cha {
|
||||
|
|
@ -59,14 +59,14 @@ impl Cha {
|
|||
|
||||
#[inline]
|
||||
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 {
|
||||
let mut attached = ChaKind::hidden(url, &meta);
|
||||
if meta.is_symlink() {
|
||||
attached |= ChaKind::LINK;
|
||||
meta = services::metadata(url).await.unwrap_or(meta);
|
||||
meta = provider::metadata(url).await.unwrap_or(meta);
|
||||
}
|
||||
if meta.is_symlink() {
|
||||
attached |= ChaKind::ORPHAN;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher
|
|||
use anyhow::Result;
|
||||
use yazi_shared::url::{Url, Urn, UrnBuf};
|
||||
|
||||
use crate::{cha::Cha, services};
|
||||
use crate::{cha::Cha, provider};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct File {
|
||||
|
|
@ -22,13 +22,13 @@ impl Deref for File {
|
|||
impl File {
|
||||
#[inline]
|
||||
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)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use tokio::{select, sync::mpsc::{self, UnboundedReceiver}};
|
|||
use yazi_shared::{Id, url::{Url, Urn, UrnBuf}};
|
||||
|
||||
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)]
|
||||
pub struct Files {
|
||||
|
|
@ -31,7 +31,7 @@ impl Files {
|
|||
pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } }
|
||||
|
||||
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();
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
|
@ -39,7 +39,7 @@ impl Files {
|
|||
select! {
|
||||
_ = tx.closed() => break,
|
||||
result = item.metadata() => {
|
||||
let url = Url::from(item.path());
|
||||
let url = item.url();
|
||||
_ = tx.send(match result {
|
||||
Ok(meta) => File::from_follow(url, meta).await,
|
||||
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>> {
|
||||
let mut it = services::read_dir(dir).await?;
|
||||
let mut it = provider::read_dir(dir).await?;
|
||||
let mut entries = Vec::with_capacity(5000);
|
||||
while let Ok(Some(entry)) = it.next_entry().await {
|
||||
entries.push(entry);
|
||||
|
|
@ -60,10 +60,10 @@ impl Files {
|
|||
|
||||
let (first, rest) = entries.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());
|
||||
for entry in entries {
|
||||
let url = Url::from(entry.path());
|
||||
let url = entry.url();
|
||||
files.push(match entry.metadata().await {
|
||||
Ok(meta) => File::from_follow(url, meta).await,
|
||||
Err(_) => File::from_dummy(url, entry.file_type().await.ok()),
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ use anyhow::{Result, bail};
|
|||
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
|
||||
use yazi_shared::url::{Component, Url};
|
||||
|
||||
use crate::{cha::Cha, services};
|
||||
use crate::{cha::Cha, provider};
|
||||
|
||||
#[inline]
|
||||
pub async fn maybe_exists(u: impl AsRef<Url>) -> bool {
|
||||
match services::symlink_metadata(u).await {
|
||||
match provider::symlink_metadata(u).await {
|
||||
Ok(_) => true,
|
||||
Err(e) => e.kind() != io::ErrorKind::NotFound,
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ pub async fn maybe_exists(u: impl AsRef<Url>) -> bool {
|
|||
|
||||
#[inline]
|
||||
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]
|
||||
|
|
@ -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> {
|
||||
let name = u.file_name()?;
|
||||
if *u == services::canonicalize(u).await.ok()? {
|
||||
if *u == provider::canonicalize(u).await.ok()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
@ -98,16 +98,16 @@ pub async fn realname(u: &Url) -> Option<OsString> {
|
|||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_realname_unchecked() {
|
||||
use crate::services::Local;
|
||||
async fn test_realname_unchecked() -> Result<()> {
|
||||
use crate::provider::local::Local;
|
||||
|
||||
Local::remove_dir_all("/tmp/issue-1173").await.ok();
|
||||
Local::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap();
|
||||
Local::create("/tmp/issue-1173/A").await.unwrap();
|
||||
Local::create("/tmp/issue-1173/b").await.unwrap();
|
||||
Local::create("/tmp/issue-1173/real-dir/C").await.unwrap();
|
||||
Local::symlink_file("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap();
|
||||
Local::symlink_dir("real-dir", "/tmp/issue-1173/link-dir").await.unwrap();
|
||||
Local::create_dir_all("/tmp/issue-1173/real-dir").await?;
|
||||
Local::create("/tmp/issue-1173/A").await?;
|
||||
Local::create("/tmp/issue-1173/b").await?;
|
||||
Local::create("/tmp/issue-1173/real-dir/C").await?;
|
||||
Local::symlink_file("/tmp/issue-1173/b", "/tmp/issue-1173/D").await?;
|
||||
Local::symlink_dir("real-dir", "/tmp/issue-1173/link-dir").await?;
|
||||
|
||||
let c = &mut HashMap::new();
|
||||
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;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// realpath(3) without resolving symlinks. This is useful for case-insensitive
|
||||
|
|
@ -199,7 +200,7 @@ pub fn copy_with_progress(
|
|||
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 {
|
||||
tx.send(Ok(len - last)).await.ok();
|
||||
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) {
|
||||
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 {
|
||||
if entry.file_type().await.is_ok_and(|t| t.is_dir()) {
|
||||
let path = entry.path().into();
|
||||
Box::pin(remove_dir_clean(&path)).await;
|
||||
services::remove_dir(path).await.ok();
|
||||
let url = entry.url();
|
||||
Box::pin(remove_dir_clean(&url)).await;
|
||||
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
|
||||
|
|
@ -369,7 +370,8 @@ pub fn max_common_root(urls: &[Url]) -> usize {
|
|||
#[test]
|
||||
fn test_max_common_root() {
|
||||
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();
|
||||
for _ in 0..comp.clone().count() - max_common_root(&urls) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#![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);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{P
|
|||
use anyhow::{Result, bail};
|
||||
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> {
|
||||
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() {
|
||||
url
|
||||
} 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
|
||||
F: Future<Output = bool>,
|
||||
{
|
||||
match services::symlink_metadata(&u).await {
|
||||
match provider::symlink_metadata(&u).await {
|
||||
Ok(_) => _unique_name(u, append.await).await,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(u),
|
||||
Err(e) => Err(e),
|
||||
|
|
@ -142,7 +142,7 @@ async fn _unique_name(mut url: Url, append: bool) -> io::Result<Url> {
|
|||
}
|
||||
|
||||
url.set_name(&name);
|
||||
match services::symlink_metadata(&url).await {
|
||||
match provider::symlink_metadata(&url).await {
|
||||
Ok(_) => i += 1,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => break,
|
||||
Err(e) => return Err(e),
|
||||
|
|
@ -214,16 +214,14 @@ pub fn backslash_to_slash(p: &Path) -> Cow<'_, Path> {
|
|||
mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use yazi_shared::url::Url;
|
||||
|
||||
use super::url_relative_to;
|
||||
|
||||
#[test]
|
||||
fn test_path_relative_to() {
|
||||
fn assert(from: &str, to: &str, ret: &str) {
|
||||
assert_eq!(
|
||||
url_relative_to(&Url::try_from(from).unwrap(), &Url::try_from(to).unwrap()).unwrap(),
|
||||
Cow::Owned(Url::try_from(ret).unwrap())
|
||||
url_relative_to(&from.parse().unwrap(), &to.parse().unwrap()).unwrap(),
|
||||
Cow::Owned(ret.parse().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
9
yazi-fs/src/provider/buffer.rs
Normal file
9
yazi-fs/src/provider/buffer.rs
Normal 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 {}
|
||||
68
yazi-fs/src/provider/dir_entry.rs
Normal file
68
yazi-fs/src/provider/dir_entry.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
46
yazi-fs/src/provider/local/dir_entry.rs
Normal file
46
yazi-fs/src/provider/local/dir_entry.rs
Normal 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() }
|
||||
}
|
||||
14
yazi-fs/src/provider/local/gate.rs
Normal file
14
yazi-fs/src/provider/local/gate.rs
Normal 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 }
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
use std::{io, path::{Path, PathBuf}};
|
||||
|
||||
use crate::provider::local::{Gate, ReadDir, ReadDirSync, RwFile};
|
||||
|
||||
pub struct Local;
|
||||
|
||||
impl Local {
|
||||
|
|
@ -9,8 +11,8 @@ impl Local {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn create(path: impl AsRef<Path>) -> io::Result<tokio::fs::File> {
|
||||
tokio::fs::File::create(path).await
|
||||
pub async fn create(path: impl AsRef<Path>) -> io::Result<RwFile> {
|
||||
Gate::default().write(true).create(true).truncate(true).open(path).await.map(Into::into)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -34,21 +36,21 @@ impl Local {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn open(path: impl AsRef<Path>) -> io::Result<tokio::fs::File> {
|
||||
tokio::fs::File::open(path).await
|
||||
pub async fn open(path: impl AsRef<Path>) -> io::Result<RwFile> {
|
||||
Gate::default().read(true).open(path).await.map(Into::into)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> { tokio::fs::read(path).await }
|
||||
|
||||
#[inline]
|
||||
pub async fn read_dir(path: impl AsRef<Path>) -> io::Result<tokio::fs::ReadDir> {
|
||||
tokio::fs::read_dir(path).await
|
||||
pub async fn read_dir(path: impl AsRef<Path>) -> io::Result<ReadDir> {
|
||||
tokio::fs::read_dir(path).await.map(Into::into)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read_dir_sync(path: impl AsRef<Path>) -> io::Result<std::fs::ReadDir> {
|
||||
std::fs::read_dir(path)
|
||||
pub fn read_dir_sync(path: impl AsRef<Path>) -> io::Result<ReadDirSync> {
|
||||
std::fs::read_dir(path).map(Into::into)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
1
yazi-fs/src/provider/local/mod.rs
Normal file
1
yazi-fs/src/provider/local/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(dir_entry gate local read_dir rw_file);
|
||||
38
yazi-fs/src/provider/local/read_dir.rs
Normal file
38
yazi-fs/src/provider/local/read_dir.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
23
yazi-fs/src/provider/local/rw_file.rs
Normal file
23
yazi-fs/src/provider/local/rw_file.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
3
yazi-fs/src/provider/mod.rs
Normal file
3
yazi-fs/src/provider/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
yazi_macro::mod_pub!(local);
|
||||
|
||||
yazi_macro::mod_flat!(buffer dir_entry provider read_dir rw_file);
|
||||
|
|
@ -2,7 +2,7 @@ use std::io;
|
|||
|
||||
use yazi_shared::url::Url;
|
||||
|
||||
use crate::services::Local;
|
||||
use crate::provider::{ReadDir, ReadDirSync, RwFile, local::Local};
|
||||
|
||||
#[inline]
|
||||
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]
|
||||
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() {
|
||||
Local::create(path).await
|
||||
Local::create(path).await.map(Into::into)
|
||||
} else {
|
||||
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]
|
||||
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() {
|
||||
Local::open(path).await
|
||||
Local::open(path).await.map(Into::into)
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
|
||||
}
|
||||
}
|
||||
|
||||
#[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() {
|
||||
Local::read_dir(path).await
|
||||
Local::read_dir(path).await.map(Into::into)
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
|
||||
}
|
||||
}
|
||||
|
||||
#[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() {
|
||||
Local::read_dir_sync(path)
|
||||
Local::read_dir_sync(path).map(Into::into)
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
|
||||
}
|
||||
30
yazi-fs/src/provider/read_dir.rs
Normal file
30
yazi-fs/src/provider/read_dir.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
21
yazi-fs/src/provider/rw_file.rs
Normal file
21
yazi-fs/src/provider/rw_file.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
yazi_macro::mod_flat!(local services);
|
||||
|
|
@ -16,7 +16,7 @@ impl From<CmdCow> for TabCreateOpt {
|
|||
return Self { wd: None };
|
||||
}
|
||||
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")
|
||||
&& let Cow::Owned(u) = expand_url(&wd)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function Header:flags()
|
|||
|
||||
local t = {}
|
||||
if cwd.is_search then
|
||||
t[#t + 1] = string.format("search: %s", cwd.frag)
|
||||
t[#t + 1] = string.format("search: %s", cwd.domain)
|
||||
end
|
||||
if filter then
|
||||
t[#t + 1] = string.format("filter: %s", filter)
|
||||
|
|
|
|||
8
yazi-plugin/src/external/highlighter.rs
vendored
8
yazi-plugin/src/external/highlighter.rs
vendored
|
|
@ -3,9 +3,9 @@ use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}, sync::OnceLock};
|
|||
use anyhow::{Result, anyhow};
|
||||
use ratatui::{layout::Size, text::{Line, Span, Text}};
|
||||
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_fs::services::Local;
|
||||
use yazi_fs::provider::local::Local;
|
||||
use yazi_shared::{Ids, errors::PeekError, replace_to_printable};
|
||||
|
||||
static INCR: Ids = Ids::new();
|
||||
|
|
@ -39,7 +39,7 @@ impl Highlighter {
|
|||
pub fn abort() { INCR.next(); }
|
||||
|
||||
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 mut plain = syntax.is_err();
|
||||
|
|
@ -143,7 +143,7 @@ impl Highlighter {
|
|||
}
|
||||
|
||||
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?;
|
||||
syntaxes.find_syntax_by_first_line(&line).ok_or_else(|| anyhow!("No syntax found"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::borrow::Cow;
|
|||
use globset::GlobBuilder;
|
||||
use mlua::{ExternalError, ExternalResult, Function, IntoLua, IntoLuaMulti, Lua, Table, Value};
|
||||
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;
|
||||
|
||||
|
|
@ -50,9 +50,9 @@ fn cwd(lua: &Lua) -> mlua::Result<Function> {
|
|||
fn cha(lua: &Lua) -> mlua::Result<Function> {
|
||||
lua.create_async_function(|lua, (url, follow): (UrlRef, Option<bool>)| async move {
|
||||
let meta = if follow.unwrap_or(false) {
|
||||
services::metadata(&*url).await
|
||||
provider::metadata(&*url).await
|
||||
} else {
|
||||
services::symlink_metadata(&*url).await
|
||||
provider::symlink_metadata(&*url).await
|
||||
};
|
||||
|
||||
match meta {
|
||||
|
|
@ -64,7 +64,7 @@ fn cha(lua: &Lua) -> mlua::Result<Function> {
|
|||
|
||||
fn write(lua: &Lua) -> mlua::Result<Function> {
|
||||
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),
|
||||
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> {
|
||||
lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move {
|
||||
let result = match r#type.as_bytes().as_ref() {
|
||||
b"dir" => services::create_dir(&*url).await,
|
||||
b"dir_all" => services::create_dir_all(&*url).await,
|
||||
b"dir" => provider::create_dir(&*url).await,
|
||||
b"dir_all" => provider::create_dir_all(&*url).await,
|
||||
_ => 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> {
|
||||
lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move {
|
||||
let result = match r#type.as_bytes().as_ref() {
|
||||
b"file" => services::remove_file(&*url).await,
|
||||
b"dir" => services::remove_dir(&*url).await,
|
||||
b"dir_all" => services::remove_dir_all(&*url).await,
|
||||
b"file" => provider::remove_file(&*url).await,
|
||||
b"dir" => provider::remove_dir(&*url).await,
|
||||
b"dir_all" => provider::remove_dir_all(&*url).await,
|
||||
b"dir_clean" => Ok(remove_dir_clean(&url).await),
|
||||
_ => 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> {
|
||||
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") {
|
||||
return Err("`glob` is disabled temporarily".into_lua_err());
|
||||
Some(
|
||||
GlobBuilder::new(&s.to_str()?)
|
||||
.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 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,
|
||||
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;
|
||||
}
|
||||
|
||||
let path = next.path();
|
||||
if glob.as_ref().is_some_and(|g| !g.is_match(&path)) {
|
||||
continue;
|
||||
}
|
||||
let url = next.url();
|
||||
// if glob.as_ref().is_some_and(|g| !g.is_match(&path)) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
let url = yazi_shared::url::Url::from(path);
|
||||
let file = if !resolve {
|
||||
yazi_fs::File::from_dummy(url, next.file_type().await.ok())
|
||||
} else if let Ok(meta) = next.metadata().await {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use anyhow::{Context, Result, bail};
|
|||
use mlua::{ChunkMode, ExternalError, Lua, Table};
|
||||
use parking_lot::RwLock;
|
||||
use yazi_boot::BOOT;
|
||||
use yazi_fs::services::Local;
|
||||
use yazi_fs::provider::local::Local;
|
||||
use yazi_macro::plugin_preset as preset;
|
||||
use yazi_shared::{LOG_LEVEL, RoCell};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use std::{borrow::Cow, collections::VecDeque};
|
||||
|
||||
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 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 super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash};
|
||||
|
|
@ -26,14 +26,14 @@ impl File {
|
|||
pub async fn work(&self, r#in: FileIn) -> Result<()> {
|
||||
match r#in {
|
||||
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());
|
||||
|
||||
while let Some(res) = it.recv().await {
|
||||
match res {
|
||||
Ok(0) => {
|
||||
if task.cut {
|
||||
services::remove_file(&task.from).await.ok();
|
||||
provider::remove_file(&task.from).await.ok();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ impl File {
|
|||
let cha = task.cha.unwrap();
|
||||
|
||||
let src = if task.resolve {
|
||||
match services::read_link(&task.from).await {
|
||||
match provider::read_link(&task.from).await {
|
||||
Ok(u) => Cow::Owned(u),
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
warn!("Link task partially done: {task:?}");
|
||||
|
|
@ -75,20 +75,20 @@ impl File {
|
|||
};
|
||||
|
||||
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 {
|
||||
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() {
|
||||
services::symlink_dir(src, &task.to).await?;
|
||||
provider::symlink_dir(src, &task.to).await?;
|
||||
} else {
|
||||
services::symlink_file(src, &task.to).await?;
|
||||
provider::symlink_file(src, &task.to).await?;
|
||||
}
|
||||
|
||||
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))?;
|
||||
}
|
||||
|
|
@ -96,14 +96,14 @@ impl File {
|
|||
let cha = task.cha.unwrap();
|
||||
let src = if !task.follow {
|
||||
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)
|
||||
} else {
|
||||
Cow::Borrowed(&task.from)
|
||||
};
|
||||
|
||||
ok_or_not_found(services::remove_file(&task.to).await)?;
|
||||
match services::hard_link(src, &task.to).await {
|
||||
ok_or_not_found(provider::remove_file(&task.to).await)?;
|
||||
match provider::hard_link(src, &task.to).await {
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
warn!("Hardlink task partially done: {task:?}");
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ impl File {
|
|||
self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?;
|
||||
}
|
||||
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
|
||||
&& maybe_exists(&task.target).await
|
||||
{
|
||||
|
|
@ -145,7 +145,7 @@ impl File {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -185,14 +185,14 @@ impl File {
|
|||
|
||||
while let Some(src) = dirs.pop_front() {
|
||||
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),
|
||||
_ => 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 {
|
||||
let from = Url::from(entry.path());
|
||||
let from = entry.url();
|
||||
let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await);
|
||||
|
||||
if cha.is_dir() {
|
||||
|
|
@ -256,14 +256,14 @@ impl File {
|
|||
|
||||
while let Some(src) = dirs.pop_front() {
|
||||
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),
|
||||
_ => 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 {
|
||||
let from = Url::from(entry.path());
|
||||
let from = entry.url();
|
||||
let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await);
|
||||
|
||||
if cha.is_dir() {
|
||||
|
|
@ -280,7 +280,7 @@ impl File {
|
|||
}
|
||||
|
||||
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() {
|
||||
let id = task.id;
|
||||
task.length = meta.len();
|
||||
|
|
@ -291,17 +291,17 @@ impl File {
|
|||
|
||||
let mut dirs = VecDeque::from([task.target]);
|
||||
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 {
|
||||
let Ok(meta) = entry.metadata().await else { continue };
|
||||
|
||||
if meta.is_dir() {
|
||||
dirs.push_front(Url::from(entry.path()));
|
||||
dirs.push_front(entry.url());
|
||||
continue;
|
||||
}
|
||||
|
||||
task.target = Url::from(entry.path());
|
||||
task.target = entry.url();
|
||||
task.length = meta.len();
|
||||
self.prog.send(TaskProg::New(task.id, meta.len()))?;
|
||||
self.queue(FileIn::Delete(task.clone()), NORMAL).await?;
|
||||
|
|
@ -321,7 +321,7 @@ impl File {
|
|||
|
||||
#[inline]
|
||||
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) })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use parking_lot::Mutex;
|
|||
use tokio::{select, sync::mpsc::{self, UnboundedReceiver}, task::JoinHandle};
|
||||
use yazi_config::{YAZI, plugin::{Fetcher, Preloader}};
|
||||
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_proxy::MgrProxy;
|
||||
use yazi_shared::{Id, Throttle, url::Url};
|
||||
|
|
@ -175,7 +175,7 @@ impl Scheduler {
|
|||
move |canceled: bool| {
|
||||
async move {
|
||||
if !canceled {
|
||||
services::remove_dir_all(&target).await.ok();
|
||||
provider::remove_dir_all(&target).await.ok();
|
||||
MgrProxy::update_tasks(&target);
|
||||
Pump::push_delete(target);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@ impl Data {
|
|||
#[inline]
|
||||
pub fn into_url(self) -> Option<Url> {
|
||||
match self {
|
||||
Self::String(s) => Url::try_from(s.as_ref()).ok(),
|
||||
Self::String(s) => s.parse().ok(),
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
@ -96,9 +96,9 @@ impl Data {
|
|||
#[inline]
|
||||
pub fn to_url(&self) -> Option<Url> {
|
||||
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::Bytes(b) => Url::try_from(b.as_slice()).ok(),
|
||||
Self::Bytes(b) => b.as_slice().try_into().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -186,9 +186,9 @@ impl DataKey {
|
|||
#[inline]
|
||||
pub fn into_url(self) -> Option<Url> {
|
||||
match self {
|
||||
Self::String(s) => Url::try_from(s.as_ref()).ok(),
|
||||
Self::String(s) => s.parse().ok(),
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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)]
|
||||
pub enum Component<'a> {
|
||||
|
|
@ -60,6 +60,7 @@ impl<'a> FromIterator<Component<'a>> for PathBuf {
|
|||
#[derive(Clone)]
|
||||
pub struct Components<'a> {
|
||||
inner: path::Components<'a>,
|
||||
loc: &'a Loc,
|
||||
scheme: &'a Scheme,
|
||||
scheme_yielded: bool,
|
||||
}
|
||||
|
|
@ -68,6 +69,7 @@ impl<'a> Components<'a> {
|
|||
pub fn new(url: &'a Url) -> Self {
|
||||
Self {
|
||||
inner: url.loc.components(),
|
||||
loc: &url.loc,
|
||||
scheme: &url.scheme,
|
||||
scheme_yielded: false,
|
||||
}
|
||||
|
|
@ -79,7 +81,7 @@ impl<'a> Components<'a> {
|
|||
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.push(path);
|
||||
s.into()
|
||||
|
|
@ -147,14 +149,16 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
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()));
|
||||
|
||||
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();
|
||||
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"));
|
||||
|
||||
let u: Url = item
|
||||
|
|
@ -162,7 +166,7 @@ mod tests {
|
|||
.take(5)
|
||||
.chain([Component::Normal(OsStr::new("target/release/yazi"))])
|
||||
.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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::url::Url;
|
||||
use crate::url::{Encode, Url};
|
||||
|
||||
pub struct Display<'a> {
|
||||
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 {
|
||||
let Url { loc, scheme } = self.inner;
|
||||
if scheme.is_virtual() {
|
||||
scheme.fmt(f)?;
|
||||
Encode::from(self.inner).fmt(f)?;
|
||||
}
|
||||
loc.display().fmt(f)
|
||||
}
|
||||
|
|
|
|||
76
yazi-shared/src/url/encode.rs
Normal file
76
yazi-shared/src/url/encode.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 anyhow::{Result, bail};
|
||||
|
||||
use crate::url::{Urn, UrnBuf};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Loc {
|
||||
inner: PathBuf,
|
||||
urn: usize,
|
||||
name: usize,
|
||||
}
|
||||
|
||||
impl Deref for Loc {
|
||||
|
|
@ -39,11 +40,7 @@ impl Hash for Loc {
|
|||
|
||||
impl Debug for Loc {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.debug_struct("Loc")
|
||||
.field("path", &self.inner)
|
||||
.field("urn", &self.urn())
|
||||
.field("name", &self.name())
|
||||
.finish()
|
||||
f.debug_struct("Loc").field("path", &self.inner).field("urn", &self.urn()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,20 +56,22 @@ impl From<PathBuf> for Loc {
|
|||
fn from(path: PathBuf) -> Self {
|
||||
let Some(name) = path.file_name() else {
|
||||
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 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();
|
||||
bytes.truncate(name_len + prefix_len as usize);
|
||||
bytes.truncate(prefix_len + name_len);
|
||||
Self {
|
||||
inner: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }),
|
||||
urn: name_len,
|
||||
name: name_len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +85,26 @@ impl<T: ?Sized + AsRef<OsStr>> From<&T> for 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);
|
||||
loc.urn = loc.inner.strip_prefix(base).unwrap_or(&loc.inner).as_os_str().len();
|
||||
loc
|
||||
|
|
@ -105,28 +123,20 @@ impl Loc {
|
|||
pub fn urn_owned(&self) -> UrnBuf { self.urn().to_owned() }
|
||||
|
||||
#[inline]
|
||||
pub fn name(&self) -> &OsStr {
|
||||
unsafe {
|
||||
OsStr::from_encoded_bytes_unchecked(
|
||||
self.bytes().get_unchecked(self.bytes().len() - self.name..),
|
||||
)
|
||||
}
|
||||
}
|
||||
pub fn name(&self) -> &OsStr { self.inner.file_name().unwrap_or(OsStr::new("")) }
|
||||
|
||||
pub fn set_name(&mut self, name: impl AsRef<OsStr>) {
|
||||
let name = name.as_ref();
|
||||
if name == self.name() {
|
||||
let (old, new) = (self.name(), name.as_ref());
|
||||
if old == new {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.name > name.len() {
|
||||
self.urn -= self.name - name.len();
|
||||
if old.len() > new.len() {
|
||||
self.urn -= old.len() - new.len();
|
||||
} else {
|
||||
self.urn += name.len() - self.name;
|
||||
self.urn += new.len() - old.len();
|
||||
}
|
||||
|
||||
self.name = name.len();
|
||||
self.inner.set_file_name(name);
|
||||
self.inner.set_file_name(new);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -143,13 +153,16 @@ impl Loc {
|
|||
|
||||
#[inline]
|
||||
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());
|
||||
|
||||
debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name));
|
||||
Self { inner: path, urn: self.name, name: self.name }
|
||||
debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name().len()));
|
||||
Self { inner: path, urn: self.urn }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_path(&self) -> PathBuf { self.inner.clone() }
|
||||
|
||||
#[inline]
|
||||
pub fn into_path(self) -> PathBuf { self.inner }
|
||||
|
||||
|
|
@ -159,8 +172,6 @@ impl Loc {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::MAIN_SEPARATOR;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
|
@ -182,18 +193,37 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
let loc = Loc::with(Path::new("/"), "/".into());
|
||||
fn test_with() -> Result<()> {
|
||||
let loc = Loc::with(0, "/".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(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.name(), OsStr::new("code"));
|
||||
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.name(), OsStr::new("foo"));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
|
||||
|
|
@ -201,18 +231,20 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
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.name(), OsStr::new("foo"));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
|
||||
|
||||
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.base().as_os_str(), OsStr::new("/root/"));
|
||||
|
||||
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.base().as_os_str(), OsStr::new("/root/"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use std::{borrow::Cow, fmt::Display};
|
||||
use std::borrow::Cow;
|
||||
|
||||
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)]
|
||||
pub enum Scheme {
|
||||
|
|
@ -11,7 +11,6 @@ pub enum Scheme {
|
|||
Regular,
|
||||
|
||||
Search(String),
|
||||
SearchItem,
|
||||
|
||||
Archive(String),
|
||||
|
||||
|
|
@ -19,32 +18,67 @@ pub enum 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 {
|
||||
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"~");
|
||||
if tilde {
|
||||
protocol = &protocol[..protocol.len() - 1];
|
||||
}
|
||||
|
||||
Ok(match protocol {
|
||||
b"regular" => (Self::Regular, 10, tilde),
|
||||
let (scheme, port) = match protocol {
|
||||
b"regular" => (Self::Regular, None),
|
||||
b"search" => {
|
||||
let (name, skip) = Self::decode_param(rest)?;
|
||||
(Self::Search(name), 9 + skip, tilde)
|
||||
let (domain, port) = Self::decode_param(rest, skip)?;
|
||||
(Self::Search(domain), Some(port))
|
||||
}
|
||||
b"archive" => {
|
||||
let (name, skip) = Self::decode_param(rest)?;
|
||||
(Self::Archive(name), 10 + skip, tilde)
|
||||
let (domain, port) = Self::decode_param(rest, skip)?;
|
||||
(Self::Archive(domain), Some(port))
|
||||
}
|
||||
b"sftp" => {
|
||||
let (name, skip) = Self::decode_param(rest)?;
|
||||
(Self::Sftp(name), 7 + skip, tilde)
|
||||
let (domain, port) = Self::decode_param(rest, skip)?;
|
||||
(Self::Sftp(domain), Some(port))
|
||||
}
|
||||
_ => 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]
|
||||
|
|
@ -55,49 +89,30 @@ impl Scheme {
|
|||
#[inline]
|
||||
pub fn is_virtual(&self) -> bool {
|
||||
match self {
|
||||
Self::Regular | Self::Search(_) | Self::SearchItem => false,
|
||||
Self::Regular | Self::Search(_) => false,
|
||||
Self::Archive(_) | Self::Sftp(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_param(bytes: &[u8]) -> Result<(String, usize)> {
|
||||
let len = bytes.iter().copied().take_while(|&b| b != b'/').count();
|
||||
fn decode_param(bytes: &[u8], skip: &mut usize) -> Result<(String, usize)> {
|
||||
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::Owned(b) => String::from_utf8(b)?,
|
||||
};
|
||||
|
||||
let slash = bytes.get(len).is_some_and(|&b| b == b'/') as usize;
|
||||
Ok((s, len + slash))
|
||||
Ok((domain, port))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn encode_param<'a>(s: &'a str) -> PercentEncode<'a> {
|
||||
const SET: AsciiSet = CONTROLS.add(b'/');
|
||||
percent_encode(s.as_bytes(), &SET)
|
||||
}
|
||||
fn decode_port(bytes: &[u8], skip: &mut usize) -> Result<usize> {
|
||||
let Some(idx) = bytes.iter().rposition(|&b| b == b':') else { return Ok(0) };
|
||||
let len = bytes.len() - idx;
|
||||
|
||||
pub fn encode_tilded(&self, loc: &Loc) -> String {
|
||||
let loc = percent_encode(loc.as_os_str().as_encoded_bytes(), CONTROLS);
|
||||
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)),
|
||||
}
|
||||
*skip -= len;
|
||||
Ok(if len == 1 { 0 } else { str::from_utf8(&bytes[idx + 1..])?.parse()? })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 percent_encoding::percent_decode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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)]
|
||||
pub struct Url {
|
||||
|
|
@ -27,6 +27,10 @@ impl From<PathBuf> for Url {
|
|||
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 {
|
||||
fn from(path: &PathBuf) -> Self { path.to_owned().into() }
|
||||
}
|
||||
|
|
@ -39,15 +43,18 @@ impl TryFrom<&[u8]> for Url {
|
|||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let (scheme, path) = Self::parse(bytes)?;
|
||||
Ok(Self { loc: path.into(), scheme })
|
||||
let (scheme, path, port) = Self::parse(bytes)?;
|
||||
|
||||
let loc = if let Some(urn) = port { Loc::with(urn, path)? } else { Loc::from(path) };
|
||||
|
||||
Ok(Self { loc, scheme })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Url {
|
||||
type Error = anyhow::Error;
|
||||
impl FromStr for Url {
|
||||
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 {
|
||||
|
|
@ -82,7 +89,7 @@ impl Url {
|
|||
pub fn with(&self, loc: impl Into<Loc>) -> Self {
|
||||
let loc: Loc = loc.into();
|
||||
// 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]
|
||||
|
|
@ -91,7 +98,6 @@ impl Url {
|
|||
match &self.scheme {
|
||||
Scheme::Regular => Self { loc, scheme: Scheme::Regular },
|
||||
Scheme::Search(_) => self.with(loc),
|
||||
Scheme::SearchItem => Self { loc, scheme: Scheme::Search(String::new()) },
|
||||
Scheme::Archive(_) => self.with(loc),
|
||||
Scheme::Sftp(_) => self.with(loc),
|
||||
}
|
||||
|
|
@ -100,12 +106,7 @@ impl Url {
|
|||
pub fn join(&self, path: impl AsRef<Path>) -> Self {
|
||||
match self.scheme {
|
||||
Scheme::Regular => Self { loc: self.loc.join(path).into(), scheme: Scheme::Regular },
|
||||
Scheme::Search(_) => {
|
||||
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::Search(_) => self.with(self.loc.join(path)),
|
||||
Scheme::Archive(_) => 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> {
|
||||
let parent = self.loc.parent()?;
|
||||
let base = self.loc.base();
|
||||
let urn = self.loc.urn();
|
||||
|
||||
Some(match &self.scheme {
|
||||
Scheme::Regular => Self { loc: parent.into(), scheme: Scheme::Regular },
|
||||
Scheme::Search(_) => Self { loc: parent.into(), scheme: Scheme::Regular },
|
||||
Scheme::SearchItem if parent == base => {
|
||||
Self { loc: parent.into(), scheme: Scheme::Search(String::new()) }
|
||||
}
|
||||
Scheme::SearchItem => {
|
||||
Self { loc: Loc::with(base, parent.to_owned()), scheme: Scheme::SearchItem }
|
||||
Scheme::Search(_) if urn == Urn::new("") => {
|
||||
Self { loc: parent.into(), scheme: Scheme::Regular }
|
||||
}
|
||||
Scheme::Search(_) => self.with(parent),
|
||||
Scheme::Archive(_) => self.with(parent),
|
||||
Scheme::Sftp(_) => self.with(parent),
|
||||
})
|
||||
|
|
@ -172,17 +170,22 @@ impl Url {
|
|||
self.loc.rebase(parent).into()
|
||||
}
|
||||
|
||||
pub fn parse(bytes: &[u8]) -> Result<(Scheme, Cow<'_, Path>)> {
|
||||
let (scheme, skip, tilde) = Scheme::parse(bytes)?;
|
||||
pub fn parse(bytes: &[u8]) -> Result<(Scheme, PathBuf, Option<usize>)> {
|
||||
let mut skip = 0;
|
||||
let (scheme, tilde, port) = Scheme::parse(bytes, &mut skip)?;
|
||||
|
||||
let rest = &bytes[skip + tilde as usize..];
|
||||
let rest =
|
||||
if tilde { Cow::from(percent_decode(rest)).into_os_str()? } else { rest.into_os_str()? };
|
||||
let rest = if tilde {
|
||||
Cow::from(percent_decode(&bytes[skip..])).into_os_str()?
|
||||
} else {
|
||||
bytes[skip..].into_os_str()?
|
||||
};
|
||||
|
||||
Ok((scheme, match rest {
|
||||
Cow::Borrowed(s) => Path::new(s).into(),
|
||||
Cow::Owned(s) => PathBuf::from(s).into(),
|
||||
}))
|
||||
let path = match rest {
|
||||
Cow::Borrowed(s) => Path::new(s).to_owned(),
|
||||
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 }
|
||||
|
||||
#[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]
|
||||
pub fn into_regular(mut self) -> Self {
|
||||
self.loc = self.loc.into_path().into();
|
||||
self.scheme = Scheme::Regular;
|
||||
self
|
||||
}
|
||||
|
|
@ -205,13 +211,17 @@ impl Url {
|
|||
pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) }
|
||||
|
||||
#[inline]
|
||||
pub fn to_search(&self, frag: impl AsRef<str>) -> Self {
|
||||
Self { loc: self.loc.clone(), scheme: Scheme::Search(frag.as_ref().to_owned()) }
|
||||
pub fn to_search(&self, domain: impl AsRef<str>) -> Self {
|
||||
Self {
|
||||
loc: Loc::zeroed(self.loc.to_path()),
|
||||
scheme: Scheme::Search(domain.as_ref().to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_search(mut self, frag: impl AsRef<str>) -> Self {
|
||||
self.scheme = Scheme::Search(frag.as_ref().to_owned());
|
||||
pub fn into_search(mut self, domain: impl AsRef<str>) -> Self {
|
||||
self.loc = Loc::zeroed(self.loc.into_path());
|
||||
self.scheme = Scheme::Search(domain.as_ref().to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +236,7 @@ impl Url {
|
|||
|
||||
impl Debug for Url {
|
||||
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;
|
||||
match (scheme.is_virtual(), loc.to_str()) {
|
||||
(false, Some(s)) => serializer.serialize_str(s),
|
||||
(true, Some(s)) => serializer.serialize_str(&format!("{scheme}{s}")),
|
||||
(_, None) => serializer.serialize_str(&scheme.encode_tilded(loc)),
|
||||
(true, Some(s)) => serializer.serialize_str(&format!("{}{s}", Encode::from(self))),
|
||||
(_, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue