feat: URL schemas (#3019)

This commit is contained in:
三咲雅 misaki masa 2025-07-27 18:06:13 +08:00 committed by GitHub
parent c2883f1e05
commit 0e3cd8545c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 924 additions and 505 deletions

View file

@ -81,13 +81,16 @@ impl UserData for File {
})?
});
methods.add_method("prefix", |lua, me, ()| {
if !me.folder.url.is_search() {
if !me.url.has_base() {
return Ok(None);
}
let Some(path) = me.url.as_path() else {
return Ok(None);
};
let mut p = me.url.strip_prefix(&me.folder.url).unwrap_or(&me.url).components();
p.next_back();
Some(lua.create_string(p.as_path().as_os_str().as_encoded_bytes())).transpose()
let mut comp = path.strip_prefix(me.url.loc.base()).unwrap_or(path).components();
comp.next_back();
Some(lua.create_string(comp.as_path().as_os_str().as_encoded_bytes())).transpose()
});
methods.add_method("style", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| {

View file

@ -1,16 +1,16 @@
use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, io::{Read, Write}, path::PathBuf};
use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, hash::Hash, io::{Read, Write}, ops::Deref};
use anyhow::{Result, anyhow};
use crossterm::{execute, style::Print};
use scopeguard::defer;
use tokio::{fs::{self, OpenOptions}, io::AsyncWriteExt};
use tokio::io::AsyncWriteExt;
use yazi_config::YAZI;
use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file};
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file, services::{self, Local}, skip_url};
use yazi_macro::{err, succ};
use yazi_parser::VoidOpt;
use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER};
use yazi_shared::{event::Data, terminal_clear, url::Url};
use yazi_shared::{OsStrJoin, event::Data, terminal_clear, url::{Component, Url}};
use yazi_term::tty::TTY;
use crate::{Actor, Ctx};
@ -22,30 +22,33 @@ impl Actor for BulkRename {
const NAME: &str = "bulk_rename";
// FIXME: VFS
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let Some(opener) = YAZI.opener.block(YAZI.open.all("bulk-rename.txt", "text/plain")) else {
succ!(AppProxy::notify_warn("Bulk rename", "No text opener found"));
};
let old: Vec<_> = cx.tab().selected_or_hovered().collect();
let selected: Vec<_> = cx.tab().selected_or_hovered().cloned().collect();
if selected.is_empty() {
succ!(AppProxy::notify_warn("Bulk rename", "No files selected"));
}
let root = max_common_root(&old);
let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect();
let root = max_common_root(&selected);
let old: Vec<_> =
selected.iter().enumerate().map(|(i, u)| Tuple::new(i, skip_url(u, root))).collect();
let cwd = cx.cwd().clone();
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk");
let s = old.iter().map(|o| o.as_os_str()).collect::<Vec<_>>().join(OsStr::new("\n"));
OpenOptions::new()
// TODO: pull `OpenOptions` into `yazi_fs`
tokio::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.await?
.write_all(s.as_encoded_bytes())
.write_all(old.join(OsStr::new("\n")).as_encoded_bytes())
.await?;
defer! { tokio::spawn(fs::remove_file(tmp.clone())); }
defer! { tokio::spawn(Local::remove_file(tmp.clone())); }
TasksProxy::process_exec(Cow::Borrowed(opener), cwd, vec![
OsString::new(),
tmp.to_owned().into(),
@ -56,16 +59,22 @@ impl Actor for BulkRename {
defer!(AppProxy::resume());
AppProxy::stop().await;
let new: Vec<_> =
fs::read_to_string(&tmp).await?.lines().take(old.len()).map(PathBuf::from).collect();
Self::r#do(root, old, new).await
let new: Vec<_> = Local::read_to_string(&tmp)
.await?
.lines()
.take(old.len())
.enumerate()
.map(|(i, s)| Tuple::new(i, s))
.collect();
Self::r#do(root, old, new, selected).await
});
succ!();
}
}
impl BulkRename {
async fn r#do(root: PathBuf, old: Vec<PathBuf>, new: Vec<PathBuf>) -> Result<()> {
async fn r#do(root: usize, old: Vec<Tuple>, new: Vec<Tuple>, selected: Vec<Url>) -> Result<()> {
terminal_clear(TTY.writer())?;
if old.len() != new.len() {
#[rustfmt::skip]
@ -100,14 +109,17 @@ impl BulkRename {
let permit = WATCHER.acquire().await.unwrap();
let (mut failed, mut succeeded) = (Vec::new(), HashMap::with_capacity(todo.len()));
for (o, n) in todo {
let (old, new) = (root.join(&o), root.join(&n));
let (old, new): (Url, Url) = (
selected[o.0].components().take(root).chain([Component::Normal(&o)]).collect(),
selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(),
);
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) = fs::rename(&old, &new).await {
} else if let Err(e) = services::rename(&old, &new).await {
failed.push((o, n, e.into()));
} else if let Ok(f) = File::new(new.into()).await {
succeeded.insert(Url::from(old), f);
} else if let Ok(f) = File::new(new).await {
succeeded.insert(old, f);
} else {
failed.push((o, n, anyhow!("Failed to retrieve file info")));
}
@ -126,7 +138,7 @@ impl BulkRename {
Ok(())
}
async fn output_failed(failed: Vec<(PathBuf, PathBuf, anyhow::Error)>) -> Result<()> {
async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout();
terminal_clear(&mut *stdout)?;
@ -141,9 +153,9 @@ impl BulkRename {
Ok(())
}
fn prioritized_paths(old: Vec<PathBuf>, new: Vec<PathBuf>) -> Vec<(PathBuf, PathBuf)> {
let orders: HashMap<_, _> = old.iter().enumerate().map(|(i, p)| (p, i)).collect();
let mut incomes: HashMap<_, _> = old.iter().map(|p| (p, false)).collect();
fn prioritized_paths(old: Vec<Tuple>, new: Vec<Tuple>) -> Vec<(Tuple, Tuple)> {
let orders: HashMap<_, _> = old.iter().enumerate().map(|(i, t)| (t, i)).collect();
let mut incomes: HashMap<_, _> = old.iter().map(|t| (t, false)).collect();
let mut todos: HashMap<_, _> = old
.iter()
.zip(new)
@ -156,7 +168,7 @@ impl BulkRename {
let mut sorted = Vec::with_capacity(old.len());
while !todos.is_empty() {
// Paths that are non-incomes and don't need to be prioritized in this round
let mut outcomes: Vec<_> = incomes.iter().filter(|&(_, b)| !b).map(|(&p, _)| p).collect();
let mut outcomes: Vec<_> = incomes.iter().filter(|&(_, b)| !b).map(|(&t, _)| t).collect();
outcomes.sort_unstable_by(|a, b| orders[b].cmp(&orders[a]));
// If there're no outcomes, it means there are cycles in the renaming
@ -180,6 +192,35 @@ impl BulkRename {
}
}
// --- Tuple
#[derive(Clone, Debug)]
struct Tuple(usize, OsString);
impl Deref for Tuple {
type Target = OsStr;
fn deref(&self) -> &Self::Target { &self.1 }
}
impl PartialEq for Tuple {
fn eq(&self, other: &Self) -> bool { self.1 == other.1 }
}
impl Eq for Tuple {}
impl Hash for Tuple {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.1.hash(state); }
}
impl AsRef<OsStr> for Tuple {
fn as_ref(&self) -> &OsStr { &self.1 }
}
impl Tuple {
fn new(index: usize, inner: impl Into<OsString>) -> Self { Self(index, inner.into()) }
}
// --- Tests
#[cfg(test)]
mod tests {
use super::*;
@ -188,8 +229,8 @@ mod tests {
fn test_sort() {
fn cmp(input: &[(&str, &str)], expected: &[(&str, &str)]) {
let sorted = BulkRename::prioritized_paths(
input.iter().map(|&(o, _)| o.into()).collect(),
input.iter().map(|&(_, n)| n.into()).collect(),
input.iter().map(|&(o, _)| Tuple::new(0, o)).collect(),
input.iter().map(|&(_, n)| Tuple::new(0, n)).collect(),
);
let sorted: Vec<_> =
sorted.iter().map(|(o, n)| (o.to_str().unwrap(), n.to_str().unwrap())).collect();

View file

@ -1,3 +1,4 @@
// FIXME: VFS
use std::{ffi::OsString, path::Path};
use anyhow::{Result, bail};
@ -41,7 +42,7 @@ impl Actor for Copy {
// Copy the CWD path regardless even if the directory is empty
if s.is_empty() && opt.r#type == "dirname" {
s.push(cx.cwd());
s.push(opt.separator.transform(cx.cwd()));
}
futures::executor::block_on(CLIPBOARD.set(s));

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use yazi_dds::Pubsub;
use yazi_macro::{act, err, render, succ};
use yazi_parser::mgr::{HoverDoOpt, HoverOpt};
use yazi_shared::{event::Data, url::Urn};
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
@ -37,8 +37,8 @@ impl Actor for HoverDo {
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
// Hover on the file
if let Ok(p) = opt.url.strip_prefix(cx.cwd()) {
render!(cx.current_mut().hover(Urn::new(p)));
if let Some(u) = opt.url.strip_prefix(cx.cwd()) {
render!(cx.current_mut().hover(u.urn()));
}
// Turn on tracing

View file

@ -1,7 +1,7 @@
use std::{ops::Deref, path::Path};
use std::{borrow::Cow, ops::Deref};
use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value};
use yazi_shared::IntoOsStr;
use yazi_shared::url::Scheme;
use crate::{Urn, cached_field};
@ -29,15 +29,18 @@ impl AsRef<yazi_shared::url::Url> for Url {
fn as_ref(&self) -> &yazi_shared::url::Url { &self.inner }
}
// FIXME: remove
impl AsRef<Path> for Url {
fn as_ref(&self) -> &Path { self.inner.as_path() }
}
impl From<Url> for yazi_shared::url::Url {
fn from(value: Url) -> Self { value.inner }
}
impl From<Url> for Cow<'_, yazi_shared::url::Url> {
fn from(value: Url) -> Self { Cow::Owned(value.inner) }
}
impl<'a> From<&'a Url> for Cow<'a, yazi_shared::url::Url> {
fn from(value: &'a Url) -> Self { Cow::Borrowed(&value.inner) }
}
impl TryFrom<&[u8]> for Url {
type Error = mlua::Error;
@ -103,7 +106,14 @@ 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())) })
});
cached_field!(fields, frag, |lua, me| lua.create_string(me.frag().as_encoded_bytes()));
// TODO: remove
cached_field!(fields, frag, |lua, me| {
if let Scheme::Search(kw) = &me.scheme {
Some(lua.create_string(kw)).transpose()
} else {
Ok(None)
}
});
fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular()));
fields.add_field_method_get("is_search", |_, me| Ok(me.is_search()));
@ -135,16 +145,16 @@ impl UserData for Url {
})
});
methods.add_method("strip_prefix", |_, me, base: Value| {
let path = match base {
Value::String(s) => me.strip_prefix(s.to_str()?.as_ref()),
let url = match base {
Value::String(s) => me.strip_prefix(Self::try_from(s.as_bytes().as_ref())?),
Value::UserData(ud) => me.strip_prefix(&ud.borrow::<Self>()?.inner),
_ => Err("must be a string or a Url".into_lua_err())?,
};
Ok(path.ok().map(Self::new))
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.as_bytes().into_os_str()?)))
methods.add_function_mut("into_search", |_, (ud, frag): (AnyUserData, String)| {
Ok(Self::new(ud.take::<Self>()?.inner.into_search(frag)))
});
methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner));

View file

@ -1,4 +1,4 @@
yazi_macro::mod_pub!(package);
yazi_macro::mod_pub!(package shared);
yazi_macro::mod_flat!(args);

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use yazi_fs::must_exists;
use super::{Dependency, Git};
use crate::shared::must_exists;
impl Dependency {
pub(super) async fn add(&mut self) -> Result<()> {

View file

@ -1,8 +1,10 @@
use anyhow::{Context, Result};
use yazi_fs::{maybe_exists, ok_or_not_found, remove_dir_clean, remove_sealed, services::Local};
use yazi_fs::{ok_or_not_found, remove_dir_clean, services::Local};
use yazi_macro::outln;
use yazi_shared::url::Url;
use super::Dependency;
use crate::shared::{maybe_exists, remove_sealed};
impl Dependency {
pub(super) async fn delete(&self) -> Result<()> {
@ -34,7 +36,7 @@ impl Dependency {
Err(e) => Err(e).context(format!("failed to read `{}`", assets.display()))?,
};
remove_dir_clean(&assets).await;
remove_dir_clean(&assets.into()).await;
Ok(())
}

View file

@ -1,10 +1,11 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use yazi_fs::{copy_and_seal, maybe_exists, remove_dir_clean, services::Local};
use yazi_fs::{remove_dir_clean, services::Local};
use yazi_macro::outln;
use super::Dependency;
use crate::shared::{copy_and_seal, maybe_exists};
impl Dependency {
pub(super) async fn deploy(&mut self) -> Result<()> {
@ -29,7 +30,7 @@ impl Dependency {
self.delete_sources().await?;
}
remove_dir_clean(&to).await;
remove_dir_clean(&to.into()).await;
self.hash = self.hash().await?;
res2?;
res1?;

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use yazi_fs::must_exists;
use super::{Dependency, Git};
use crate::shared::must_exists;
impl Dependency {
pub(super) async fn install(&mut self) -> Result<()> {

View file

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

View file

@ -0,0 +1,45 @@
use std::{io, path::Path};
use tokio::io::AsyncWriteExt;
use yazi_fs::{ok_or_not_found, services::Local};
#[inline]
pub async fn must_exists(path: impl AsRef<Path>) -> bool {
Local::symlink_metadata(path).await.is_ok()
}
#[inline]
pub async fn maybe_exists(path: impl AsRef<Path>) -> bool {
match Local::symlink_metadata(path).await {
Ok(_) => true,
Err(e) => e.kind() != std::io::ErrorKind::NotFound,
}
}
// 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?;
file.write_all(&b).await?;
let mut perm = file.metadata().await?.permissions();
perm.set_readonly(true);
file.set_permissions(perm).await?;
Ok(())
}
// TODO: use `yazi_fs` instead of `tokio::fs`
pub async fn remove_sealed(p: &Path) -> io::Result<()> {
#[cfg(windows)]
{
let mut perm = tokio::fs::metadata(p).await?.permissions();
perm.set_readonly(false);
tokio::fs::set_permissions(p, perm).await?;
}
tokio::fs::remove_file(p).await
}

View file

@ -1,6 +1,6 @@
#![allow(clippy::module_inception)]
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which);
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview scheme tasks theme which);
yazi_macro::mod_flat!(color icon layout pattern platform preset priority style yazi);

View file

@ -2,7 +2,7 @@ use anyhow::{Result, bail};
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_fs::{CWD, SortBy};
use yazi_shared::SyncCell;
use yazi_shared::{SyncCell, url::Url};
use super::{MgrRatio, MouseEvents};
@ -32,9 +32,9 @@ impl Mgr {
return None;
}
let home = dirs::home_dir().unwrap_or_default();
let cwd = if let Ok(p) = CWD.load().strip_prefix(home) {
format!("~{}{}", std::path::MAIN_SEPARATOR, p.display())
let home = Url::from(dirs::home_dir().unwrap_or_default());
let cwd = if let Some(u) = CWD.load().strip_prefix(home) {
format!("~{}{}", std::path::MAIN_SEPARATOR, u.display())
} else {
format!("{}", CWD.load().display())
};

View file

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

View file

@ -0,0 +1,9 @@
use std::path::PathBuf;
pub struct Sftp {
pub host: String,
pub user: String,
pub port: u16,
pub password: Option<String>,
pub key_file: Option<PathBuf>,
}

View file

@ -1,22 +1,14 @@
use std::{borrow::Cow, collections::HashMap, path::PathBuf};
use std::{borrow::Cow, collections::HashMap};
use yazi_fs::File;
use yazi_shared::{MIME_DIR, SStr, url::{Scheme, Url}};
use yazi_shared::{MIME_DIR, SStr, url::Url};
#[derive(Default)]
pub struct Mimetype(HashMap<PathBuf, String>);
pub struct Mimetype(HashMap<Url, String>);
impl Mimetype {
#[inline]
pub fn by_url(&self, url: &Url) -> Option<&str> {
match url.scheme() {
Scheme::Regular => self.0.get(url.as_path()),
Scheme::Search => None,
Scheme::SearchItem => self.0.get(url.as_path()),
Scheme::Archive => None,
}
.map(|s| s.as_str())
}
pub fn by_url(&self, url: &Url) -> Option<&str> { self.0.get(url).map(|s| s.as_str()) }
#[inline]
pub fn by_url_owned(&self, url: &Url) -> Option<SStr> {
@ -34,26 +26,8 @@ impl Mimetype {
}
#[inline]
pub fn contains(&self, url: &Url) -> bool {
match url.scheme() {
Scheme::Regular => self.0.contains_key(url.as_path()),
Scheme::Search => false,
Scheme::SearchItem => self.0.contains_key(url.as_path()),
Scheme::Archive => false,
}
}
pub fn contains(&self, url: &Url) -> bool { self.0.contains_key(url) }
pub fn extend(&mut self, iter: impl IntoIterator<Item = (Url, String)>) {
self.0.extend(iter.into_iter().filter_map(|(u, s)| {
Some((
match u.scheme() {
Scheme::Regular => u.into_path(),
Scheme::Search => None?,
Scheme::SearchItem => u.into_path(),
Scheme::Archive => None?,
},
s,
))
}))
}
#[inline]
pub fn extend(&mut self, iter: impl IntoIterator<Item = (Url, String)>) { self.0.extend(iter) }
}

View file

@ -41,6 +41,7 @@ impl Yanked {
pub fn contains_in(&self, dir: &Url) -> bool {
self.urls.iter().any(|u| {
let mut it = u.components();
// FIXME
it.next_back().is_some() && it == dir.components() && u.parent_url().as_ref() == Some(dir)
})
}

View file

@ -1,9 +1,11 @@
use std::{collections::VecDeque, fs::ReadDir, future::poll_fn, mem, path::{Path, PathBuf}, pin::Pin, task::{Poll, ready}, time::{Duration, Instant}};
use std::{collections::VecDeque, fs::ReadDir, future::poll_fn, io, mem, pin::Pin, task::{Poll, ready}, time::{Duration, Instant}};
use tokio::task::JoinHandle;
use yazi_shared::Either;
use yazi_shared::{Either, url::Url};
type Task = Either<PathBuf, ReadDir>;
use crate::services;
type Task = Either<Url, ReadDir>;
pub enum SizeCalculator {
Idle((VecDeque<Task>, Option<u64>)),
@ -11,23 +13,23 @@ pub enum SizeCalculator {
}
impl SizeCalculator {
pub async fn new(path: impl AsRef<Path>) -> std::io::Result<Self> {
let p = path.as_ref().to_owned();
pub async fn new(url: &Url) -> io::Result<Self> {
let u = url.to_owned();
tokio::task::spawn_blocking(move || {
let meta = std::fs::symlink_metadata(&p)?;
let meta = services::symlink_metadata_sync(&u)?;
if !meta.is_dir() {
return Ok(Self::Idle((VecDeque::new(), Some(meta.len()))));
}
let mut buf = VecDeque::from([Either::Right(std::fs::read_dir(p)?)]);
let mut buf = VecDeque::from([Either::Right(services::read_dir_sync(u)?)]);
let size = Self::next_chunk(&mut buf);
Ok(Self::Idle((buf, size)))
})
.await?
}
pub async fn total(path: impl AsRef<Path>) -> std::io::Result<u64> {
let mut it = Self::new(path).await?;
pub async fn total(url: &Url) -> io::Result<u64> {
let mut it = Self::new(url).await?;
let mut total = 0;
while let Some(n) = it.next().await? {
total += n;
@ -35,7 +37,7 @@ impl SizeCalculator {
Ok(total)
}
pub async fn next(&mut self) -> std::io::Result<Option<u64>> {
pub async fn next(&mut self) -> io::Result<Option<u64>> {
poll_fn(|cx| {
loop {
match self {
@ -61,7 +63,7 @@ impl SizeCalculator {
.await
}
fn next_chunk(buf: &mut VecDeque<Either<PathBuf, ReadDir>>) -> Option<u64> {
fn next_chunk(buf: &mut VecDeque<Either<Url, ReadDir>>) -> Option<u64> {
let (mut i, mut size, now) = (0, 0, Instant::now());
macro_rules! pop_and_continue {
() => {{
@ -77,8 +79,8 @@ impl SizeCalculator {
i += 1;
let front = buf.front_mut()?;
if let Either::Left(p) = front {
*front = match std::fs::read_dir(p) {
if let Either::Left(u) = front {
*front = match services::read_dir_sync(u) {
Ok(it) => Either::Right(it),
Err(_) => pop_and_continue!(),
};
@ -91,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()));
buf.push_back(Either::Left(ent.path().into()));
} else if let Ok(meta) = ent.metadata() {
size += meta.len();
}

View file

@ -1,4 +1,4 @@
use std::{env::{current_dir, set_current_dir}, ops::Deref, path::PathBuf, sync::{Arc, atomic::{self, AtomicBool}}};
use std::{env::{current_dir, set_current_dir}, ops::Deref, path::PathBuf, sync::{Arc, atomic::{AtomicBool, Ordering}}};
use arc_swap::ArcSwap;
use yazi_shared::{RoCell, url::Url};
@ -32,25 +32,32 @@ impl Cwd {
}
self.store(Arc::new(url.clone()));
unsafe { std::env::set_var("PWD", url) };
if let Some(p) = url.as_path() {
unsafe { std::env::set_var("PWD", p) };
Self::sync_cwd();
}
Self::sync_cwd();
true
}
fn sync_cwd() {
static SYNCING: AtomicBool = AtomicBool::new(false);
if SYNCING.swap(true, atomic::Ordering::Relaxed) {
if SYNCING.swap(true, Ordering::Relaxed) {
return;
}
tokio::task::spawn_blocking(move || {
_ = set_current_dir(CWD.load().as_ref());
let p = current_dir().unwrap_or_default();
if let Some(p) = CWD.load().as_path() {
_ = set_current_dir(p);
}
SYNCING.store(false, atomic::Ordering::Relaxed);
if p != CWD.load().as_path() {
set_current_dir(CWD.load().as_ref()).ok();
let cur = current_dir().unwrap_or_default();
SYNCING.store(false, Ordering::Relaxed);
if let Some(p) = CWD.load().as_path()
&& cur != p
{
set_current_dir(p).ok();
}
});
}

View file

@ -3,24 +3,22 @@
use std::{borrow::Cow, collections::{HashMap, HashSet}, ffi::{OsStr, OsString}, path::{Path, PathBuf}};
use anyhow::{Result, bail};
use tokio::{fs, io::{self, AsyncWriteExt}, select, sync::{mpsc, oneshot}, time};
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
use yazi_shared::url::{Component, Url};
use crate::cha::Cha;
use crate::{cha::Cha, services};
#[inline]
pub async fn must_exists(p: impl AsRef<Path>) -> bool { fs::symlink_metadata(p).await.is_ok() }
#[inline]
pub async fn maybe_exists(p: impl AsRef<Path>) -> bool {
match fs::symlink_metadata(p).await {
pub async fn maybe_exists(u: impl AsRef<Url>) -> bool {
match services::symlink_metadata(u).await {
Ok(_) => true,
Err(e) => e.kind() != io::ErrorKind::NotFound,
}
}
#[inline]
pub async fn must_be_dir(p: impl AsRef<Path>) -> bool {
fs::metadata(p).await.is_ok_and(|m| m.is_dir())
pub async fn must_be_dir(u: impl AsRef<Url>) -> bool {
services::metadata(u).await.is_ok_and(|m| m.is_dir())
}
#[inline]
@ -85,39 +83,13 @@ async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result<bool> {
Ok(final_name(a).await? == final_name(b).await?)
}
pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> {
let b = fs::read(from).await?;
ok_or_not_found(remove_sealed(to).await)?;
let mut file =
fs::OpenOptions::new().create_new(true).write(true).truncate(true).open(to).await?;
file.write_all(&b).await?;
let mut perm = file.metadata().await?.permissions();
perm.set_readonly(true);
file.set_permissions(perm).await?;
Ok(())
}
pub async fn remove_sealed(p: &Path) -> io::Result<()> {
#[cfg(windows)]
{
let mut perm = fs::metadata(p).await?.permissions();
perm.set_readonly(false);
fs::set_permissions(p, perm).await?;
}
fs::remove_file(p).await
}
pub async fn realname(p: &Path) -> Option<OsString> {
let name = p.file_name()?;
if p == fs::canonicalize(p).await.ok()? {
pub async fn realname(u: &Url) -> Option<OsString> {
let name = u.file_name()?;
if *u == services::canonicalize(u).await.ok()? {
return None;
}
realname_unchecked(p, &mut HashMap::new())
realname_unchecked(u, &mut HashMap::new())
.await
.ok()
.filter(|s| s != name)
@ -127,13 +99,15 @@ pub async fn realname(p: &Path) -> Option<OsString> {
#[cfg(unix)]
#[tokio::test]
async fn test_realname_unchecked() {
fs::remove_dir_all("/tmp/issue-1173").await.ok();
fs::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap();
fs::File::create("/tmp/issue-1173/A").await.unwrap();
fs::File::create("/tmp/issue-1173/b").await.unwrap();
fs::File::create("/tmp/issue-1173/real-dir/C").await.unwrap();
fs::symlink("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap();
fs::symlink("real-dir", "/tmp/issue-1173/link-dir").await.unwrap();
use crate::services::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();
let c = &mut HashMap::new();
async fn check(a: &str, b: &str, c: &mut HashMap<PathBuf, HashSet<OsString>>) {
@ -184,25 +158,22 @@ pub async fn realname_unchecked<'a>(
}
pub fn copy_with_progress(
from: &Path,
to: &Path,
from: &Url,
to: &Url,
cha: Cha,
) -> mpsc::Receiver<Result<u64, io::Error>> {
let (tx, rx) = mpsc::channel(1);
let (tick_tx, mut tick_rx) = oneshot::channel();
tokio::spawn({
let (from, to) = (from.to_owned(), to.to_owned());
let (from, to) = (from.clone(), to.clone());
async move {
tick_tx.send(_copy_with_progress(from, to, cha).await).ok();
}
});
tokio::spawn({
let tx = tx.clone();
let to = to.to_path_buf();
let (tx, to) = (tx.clone(), to.clone());
async move {
let mut last = 0;
let mut exit = None;
@ -228,7 +199,7 @@ pub fn copy_with_progress(
None => {}
}
let len = fs::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0);
let len = services::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0);
if len > last {
tx.send(Ok(len - last)).await.ok();
last = len;
@ -240,7 +211,7 @@ pub fn copy_with_progress(
rx
}
async fn _copy_with_progress(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result<u64> {
async fn _copy_with_progress(from: Url, to: Url, cha: Cha) -> io::Result<u64> {
let mut ft = std::fs::FileTimes::new();
cha.atime.map(|t| ft = ft.set_accessed(t));
cha.mtime.map(|t| ft = ft.set_modified(t));
@ -288,18 +259,18 @@ async fn _copy_with_progress(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result
}
}
pub async fn remove_dir_clean(dir: &Path) {
let Ok(mut it) = fs::read_dir(dir).await else { return };
pub async fn remove_dir_clean(dir: &Url) {
let Ok(mut it) = services::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();
let path = entry.path().into();
Box::pin(remove_dir_clean(&path)).await;
fs::remove_dir(path).await.ok();
services::remove_dir(path).await.ok();
}
}
fs::remove_dir(dir).await.ok();
services::remove_dir(dir).await.ok();
}
// Convert a file mode to a string representation
@ -355,50 +326,61 @@ pub fn permissions(m: libc::mode_t, dummy: bool) -> String {
s
}
// Find the max common root in a list of files
// Find the max common root in a list of urls
// e.g. /a/b/c, /a/b/d -> /a/b
// /aa/bb/cc, /aa/dd/ee -> /aa
pub fn max_common_root(files: &[impl AsRef<Path>]) -> PathBuf {
if files.is_empty() {
return PathBuf::new();
pub fn max_common_root(urls: &[Url]) -> usize {
if urls.is_empty() {
return 0;
} else if urls.len() == 1 {
return urls[0].components().count().saturating_sub(1);
}
let mut it = files.iter().map(|p| p.as_ref().parent().unwrap_or(Path::new("")).components());
let mut root = it.next().unwrap().collect::<PathBuf>();
for components in it {
let mut new_root = PathBuf::new();
for (a, b) in root.components().zip(components) {
if a != b {
break;
}
new_root.push(a);
}
root = new_root;
let mut it = urls.iter().map(|u| u.parent_url());
let Some(first) = it.next().unwrap() else {
return 0; // The first URL has no parent
};
let mut min = first.components().count();
for parent in it {
let Some(parent) = parent else {
return 0; // One of the URLs has no parent
};
min = first
.components()
.zip(parent.components())
.take_while(|(a, b)| match (a, b) {
(Component::Scheme(a), Component::Scheme(b)) => a.covariant(b),
(a, b) => a == b,
})
.count()
.min(min);
}
root
min
}
#[cfg(unix)]
#[test]
fn test_max_common_root() {
assert_eq!(max_common_root(&[] as &[PathBuf]).as_os_str(), "");
assert_eq!(max_common_root(&["".into()] as &[PathBuf]).as_os_str(), "");
assert_eq!(max_common_root(&["a".into()] as &[PathBuf]).as_os_str(), "");
assert_eq!(max_common_root(&["/a".into()] as &[PathBuf]).as_os_str(), "/");
assert_eq!(max_common_root(&["/a/b".into()] as &[PathBuf]).as_os_str(), "/a");
assert_eq!(
max_common_root(&["/a/b/c".into(), "/a/b/d".into()] as &[PathBuf]).as_os_str(),
"/a/b"
);
assert_eq!(
max_common_root(&["/aa/bb/cc".into(), "/aa/dd/ee".into()] as &[PathBuf]).as_os_str(),
"/aa"
);
assert_eq!(
max_common_root(
&["/aa/bb/cc".into(), "/aa/bb/cc/dd/ee".into(), "/aa/bb/cc/ff".into()] as &[PathBuf]
)
.as_os_str(),
"/aa/bb"
);
fn assert(input: &[&str], expected: &str) {
let urls: Vec<_> = input.iter().copied().map(Url::try_from).collect::<Result<_>>().unwrap();
let mut comp = urls[0].components();
for _ in 0..comp.clone().count() - max_common_root(&urls) {
comp.next_back();
}
assert_eq!(comp.os_str(), OsStr::new(expected));
}
assert_eq!(max_common_root(&[]), 0);
assert(&[""], "");
assert(&["a"], "");
assert(&["/a"], "/");
assert(&["/a/b"], "/a");
assert(&["/a/b/c", "/a/b/d"], "/a/b");
assert(&["/aa/bb/cc", "/aa/dd/ee"], "/aa");
assert(&["/aa/bb/cc", "/aa/bb/cc/dd/ee", "/aa/bb/cc/ff"], "/aa/bb");
}

View file

@ -1,5 +1,6 @@
use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{Component, Path, PathBuf}};
use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{Path, PathBuf}};
use anyhow::{Result, bail};
use yazi_shared::url::Url;
use crate::{CWD, services};
@ -11,17 +12,16 @@ pub fn clean_url(url: &Url) -> Url { Url::from(clean_path(url)) }
pub fn clean_path(path: impl AsRef<Path>) -> PathBuf { _clean_path(path.as_ref()) }
fn _clean_path(path: &Path) -> PathBuf {
use std::path::Component::*;
let mut out = vec![];
for c in path.components() {
match c {
Component::CurDir => {}
Component::ParentDir => match out.last() {
Some(Component::RootDir) => {}
Some(Component::Normal(_)) => _ = out.pop(),
None
| Some(Component::CurDir)
| Some(Component::ParentDir)
| Some(Component::Prefix(_)) => out.push(c),
CurDir => {}
ParentDir => match out.last() {
Some(RootDir) => {}
Some(Normal(_)) => _ = out.pop(),
None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => out.push(c),
},
c => out.push(c),
}
@ -33,6 +33,12 @@ fn _clean_path(path: &Path) -> PathBuf {
#[inline]
pub fn expand_path(p: impl AsRef<Path>) -> PathBuf { _expand_path(p.as_ref()) }
#[inline]
pub fn expand_url<'a>(url: impl Into<Cow<'a, Url>>) -> Cow<'a, Url> {
let u: Cow<'a, Url> = url.into();
if let Some(p) = u.as_path() { Url::from(_expand_path(p)).into() } else { u }
}
fn _expand_path(p: &Path) -> PathBuf {
// ${HOME} or $HOME
#[cfg(unix)]
@ -68,14 +74,14 @@ fn _expand_path(p: &Path) -> PathBuf {
}
}
pub fn skip_path(p: &Path, u: usize) -> &Path {
let mut it = p.components();
for _ in 0..u {
pub fn skip_url(url: &Url, n: usize) -> Cow<'_, OsStr> {
let mut it = url.components();
for _ in 0..n {
if it.next().is_none() {
return Path::new("");
return OsStr::new("").into();
}
}
it.as_path()
it.os_str()
}
pub async fn unique_name<F>(u: Url, append: F) -> io::Result<Url>
@ -128,34 +134,29 @@ async fn _unique_name(mut url: Url, append: bool) -> io::Result<Url> {
Ok(url)
}
// Parameters
// * `path`: The absolute path(contains no `/./`) to get relative path.
// * `root`: The absolute path(contains no `/./`) to be compared.
//
// Return
// * Unix: The relative format to `root` of `path`.
// * Windows: The relative format to `root` of `path`; or `path` itself when
// `path` and `root` are both under different disk drives.
pub fn path_relative_to<'a>(path: &'a Path, root: &Path) -> Cow<'a, Path> {
assert!(path.is_absolute());
assert!(root.is_absolute());
let mut p_comps = path.components();
let mut r_comps = root.components();
pub fn url_relative_to<'a>(from: &Url, to: &'a Url) -> Result<Cow<'a, Url>> {
use yazi_shared::url::Component::*;
// 1. Ensure that the two paths have the same prefix.
// 2. Strips any common prefix the two paths do have.
//
// NOTE:
// Prefixes are platform dependent,
// but different prefixes would for example indicate paths for different drives
// on Windows.
let (p_head, r_head) = loop {
use std::path::Component::*;
match (p_comps.next(), r_comps.next()) {
(Some(RootDir), Some(RootDir)) => (),
(Some(Prefix(a)), Some(Prefix(b))) if a == b => (),
(Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => {
return Cow::from(path);
if from.is_absolute() != to.is_absolute() {
return if to.is_absolute() {
Ok(to.into())
} else {
bail!("Urls must be both absolute or both relative: {from:?} and {to:?}");
};
}
if from.covariant(to) {
return Ok(Url { loc: Path::new(".").into(), scheme: to.scheme.clone() }.into());
}
let (mut f_it, mut t_it) = (from.components(), to.components());
let (f_head, t_head) = loop {
match (f_it.next(), t_it.next()) {
(Some(Scheme(a)), Some(Scheme(b))) if a.covariant(b) => {}
(Some(RootDir), Some(RootDir)) => {}
(Some(Prefix(a)), Some(Prefix(b))) if a == b => {}
(Some(Scheme(_) | Prefix(_) | RootDir), _) | (_, Some(Scheme(_) | Prefix(_) | RootDir)) => {
return Ok(to.into());
}
(None, None) => break (None, None),
(a, b) if a != b => break (a, b),
@ -163,14 +164,11 @@ pub fn path_relative_to<'a>(path: &'a Path, root: &Path) -> Cow<'a, Path> {
}
};
let p_comps = p_head.into_iter().chain(p_comps);
let walk_up = r_head.into_iter().chain(r_comps).map(|_| Component::ParentDir);
let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir);
let rest = t_head.into_iter().chain(t_it);
let mut buf = PathBuf::new();
buf.extend(walk_up);
buf.extend(p_comps);
Cow::from(buf)
let buf: PathBuf = dots.chain(rest).collect();
Ok(Url { loc: buf.into(), scheme: to.scheme.clone() }.into())
}
#[cfg(windows)]
@ -196,35 +194,60 @@ pub fn backslash_to_slash(p: &Path) -> Cow<'_, Path> {
#[cfg(test)]
mod tests {
use std::{borrow::Cow, path::Path};
use std::borrow::Cow;
use super::path_relative_to;
use yazi_shared::url::Url;
use super::url_relative_to;
#[cfg(unix)]
#[test]
fn test_path_relative_to() {
fn assert(path: &str, root: &str, res: &str) {
assert_eq!(path_relative_to(Path::new(path), Path::new(root)), Cow::Borrowed(Path::new(res)));
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())
);
}
assert("/a/b", "/a/b/c", "../");
assert("/a/b/c", "/a/b", "c");
assert("/a/b/c", "/a/b/d", "../c");
assert("/a", "/a/b/c", "../../");
assert("/a/a/b", "/a/b/b", "../../a/b");
}
#[cfg(unix)]
{
// Same urls
assert("", "", ".");
assert(".", ".", ".");
assert("/a", "/a", ".");
assert("regular:///", "/", ".");
assert("regular://", "regular://", ".");
assert("regular://", "search://kw/", "search://kw/.");
assert("regular:///b", "search://kw//b", "search://kw/.");
#[cfg(windows)]
#[test]
fn test_path_relative_to() {
fn assert(path: &str, root: &str, res: &str) {
assert_eq!(path_relative_to(Path::new(path), Path::new(root)), Cow::Borrowed(Path::new(res)));
// Relative urls
assert("foo", "bar", "../bar");
// Absolute urls
assert("/a/b/c", "/a/b", "../");
assert("/a/b", "/a/b/c", "c");
assert("/a/b/d", "/a/b/c", "../c");
assert("/a/b/c", "/a", "../../");
assert("/a/b/b", "/a/a/b", "../../a/b");
assert("regular:///a/b", "regular:///a/b/c", "c");
assert("/a/b/c/", "search://kw//a/d/", "search://kw/../../d");
assert("search://kw//a/b/c", "search://kw//a/b", "search://kw/../");
// Different schemes
assert("", "sftp://test/", "sftp://test/");
assert("a", "sftp://test/", "sftp://test/");
assert("a", "sftp://test/b", "sftp://test/b");
assert("/a", "sftp://test//b", "sftp://test//b");
assert("sftp://test//a/b", "sftp://test//a/d", "sftp://test/../d");
}
#[cfg(windows)]
{
assert(r"C:\a\b\c", r"C:\a\b", r"..\");
assert(r"C:\a\b", r"C:\a\b\c", "c");
assert(r"C:\a\b\d", r"C:\a\b\c", r"..\c");
assert(r"C:\a\b\c", r"C:\a", r"..\..\");
assert(r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b");
}
assert("C:\\a\\b", "C:\\a\\b\\c", "..\\");
assert("C:\\a\\b\\c", "C:\\a\\b", "c");
assert("C:\\a\\b\\c", "C:\\a\\b\\d", "..\\c");
assert("C:\\a", "C:\\a\\b\\c", "..\\..\\");
assert("C:\\a\\a\\b", "C:\\a\\b\\b", "..\\..\\a\\b");
}
}

View file

@ -23,6 +23,11 @@ impl Local {
tokio::fs::create_dir_all(path).await
}
#[inline]
pub async fn hard_link(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
tokio::fs::hard_link(original, link).await
}
#[inline]
pub async fn metadata(url: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
tokio::fs::metadata(url).await
@ -41,6 +46,11 @@ impl Local {
tokio::fs::read_dir(path).await
}
#[inline]
pub fn read_dir_sync(path: impl AsRef<Path>) -> io::Result<std::fs::ReadDir> {
std::fs::read_dir(path)
}
#[inline]
pub async fn read_link(url: impl AsRef<Path>) -> io::Result<PathBuf> {
tokio::fs::read_link(url).await
@ -71,11 +81,40 @@ impl Local {
tokio::fs::rename(from, to).await
}
#[inline]
pub async fn symlink_dir(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
#[cfg(unix)]
{
tokio::fs::symlink(original, link).await
}
#[cfg(windows)]
{
tokio::fs::symlink_dir(original, link).await
}
}
#[inline]
pub async fn symlink_file(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
#[cfg(unix)]
{
tokio::fs::symlink(original, link).await
}
#[cfg(windows)]
{
tokio::fs::symlink_file(original, link).await
}
}
#[inline]
pub async fn symlink_metadata(path: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
tokio::fs::symlink_metadata(path).await
}
#[inline]
pub fn symlink_metadata_sync(path: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
std::fs::symlink_metadata(path)
}
#[inline]
pub async fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
tokio::fs::write(path, contents).await

View file

@ -24,6 +24,11 @@ pub async fn create_dir_all(url: impl AsRef<Url>) -> io::Result<()> {
Local::create_dir_all(url.as_ref()).await
}
#[inline]
pub async fn hard_link(original: impl AsRef<Url>, link: impl AsRef<Url>) -> io::Result<()> {
Local::hard_link(original.as_ref(), link.as_ref()).await
}
#[inline]
pub async fn metadata(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
Local::metadata(url.as_ref()).await
@ -39,6 +44,11 @@ pub async fn read_dir(url: impl AsRef<Url>) -> io::Result<tokio::fs::ReadDir> {
Local::read_dir(url.as_ref()).await
}
#[inline]
pub fn read_dir_sync(url: impl AsRef<Url>) -> io::Result<std::fs::ReadDir> {
Local::read_dir_sync(url.as_ref())
}
#[inline]
pub async fn read_link(url: impl AsRef<Url>) -> io::Result<Url> {
Local::read_link(url.as_ref()).await.map(Into::into)
@ -64,11 +74,25 @@ pub async fn rename(from: impl AsRef<Url>, to: impl AsRef<Url>) -> io::Result<()
Local::rename(from.as_ref(), to.as_ref()).await
}
#[inline]
pub async fn symlink_dir(original: impl AsRef<Url>, link: impl AsRef<Url>) -> io::Result<()> {
Local::symlink_dir(original.as_ref(), link.as_ref()).await
}
#[inline]
pub async fn symlink_file(original: impl AsRef<Url>, link: impl AsRef<Url>) -> io::Result<()> {
Local::symlink_file(original.as_ref(), link.as_ref()).await
}
#[inline]
pub async fn symlink_metadata(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
Local::symlink_metadata(url.as_ref()).await
}
pub fn symlink_metadata_sync(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
Local::symlink_metadata_sync(url.as_ref())
}
#[inline]
pub async fn write(url: impl AsRef<Url>, contents: impl AsRef<[u8]>) -> io::Result<()> {
Local::write(url.as_ref(), contents).await

View file

@ -30,9 +30,16 @@ pub fn fd(opt: FdOpt) -> Result<UnboundedReceiver<File>> {
}
fn spawn(program: &str, opt: &FdOpt) -> std::io::Result<Child> {
let Some(path) = opt.cwd.as_path() else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"fd can only search local filesystem",
));
};
Command::new(program)
.arg("--base-directory")
.arg(&opt.cwd)
.arg(path)
.arg("--regex")
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
.args(&opt.args)

View file

@ -1,6 +1,6 @@
use std::process::Stdio;
use anyhow::Result;
use anyhow::{Result, bail};
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}};
use yazi_fs::File;
use yazi_shared::url::Url;
@ -13,12 +13,16 @@ pub struct RgOpt {
}
pub fn rg(opt: RgOpt) -> Result<UnboundedReceiver<File>> {
let Some(path) = opt.cwd.as_path() else {
bail!("rg can only search local filesystem");
};
let mut child = Command::new("rg")
.args(["--color=never", "--files-with-matches", "--smart-case"])
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
.args(opt.args)
.arg(opt.subject)
.arg(&opt.cwd)
.arg(path)
.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::null())

View file

@ -1,6 +1,6 @@
use std::process::Stdio;
use anyhow::Result;
use anyhow::{Result, bail};
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}};
use yazi_fs::File;
use yazi_shared::url::Url;
@ -13,12 +13,16 @@ pub struct RgaOpt {
}
pub fn rga(opt: RgaOpt) -> Result<UnboundedReceiver<File>> {
let Some(path) = opt.cwd.as_path() else {
bail!("rga can only search local filesystem");
};
let mut child = Command::new("rga")
.args(["--color=never", "--files-with-matches", "--smart-case"])
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
.args(opt.args)
.arg(opt.subject)
.arg(&opt.cwd)
.arg(path)
.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::null())

View file

@ -1,3 +1,5 @@
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};
@ -159,7 +161,7 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
fn calc_size(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, url: UrlRef| async move {
match yazi_fs::SizeCalculator::new(&*url).await {
match yazi_fs::SizeCalculator::new(&url).await {
Ok(it) => SizeCalculator(it).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
@ -167,13 +169,16 @@ fn calc_size(lua: &Lua) -> mlua::Result<Function> {
}
fn expand_url(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, value: Value| {
use yazi_fs::expand_path;
Ok(Url::new(match value {
Value::String(s) => expand_path(s.to_str()?.as_ref()),
Value::UserData(ud) => expand_path(&*ud.borrow::<yazi_binding::Url>()?),
_ => Err("must be a string or a Url".into_lua_err())?,
}))
lua.create_function(|lua, value: Value| {
use yazi_fs::expand_url;
match &value {
Value::String(s) => Url::new(expand_url(Url::try_from(s.as_bytes().as_ref())?)).into_lua(lua),
Value::UserData(ud) => match expand_url(&*ud.borrow::<yazi_binding::Url>()?) {
Cow::Borrowed(_) => Ok(value),
Cow::Owned(u) => Url::new(u).into_lua(lua),
},
_ => Err("must be a string or a Url".into_lua_err()),
}
})
}

View file

@ -1,11 +1,10 @@
// FIXME: VFS, depends on yazi_fs::fns
use std::{borrow::Cow, collections::VecDeque};
use anyhow::{Result, anyhow};
use tokio::{fs::{self, DirEntry}, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tokio::{fs::DirEntry, 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, path_relative_to, services, skip_path};
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, services, skip_url, url_relative_to};
use yazi_shared::{Id, url::Url};
use super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash};
@ -27,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(fs::remove_file(&task.to).await)?;
ok_or_not_found(services::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 {
fs::remove_file(&task.from).await.ok();
services::remove_file(&task.from).await.ok();
}
break;
}
@ -63,8 +62,8 @@ impl File {
let cha = task.cha.unwrap();
let src = if task.resolve {
match fs::read_link(&task.from).await {
Ok(p) => Cow::Owned(p),
match services::read_link(&task.from).await {
Ok(u) => Cow::Owned(u),
Err(e) if e.kind() == NotFound => {
warn!("Link task partially done: {task:?}");
return Ok(self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?);
@ -72,46 +71,39 @@ impl File {
Err(e) => Err(e)?,
}
} else {
Cow::Borrowed(task.from.as_path())
Cow::Borrowed(&task.from)
};
let src = if task.relative {
path_relative_to(&src, &fs::canonicalize(task.to.parent().unwrap()).await?)
url_relative_to(&services::canonicalize(task.to.parent_url().unwrap()).await?, &src)?
} else {
src
};
ok_or_not_found(fs::remove_file(&task.to).await)?;
#[cfg(unix)]
{
fs::symlink(src, &task.to).await?;
}
#[cfg(windows)]
{
if cha.is_dir() {
fs::symlink_dir(src, &task.to).await?;
} else {
fs::symlink_file(src, &task.to).await?;
}
ok_or_not_found(services::remove_file(&task.to).await)?;
if cha.is_dir() {
services::symlink_dir(src, &task.to).await?;
} else {
services::symlink_file(src, &task.to).await?;
}
if task.delete {
fs::remove_file(&task.from).await.ok();
services::remove_file(&task.from).await.ok();
}
self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?;
}
FileIn::Hardlink(task) => {
let cha = task.cha.unwrap();
let src = if !task.follow {
Cow::Borrowed(task.from.as_path())
} else if let Ok(p) = fs::canonicalize(&task.from).await {
Cow::Borrowed(&task.from)
} else if let Ok(p) = services::canonicalize(&task.from).await {
Cow::Owned(p)
} else {
Cow::Borrowed(task.from.as_path())
Cow::Borrowed(&task.from)
};
ok_or_not_found(fs::remove_file(&task.to).await)?;
match fs::hard_link(src, &task.to).await {
ok_or_not_found(services::remove_file(&task.to).await)?;
match services::hard_link(src, &task.to).await {
Err(e) if e.kind() == NotFound => {
warn!("Hardlink task partially done: {task:?}");
}
@ -121,7 +113,7 @@ impl File {
self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?;
}
FileIn::Delete(task) => {
if let Err(e) = fs::remove_file(&task.target).await
if let Err(e) = services::remove_file(&task.target).await
&& e.kind() != NotFound
&& maybe_exists(&task.target).await
{
@ -153,7 +145,7 @@ impl File {
}
pub async fn paste(&self, mut task: FileInPaste) -> Result<()> {
if task.cut && ok_or_not_found(fs::rename(&task.from, &task.to).await).is_ok() {
if task.cut && ok_or_not_found(services::rename(&task.from, &task.to).await).is_ok() {
return self.succ(task.id);
}
@ -192,13 +184,13 @@ impl File {
let mut dirs = VecDeque::from([task.from.clone()]);
while let Some(src) = dirs.pop_front() {
let dest = root.join(skip_path(&src, skip));
continue_unless_ok!(match fs::create_dir(&dest).await {
let dest = root.join(skip_url(&src, skip));
continue_unless_ok!(match services::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => Err(e),
_ => Ok(()),
});
let mut it = continue_unless_ok!(fs::read_dir(&src).await);
let mut it = continue_unless_ok!(services::read_dir(&src).await);
while let Ok(Some(entry)) = it.next_entry().await {
let from = Url::from(entry.path());
let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await);
@ -263,13 +255,13 @@ impl File {
let mut dirs = VecDeque::from([task.from.clone()]);
while let Some(src) = dirs.pop_front() {
let dest = root.join(skip_path(&src, skip));
continue_unless_ok!(match fs::create_dir(&dest).await {
let dest = root.join(skip_url(&src, skip));
continue_unless_ok!(match services::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => Err(e),
_ => Ok(()),
});
let mut it = continue_unless_ok!(fs::read_dir(&src).await);
let mut it = continue_unless_ok!(services::read_dir(&src).await);
while let Ok(Some(entry)) = it.next_entry().await {
let from = Url::from(entry.path());
let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await);
@ -288,7 +280,7 @@ impl File {
}
pub async fn delete(&self, mut task: FileInDelete) -> Result<()> {
let meta = fs::symlink_metadata(&task.target).await?;
let meta = services::symlink_metadata(&task.target).await?;
if !meta.is_dir() {
let id = task.id;
task.length = meta.len();
@ -299,7 +291,7 @@ impl File {
let mut dirs = VecDeque::from([task.target]);
while let Some(target) = dirs.pop_front() {
let Ok(mut it) = fs::read_dir(target).await else { continue };
let Ok(mut it) = services::read_dir(target).await else { continue };
while let Ok(Some(entry)) = it.next_entry().await {
let Ok(meta) = entry.metadata().await else { continue };

View file

@ -33,3 +33,28 @@ impl<'a> IntoOsStr<'a> for &'a [u8] {
fn into_os_str(self) -> Result<Cow<'a, OsStr>, Self::Error> { Cow::Borrowed(self).into_os_str() }
}
// --- OsStrJoin
pub trait OsStrJoin {
fn join(&self, sep: &OsStr) -> OsString;
}
impl<T> OsStrJoin for Vec<T>
where
T: AsRef<OsStr>,
{
fn join(&self, sep: &OsStr) -> OsString {
if self.is_empty() {
return OsString::new();
}
let mut result = OsString::new();
for (i, item) in self.iter().enumerate() {
if i > 0 {
result.push(sep);
}
result.push(item.as_ref());
}
result
}
}

View file

@ -0,0 +1,157 @@
use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, path::{self, PathBuf, PrefixComponent}};
use crate::url::{Scheme, Url};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Component<'a> {
Scheme(&'a Scheme),
Prefix(PrefixComponent<'a>),
RootDir,
CurDir,
ParentDir,
Normal(&'a OsStr),
}
impl<'a> From<path::Component<'a>> for Component<'a> {
fn from(comp: path::Component<'a>) -> Self {
match comp {
path::Component::Prefix(p) => Component::Prefix(p),
path::Component::RootDir => Component::RootDir,
path::Component::CurDir => Component::CurDir,
path::Component::ParentDir => Component::ParentDir,
path::Component::Normal(s) => Component::Normal(s),
}
}
}
impl<'a> FromIterator<Component<'a>> for Url {
fn from_iter<I: IntoIterator<Item = Component<'a>>>(iter: I) -> Self {
let mut scheme = Scheme::Regular;
let mut buf = PathBuf::new();
iter.into_iter().for_each(|c| match c {
Component::Scheme(s) => scheme = s.clone(),
Component::Prefix(p) => buf.push(path::Component::Prefix(p)),
Component::RootDir => buf.push(path::Component::RootDir),
Component::CurDir => buf.push(path::Component::CurDir),
Component::ParentDir => buf.push(path::Component::ParentDir),
Component::Normal(s) => buf.push(path::Component::Normal(s)),
});
Self { loc: buf.into(), scheme }
}
}
impl<'a> FromIterator<Component<'a>> for PathBuf {
fn from_iter<I: IntoIterator<Item = Component<'a>>>(iter: I) -> Self {
let mut buf = PathBuf::new();
iter.into_iter().for_each(|c| match c {
Component::Scheme(_) => {}
Component::Prefix(p) => buf.push(path::Component::Prefix(p)),
Component::RootDir => buf.push(path::Component::RootDir),
Component::CurDir => buf.push(path::Component::CurDir),
Component::ParentDir => buf.push(path::Component::ParentDir),
Component::Normal(s) => buf.push(path::Component::Normal(s)),
});
buf
}
}
// --- Components
#[derive(Clone)]
pub struct Components<'a> {
inner: path::Components<'a>,
scheme: &'a Scheme,
scheme_yielded: bool,
}
impl<'a> Components<'a> {
pub fn new(url: &'a Url) -> Self {
Self {
inner: url.loc.components(),
scheme: &url.scheme,
scheme_yielded: false,
}
}
pub fn os_str(&self) -> Cow<'a, OsStr> {
if !self.scheme.is_virtual() || self.scheme_yielded {
return Cow::Borrowed(self.inner.as_path().as_os_str());
}
let mut oss = OsString::from(format!("{}", self.scheme));
oss.push(self.inner.as_path());
Cow::Owned(oss)
}
}
impl<'a> Iterator for Components<'a> {
type Item = Component<'a>;
fn next(&mut self) -> Option<Self::Item> {
if !self.scheme_yielded {
self.scheme_yielded = true;
Some(Component::Scheme(self.scheme))
} else {
self.inner.next().map(Into::into)
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let (min, max) = self.inner.size_hint();
let scheme = self.scheme_yielded.not() as usize;
(min + scheme, max.map(|n| n + scheme))
}
}
impl<'a> DoubleEndedIterator for Components<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
if let Some(comp) = self.inner.next_back() {
Some(comp.into())
} else if !self.scheme_yielded {
self.scheme_yielded = true;
Some(Component::Scheme(self.scheme))
} else {
None
}
}
}
impl<'a> FusedIterator for Components<'a> {}
impl<'a> PartialEq for Components<'a> {
fn eq(&self, other: &Self) -> bool {
Some(self.scheme).filter(|_| !self.scheme_yielded)
== Some(other.scheme).filter(|_| !other.scheme_yielded)
&& self.inner == other.inner
}
}
// --- Tests
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn test_collect() {
let search = Url::try_from("search://keyword//root/projects/yazi").unwrap();
assert_eq!(search.scheme, Scheme::Search("keyword".to_owned()));
let item = search.join("main.rs");
assert_eq!(item.scheme, Scheme::SearchItem);
let u: Url = item.components().take(4).collect();
assert_eq!(u.scheme, Scheme::SearchItem);
assert_eq!(u.loc.as_path(), Path::new("/root/projects"));
let u: Url = item
.components()
.take(5)
.chain([Component::Normal(OsStr::new("target/release/yazi"))])
.collect();
assert_eq!(u.scheme, Scheme::SearchItem);
assert_eq!(u.loc.as_path(), Path::new("/root/projects/yazi/target/release/yazi"));
}
}

View file

@ -1,32 +1,28 @@
use std::{cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use std::{borrow::Cow, cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use crate::url::{Urn, UrnBuf};
#[derive(Clone, Default)]
pub struct Loc {
path: PathBuf,
urn: usize,
name: usize,
inner: PathBuf,
urn: usize,
name: usize,
}
unsafe impl Send for Loc {}
unsafe impl Sync for Loc {}
impl Deref for Loc {
type Target = PathBuf;
fn deref(&self) -> &Self::Target { &self.path }
fn deref(&self) -> &Self::Target { &self.inner }
}
impl PartialEq for Loc {
fn eq(&self, other: &Self) -> bool { self.path == other.path }
fn eq(&self, other: &Self) -> bool { self.inner == other.inner }
}
impl Eq for Loc {}
impl Ord for Loc {
fn cmp(&self, other: &Self) -> cmp::Ordering { self.path.cmp(&other.path) }
fn cmp(&self, other: &Self) -> cmp::Ordering { self.inner.cmp(&other.inner) }
}
impl PartialOrd for Loc {
@ -34,24 +30,36 @@ impl PartialOrd for Loc {
}
impl Hash for Loc {
fn hash<H: Hasher>(&self, state: &mut H) { self.path.hash(state) }
fn hash<H: Hasher>(&self, state: &mut H) { self.inner.hash(state) }
}
impl Debug for Loc {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("Loc")
.field("path", &self.path)
.field("path", &self.inner)
.field("urn", &self.urn())
.field("name", &self.name())
.finish()
}
}
impl Loc {
pub fn new(path: PathBuf) -> Self {
impl From<&Path> for Loc {
fn from(value: &Path) -> Self { Self::from(value.to_path_buf()) }
}
impl From<Cow<'_, Path>> for Loc {
fn from(value: Cow<'_, Path>) -> Self { Self::from(value.into_owned()) }
}
impl From<Cow<'_, OsStr>> for Loc {
fn from(value: Cow<'_, OsStr>) -> Self { Self::from(PathBuf::from(value.into_owned())) }
}
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 { path, urn, name: 0 };
return Self { inner: path, urn, name: 0 };
};
let name_len = name.len();
@ -62,15 +70,17 @@ impl Loc {
let mut bytes = path.into_os_string().into_encoded_bytes();
bytes.truncate(name_len + prefix_len as usize);
Self {
path: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }),
urn: name_len,
name: name_len,
inner: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }),
urn: name_len,
name: name_len,
}
}
}
pub fn from(base: &Path, path: PathBuf) -> Self {
let mut loc = Self::new(path);
loc.urn = loc.path.strip_prefix(base).unwrap_or(&loc.path).as_os_str().len();
impl Loc {
pub fn with(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
}
@ -108,7 +118,7 @@ impl Loc {
}
self.name = name.len();
self.path.set_file_name(name);
self.inner.set_file_name(name);
}
#[inline]
@ -120,20 +130,23 @@ impl Loc {
})
}
#[inline]
pub fn has_base(&self) -> bool { self.bytes().len() != self.urn }
#[inline]
pub fn rebase(&self, parent: &Path) -> Self {
debug_assert!(self.urn == self.name);
let path = parent.join(self.name());
debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name));
Self { path, urn: self.name, name: self.name }
Self { inner: path, urn: self.name, name: self.name }
}
#[inline]
pub fn into_path(self) -> PathBuf { self.path }
pub fn into_path(self) -> PathBuf { self.inner }
#[inline]
fn bytes(&self) -> &[u8] { self.path.as_os_str().as_encoded_bytes() }
fn bytes(&self) -> &[u8] { self.inner.as_os_str().as_encoded_bytes() }
}
#[cfg(test)]
@ -144,17 +157,17 @@ mod tests {
#[test]
fn test_new() {
let loc = Loc::new("/".into());
let loc: Loc = Path::new("/").into();
assert_eq!(loc.urn(), Urn::new("/"));
assert_eq!(loc.name(), OsStr::new(""));
assert_eq!(loc.base(), Path::new(""));
let loc = Loc::new("/root".into());
let loc: Loc = Path::new("/root").into();
assert_eq!(loc.urn(), Urn::new("root"));
assert_eq!(loc.name(), OsStr::new("root"));
assert_eq!(loc.base(), Path::new("/"));
let loc = Loc::new("/root/code/foo/".into());
let loc: Loc = Path::new("/root/code/foo/").into();
assert_eq!(loc.urn(), Urn::new("foo"));
assert_eq!(loc.name(), OsStr::new("foo"));
assert_eq!(loc.base(), Path::new("/root/code/"));
@ -162,17 +175,17 @@ mod tests {
#[test]
fn test_from() {
let loc = Loc::from(Path::new("/"), "/".into());
let loc = Loc::with(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::from(Path::new("/root/"), "/root/code/".into());
let loc = Loc::with(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::from(Path::new("/root//"), "/root/code/foo//".into());
let loc = Loc::with(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/"));
@ -180,7 +193,7 @@ mod tests {
#[test]
fn test_set_name() {
let mut loc = Loc::from(Path::new("/root"), "/root/code/foo/".into());
let mut loc = Loc::with(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/"));

View file

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

View file

@ -1,36 +1,92 @@
use std::fmt::Display;
use std::{borrow::Cow, fmt::Display};
use anyhow::bail;
use anyhow::{Result, bail};
use percent_encoding::{CONTROLS, PercentEncode, percent_decode, percent_encode};
#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
use crate::BytesExt;
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Scheme {
#[default]
Regular,
Search,
Search(String),
SearchItem,
Archive,
Archive(String),
Sftp(String),
}
impl TryFrom<&[u8]> for Scheme {
type Error = anyhow::Error;
impl Scheme {
pub(super) fn parse(bytes: &[u8]) -> Result<(Self, usize)> {
let Some((protocol, rest)) = bytes.split_by_seq(b"://") else {
return Ok((Self::Regular, 0));
};
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Ok(match value {
b"regular" => Scheme::Regular,
b"search" => Scheme::Search,
b"archive" => Scheme::Archive,
_ => bail!("Unknown URL scheme: {}", String::from_utf8_lossy(value)),
Ok(match protocol {
b"regular" => (Self::Regular, 10),
b"search" => {
let (name, skip) = Self::decode_param(rest)?;
(Self::Search(name), 9 + skip)
}
b"archive" => {
let (name, skip) = Self::decode_param(rest)?;
(Self::Archive(name), 10 + skip)
}
b"sftp" => {
let (name, skip) = Self::decode_param(rest)?;
(Self::Sftp(name), 7 + skip)
}
_ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)),
})
}
pub fn covariant(&self, other: &Self) -> bool {
match (self, other) {
// Local files
(
Self::Regular | Self::Search(_) | Self::SearchItem,
Self::Regular | Self::Search(_) | Self::SearchItem,
) => true,
// Virtual files within the same namespace
(a, b) => a == b,
}
}
#[inline]
pub(super) fn is_virtual(&self) -> bool {
match self {
Self::Regular | Self::Search(_) | Self::SearchItem => 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();
let s = 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))
}
#[inline]
fn encode_param<'a>(s: &'a str) -> PercentEncode<'a> { percent_encode(s.as_bytes(), CONTROLS) }
}
impl Display for Scheme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Scheme::Regular => write!(f, "regular"),
Scheme::Search => write!(f, "search"),
Scheme::SearchItem => write!(f, "search_item"),
Scheme::Archive => write!(f, "archive"),
Self::Regular => write!(f, "regular://"),
Self::Search(kw) => write!(f, "search://{}/", Self::encode_param(kw)),
Self::SearchItem => write!(f, "search_item://"),
Self::Archive(id) => write!(f, "archive://{}/", Self::encode_param(id)),
Self::Sftp(id) => write!(f, "sftp://{}/", Self::encode_param(id)),
}
}
}

View file

@ -1,18 +1,17 @@
use std::{borrow::Cow, ffi::{OsStr, OsString}, fmt::{Debug, Display, Formatter}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Display, Formatter}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode};
use percent_encoding::{AsciiSet, CONTROLS, percent_encode};
use serde::{Deserialize, Serialize};
use super::UrnBuf;
use crate::{BytesExt, IntoOsStr, url::{Loc, Scheme}};
use crate::{IntoOsStr, url::{Components, Loc, Scheme}};
const ENCODE_SET: &AsciiSet = &CONTROLS.add(b'#');
#[derive(Clone, Default, Eq, Ord, PartialOrd)]
pub struct Url {
loc: Loc,
scheme: Scheme,
frag: OsString,
pub loc: Loc,
pub scheme: Scheme,
}
impl Deref for Url {
@ -23,21 +22,23 @@ impl Deref for Url {
impl Debug for Url {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.scheme {
Scheme::Regular => write!(f, "Regular({:?})", self.loc),
Scheme::Search => write!(f, "Search({:?}, {})", self.loc, self.frag.display()),
Scheme::SearchItem => write!(f, "SearchItem({:?})", self.loc),
Scheme::Archive => write!(f, "Archive({:?})", self.loc),
let Self { scheme, loc } = self;
match scheme {
Scheme::Regular => write!(f, "{scheme}{}", loc.display()),
Scheme::Search(_) => write!(f, "{scheme}{}", loc.display()),
Scheme::SearchItem => write!(f, "{scheme}{}", loc.display()),
Scheme::Archive(_) => write!(f, "{scheme}{}", loc.display()),
Scheme::Sftp(_) => write!(f, "{scheme}{}", loc.display()),
}
}
}
impl From<Loc> for Url {
fn from(loc: Loc) -> Self { Self { loc, ..Default::default() } }
fn from(loc: Loc) -> Self { Self { loc, scheme: Scheme::Regular } }
}
impl From<PathBuf> for Url {
fn from(path: PathBuf) -> Self { Loc::new(path).into() }
fn from(path: PathBuf) -> Self { Loc::from(path).into() }
}
impl From<&PathBuf> for Url {
@ -45,35 +46,16 @@ impl From<&PathBuf> for Url {
}
impl From<&Path> for Url {
fn from(path: &Path) -> Self { path.to_owned().into() }
fn from(path: &Path) -> Self { path.to_path_buf().into() }
}
impl TryFrom<&[u8]> for Url {
type Error = anyhow::Error;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
let mut url = Url::default();
let Some((scheme, rest)) = value.split_by_seq(b"://") else {
url.loc = Loc::new(value.into_os_str()?.into_owned().into());
return Ok(url);
};
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let (scheme, skip) = Scheme::parse(bytes)?;
url.scheme = Scheme::try_from(scheme)?;
if url.scheme == Scheme::Regular {
url.loc = Loc::new(rest.into_os_str()?.into_owned().into());
return Ok(url);
}
let (loc, frag) = match rest.split_by_seq(b"#") {
None => (rest, OsString::new()),
Some((a, b)) => (a, b.into_os_str()?.into_owned()),
};
// FIXME: use `Loc::from(base, path)` instead
url.loc = Loc::new(Cow::from(percent_decode(loc)).into_os_str()?.into_owned().into());
url.frag = frag;
Ok(url)
Ok(Url { loc: bytes[skip..].into_os_str()?.into(), scheme })
}
}
@ -93,14 +75,12 @@ impl AsRef<Url> for Url {
fn as_ref(&self) -> &Url { self }
}
// FIXME: remove
impl AsRef<Path> for Url {
fn as_ref(&self) -> &Path { &self.loc }
}
impl AsRef<OsStr> for Url {
fn as_ref(&self) -> &OsStr { self.loc.as_os_str() }
}
// FIXME: remove
impl Display for Url {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if matches!(self.scheme, Scheme::Regular | Scheme::SearchItem) {
@ -108,16 +88,13 @@ impl Display for Url {
}
let loc = percent_encode(self.loc.as_os_str().as_encoded_bytes(), ENCODE_SET);
write!(f, "{}://{loc}", self.scheme)?;
if !self.frag.is_empty() {
write!(f, "#{}", percent_encode(self.frag.as_encoded_bytes(), ENCODE_SET))?;
}
write!(f, "{}{loc}", self.scheme)?;
Ok(())
}
}
// FIXME: remove
impl From<&Url> for String {
fn from(url: &Url) -> Self { url.to_string() }
}
@ -136,37 +113,77 @@ impl From<Cow<'_, Url>> for Url {
impl Url {
#[inline]
pub fn join(&self, path: impl AsRef<Path>) -> Self {
match self.scheme {
Scheme::Regular => Self::from(self.loc.join(path)),
Scheme::Search => {
let loc = Loc::from(&self.loc, self.loc.join(path));
Self::from(loc).into_search_item()
}
Scheme::SearchItem => {
let loc = Loc::from(self.loc.base(), self.loc.join(path));
Self::from(loc).into_search_item()
}
Scheme::Archive => Self::from(self.loc.join(path)).into_archive(),
pub fn base(&self) -> Url {
let loc: Loc = self.loc.base().into();
match &self.scheme {
Scheme::Regular => Self { loc, scheme: Scheme::Regular },
Scheme::Search(_) => Self { loc, scheme: self.scheme.clone() },
Scheme::SearchItem => Self { loc, scheme: Scheme::Search(String::new()) },
Scheme::Archive(_) => Self { loc, scheme: self.scheme.clone() },
Scheme::Sftp(_) => Self { loc, scheme: self.scheme.clone() },
}
}
#[inline]
pub fn parent_url(&self) -> Option<Url> {
let p = self.loc.parent()?;
Some(match self.scheme {
Scheme::Regular | Scheme::Search => Self::from(p),
Scheme::SearchItem => {
if p == self.loc.base() {
Self::from(p).into_search("")
} else {
Self::from(p).into_search_item()
}
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::Archive => Self::from(p),
Scheme::SearchItem => {
Self { loc: Loc::with(self.loc.base(), self.loc.join(path)), scheme: Scheme::SearchItem }
}
Scheme::Archive(_) => {
Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() }
}
Scheme::Sftp(_) => Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() },
}
}
// FIXME: check usages
#[inline]
pub fn components(&self) -> Components<'_> { Components::new(self) }
#[inline]
pub fn covariant(&self, other: &Self) -> bool {
self.scheme.covariant(&other.scheme) && self.loc == other.loc
}
pub fn parent_url(&self) -> Option<Url> {
let parent = self.loc.parent()?;
let base = self.loc.base();
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::Archive(_) => Self { loc: parent.into(), scheme: self.scheme.clone() },
Scheme::Sftp(_) => Self { loc: parent.into(), scheme: self.scheme.clone() },
})
}
pub fn strip_prefix(&self, base: impl AsRef<Url>) -> Option<Self> {
let base = base.as_ref();
if !self.scheme.covariant(&base.scheme) {
return None;
}
Some(Self {
loc: self.loc.strip_prefix(base.loc.as_path()).ok()?.into(),
scheme: self.scheme.clone(),
})
}
#[inline]
pub fn as_path(&self) -> Option<&Path> {
Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual())
}
#[inline]
pub fn set_name(&mut self, name: impl AsRef<OsStr>) { self.loc.set_name(name); }
@ -189,82 +206,53 @@ impl Url {
pub fn is_regular(&self) -> bool { self.scheme == Scheme::Regular }
#[inline]
pub fn to_regular(&self) -> Self { Self { loc: self.loc.clone(), ..Default::default() } }
pub fn to_regular(&self) -> Self { Self { loc: self.loc.clone(), scheme: Scheme::Regular } }
#[inline]
pub fn into_regular(mut self) -> Self {
self.scheme = Scheme::Regular;
self.frag = OsString::new();
self
}
// --- Search
#[inline]
pub fn is_search(&self) -> bool { self.scheme == Scheme::Search }
pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) }
#[inline]
pub fn to_search(&self, frag: impl AsRef<OsStr>) -> Self {
Self { loc: self.loc.clone(), scheme: Scheme::Search, frag: frag.as_ref().to_owned() }
pub fn to_search(&self, frag: impl AsRef<str>) -> Self {
Self { loc: self.loc.clone(), scheme: Scheme::Search(frag.as_ref().to_owned()) }
}
#[inline]
pub fn into_search(mut self, frag: impl AsRef<OsStr>) -> Self {
self.scheme = Scheme::Search;
self.frag = frag.as_ref().to_owned();
pub fn into_search(mut self, frag: impl AsRef<str>) -> Self {
self.scheme = Scheme::Search(frag.as_ref().to_owned());
self
}
// --- Archive
#[inline]
pub fn is_search_item(&self) -> bool { self.scheme == Scheme::SearchItem }
#[inline]
pub fn into_search_item(mut self) -> Self {
self.scheme = Scheme::SearchItem;
self.frag = OsString::new();
self
}
#[inline]
pub fn is_archive(&self) -> bool { self.scheme == Scheme::Archive }
#[inline]
pub fn to_archive(&self) -> Self {
Self { loc: self.loc.clone(), scheme: Scheme::Archive, ..Default::default() }
}
#[inline]
pub fn into_archive(mut self) -> Self {
self.scheme = Scheme::Archive;
self.frag = OsString::new();
self
}
// --- Loc
#[inline]
pub fn set_loc(&mut self, loc: Loc) { self.loc = loc; }
pub fn is_archive(&self) -> bool { matches!(self.scheme, Scheme::Archive(_)) }
// FIXME: remove
#[inline]
pub fn into_path(self) -> PathBuf { self.loc.into_path() }
// --- Scheme
#[inline]
pub fn scheme(&self) -> Scheme { self.scheme }
// --- Frag
#[inline]
pub fn frag(&self) -> &OsStr { &self.frag }
}
impl Hash for Url {
fn hash<H: Hasher>(&self, state: &mut H) {
self.loc.hash(state);
match self.scheme {
Scheme::Regular | Scheme::SearchItem => {}
Scheme::Search | Scheme::Archive => {
match &self.scheme {
Scheme::Regular => {}
Scheme::Search(_) => {
self.scheme.hash(state);
}
Scheme::SearchItem => {}
Scheme::Archive(_) => {
self.scheme.hash(state);
}
Scheme::Sftp(_) => {
self.scheme.hash(state);
self.frag.hash(state);
}
}
}
@ -272,11 +260,13 @@ impl Hash for Url {
impl PartialEq for Url {
fn eq(&self, other: &Self) -> bool {
match (self.scheme, other.scheme) {
(Scheme::Regular | Scheme::SearchItem, Scheme::Regular | Scheme::SearchItem) => {
self.loc == other.loc
}
_ => self.loc == other.loc && self.scheme == other.scheme,
if self.loc != other.loc {
return false;
}
match (&self.scheme, &other.scheme) {
(Scheme::Regular | Scheme::SearchItem, Scheme::Regular | Scheme::SearchItem) => true,
_ => self.scheme == other.scheme,
}
}
}
@ -293,6 +283,6 @@ impl<'de> Deserialize<'de> for Url {
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Url::try_from(s).map_err(serde::de::Error::custom)
Self::try_from(s).map_err(serde::de::Error::custom)
}
}