From 0e3cd8545c07e79d7cf1f58632c87cc14faa16d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sun, 27 Jul 2025 18:06:13 +0800 Subject: [PATCH] feat: URL schemas (#3019) --- yazi-actor/src/lives/file.rs | 11 +- yazi-actor/src/mgr/bulk_rename.rs | 95 ++++++++---- yazi-actor/src/mgr/copy.rs | 3 +- yazi-actor/src/mgr/hover.rs | 6 +- yazi-binding/src/url.rs | 36 +++-- yazi-cli/src/main.rs | 2 +- yazi-cli/src/package/add.rs | 2 +- yazi-cli/src/package/delete.rs | 6 +- yazi-cli/src/package/deploy.rs | 5 +- yazi-cli/src/package/install.rs | 2 +- yazi-cli/src/shared/mod.rs | 1 + yazi-cli/src/shared/shared.rs | 45 ++++++ yazi-config/src/lib.rs | 2 +- yazi-config/src/mgr/mgr.rs | 8 +- yazi-config/src/scheme/mod.rs | 1 + yazi-config/src/scheme/sftp.rs | 9 ++ yazi-core/src/mgr/mimetype.rs | 40 +---- yazi-core/src/mgr/yanked.rs | 1 + yazi-fs/src/calculator.rs | 30 ++-- yazi-fs/src/cwd.rs | 25 ++-- yazi-fs/src/fns.rs | 174 ++++++++++------------ yazi-fs/src/path.rs | 163 ++++++++++++--------- yazi-fs/src/services/local.rs | 39 +++++ yazi-fs/src/services/services.rs | 24 +++ yazi-plugin/src/external/fd.rs | 9 +- yazi-plugin/src/external/rg.rs | 8 +- yazi-plugin/src/external/rga.rs | 8 +- yazi-plugin/src/fs/fs.rs | 21 ++- yazi-scheduler/src/file/file.rs | 66 ++++----- yazi-shared/src/osstr.rs | 25 ++++ yazi-shared/src/url/component.rs | 157 ++++++++++++++++++++ yazi-shared/src/url/loc.rs | 79 +++++----- yazi-shared/src/url/mod.rs | 2 +- yazi-shared/src/url/scheme.rs | 90 +++++++++--- yazi-shared/src/url/url.rs | 234 ++++++++++++++---------------- 35 files changed, 924 insertions(+), 505 deletions(-) create mode 100644 yazi-cli/src/shared/mod.rs create mode 100644 yazi-cli/src/shared/shared.rs create mode 100644 yazi-config/src/scheme/mod.rs create mode 100644 yazi-config/src/scheme/sftp.rs create mode 100644 yazi-shared/src/url/component.rs diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index b8eef2a3..2de0a85b 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -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::("cx")?.borrow_scoped(|core: &yazi_core::Core| { diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 941fda27..e41c2baa 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -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 { 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::>().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, new: Vec) -> Result<()> { + async fn r#do(root: usize, old: Vec, new: Vec, selected: Vec) -> 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, new: Vec) -> 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, new: Vec) -> 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(&self, state: &mut H) { self.1.hash(state); } +} + +impl AsRef for Tuple { + fn as_ref(&self) -> &OsStr { &self.1 } +} + +impl Tuple { + fn new(index: usize, inner: impl Into) -> 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(); diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index 74b7f93c..d615d0f2 100644 --- a/yazi-actor/src/mgr/copy.rs +++ b/yazi-actor/src/mgr/copy.rs @@ -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)); diff --git a/yazi-actor/src/mgr/hover.rs b/yazi-actor/src/mgr/hover.rs index 9b4b1489..ec2a6d81 100644 --- a/yazi-actor/src/mgr/hover.rs +++ b/yazi-actor/src/mgr/hover.rs @@ -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 { // 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 diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index eed87519..91261abb 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -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 for Url { fn as_ref(&self) -> &yazi_shared::url::Url { &self.inner } } -// FIXME: remove -impl AsRef for Url { - fn as_ref(&self) -> &Path { self.inner.as_path() } -} - impl From for yazi_shared::url::Url { fn from(value: Url) -> Self { value.inner } } +impl From 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::()?.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::()?.inner.into_search(frag.as_bytes().into_os_str()?))) + methods.add_function_mut("into_search", |_, (ud, frag): (AnyUserData, String)| { + Ok(Self::new(ud.take::()?.inner.into_search(frag))) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner)); diff --git a/yazi-cli/src/main.rs b/yazi-cli/src/main.rs index 4f1265ad..9902447b 100644 --- a/yazi-cli/src/main.rs +++ b/yazi-cli/src/main.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_pub!(package); +yazi_macro::mod_pub!(package shared); yazi_macro::mod_flat!(args); diff --git a/yazi-cli/src/package/add.rs b/yazi-cli/src/package/add.rs index 567d2594..b3d019e8 100644 --- a/yazi-cli/src/package/add.rs +++ b/yazi-cli/src/package/add.rs @@ -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<()> { diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs index d42b6caf..e4c220a5 100644 --- a/yazi-cli/src/package/delete.rs +++ b/yazi-cli/src/package/delete.rs @@ -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(()) } diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index 226e1192..95275de4 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -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?; diff --git a/yazi-cli/src/package/install.rs b/yazi-cli/src/package/install.rs index 0fc6aec5..61f25d33 100644 --- a/yazi-cli/src/package/install.rs +++ b/yazi-cli/src/package/install.rs @@ -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<()> { diff --git a/yazi-cli/src/shared/mod.rs b/yazi-cli/src/shared/mod.rs new file mode 100644 index 00000000..4b4c220d --- /dev/null +++ b/yazi-cli/src/shared/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(shared); diff --git a/yazi-cli/src/shared/shared.rs b/yazi-cli/src/shared/shared.rs new file mode 100644 index 00000000..8578e544 --- /dev/null +++ b/yazi-cli/src/shared/shared.rs @@ -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) -> bool { + Local::symlink_metadata(path).await.is_ok() +} + +#[inline] +pub async fn maybe_exists(path: impl AsRef) -> 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 +} diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 57f8b011..8107b79a 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -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); diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index 2277a934..62d00093 100644 --- a/yazi-config/src/mgr/mgr.rs +++ b/yazi-config/src/mgr/mgr.rs @@ -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()) }; diff --git a/yazi-config/src/scheme/mod.rs b/yazi-config/src/scheme/mod.rs new file mode 100644 index 00000000..330ec80f --- /dev/null +++ b/yazi-config/src/scheme/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(sftp); diff --git a/yazi-config/src/scheme/sftp.rs b/yazi-config/src/scheme/sftp.rs new file mode 100644 index 00000000..30cb8b6a --- /dev/null +++ b/yazi-config/src/scheme/sftp.rs @@ -0,0 +1,9 @@ +use std::path::PathBuf; + +pub struct Sftp { + pub host: String, + pub user: String, + pub port: u16, + pub password: Option, + pub key_file: Option, +} diff --git a/yazi-core/src/mgr/mimetype.rs b/yazi-core/src/mgr/mimetype.rs index bb739bbf..16320c34 100644 --- a/yazi-core/src/mgr/mimetype.rs +++ b/yazi-core/src/mgr/mimetype.rs @@ -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); +pub struct Mimetype(HashMap); 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 { @@ -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) { - 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) { self.0.extend(iter) } } diff --git a/yazi-core/src/mgr/yanked.rs b/yazi-core/src/mgr/yanked.rs index 11ba3235..e585ceff 100644 --- a/yazi-core/src/mgr/yanked.rs +++ b/yazi-core/src/mgr/yanked.rs @@ -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) }) } diff --git a/yazi-fs/src/calculator.rs b/yazi-fs/src/calculator.rs index 554274b5..d6388f10 100644 --- a/yazi-fs/src/calculator.rs +++ b/yazi-fs/src/calculator.rs @@ -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; +use crate::services; + +type Task = Either; pub enum SizeCalculator { Idle((VecDeque, Option)), @@ -11,23 +13,23 @@ pub enum SizeCalculator { } impl SizeCalculator { - pub async fn new(path: impl AsRef) -> std::io::Result { - let p = path.as_ref().to_owned(); + pub async fn new(url: &Url) -> io::Result { + 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) -> std::io::Result { - let mut it = Self::new(path).await?; + pub async fn total(url: &Url) -> io::Result { + 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> { + pub async fn next(&mut self) -> io::Result> { poll_fn(|cx| { loop { match self { @@ -61,7 +63,7 @@ impl SizeCalculator { .await } - fn next_chunk(buf: &mut VecDeque>) -> Option { + fn next_chunk(buf: &mut VecDeque>) -> Option { 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(); } diff --git a/yazi-fs/src/cwd.rs b/yazi-fs/src/cwd.rs index d9c49bde..e10e642f 100644 --- a/yazi-fs/src/cwd.rs +++ b/yazi-fs/src/cwd.rs @@ -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(); } }); } diff --git a/yazi-fs/src/fns.rs b/yazi-fs/src/fns.rs index 68b14980..2be87d02 100644 --- a/yazi-fs/src/fns.rs +++ b/yazi-fs/src/fns.rs @@ -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) -> bool { fs::symlink_metadata(p).await.is_ok() } - -#[inline] -pub async fn maybe_exists(p: impl AsRef) -> bool { - match fs::symlink_metadata(p).await { +pub async fn maybe_exists(u: impl AsRef) -> 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) -> bool { - fs::metadata(p).await.is_ok_and(|m| m.is_dir()) +pub async fn must_be_dir(u: impl AsRef) -> 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 { 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 { - let name = p.file_name()?; - if p == fs::canonicalize(p).await.ok()? { +pub async fn realname(u: &Url) -> Option { + 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 { #[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>) { @@ -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> { 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 { +async fn _copy_with_progress(from: Url, to: Url, cha: Cha) -> io::Result { 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]) -> 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::(); - 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::>().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"); } diff --git a/yazi-fs/src/path.rs b/yazi-fs/src/path.rs index 0194d2e7..e97aaa43 100644 --- a/yazi-fs/src/path.rs +++ b/yazi-fs/src/path.rs @@ -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) -> 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) -> PathBuf { _expand_path(p.as_ref()) } +#[inline] +pub fn expand_url<'a>(url: impl Into>) -> 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(u: Url, append: F) -> io::Result @@ -128,34 +134,29 @@ async fn _unique_name(mut url: Url, append: bool) -> io::Result { 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> { + 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"); } } diff --git a/yazi-fs/src/services/local.rs b/yazi-fs/src/services/local.rs index 1770dfbe..a832115d 100644 --- a/yazi-fs/src/services/local.rs +++ b/yazi-fs/src/services/local.rs @@ -23,6 +23,11 @@ impl Local { tokio::fs::create_dir_all(path).await } + #[inline] + pub async fn hard_link(original: impl AsRef, link: impl AsRef) -> io::Result<()> { + tokio::fs::hard_link(original, link).await + } + #[inline] pub async fn metadata(url: impl AsRef) -> io::Result { 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) -> io::Result { + std::fs::read_dir(path) + } + #[inline] pub async fn read_link(url: impl AsRef) -> io::Result { 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, link: impl AsRef) -> 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, link: impl AsRef) -> 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) -> io::Result { tokio::fs::symlink_metadata(path).await } + #[inline] + pub fn symlink_metadata_sync(path: impl AsRef) -> io::Result { + std::fs::symlink_metadata(path) + } + #[inline] pub async fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> io::Result<()> { tokio::fs::write(path, contents).await diff --git a/yazi-fs/src/services/services.rs b/yazi-fs/src/services/services.rs index 4eaac448..c797501b 100644 --- a/yazi-fs/src/services/services.rs +++ b/yazi-fs/src/services/services.rs @@ -24,6 +24,11 @@ pub async fn create_dir_all(url: impl AsRef) -> io::Result<()> { Local::create_dir_all(url.as_ref()).await } +#[inline] +pub async fn hard_link(original: impl AsRef, link: impl AsRef) -> io::Result<()> { + Local::hard_link(original.as_ref(), link.as_ref()).await +} + #[inline] pub async fn metadata(url: impl AsRef) -> io::Result { Local::metadata(url.as_ref()).await @@ -39,6 +44,11 @@ pub async fn read_dir(url: impl AsRef) -> io::Result { Local::read_dir(url.as_ref()).await } +#[inline] +pub fn read_dir_sync(url: impl AsRef) -> io::Result { + Local::read_dir_sync(url.as_ref()) +} + #[inline] pub async fn read_link(url: impl AsRef) -> io::Result { Local::read_link(url.as_ref()).await.map(Into::into) @@ -64,11 +74,25 @@ pub async fn rename(from: impl AsRef, to: impl AsRef) -> io::Result<() Local::rename(from.as_ref(), to.as_ref()).await } +#[inline] +pub async fn symlink_dir(original: impl AsRef, link: impl AsRef) -> io::Result<()> { + Local::symlink_dir(original.as_ref(), link.as_ref()).await +} + +#[inline] +pub async fn symlink_file(original: impl AsRef, link: impl AsRef) -> io::Result<()> { + Local::symlink_file(original.as_ref(), link.as_ref()).await +} + #[inline] pub async fn symlink_metadata(url: impl AsRef) -> io::Result { Local::symlink_metadata(url.as_ref()).await } +pub fn symlink_metadata_sync(url: impl AsRef) -> io::Result { + Local::symlink_metadata_sync(url.as_ref()) +} + #[inline] pub async fn write(url: impl AsRef, contents: impl AsRef<[u8]>) -> io::Result<()> { Local::write(url.as_ref(), contents).await diff --git a/yazi-plugin/src/external/fd.rs b/yazi-plugin/src/external/fd.rs index 5739f32d..bd1a70db 100644 --- a/yazi-plugin/src/external/fd.rs +++ b/yazi-plugin/src/external/fd.rs @@ -30,9 +30,16 @@ pub fn fd(opt: FdOpt) -> Result> { } fn spawn(program: &str, opt: &FdOpt) -> std::io::Result { + 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) diff --git a/yazi-plugin/src/external/rg.rs b/yazi-plugin/src/external/rg.rs index a14d9d1e..3944677a 100644 --- a/yazi-plugin/src/external/rg.rs +++ b/yazi-plugin/src/external/rg.rs @@ -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> { + 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()) diff --git a/yazi-plugin/src/external/rga.rs b/yazi-plugin/src/external/rga.rs index b4f6aaf5..ce4e9656 100644 --- a/yazi-plugin/src/external/rga.rs +++ b/yazi-plugin/src/external/rga.rs @@ -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> { + 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()) diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 7340c3b4..1e062d02 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -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 { fn calc_size(lua: &Lua) -> mlua::Result { 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 { } fn expand_url(lua: &Lua) -> mlua::Result { - 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::()?), - _ => 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::()?) { + Cow::Borrowed(_) => Ok(value), + Cow::Owned(u) => Url::new(u).into_lua(lua), + }, + _ => Err("must be a string or a Url".into_lua_err()), + } }) } diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 03fa47e7..21be8206 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -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 }; diff --git a/yazi-shared/src/osstr.rs b/yazi-shared/src/osstr.rs index 096e930b..7fc560af 100644 --- a/yazi-shared/src/osstr.rs +++ b/yazi-shared/src/osstr.rs @@ -33,3 +33,28 @@ impl<'a> IntoOsStr<'a> for &'a [u8] { fn into_os_str(self) -> Result, Self::Error> { Cow::Borrowed(self).into_os_str() } } + +// --- OsStrJoin +pub trait OsStrJoin { + fn join(&self, sep: &OsStr) -> OsString; +} + +impl OsStrJoin for Vec +where + T: AsRef, +{ + 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 + } +} diff --git a/yazi-shared/src/url/component.rs b/yazi-shared/src/url/component.rs new file mode 100644 index 00000000..164361af --- /dev/null +++ b/yazi-shared/src/url/component.rs @@ -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> 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> for Url { + fn from_iter>>(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> for PathBuf { + fn from_iter>>(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 { + 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) { + 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 { + 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")); + } +} diff --git a/yazi-shared/src/url/loc.rs b/yazi-shared/src/url/loc.rs index c106dab5..17db445b 100644 --- a/yazi-shared/src/url/loc.rs +++ b/yazi-shared/src/url/loc.rs @@ -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(&self, state: &mut H) { self.path.hash(state) } + fn hash(&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> for Loc { + fn from(value: Cow<'_, Path>) -> Self { Self::from(value.into_owned()) } +} + +impl From> for Loc { + fn from(value: Cow<'_, OsStr>) -> Self { Self::from(PathBuf::from(value.into_owned())) } +} + +impl From 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/")); diff --git a/yazi-shared/src/url/mod.rs b/yazi-shared/src/url/mod.rs index 8cac7ad0..650ea0b7 100644 --- a/yazi-shared/src/url/mod.rs +++ b/yazi-shared/src/url/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(loc scheme url urn); +yazi_macro::mod_flat!(component loc scheme url urn); diff --git a/yazi-shared/src/url/scheme.rs b/yazi-shared/src/url/scheme.rs index 3a8bf357..655d9d19 100644 --- a/yazi-shared/src/url/scheme.rs +++ b/yazi-shared/src/url/scheme.rs @@ -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 { - 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)), } } } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 29854c72..885aa2e7 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -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 for Url { - fn from(loc: Loc) -> Self { Self { loc, ..Default::default() } } + fn from(loc: Loc) -> Self { Self { loc, scheme: Scheme::Regular } } } impl From 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 { - 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 { + 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 for Url { fn as_ref(&self) -> &Url { self } } +// FIXME: remove impl AsRef for Url { fn as_ref(&self) -> &Path { &self.loc } } -impl AsRef 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> for Url { impl Url { #[inline] - pub fn join(&self, path: impl AsRef) -> 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 { - 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) -> 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 { + 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) -> Option { + 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) { 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) -> Self { - Self { loc: self.loc.clone(), scheme: Scheme::Search, frag: frag.as_ref().to_owned() } + pub fn to_search(&self, frag: impl AsRef) -> Self { + Self { loc: self.loc.clone(), scheme: Scheme::Search(frag.as_ref().to_owned()) } } #[inline] - pub fn into_search(mut self, frag: impl AsRef) -> Self { - self.scheme = Scheme::Search; - self.frag = frag.as_ref().to_owned(); + pub fn into_search(mut self, frag: impl AsRef) -> 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(&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) } }