diff --git a/yazi-actor/src/cmp/show.rs b/yazi-actor/src/cmp/show.rs index a29c98db..dba4420c 100644 --- a/yazi-actor/src/cmp/show.rs +++ b/yazi-actor/src/cmp/show.rs @@ -4,7 +4,7 @@ use anyhow::Result; use yazi_core::cmp::CmpItem; use yazi_macro::{render, succ}; use yazi_parser::cmp::ShowForm; -use yazi_shared::{data::Data, path::{AsPath, PathDyn}, strand::StrandLike}; +use yazi_shared::{data::Data, path::{DynPath, PathDyn}, strand::StrandLike}; use crate::{Actor, Ctx}; @@ -30,7 +30,7 @@ impl Actor for Show { succ!(); }; - cmp.matches = Self::match_candidates(opt.word.as_path(), cache); + cmp.matches = Self::match_candidates(opt.word.dyn_path(), cache); if cmp.matches.is_empty() { succ!(render!(mem::replace(&mut cmp.visible, false))); } diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index f53c8cba..e67aad65 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -6,7 +6,7 @@ use yazi_fs::{engine::{DirReader, FileHolder}, path::clean_url}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::TriggerForm; use yazi_proxy::CmpProxy; -use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{DynPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::engine; use crate::{Actor, Ctx}; @@ -78,7 +78,7 @@ impl Trigger { } // Child - let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.as_path(), |(_, c)| c).to_owned(); + let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.dyn_path(), |(_, c)| c).to_owned(); // Parent let url = UrlCow::try_from((spec.clone().zeroed(), path)).ok()?; diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index 8a283bed..a3fbb0b0 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -4,7 +4,7 @@ use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods}; use yazi_binding::{Range, style::Style}; use yazi_config::THEME; use yazi_fs::file::FileInventory; -use yazi_shared::{path::AsPath, url::UrlLike}; +use yazi_shared::{path::DynPath, url::UrlLike}; use super::{FILE_CACHE, Lives}; use crate::lives::{CoreRef, PtrCell}; @@ -73,7 +73,11 @@ impl UserData for File { Ok(yazi_config::THEME.icon.matches(me, me.is_hovered())) }); methods.add_method("size", |_, me, ()| { - Ok(if me.is_dir() { me.folder.entries.sizes.get(&me.urn()).copied() } else { Some(me.len) }) + Ok(if me.is_dir() { + me.folder.entries.sizes.get(&me.entry_key()).copied() + } else { + Some(me.len) + }) }); methods.add_method("mime", |lua, me, ()| { let core: CoreRef = lua.named_registry_value("cx")?; @@ -86,7 +90,7 @@ impl UserData for File { let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components(); comp.next_back(); - Some(lua.create_string(comp.as_path().encoded_bytes())).transpose() + Some(lua.create_string(comp.dyn_path().encoded_bytes())).transpose() }); methods.add_method("style", |lua, me, ()| { let core: CoreRef = lua.named_registry_value("cx")?; @@ -122,7 +126,7 @@ impl UserData for File { return Ok(None); }; - let Some(idx) = finder.matched_idx(&me.folder, me.urn()) else { + let Some(idx) = finder.matched_idx(&me.folder, me.entry_key()) else { return Ok(None); }; diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index ab852086..2e6b9efc 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -81,8 +81,8 @@ impl Cd { return MgrProxy::cd(&url, CdSource::Cd); } - if let Some(p) = url.parent() { - FilesOp::Upserting(p.into(), [(url.urn().into(), file)].into()).emit(); + if let Some((p, k)) = url.pair2() { + FilesOp::Upserting(p.into(), [(k.into(), file)].into()).emit(); } MgrProxy::reveal(url); } diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 0613a212..3bd14da7 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -64,10 +64,10 @@ impl Create { if dir { engine::create_dir_all(&new).await?; } else if let Ok(real) = engine::casefold(&new).await - && let Some((parent, urn)) = real.pair() + && let Some((parent, key)) = real.pair2() { ok_or_not_found!(engine::remove_file(&new).await); - FilesOp::Deleting(parent.into(), [urn.into()].into()).emit(); + FilesOp::Deleting(parent.into(), [key.into()].into()).emit(); engine::create(&new).await?; } else if let Some(parent) = new.parent() { engine::create_dir_all(parent).await.ok(); @@ -78,10 +78,10 @@ impl Create { } if let Ok(real) = engine::casefold(&new).await - && let Some((parent, urn)) = real.pair() + && let Some((parent, key)) = real.pair2() { let file = File::new(&real).await?; - FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()).emit(); + FilesOp::Upserting(parent.into(), [(key.into(), file)].into()).emit(); MgrProxy::reveal(&real); } diff --git a/yazi-actor/src/mgr/filter_do.rs b/yazi-actor/src/mgr/filter_do.rs index 0d76cf11..a995c65f 100644 --- a/yazi-actor/src/mgr/filter_do.rs +++ b/yazi-actor/src/mgr/filter_do.rs @@ -16,10 +16,10 @@ impl Actor for FilterDo { fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result { let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) }; - let hovered = cx.hovered().map(|f| f.urn().into()); + let hovered = cx.hovered().map(|f| f.entry_key().into()); cx.current_mut().entries.set_filter(filter); - if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { + if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) { act!(mgr:hover, cx, hovered)?; act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; diff --git a/yazi-actor/src/mgr/hidden.rs b/yazi-actor/src/mgr/hidden.rs index 6113805a..02cb0ad5 100644 --- a/yazi-actor/src/mgr/hidden.rs +++ b/yazi-actor/src/mgr/hidden.rs @@ -18,7 +18,7 @@ impl Actor for Hidden { let state = form.state.bool(cx.tab().pref.show_hidden); cx.tab_mut().pref.show_hidden = state; - let hovered = cx.hovered().map(|f| f.urn().to_owned()); + let hovered = cx.hovered().map(|f| f.entry_key().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); @@ -43,7 +43,7 @@ impl Actor for Hidden { { render!(h.repos(None)); act!(mgr:peek, cx, true)?; - } else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { + } else if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) { act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; } diff --git a/yazi-actor/src/mgr/hover.rs b/yazi-actor/src/mgr/hover.rs index b702d837..4cdfcebb 100644 --- a/yazi-actor/src/mgr/hover.rs +++ b/yazi-actor/src/mgr/hover.rs @@ -18,19 +18,19 @@ impl Actor for Hover { // Parent should always track CWD if let Some(p) = &mut tab.parent { - render!(p.repos(tab.current.url.try_strip_prefix(&p.url).ok())); + render!(p.repos(Some(tab.current.url.entry_key()))); } // Repos CWD - render!(tab.current.repos(form.urn.as_ref().map(Into::into))); + render!(tab.current.repos(form.key.as_ref().map(Into::into))); // Turn on tracing - if let (Some(h), Some(u)) = (tab.hovered(), form.urn) - && h.urn() == u + if let (Some(h), Some(key)) = (tab.hovered(), form.key) + && h.entry_key() == key { // `hover(Some)` occurs after user actions, such as create, rename, reveal, etc. - // At this point, it's intuitive to track the file location regardless. - tab.current.trace = Some(u.clone()); + // At this point, it's intuitive to track the entry regardless. + tab.current.trace = Some(key.clone()); cx.tasks.scheduler.behavior.reset(); } diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 423640de..031134fe 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -66,8 +66,8 @@ impl Actor for Rename { impl Rename { async fn r#do(tab: Id, old: UrlBuf, new: UrlBuf) -> Result<()> { - let Some((old_p, old_n)) = old.pair() else { return Ok(()) }; - let Some(_) = new.pair() else { return Ok(()) }; + let Some((old_p, old_k)) = old.pair2() else { return Ok(()) }; + let Some(_) = new.pair2() else { return Ok(()) }; let _permit = WATCHER.acquire().await.unwrap(); let overwritten = engine::casefold(&new).await; @@ -75,21 +75,21 @@ impl Rename { if let Ok(u) = overwritten && u != new - && let Some((parent, urn)) = u.pair() + && let Some((parent, key)) = u.pair2() { ok_or_not_found!(engine::rename(&u, &new).await); - FilesOp::Deleting(parent.to_owned(), [urn.into()].into()).emit(); + FilesOp::Deleting(parent.to_owned(), [key.into()].into()).emit(); } let new = engine::casefold(&new).await?; - let Some((new_p, new_n)) = new.pair() else { return Ok(()) }; + let Some((new_p, new_k)) = new.pair2() else { return Ok(()) }; let file = File::new(&new).await?; if new_p == old_p { - FilesOp::Upserting(old_p.into(), [(old_n.into(), file)].into()).emit(); + FilesOp::Upserting(old_p.into(), [(old_k.into(), file)].into()).emit(); } else { - FilesOp::Deleting(old_p.into(), [old_n.into()].into()).emit(); - FilesOp::Upserting(new_p.into(), [(new_n.into(), file)].into()).emit(); + FilesOp::Deleting(old_p.into(), [old_k.into()].into()).emit(); + FilesOp::Upserting(new_p.into(), [(new_k.into(), file)].into()).emit(); } MgrProxy::reveal(&new); diff --git a/yazi-actor/src/mgr/reveal.rs b/yazi-actor/src/mgr/reveal.rs index e7133d06..ad197364 100644 --- a/yazi-actor/src/mgr/reveal.rs +++ b/yazi-actor/src/mgr/reveal.rs @@ -14,7 +14,7 @@ impl Actor for Reveal { const NAME: &str = "reveal"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { - let Some((parent, child)) = form.target.pair() else { succ!() }; + let Some((parent, child)) = form.target.pair2() else { succ!() }; // Cd to the parent directory act!(mgr:cd, cx, (parent, form.source))?; @@ -25,7 +25,7 @@ impl Actor for Reveal { // If the child is not hovered, which means it doesn't exist, // create a dummy file - if !form.no_dummy && tab.hovered().is_none_or(|f| child != f.urn()) { + if !form.no_dummy && tab.hovered().is_none_or(|f| f.entry_key() != child) { let op = FilesOp::Creating(parent.into(), vec![File::from_dummy(&form.target, None)]); tab.current.update_pub(tab.id, op); } diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index 270e2067..dd94b4f8 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -24,7 +24,7 @@ impl Actor for Sort { pref.sort_fallback = form.fallback.unwrap_or(pref.sort_fallback); let sorter = FilesSorter::from(&*pref); - let hovered = cx.hovered().map(|f| f.urn().to_owned()); + let hovered = cx.hovered().map(|f| f.entry_key().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); @@ -50,7 +50,7 @@ impl Actor for Sort { { render!(h.repos(None)); act!(mgr:peek, cx, true)?; - } else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { + } else if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) { act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; } diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index ff4a7b3a..94ceb1a1 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -57,12 +57,12 @@ impl UpdateFiles { fn update_parent(cx: &mut Ctx, op: FilesOp) -> Result { let tab = cx.tab_mut(); - let urn = tab.current.url.urn(); - let leave = matches!(op, FilesOp::Deleting(_, ref urns) if urns.contains(&urn)); + let key = tab.current.url.entry_key(); + let leave = matches!(op, FilesOp::Deleting(_, ref keys) if keys.contains(&key)); if let Some(f) = tab.parent.as_mut() { render!(f.update_pub(tab.id, op)); - render!(f.hover(urn)); + render!(f.hover(key)); } if leave { @@ -97,8 +97,8 @@ impl UpdateFiles { fn update_history(cx: &mut Ctx, op: FilesOp) -> Result { let tab = &mut cx.tab_mut(); - let leave = tab.parent.as_ref().and_then(|f| f.url.parent().map(|p| (p, f.url.urn()))).is_some_and( - |(p, n)| matches!(op, FilesOp::Deleting(ref parent, ref urns) if *parent == p && urns.contains(&n)), + let leave = tab.parent.as_ref().and_then(|f| f.url.pair2()).is_some_and( + |(pp, key)| matches!(&op, FilesOp::Deleting(parent, keys) if parent == pp && keys.contains(&key)), ); tab.history.get_or_insert_with(op.cwd(), |u| Folder::from(u)).update_pub(tab.id, op); diff --git a/yazi-actor/src/tasks/update_succeed.rs b/yazi-actor/src/tasks/update_succeed.rs index 0b286951..7d0186a5 100644 --- a/yazi-actor/src/tasks/update_succeed.rs +++ b/yazi-actor/src/tasks/update_succeed.rs @@ -19,10 +19,10 @@ impl Actor for UpdateSucceed { if form.track && form.id == cx.tasks.scheduler.behavior.first_id() - && let Some((parent, urn)) = form.urls[0].pair() + && let Some((parent, key)) = form.urls[0].pair2() && parent == *cx.cwd() { - cx.current_mut().trace = Some(urn.into()); + cx.current_mut().trace = Some(key.into()); act!(mgr:hover, cx)?; } diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index bb4d1659..33bde408 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use yazi_binding::position::{Offset, Origin, Position}; use yazi_codegen::{DeserializeOver, DeserializeOver2}; -use yazi_shared::{spec::Encode as EncodeSpec, url::Url}; +use yazi_shared::{spec::EncodeSpec, url::Url}; use yazi_widgets::input::InputOpt; #[derive(Deserialize, DeserializeOver, DeserializeOver2)] diff --git a/yazi-config/src/vfs/authorities.rs b/yazi-config/src/vfs/authorities.rs index 23ba0a19..e7899798 100644 --- a/yazi-config/src/vfs/authorities.rs +++ b/yazi-config/src/vfs/authorities.rs @@ -1,16 +1,28 @@ +use std::sync::Arc; + use hashbrown::HashMap; use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}}; -use yazi_shared::auth::Scheme; +use yazi_shared::auth::{Auth, Domain, Scheme}; use yazi_shim::toml::DeserializeOverWith; -use super::{DomainSeed, Domains, Service}; +use super::{DomainSeed, Domains}; +use crate::vfs::Service; pub struct Authorities(HashMap); impl Authorities { - pub fn get(&self, scheme: &Scheme, domain: &str) -> Option<&Service> { + pub fn service(&self, scheme: &Scheme, domain: &Domain<'_>) -> Option<&Service> { self.0.get(scheme)?.get(domain) } + + pub fn auth(&self, scheme: &Scheme, domain: &Domain<'_>) -> Option> { + let service = self.service(scheme, domain)?; + if service.auth().domain.is_catchall() { + Some(Auth::new(service.kind(), scheme.clone(), domain.clone())) + } else { + Some(service.auth().clone()) + } + } } impl<'de> Deserialize<'de> for Authorities { @@ -41,7 +53,7 @@ impl<'de> Deserialize<'de> for Authorities { impl DeserializeOverWith for Authorities { fn deserialize_over_with<'de, D: Deserializer<'de>>(mut self, de: D) -> Result { for (scheme, domains) in Self::deserialize(de)?.0 { - self.0.entry(scheme).or_default().extend(domains.0); + self.0.entry(scheme).or_default().extend(domains); } Ok(self) } diff --git a/yazi-config/src/vfs/domains.rs b/yazi-config/src/vfs/domains.rs index 665e07d3..d6f81b77 100644 --- a/yazi-config/src/vfs/domains.rs +++ b/yazi-config/src/vfs/domains.rs @@ -1,34 +1,66 @@ -use std::{ops::{Deref, DerefMut}, sync::Arc}; +use std::sync::Arc; use hashbrown::HashMap; -use serde::{Deserialize, Deserializer, de::{DeserializeSeed, Error}}; -use yazi_shared::{KebabCasedKey, auth::Scheme}; +use serde::{Deserialize, Deserializer, de::{self, DeserializeSeed, Error}}; +use yazi_shared::auth::{AuthKind, Domain, Scheme}; use super::{Service, ServiceSftp}; #[derive(Default)] -pub struct Domains(pub(super) HashMap); - -impl Deref for Domains { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for Domains { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +pub struct Domains { + exact: HashMap, Service>, + catchall: Option, } impl Domains { + pub fn get(&self, domain: &Domain<'_>) -> Option<&Service> { + self.exact.get(domain.as_ref()).or(self.catchall.as_ref()) + } + + pub fn extend(&mut self, other: Self) { + self.exact.extend(other.exact); + if other.catchall.is_some() { + self.catchall = other.catchall; + } + } + fn init(&mut self, scheme: &Scheme) { - for (domain, service) in &mut self.0 { + for (domain, service) in &mut self.exact { let kind = service.kind(); let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); auth.kind = kind; auth.scheme = scheme.clone(); - auth.domain = domain.clone().into(); + auth.domain = domain.clone(); } + + if let Some(service) = &mut self.catchall { + let kind = service.kind(); + let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); + + auth.kind = kind; + auth.scheme = scheme.clone(); + auth.domain = Domain::CATCHALL; + } + } + + fn from_map(map: HashMap, Service>) -> Result + where + E: de::Error, + { + let mut domains = Self::default(); + for (domain, service) in map { + if domain.is_catchall() { + domains.catchall = Some(service); + continue; + } + + if service.kind() == AuthKind::Hub { + return Err(E::custom("Hub services require a `*` catch-all domain")); + } + domains.exact.insert(domain, service); + } + Ok(domains) } } @@ -39,22 +71,24 @@ impl<'de> DeserializeSeed<'de> for DomainSeed<'_> { type Value = Domains; fn deserialize>(self, deserializer: D) -> Result { - let mut domains = Domains(match self.0 { + let mut domains = match self.0 { Scheme::Regular | Scheme::Search => { return Err(D::Error::custom("scheme cannot be configured")); } Scheme::Sftp => { - let map = HashMap::::deserialize(deserializer)?; - map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect() + let map = HashMap::, ServiceSftp>::deserialize(deserializer)?; + Domains::from_map( + map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect(), + )? } Scheme::Custom(_) => { - let map = HashMap::::deserialize(deserializer)?; + let map = HashMap::, Service>::deserialize(deserializer)?; if map.values().any(|service| matches!(service, Service::Sftp(_))) { return Err(D::Error::custom("SFTP services must use the `sftp` scheme")); } - map + Domains::from_map(map)? } - }); + }; domains.init(self.0); Ok(domains) diff --git a/yazi-config/src/vfs/service.rs b/yazi-config/src/vfs/service.rs index 9c5604fa..4e47e49e 100644 --- a/yazi-config/src/vfs/service.rs +++ b/yazi-config/src/vfs/service.rs @@ -10,6 +10,7 @@ use super::{ServiceLua, ServiceSftp}; pub enum Service { Sftp(ServiceSftp), Mount(ServiceLua), + Hub(ServiceLua), Scope(ServiceLua), } @@ -19,7 +20,7 @@ impl TryFrom<&'static Service> for &'static ServiceSftp { fn try_from(value: &'static Service) -> Result { match value { Service::Sftp(p) => Ok(p), - Service::Mount(_) | Service::Scope(_) => { + Service::Mount(_) | Service::Hub(_) | Service::Scope(_) => { Err("expected an SFTP service, got a custom VFS service") } } @@ -32,7 +33,7 @@ impl TryFrom<&'static Service> for &'static ServiceLua { fn try_from(value: &'static Service) -> Result { match value { Service::Sftp(_) => Err("expected a custom VFS service, got an SFTP service"), - Service::Mount(lua) | Service::Scope(lua) => Ok(lua), + Service::Mount(lua) | Service::Hub(lua) | Service::Scope(lua) => Ok(lua), } } } @@ -42,6 +43,7 @@ impl Service { match self { Self::Sftp(_) => AuthKind::Sftp, Self::Mount(_) => AuthKind::Mount, + Self::Hub(_) => AuthKind::Hub, Self::Scope(_) => AuthKind::Scope, } } @@ -50,6 +52,7 @@ impl Service { match self { Self::Sftp(sftp) => &sftp.auth, Self::Mount(lua) => &lua.auth, + Self::Hub(lua) => &lua.auth, Self::Scope(lua) => &lua.auth, } } @@ -58,6 +61,7 @@ impl Service { match self { Self::Sftp(sftp) => &mut sftp.auth, Self::Mount(lua) => &mut lua.auth, + Self::Hub(lua) => &mut lua.auth, Self::Scope(lua) => &mut lua.auth, } } diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs index 6ed4ca24..902b8357 100644 --- a/yazi-config/src/vfs/vfs.rs +++ b/yazi-config/src/vfs/vfs.rs @@ -21,7 +21,7 @@ impl Vfs { where P: TryFrom<&'static Service, Error = &'static str>, { - let Some(value) = VFS.authorities.get(&auth.scheme, &auth.domain) else { + let Some(value) = VFS.authorities.service(&auth.scheme, &auth.domain) else { return Err(io::Error::other(format!("No such VFS service: {auth}"))); }; @@ -47,8 +47,6 @@ impl DeserializeOverWith for Vfs { // --- Inject inventory::submit! { AuthInventory { - get: |scheme, domain| { - VFS.authorities.get(scheme, domain).map(|service| service.auth().clone()) - }, + get: |scheme, domain| VFS.authorities.auth(scheme, domain), } } diff --git a/yazi-core/src/mgr/yanked.rs b/yazi-core/src/mgr/yanked.rs index babb92be..008189a6 100644 --- a/yazi-core/src/mgr/yanked.rs +++ b/yazi-core/src/mgr/yanked.rs @@ -67,7 +67,7 @@ impl Yanked { } pub fn apply_op(&mut self, op: &FilesOp) { - let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); + let (removal, addition) = op.diff_recoverable(self.urls()); if !removal.is_empty() { let old = self.files.len(); self.files.retain(|f| !removal.iter().any(|u| f.url.covariant(u))); diff --git a/yazi-core/src/tab/finder.rs b/yazi-core/src/tab/finder.rs index fa8111e0..0594ba97 100644 --- a/yazi-core/src/tab/finder.rs +++ b/yazi-core/src/tab/finder.rs @@ -1,7 +1,7 @@ use anyhow::Result; use hashbrown::HashMap; use yazi_fs::{Entries, Filter, FilterCase}; -use yazi_shared::{path::{AsPath, PathBufDyn}, url::UrlBuf}; +use yazi_shared::{path::{DynPath, PathBufDyn}, url::UrlBuf}; use crate::tab::Folder; @@ -62,7 +62,7 @@ impl Finder { continue; } - self.matched.insert(file.urn().into(), i); + self.matched.insert(file.entry_key().into(), i); if self.matched.len() > 99 { break; } @@ -78,9 +78,9 @@ impl Finder { impl Finder { pub fn matched_idx(&self, folder: &Folder, urn: T) -> Option where - T: AsPath, + T: DynPath, { - if self.lock == *folder { self.matched.get(&urn.as_path()).copied() } else { None } + if self.lock == *folder { self.matched.get(&urn.dyn_path()).copied() } else { None } } } diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index 60f68565..e9b75d49 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -4,7 +4,7 @@ use yazi_config::{LAYOUT, YAZI}; use yazi_dds::Pubsub; use yazi_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File}; use yazi_macro::err; -use yazi_shared::{id::Id, path::{AsPath, PathBufDyn, PathDyn}, url::UrlBuf}; +use yazi_shared::{id::Id, path::{DynPath, PathBufDyn, PathDyn}, url::UrlBuf}; use yazi_widgets::{Scrollable, Step}; use crate::MgrProxy; @@ -104,24 +104,26 @@ impl Folder { b } - pub fn hover(&mut self, urn: PathDyn) -> bool { - if self.hovered().map(|h| h.urn()) == Some(urn) { + pub fn hover(&mut self, key: PathDyn) -> bool { + if key.is_empty() { + return self.arrow(0); + } else if self.hovered().map(|h| h.entry_key()) == Some(key) { return self.arrow(0); } - let new = self.entries.position(urn).unwrap_or(self.cursor) as isize; + let new = self.entries.position(key).unwrap_or(self.cursor) as isize; let b = self.arrow(new - self.cursor as isize); self.retrace(); b } - pub fn repos(&mut self, urn: Option) -> bool { - if let Some(u) = urn { - self.hover(u) - } else if let Some(u) = self.trace.take() { - let b = self.hover(u.as_path()); - self.trace = Some(u); + pub fn repos(&mut self, key: Option) -> bool { + if let Some(k) = key { + self.hover(k) + } else if let Some(k) = self.trace.take() { + let b = self.hover(k.dyn_path()); + self.trace = Some(k); b } else { self.arrow(0) @@ -129,7 +131,7 @@ impl Folder { } pub fn retrace(&mut self) { - self.trace = self.hovered().map(|h| h.urn().into()).or(self.trace.take()); + self.trace = self.hovered().map(|h| h.entry_key().into()).or(self.trace.take()); } pub fn sync_page(&mut self, force: bool) { diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs index 5805d022..c41fab77 100644 --- a/yazi-core/src/tab/selected.rs +++ b/yazi-core/src/tab/selected.rs @@ -133,7 +133,7 @@ impl Selected { } pub fn apply_op(&mut self, op: &FilesOp) { - let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); + let (removal, addition) = op.diff_recoverable(self.urls()); if !removal.is_empty() { self.remove_many(&removal); } @@ -190,6 +190,12 @@ mod tests { assert!(!s.remove(Path::new("/a/b"))); assert!(s.inner.is_empty()); assert!(s.parents.is_empty()); + + // Relative path with an empty parent (root) + assert!(s.add(f("a"))); + assert!(s.remove(Path::new("a"))); + assert!(s.inner.is_empty()); + assert!(s.parents.is_empty()); } #[test] diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 958ba927..61ebd7c0 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -85,7 +85,7 @@ impl Tab { pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() } pub fn hovered_rect(&self) -> Option { - let y = self.current.entries.position(self.hovered()?.urn())? - self.current.offset; + let y = self.current.entries.position(self.hovered()?.entry_key())? - self.current.offset; let mut rect = LAYOUT.get().current; rect.y = rect.y.saturating_sub(1) + y as u16; diff --git a/yazi-core/src/tasks/prework.rs b/yazi-core/src/tasks/prework.rs index be641480..23ec2ca5 100644 --- a/yazi-core/src/tasks/prework.rs +++ b/yazi-core/src/tasks/prework.rs @@ -54,7 +54,11 @@ impl Tasks { targets .iter() .filter(|f| { - f.is_dir() && !targets.sizes.contains_key(&f.urn()) && !loading.contains(&f.url) + let key = f.entry_key(); + f.is_dir() + && !key.is_empty() + && !targets.sizes.contains_key(&key) + && !loading.contains(&f.url) }) .map(|f| &f.url) .collect() diff --git a/yazi-fs/src/engine/local/absolute.rs b/yazi-fs/src/engine/local/absolute.rs index 091c2d84..4f362fb4 100644 --- a/yazi-fs/src/engine/local/absolute.rs +++ b/yazi-fs/src/engine/local/absolute.rs @@ -51,6 +51,6 @@ fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option> { Some(match url.as_url() { Url::Regular(_) => UrlBuf::Regular(loc).into(), Url::Search { auth, .. } => UrlBuf::Search { loc, auth: auth.clone() }.into(), - Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => None?, + Url::Mount { .. } | Url::Hub { .. } | Url::Scope { .. } | Url::Sftp { .. } => None?, }) } diff --git a/yazi-fs/src/engine/local/local.rs b/yazi-fs/src/engine/local/local.rs index 32e476d3..7f1dcc63 100644 --- a/yazi-fs/src/engine/local/local.rs +++ b/yazi-fs/src/engine/local/local.rs @@ -1,7 +1,7 @@ use std::{fs::FileTimes, io, path::Path, sync::Arc}; use tokio::sync::mpsc; -use yazi_shared::{auth::AuthKind, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; +use yazi_shared::{auth::AuthKind, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; use crate::{cha::{Cha, ChaMode}, engine::{Attrs, Capabilities, Engine}}; @@ -45,19 +45,19 @@ impl<'a> Engine for Local<'a> { #[inline] async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsPath, + P: DynPath, { - let to = to.as_path().to_os_owned()?; + let to = to.dyn_path().to_os_owned()?; let from = self.path.to_owned(); super::copy_impl(from, to, attrs).await } fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into, { - let to = to.as_path().to_os_owned()?; + let to = to.dyn_path().to_os_owned()?; let from = self.path.to_owned(); Ok(super::copy_progressive_impl(from, to, attrs.into())) } @@ -71,9 +71,9 @@ impl<'a> Engine for Local<'a> { #[inline] async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_os()?; + let to = to.dyn_path().as_os()?; tokio::fs::hard_link(self.path, to).await } @@ -87,7 +87,7 @@ impl<'a> Engine for Local<'a> { async fn new<'b>(url: Url<'b>) -> io::Result> { match url { Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_inner() }), - Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => { + Url::Mount { .. } | Url::Hub { .. } | Url::Scope { .. } | Url::Sftp { .. } => { Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}"))) } } @@ -101,7 +101,7 @@ impl<'a> Engine for Local<'a> { reader: tokio::fs::read_dir(self.path).await?, dir: Arc::new(self.url.to_owned()), }, - AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Err(io::Error::new( + AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Not a local URL: {:?}", self.url), ))?, @@ -125,9 +125,9 @@ impl<'a> Engine for Local<'a> { #[inline] async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_os()?; + let to = to.dyn_path().as_os()?; tokio::fs::rename(self.path, to).await } diff --git a/yazi-fs/src/engine/traits.rs b/yazi-fs/src/engine/traits.rs index c26ee590..6f42e63e 100644 --- a/yazi-fs/src/engine/traits.rs +++ b/yazi-fs/src/engine/traits.rs @@ -2,7 +2,7 @@ use std::io; use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use yazi_macro::ok_or_not_found; -use yazi_shared::{path::{AsPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; +use yazi_shared::{path::{DynPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}}; @@ -23,11 +23,11 @@ pub trait Engine: Sized { fn copy

(&self, to: P, attrs: Attrs) -> impl Future> where - P: AsPath; + P: DynPath; fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into; fn create(&self) -> impl Future> { @@ -80,7 +80,7 @@ pub trait Engine: Sized { fn hard_link

(&self, to: P) -> impl Future> where - P: AsPath; + P: DynPath; fn metadata(&self) -> impl Future>; @@ -157,7 +157,7 @@ pub trait Engine: Sized { fn rename

(&self, to: P) -> impl Future> where - P: AsPath; + P: DynPath; fn set_attrs(&self, attrs: Attrs) -> impl Future>; diff --git a/yazi-fs/src/entries.rs b/yazi-fs/src/entries.rs index 84c7c426..86ac0796 100644 --- a/yazi-fs/src/entries.rs +++ b/yazi-fs/src/entries.rs @@ -1,7 +1,7 @@ use std::{mem, ops::{Deref, DerefMut, Not}}; use hashbrown::{HashMap, HashSet}; -use yazi_shared::{id::Id, path::{PathBufDyn, PathDyn}}; +use yazi_shared::{id::Id, path::{PathBufDyn, PathDyn, PathLike}}; use super::{FilesSorter, Filter}; use crate::{FILES_TICKET, SortBy, file::File}; @@ -69,19 +69,19 @@ impl Entries { } } - pub fn update_size(&mut self, mut sizes: HashMap) { - if sizes.len() <= 50 { - sizes.retain(|k, v| self.sizes.get(k) != Some(v)); + pub fn update_size(&mut self, sizes: HashMap) { + self.sizes.reserve(if self.sizes.is_empty() { sizes.len() } else { sizes.len().div_ceil(2) }); + + let mut changed = false; + for (key, size) in sizes { + if !key.is_empty() { + changed |= self.sizes.insert(key, size) != Some(size); + } } - if sizes.is_empty() { - return; - } - - if self.sorter.by == SortBy::Size { + if changed && self.sorter.by == SortBy::Size { self.revision += 1; } - self.sizes.extend(sizes); } pub fn update_ioerr(&mut self) { @@ -97,9 +97,10 @@ impl Entries { macro_rules! go { ($dist:expr, $src:expr, $inc:literal) => { - let mut todo: HashMap<_, _> = $src.into_iter().map(|f| (f.urn().to_owned(), f)).collect(); + let mut todo: HashMap<_, _> = + $src.into_iter().map(|f| (f.entry_key().to_owned(), f)).collect(); for f in &$dist { - if todo.remove(&f.urn()).is_some() && todo.is_empty() { + if todo.remove(&f.entry_key()).is_some() && todo.is_empty() { break; } } @@ -119,48 +120,14 @@ impl Entries { } } - #[cfg(unix)] - pub fn update_deleting(&mut self, urns: HashSet) -> Vec { - use yazi_shared::path::PathLike; - if urns.is_empty() { - return vec![]; - } + pub fn update_deleting(&mut self, mut keys: HashSet) -> Vec { + keys.retain(|k| !k.is_empty()); + let mut deleted = Vec::with_capacity(keys.len()); - let (mut hidden, mut items) = if let Some(filter) = &self.filter { - urns.into_iter().partition(|u| (!self.show_hidden && u.is_hidden()) || !filter.matches(u)) - } else if self.show_hidden { - (HashSet::new(), urns) - } else { - urns.into_iter().partition(|u| u.is_hidden()) - }; - - let mut deleted = Vec::with_capacity(items.len()); - if !items.is_empty() { + if !keys.is_empty() { let mut i = 0; self.items.retain(|f| { - let b = items.remove(&f.urn()); - if b { - deleted.push(i); - } - i += 1; - !b - }); - } - if !hidden.is_empty() { - self.hidden.retain(|f| !hidden.remove(&f.urn())); - } - - self.revision += deleted.is_empty().not() as u64; - deleted - } - - #[cfg(windows)] - pub fn update_deleting(&mut self, mut urns: HashSet) -> Vec { - let mut deleted = Vec::with_capacity(urns.len()); - if !urns.is_empty() { - let mut i = 0; - self.items.retain(|f| { - let b = urns.remove(&f.urn()); + let b = keys.remove(&f.entry_key()); if b { deleted.push(i) } @@ -168,8 +135,9 @@ impl Entries { !b }); } - if !urns.is_empty() { - self.hidden.retain(|f| !urns.remove(&f.urn())); + + if !keys.is_empty() { + self.hidden.retain(|f| !keys.remove(&f.entry_key())); } self.revision += deleted.is_empty().not() as u64; @@ -178,8 +146,9 @@ impl Entries { pub fn update_updating( &mut self, - files: HashMap, + mut files: HashMap, ) -> (HashMap, HashMap) { + files.retain(|k, f| !k.is_empty() && !f.entry_key().is_empty()); if files.is_empty() { return Default::default(); } @@ -188,9 +157,9 @@ impl Entries { ($dist:expr, $src:expr, $inc:literal) => { let mut b = true; for i in 0..$dist.len() { - if let Some(f) = $src.remove(&$dist[i].urn()) { + if let Some(f) = $src.remove(&$dist[i].entry_key()) { b = b && $dist[i].cha.hits(f.cha); - b = b && $dist[i].urn() == f.urn(); + b = b && $dist[i].entry_key() == f.entry_key(); $dist[i] = f; if $src.is_empty() { @@ -221,13 +190,18 @@ impl Entries { (hidden, items) } - pub fn update_upserting(&mut self, files: HashMap) { + pub fn update_upserting(&mut self, mut files: HashMap) { + files.retain(|k, f| !k.is_empty() && !f.entry_key().is_empty()); if files.is_empty() { return; } self.update_deleting( - files.iter().filter(|&(u, f)| u != f.urn()).map(|(_, f)| f.urn().into()).collect(), + files + .iter() + .filter(|&(k, f)| k != f.entry_key()) + .map(|(_, f)| f.entry_key().into()) + .collect(), ); let (hidden, items) = self.update_updating(files); @@ -255,14 +229,13 @@ impl Entries { } fn split_files(&self, files: impl IntoIterator) -> (Vec, Vec) { + let files = files.into_iter().filter(|f| !f.entry_key().is_empty()); if let Some(filter) = &self.filter { - files - .into_iter() - .partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn())) + files.partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn())) } else if self.show_hidden { - (vec![], files.into_iter().collect()) + (vec![], files.collect()) } else { - files.into_iter().partition(|f| f.is_hidden()) + files.partition(|f| f.is_hidden()) } } } @@ -270,7 +243,9 @@ impl Entries { impl Entries { // --- Items #[inline] - pub fn position(&self, urn: PathDyn) -> Option { self.iter().position(|f| urn == f.urn()) } + pub fn position(&self, key: PathDyn) -> Option { + if key.is_empty() { None } else { self.iter().position(|f| f.entry_key() == key) } + } // --- Ticket #[inline] diff --git a/yazi-fs/src/file/file.rs b/yazi-fs/src/file/file.rs index 1318dbf1..58a97779 100644 --- a/yazi-fs/src/file/file.rs +++ b/yazi-fs/src/file/file.rs @@ -63,6 +63,9 @@ impl File { #[inline] pub fn urn(&self) -> PathDyn<'_> { self.url.urn() } + #[inline] + pub fn entry_key(&self) -> PathDyn<'_> { self.url.entry_key() } + #[inline] pub fn name(&self) -> Option> { self.url.name() } diff --git a/yazi-fs/src/fns.rs b/yazi-fs/src/fns.rs index 61239627..4269d736 100644 --- a/yazi-fs/src/fns.rs +++ b/yazi-fs/src/fns.rs @@ -70,6 +70,8 @@ fn test_max_common_root() { assert_eq!(max_common_root(&[]), 0); assert(&[""], ""); assert(&["a"], ""); + assert(&["search://kw:1:1/a", "search://kw:1:1/b"], "search://kw/"); + assert(&["test-hub://a1/@root/a", "test-hub://b1/@root/b"], "test-hub://root/@/"); assert(&["/a"], "/"); assert(&["/a/b"], "/a"); diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index 421f7452..e75733b6 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -2,7 +2,7 @@ use std::{iter, path::Path}; use hashbrown::{HashMap, HashSet}; use yazi_macro::{impl_data_any, relay}; -use yazi_shared::{id::{Id, Ids}, path::PathBufDyn, url::{UrlBuf, UrlLike, UrlMapExt}}; +use yazi_shared::{id::{Id, Ids}, path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlLike, UrlMapExt}}; use crate::{cha::Cha, file::File}; @@ -42,16 +42,17 @@ impl FilesOp { pub fn files(&self) -> Box + '_> { match self { - Self::Full(_, files, _) => Box::new(files.iter()), - Self::Part(_, files, _) => Box::new(files.iter()), + Self::Full(_, files, _) | Self::Part(_, files, _) | Self::Creating(_, files) => { + Box::new(files.iter().filter(|f| !f.entry_key().is_empty())) + } Self::Done(..) => Box::new(iter::empty()), Self::Size(..) => Box::new(iter::empty()), Self::IOErr(..) => Box::new(iter::empty()), - Self::Creating(_, files) => Box::new(files.iter()), Self::Deleting(..) => Box::new(iter::empty()), - Self::Updating(_, map) => Box::new(map.values()), - Self::Upserting(_, map) => Box::new(map.values()), + Self::Updating(_, map) | Self::Upserting(_, map) => { + Box::new(map.values().filter(|f| !f.entry_key().is_empty())) + } } } @@ -68,7 +69,7 @@ impl FilesOp { pub fn create(files: Vec) { let mut parents: HashMap> = Default::default(); for file in files { - if let Some(p) = file.url.parent() { + if let Some((p, _)) = file.url.pair2() { parents.get_or_insert_default(p).push(file); } } @@ -80,18 +81,18 @@ impl FilesOp { pub fn rename(map: HashMap) { let mut parents: HashMap, HashMap<_, _>)> = Default::default(); for (o, n) in map { - let Some(o_p) = o.parent() else { continue }; - let Some(n_p) = n.url.parent() else { continue }; + let Some((o_p, o_k)) = o.pair2() else { continue }; + let Some((n_p, n_k)) = n.url.pair2() else { continue }; if o_p == n_p { - parents.get_or_insert_default(o_p).1.insert(o.urn().into(), n); + parents.get_or_insert_default(o_p).1.insert(o_k.into(), n); } else { - parents.get_or_insert_default(o_p).0.insert(o.urn().into()); - parents.get_or_insert_default(n_p).1.insert(n.urn().into(), n); + parents.get_or_insert_default(o_p).0.insert(o_k.into()); + parents.get_or_insert_default(n_p).1.insert(n_k.into(), n); } } for (p, (o, n)) in parents { match (o.is_empty(), n.is_empty()) { - (true, true) => unreachable!(), + (true, true) => {} (true, false) => Self::Upserting(p, n).emit(), (false, true) => Self::Deleting(p, o).emit(), (false, false) => { @@ -106,14 +107,20 @@ impl FilesOp { let mut parents: HashMap<_, (HashMap<_, _>, HashSet<_>)> = Default::default(); for op in ops { match op { - Self::Upserting(p, map) => parents.entry(p).or_default().0.extend(map), - Self::Deleting(p, urns) => parents.entry(p).or_default().1.extend(urns), + Self::Upserting(p, map) => parents + .entry(p) + .or_default() + .0 + .extend(map.into_iter().filter(|(k, f)| !k.is_empty() && !f.entry_key().is_empty())), + Self::Deleting(p, keys) => { + parents.entry(p).or_default().1.extend(keys.into_iter().filter(|k| !k.is_empty())) + } _ => unreachable!(), } } for (p, (u, d)) in parents { match (u.is_empty(), d.is_empty()) { - (true, true) => unreachable!(), + (true, true) => {} (true, false) => Self::Deleting(p, d).emit(), (false, true) => Self::Upserting(p, u).emit(), (false, false) => { @@ -147,17 +154,25 @@ impl FilesOp { } } - pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec, Vec) { + pub fn diff_recoverable<'a, I>(&self, urls: I) -> (Vec, Vec) + where + I: IntoIterator, + { + let cwd = self.cwd(); + let it = urls + .into_iter() + .filter(|u| u.parent().is_some_and(|p| p == *cwd)) + .filter(|u| !u.entry_key().is_empty()); + match self { - Self::Deleting(cwd, urns) => { - (urns.iter().filter_map(|u| cwd.try_join(u).ok()).collect(), vec![]) + Self::Deleting(_, keys) => { + (it.filter(|u| keys.contains(&u.entry_key())).cloned().collect(), vec![]) } - Self::Updating(cwd, urns) | Self::Upserting(cwd, urns) => urns - .iter() - .filter(|&(u, f)| u != f.urn()) - .filter_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f))) - .filter(|(u, _)| contains(u)) - .map(|(u, f)| (u, f.clone())) + Self::Updating(_, files) | Self::Upserting(_, files) => it + .filter_map(|u| files.get(&u.entry_key()).map(|f| (u, f))) + .filter(|(_, f)| !f.entry_key().is_empty()) + .filter(|&(u, f)| u != f.url) + .map(|(u, f)| (u.clone(), f.clone())) .unzip(), _ => (vec![], vec![]), } diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs index 2fef639f..1ce63fac 100644 --- a/yazi-fs/src/path/clean.rs +++ b/yazi-fs/src/path/clean.rs @@ -1,27 +1,44 @@ -use yazi_shared::{path::{PathBufDyn, PathDyn}, url::{UrlBuf, UrlCow, UrlLike}}; +use std::{iter, sync::Arc}; + +use yazi_shared::{auth::{Auth, AuthKind}, path::{DynPath, PathBufDyn, PathDyn}, url::{UrlBuf, UrlCow, UrlLike}}; pub fn clean_url<'a>(url: impl Into>) -> UrlBuf { let cow: UrlCow = url.into(); - let (path, uri, urn) = clean_path_impl( - cow.loc(), - cow.base().components().count() - 1, - cow.trail().components().count() - 1, + + let depth = cow.loc().components().count(); + let base = depth - cow.uri().components().count(); + let trail = depth - cow.urn().components().count(); + + let (mut spec, path) = cow.into_pair(); + let (path, uri, urn, auth) = clean_path_impl( + path.dyn_path(), + base, + trail, + (spec.kind == AuthKind::Hub).then(|| spec.auth.clone()), ); - let spec = cow.into_spec().with_ports(uri, urn); - (spec, path).try_into().expect("UrlBuf from cleaned path") + spec.auth = auth.unwrap_or(spec.auth); + (spec.with_ports(uri, urn), path).try_into().expect("UrlBuf from cleaned path") } -fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usize, usize) { +fn clean_path_impl( + path: PathDyn, + base: usize, + trail: usize, + auth: Option>, +) -> (PathBufDyn, usize, usize, Option>) { use yazi_shared::path::Component::*; + let mut auths: Vec<_> = iter::successors(auth, |auth| auth.parent.clone()).collect(); + let root = auths.pop(); + let mut out = vec![]; let mut uri_count = 0; let mut urn_count = 0; macro_rules! push { - ($i:ident, $c:ident) => {{ - out.push(($i, $c)); + ($i:ident, $c:ident, $auth:ident) => {{ + out.push(($i, $c, $auth)); if $i >= base { uri_count += 1; } @@ -33,7 +50,7 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi macro_rules! pop { () => {{ - if let Some((i, _)) = out.pop() { + if let Some((i, ..)) = out.pop() { if i >= base { uri_count -= 1; } @@ -45,14 +62,15 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi } for (i, c) in path.components().enumerate() { + let auth = (root.is_some() && c.has_auth()).then(|| auths.pop().unwrap()); match c { CurDir => {} - ParentDir => match out.last().map(|(_, c)| c) { + ParentDir => match out.last().map(|(_, c, _)| c) { Some(RootDir) => {} Some(Normal(_)) => pop!(), - None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c), + None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c, auth), }, - c => push!(i, c), + c => push!(i, c, auth), } } @@ -60,11 +78,18 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi let path = if out.is_empty() { PathBufDyn::with_str(kind, ".") } else { - PathBufDyn::from_components(kind, out.into_iter().map(|(_, c)| c)) + PathBufDyn::from_components(kind, out.iter().map(|(_, c, _)| *c)) .expect("components with same kind") }; - (path, uri_count, urn_count) + let auth = root.map(|mut parent| { + for mut auth in out.into_iter().filter_map(|(_, _, auth)| auth) { + Arc::make_mut(&mut auth).parent = Some(parent); + parent = auth; + } + parent + }); + (path, uri_count, urn_count, auth) } #[cfg(test)] @@ -106,4 +131,24 @@ mod tests { } Ok(()) } + + #[test] + fn test_clean_hub() -> anyhow::Result<()> { + yazi_shared::init_tests(); + + for (input, expected) in [ + ("test-hub://dotdot/@foo,root/foo/..", "test-hub://root/@/."), + ("test-hub://bar/@dotdot,foo,root/foo/../bar", "test-hub://bar/@root/bar"), + ("test-hub://dotdot/@root//..", "test-hub://root/@//"), + ("test-hub://bar/@dotdot,foo,root//foo/../bar", "test-hub://bar/@root//bar"), + ("test-hub://d3/@d2,d1,root/../../..", "test-hub://d3/@d2,d1,root/../../.."), + ] { + let input: UrlBuf = input.parse()?; + #[cfg(unix)] + assert_eq!(format!("{:?}", clean_url(input)), expected); + #[cfg(windows)] + assert_eq!(format!("{:?}", clean_url(input)).replace(r"\", "/"), expected); + } + Ok(()) + } } diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index 18d95794..1b8d9d71 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -40,6 +40,10 @@ fn expand_url_impl(url: UrlCow) -> UrlCow { loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), auth: auth.clone(), }, + Url::Hub { auth, .. } => UrlBuf::Hub { + auth: auth.clone().with_parent_depth(path.components().auth_depth()), + loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + }, Url::Scope { auth, .. } => UrlBuf::Scope { loc: LocBuf::::with(path.into_unix().unwrap(), uri, urn).unwrap(), auth: auth.clone(), @@ -112,6 +116,7 @@ mod tests { ("test-mount://7z:2:1//tmp/test.zip/$BAR_BAZ", "test-mount://7z:3:2//tmp/test.zip/bar/baz"), ("test-mount://7z:2:2//tmp/$BAR_BAZ/test.zip", "test-mount://7z:3:3//tmp/bar/baz/test.zip"), ("test-mount://7z:2:2//$BAR_BAZ/tmp/test.zip", "test-mount://7z:2:2//bar/baz/tmp/test.zip"), + ("test-hub://a1/@root/$BAR_BAZ", "test-hub://a1:2:2/@,root/bar/baz"), // -1 component ("test-mount://7z//tmp/test.zip/${BAR/BAZ}", "test-mount://7z//tmp/test.zip/bar_baz"), ("test-mount://7z:1//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:1//tmp/test.zip/${BAR/BAZ}"), @@ -134,6 +139,7 @@ mod tests { ), ("test-mount://7z:3:3//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:2:2//tmp/test.zip/bar_baz"), ("test-mount://7z:3:3//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:2:2//tmp/bar_baz/test.zip"), + ("test-hub://a1:2:2/@b1,root/${BAR/BAZ}", "test-hub://a1/@root/bar_baz"), // Zeros all components ("test-mount://7z//${EM/PT/Y}", "test-mount://7z//"), ("test-mount://7z:1//${EM/PT/Y}", "test-mount://7z:1//${EM/PT/Y}"), diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs index 0656a347..2b5358da 100644 --- a/yazi-fs/src/path/percent.rs +++ b/yazi-fs/src/path/percent.rs @@ -4,8 +4,16 @@ use anyhow::Result; use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; use yazi_shared::path::{PathCow, PathDyn, PathKind}; -const SET: &AsciiSet = - &CONTROLS.add(b'"').add(b'*').add(b':').add(b'<').add(b'>').add(b'?').add(b'\\').add(b'|'); +const SET: &AsciiSet = &CONTROLS + .add(b'"') + .add(b'*') + .add(b':') + .add(b'<') + .add(b'>') + .add(b'?') + .add(b'\\') + .add(b'|') + .add(b'%'); pub trait PercentEncoding<'a> { fn percent_encode(self) -> Cow<'a, Path>; diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index 7ff7acc0..13751e30 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -67,8 +67,8 @@ impl FilesSorter { }), SortBy::Size => items.sort_unstable_by(|a, b| { promote!(a, b); - let aa = if a.is_dir() { sizes.get(&a.urn()).copied() } else { None }; - let bb = if b.is_dir() { sizes.get(&b.urn()).copied() } else { None }; + let aa = if a.is_dir() { sizes.get(&a.entry_key()).copied() } else { None }; + let bb = if b.is_dir() { sizes.get(&b.entry_key()).copied() } else { None }; self.fallback(a, b, self.cmp(aa.unwrap_or(a.len), bb.unwrap_or(b.len))) }), SortBy::Random => { diff --git a/yazi-fs/src/spec.rs b/yazi-fs/src/spec.rs index 1af5029a..6768e29a 100644 --- a/yazi-fs/src/spec.rs +++ b/yazi-fs/src/spec.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use yazi_shared::{auth::{Auth, AuthKind, Encode}, path::PathBufDyn, spec::SpecInventory}; +use yazi_shared::{auth::{Auth, AuthKind, EncodeAuth}, path::PathBufDyn, spec::SpecInventory}; use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr}; use crate::Xdg; @@ -13,12 +13,14 @@ impl FsSpec for Auth { fn cache(&self) -> Option { match self.kind { AuthKind::Regular | AuthKind::Search => None, - AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Some(Xdg::temp_dir().join(format!( - "{}_{}_{}", - self.kind.into_str(), - self.scheme, - Encode::domain(&self.domain) - ))), + AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { + Some(Xdg::temp_dir().join(format!( + "{}_{}_{}", + self.kind.into_str(), + self.scheme, + EncodeAuth::domain(&self.domain) + ))) + } } } } diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index 32f14201..effbf782 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; use mlua::UserDataFields; -use yazi_shared::{path::{AsPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; +use yazi_shared::{path::{DynPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; use yazi_shim::mlua::UserDataFieldsExt; use crate::{FsHash128, FsSpec, path::PercentEncoding}; @@ -29,7 +29,7 @@ impl<'a> FsUrl<'a> for Url<'a> { fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { let mut it = loc.components(); if it.next() == Some(yazi_shared::path::Component::RootDir) { - root.push(it.as_path().percent_encode()); + root.push(it.dyn_path().percent_encode()); } else { root.push(".%2F"); root.push(loc.percent_encode()); @@ -51,7 +51,7 @@ impl<'a> FsUrl<'a> for Url<'a> { fn unified_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(), - Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { + Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } } @@ -66,7 +66,7 @@ impl FsUrl<'_> for UrlBuf { fn unified_path(self) -> Cow<'static, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(), - Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { + Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } } @@ -81,7 +81,7 @@ impl<'a> FsUrl<'a> for UrlCow<'a> { fn unified_path(self) -> Cow<'a, Path> { match self { Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(), - Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { + Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } } diff --git a/yazi-parser/src/mgr/hover.rs b/yazi-parser/src/mgr/hover.rs index cab56f6f..d0138841 100644 --- a/yazi-parser/src/mgr/hover.rs +++ b/yazi-parser/src/mgr/hover.rs @@ -3,11 +3,11 @@ use yazi_shared::path::PathBufDyn; #[derive(Debug, Default)] pub struct HoverForm { - pub urn: Option, + pub key: Option, } impl From> for HoverForm { - fn from(urn: Option) -> Self { Self { urn } } + fn from(key: Option) -> Self { Self { key } } } impl FromLua for HoverForm { diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 17cd8e47..57b32469 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -206,7 +206,7 @@ impl Drop for FileInCut { impl FileInCut { pub fn new(from: UrlBuf, to: UrlBuf, force: bool) -> Self { Self { - follow: !from.auth().covariant(to.auth()), + follow: !from.auth().same_service(to.auth()), id: Id::ZERO, from, to, diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 67e5ac9b..1e2fb594 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -78,7 +78,7 @@ impl Scheduler { } pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) { - let follow = follow || !from.auth().covariant(to.auth()); + let follow = follow || !from.auth().same_service(to.auth()); let mut r#in = FileInCopy { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0 }; self.add(&mut r#in, |_| ()); @@ -109,7 +109,7 @@ impl Scheduler { let mut r#in = FileInHardlink { id: Id::ZERO, from, to, force, cha: None, follow }; self.add(&mut r#in, |_| ()); - if !r#in.from.auth().covariant(r#in.to.auth()) { + if !r#in.from.auth().same_service(r#in.to.auth()) { return self .ops .out(r#in.id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned())); diff --git a/yazi-scheduler/src/size/size.rs b/yazi-scheduler/src/size/size.rs index bcd01646..0497ea80 100644 --- a/yazi-scheduler/src/size/size.rs +++ b/yazi-scheduler/src/size/size.rs @@ -35,9 +35,10 @@ impl Size { } let parent = buf[0].0.parent().unwrap(); + // FIXME: use PathBufDyn instead of UrlBuf in SizeIn FilesOp::Size( parent.into(), - HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn().into(), s))), + HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.entry_key().into(), s))), ) .emit(); }); diff --git a/yazi-shared/src/auth/auth.rs b/yazi-shared/src/auth/auth.rs index d1027b76..16bbebe1 100644 --- a/yazi-shared/src/auth/auth.rs +++ b/yazi-shared/src/auth/auth.rs @@ -1,19 +1,37 @@ -use std::{fmt, sync::Arc}; +use std::{fmt, hash::{Hash, Hasher}, sync::Arc}; use anyhow::{Result, anyhow}; use percent_encoding::percent_decode_str; use serde::Deserialize; -use yazi_shim::{SStr, cell::RoCell}; +use yazi_shim::cell::RoCell; -use crate::auth::{AuthInventory, AuthKind, Encode, Scheme}; +use crate::{auth::{AuthInventory, AuthKind, Domain, EncodeAuth, Scheme}, path::{Component, Components}}; pub(super) static DEFAULT_ARC: RoCell> = RoCell::new(); -#[derive(Debug, Deserialize, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize)] pub struct Auth { pub kind: AuthKind, pub scheme: Scheme, - pub domain: SStr, + pub domain: Domain<'static>, + #[serde(default)] + pub parent: Option>, +} + +impl PartialEq for Auth { + fn eq(&self, other: &Self) -> bool { + self.kind == other.kind && self.scheme == other.scheme && self.domain == other.domain + } +} + +impl Eq for Auth {} + +impl Hash for Auth { + fn hash(&self, state: &mut H) { + self.kind.hash(state); + self.scheme.hash(state); + self.domain.hash(state); + } } impl Default for Auth { @@ -21,20 +39,28 @@ impl Default for Auth { } impl fmt::Display for Auth { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Encode(self, false).fmt(f) } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { EncodeAuth(self, false).fmt(f) } } impl Auth { - pub const DEFAULT: Self = - Self { kind: AuthKind::Regular, scheme: Scheme::Regular, domain: SStr::Borrowed("") }; + pub const DEFAULT: Self = Self { + kind: AuthKind::Regular, + scheme: Scheme::Regular, + domain: Domain::EMPTY, + parent: None, + }; pub fn default_arc() -> Arc { DEFAULT_ARC.clone() } - pub fn new(kind: AuthKind, scheme: Scheme, domain: impl Into) -> Arc { - Arc::new(Self { kind, scheme, domain: domain.into() }) + pub fn new<'a>(kind: AuthKind, scheme: Scheme, domain: impl Into>) -> Arc { + Arc::new(Self { kind, scheme, domain: domain.into().into_owned(), parent: None }) } - pub fn get(scheme: &Scheme, domain: &str) -> Option> { + pub fn search<'a>(query: impl Into>) -> Arc { + Self::new(AuthKind::Search, Scheme::Search, query) + } + + pub fn get(scheme: &Scheme, domain: &Domain<'_>) -> Option> { match scheme { Scheme::Regular => Some(Self::default_arc()), Scheme::Search => Some(Self::search(domain)), @@ -42,22 +68,88 @@ impl Auth { } } - pub fn search(query: impl Into) -> Arc { - Self::new(AuthKind::Search, Scheme::Search, query.into()) + pub fn child(self: Arc) -> Arc { + Arc::new(Self { + kind: self.kind, + scheme: self.scheme.clone(), + domain: Domain::default(), + parent: Some(self), + }) + } + + pub fn root(mut self: &Arc) -> &Arc { + while let Some(parent) = &self.parent { + self = parent; + } + self + } + + pub fn descend<'a>(mut self: Arc, components: impl Into>) -> Arc { + for component in components.into() { + match component { + Component::RootDir => self = Self::new(self.kind, self.scheme.clone(), Domain::EMPTY), + c if c.has_auth() => self = self.child(), + _ => {} + } + } + self + } + + pub fn parent_at(mut self: &Arc, depth: usize) -> &Arc { + for _ in 0..depth { + self = self.parent.as_ref().expect("Auth parent depth out of bounds"); + } + self + } + + pub fn with_parent_depth(mut self: Arc, depth: usize) -> Arc { + let current = self.parent_depth(); + if current == depth { + return self; + } + + let mut parent = if current < depth { + self.parent.clone() + } else { + self.parent_at(current - depth).parent.clone() + }; + + for _ in current..depth { + parent = Some(Arc::new(Self { + kind: self.kind, + scheme: self.scheme.clone(), + domain: Domain::default(), + parent, + })); + } + + Arc::make_mut(&mut self).parent = parent; + self + } + + pub fn parent_depth(&self) -> usize { + let mut depth = 0; + let mut parent = self.parent.as_deref(); + while let Some(auth) = parent { + depth += 1; + parent = auth.parent.as_deref(); + } + depth } pub fn covariant(&self, other: &Self) -> bool { !self.kind.is_virtual() && !other.kind.is_virtual() || self == other } + pub fn same_service(&self, other: &Self) -> bool { + self.covariant(other) + || self.kind == AuthKind::Hub && other.kind == AuthKind::Hub && self.scheme == other.scheme + } + pub fn parse_cache(cache: &str) -> Result> { let (kind, rest) = cache.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; let (scheme, domain) = rest.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; - Ok(Arc::new(Self { - kind: kind.parse()?, - scheme: scheme.parse()?, - domain: percent_decode_str(domain).decode_utf8()?.into_owned().into(), - })) + Ok(Self::new(kind.parse()?, scheme.parse()?, percent_decode_str(domain).decode_utf8()?)) } } diff --git a/yazi-shared/src/auth/domain.rs b/yazi-shared/src/auth/domain.rs new file mode 100644 index 00000000..98297e98 --- /dev/null +++ b/yazi-shared/src/auth/domain.rs @@ -0,0 +1,69 @@ +use std::{borrow::{Borrow, Cow}, fmt::{self, Display, Formatter}, ops::Deref}; + +use serde::{Deserialize, Deserializer, Serialize, de::{self, IntoDeserializer, value::CowStrDeserializer}}; + +#[derive(Default, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(transparent)] +pub struct Domain<'a>(Cow<'a, str>); + +impl Domain<'static> { + pub const CATCHALL: Self = Self(Cow::Borrowed("*")); + pub const EMPTY: Self = Self(Cow::Borrowed("")); +} + +impl Domain<'_> { + #[inline] + pub fn into_owned(self) -> Domain<'static> { Domain(Cow::Owned(self.0.into_owned())) } + + #[inline] + pub fn is_catchall(&self) -> bool { *self == Domain::CATCHALL } +} + +impl Deref for Domain<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl Borrow for Domain<'_> { + fn borrow(&self) -> &str { &self.0 } +} + +impl AsRef for Domain<'_> { + fn as_ref(&self) -> &str { &self.0 } +} + +impl<'a> From for Domain<'a> { + fn from(value: String) -> Self { Self(Cow::Owned(value)) } +} + +impl<'a> From<&'a str> for Domain<'a> { + fn from(value: &'a str) -> Self { Self(Cow::Borrowed(value)) } +} + +impl<'a> From> for Domain<'a> { + fn from(value: Cow<'a, str>) -> Self { Self(value) } +} + +impl<'a> From<&'a Domain<'_>> for Domain<'a> { + fn from(value: &'a Domain<'_>) -> Self { Self(Cow::Borrowed(&value.0)) } +} + +impl Display for Domain<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self.0, f) } +} + +impl<'de> Deserialize<'de> for Domain<'_> { + fn deserialize>(deserializer: D) -> Result { + Ok(Self(Cow::Owned(String::deserialize(deserializer)?))) + } +} + +impl<'de, 'a, E> IntoDeserializer<'de, E> for Domain<'a> +where + E: de::Error, +{ + type Deserializer = CowStrDeserializer<'a, E>; + + fn into_deserializer(self) -> Self::Deserializer { self.0.into_deserializer() } +} diff --git a/yazi-shared/src/auth/encode.rs b/yazi-shared/src/auth/encode.rs index 043da080..662a8b0e 100644 --- a/yazi-shared/src/auth/encode.rs +++ b/yazi-shared/src/auth/encode.rs @@ -2,18 +2,18 @@ use std::fmt; use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; -use super::Auth; +use super::{Auth, AuthKind, Domain}; -pub struct Encode<'a>(pub &'a Auth, pub bool); +pub struct EncodeAuth<'a>(pub &'a Auth, pub bool); -impl Encode<'_> { - pub fn domain(s: &str) -> PercentEncode<'_> { - const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); +impl EncodeAuth<'_> { + pub fn domain<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> { + const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':').add(b'%'); percent_encode(s.as_bytes(), SET) } } -impl fmt::Display for Encode<'_> { +impl fmt::Display for EncodeAuth<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -24,3 +24,32 @@ impl fmt::Display for Encode<'_> { ) } } + +// --- EncodePrefix +pub struct EncodePrefix<'a>(pub &'a Auth); + +impl EncodePrefix<'_> { + pub fn parent<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> { + const SET: &AsciiSet = &CONTROLS.add(b'/').add(b',').add(b'@').add(b'%'); + percent_encode(s.as_bytes(), SET) + } +} + +impl fmt::Display for EncodePrefix<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.kind != AuthKind::Hub { + return f.write_str("/"); + } + + f.write_str("/@")?; + let (mut first, mut parent) = (true, self.0.parent.as_deref()); + while let Some(auth) = parent { + if !first { + f.write_str(",")?; + } + Self::parent(&auth.domain).fmt(f)?; + (first, parent) = (false, auth.parent.as_deref()); + } + f.write_str("/") + } +} diff --git a/yazi-shared/src/auth/inventory.rs b/yazi-shared/src/auth/inventory.rs index a722d68d..b16dcb53 100644 --- a/yazi-shared/src/auth/inventory.rs +++ b/yazi-shared/src/auth/inventory.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use crate::auth::{Auth, Scheme}; +use crate::auth::{Auth, Domain, Scheme}; pub struct AuthInventory { - pub get: fn(&Scheme, &str) -> Option>, + pub get: fn(&Scheme, &Domain<'_>) -> Option>, } inventory::collect!(AuthInventory); diff --git a/yazi-shared/src/auth/kind.rs b/yazi-shared/src/auth/kind.rs index ed794c77..35f00d9b 100644 --- a/yazi-shared/src/auth/kind.rs +++ b/yazi-shared/src/auth/kind.rs @@ -11,6 +11,7 @@ pub enum AuthKind { Regular, Search, Mount, + Hub, Scope, Sftp, } @@ -20,7 +21,7 @@ impl AuthKind { pub fn is_local(self) -> bool { match self { Self::Regular | Self::Search => true, - Self::Mount | Self::Scope | Self::Sftp => false, + Self::Mount | Self::Hub | Self::Scope | Self::Sftp => false, } } @@ -28,7 +29,7 @@ impl AuthKind { pub fn is_remote(self) -> bool { match self { Self::Regular | Self::Search | Self::Mount => false, - Self::Scope | Self::Sftp => true, + Self::Hub | Self::Scope | Self::Sftp => true, } } @@ -36,7 +37,7 @@ impl AuthKind { pub fn is_virtual(self) -> bool { match self { Self::Regular | Self::Search => false, - Self::Mount | Self::Scope | Self::Sftp => true, + Self::Mount | Self::Hub | Self::Scope | Self::Sftp => true, } } } diff --git a/yazi-shared/src/auth/mod.rs b/yazi-shared/src/auth/mod.rs index 55f581e7..8ce33bb5 100644 --- a/yazi-shared/src/auth/mod.rs +++ b/yazi-shared/src/auth/mod.rs @@ -1,3 +1,3 @@ -yazi_macro::mod_flat!(auth encode inventory kind scheme); +yazi_macro::mod_flat!(auth domain encode inventory kind scheme); pub(super) fn init() { DEFAULT_ARC.with(Default::default); } diff --git a/yazi-shared/src/loc/able.rs b/yazi-shared/src/loc/able.rs index a87e17a1..4be601a5 100644 --- a/yazi-shared/src/loc/able.rs +++ b/yazi-shared/src/loc/able.rs @@ -1,6 +1,6 @@ use std::{ffi::{OsStr, OsString}, fmt::Debug, hash::Hash}; -use crate::{path::{AsPath, AsPathView}, strand::AsStrandView}; +use crate::{path::{DynPath, PathView}, strand::AsStrandView}; // --- LocAble pub trait LocAble<'p> @@ -27,14 +27,10 @@ impl<'p> LocAble<'p> for &'p typed_path::UnixPath { // --- LocBufAble pub trait LocBufAble where - Self: 'static + AsPath + Default, + Self: 'static + DynPath + Default, { type Strand<'a>: StrandAble<'a>; - type Borrowed<'a>: LocAble<'a> - + LocAbleImpl<'a> - + AsPathView<'a, Self::Borrowed<'a>> - + Debug - + Hash; + type Borrowed<'a>: LocAble<'a> + LocAbleImpl<'a> + PathView<'a, Self::Borrowed<'a>> + Debug + Hash; } impl LocBufAble for std::path::PathBuf { diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index f00c77eb..eef2cb78 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -2,7 +2,7 @@ use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, use anyhow::Result; -use crate::{auth::AuthKind, loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{AsPath, AsPathView, PathDyn, SetNameError}, strand::AsStrandView}; +use crate::{auth::AuthKind, loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{DynPath, PathDyn, PathView, SetNameError}, strand::AsStrandView}; #[derive(Clone, Default, Eq, PartialEq)] pub struct LocBuf

{ @@ -25,18 +25,18 @@ impl AsRef for LocBuf { fn as_ref(&self) -> &std::path::Path { self.inner.as_ref() } } -impl AsPath for LocBuf +impl DynPath for LocBuf where - T: LocBufAble + AsPath, + T: LocBufAble + DynPath, { - fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } + fn dyn_path(&self) -> PathDyn<'_> { self.inner.dyn_path() } } -impl AsPath for &LocBuf +impl DynPath for &LocBuf where - T: LocBufAble + AsPath, + T: LocBufAble + DynPath, { - fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } + fn dyn_path(&self) -> PathDyn<'_> { self.inner.dyn_path() } } impl

Ord for LocBuf

@@ -59,7 +59,7 @@ where impl

Hash for LocBuf

where P: LocBufAble + LocBufAbleImpl, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { fn hash(&self, state: &mut H) { self.as_loc().hash(state) } } @@ -67,7 +67,7 @@ where impl

Debug for LocBuf

where P: LocBufAble + LocBufAbleImpl + Debug, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_struct("LocBuf") @@ -81,7 +81,7 @@ where impl

From

for LocBuf

where P: LocBufAble + LocBufAbleImpl, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { fn from(path: P) -> Self { let Loc { inner, uri, urn, _phantom } = Loc::bare(&path); @@ -100,7 +100,7 @@ impl> From<&T> for LocBuf { impl

LocBuf

where P: LocBufAble + LocBufAbleImpl, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { pub fn new<'a, S>(path: P, base: S, trail: S) -> Self where @@ -157,7 +157,7 @@ where #[inline] pub fn as_loc<'a>(&'a self) -> Loc<'a, P::Borrowed<'a>> { Loc { - inner: self.inner.as_path_view(), + inner: self.inner.path_view(), uri: self.uri, urn: self.urn, _phantom: PhantomData, @@ -223,7 +223,7 @@ where impl

LocBuf

where P: LocBufAble + LocBufAbleImpl, - for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, + for<'a> &'a P: PathView<'a, P::Borrowed<'a>>, { #[inline] pub fn uri(&self) -> P::Borrowed<'_> { self.as_loc().uri() } diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 533b7c2a..df983bab 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -3,7 +3,7 @@ use std::{hash::{Hash, Hasher}, marker::PhantomData, ops::Deref}; use anyhow::{Result, bail}; use super::LocAbleImpl; -use crate::{auth::AuthKind, loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{AsPath, AsPathView, PathDyn}, strand::AsStrandView}; +use crate::{auth::AuthKind, loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{DynPath, PathDyn, PathView}, strand::AsStrandView}; #[derive(Clone, Copy, Debug)] pub struct Loc<'p, P = &'p std::path::Path> { @@ -29,11 +29,11 @@ where fn deref(&self) -> &Self::Target { &self.inner } } -impl<'p, P> AsPath for Loc<'p, P> +impl<'p, P> DynPath for Loc<'p, P> where - P: LocAble<'p> + AsPath, + P: LocAble<'p> + DynPath, { - fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } + fn dyn_path(&self) -> PathDyn<'_> { self.inner.dyn_path() } } // FIXME: remove @@ -81,9 +81,9 @@ where pub fn bare(path: T) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, { - let path = path.as_path_view(); + let path = path.path_view(); let Some(name) = path.file_name() else { let p = path.strip_prefix(P::empty()).unwrap(); return Self { inner: p, uri: 0, urn: 0, _phantom: PhantomData }; @@ -114,7 +114,7 @@ where pub fn floated<'a, T, S>(path: T, base: S) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); @@ -133,7 +133,7 @@ where pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); @@ -143,18 +143,17 @@ where } #[inline] - pub fn parent(self) -> Option

{ - self.inner.parent().filter(|p| !p.as_encoded_bytes().is_empty()) - } + pub fn parent(self) -> Option

{ self.inner.parent() } pub fn saturated<'a, T>(path: T, kind: AuthKind) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, { match kind { AuthKind::Regular => Self::bare(path), AuthKind::Search => Self::zeroed(path), AuthKind::Mount => Self::zeroed(path), + AuthKind::Hub => Self::bare(path), AuthKind::Scope => Self::bare(path), AuthKind::Sftp => Self::bare(path), } @@ -206,7 +205,7 @@ where pub fn with(path: T, uri: usize, urn: usize) -> Result where - T: AsPathView<'p, P>, + T: PathView<'p, P>, { if urn > uri { bail!("URN cannot be longer than URI"); @@ -238,7 +237,7 @@ where pub fn zeroed(path: T) -> Self where - T: AsPathView<'p, P>, + T: PathView<'p, P>, { let mut loc = Self::bare(path); (loc.uri, loc.urn) = (0, 0); @@ -248,8 +247,21 @@ where #[cfg(test)] mod tests { + use std::path::Path; + + use typed_path::UnixPath; + use super::*; + #[test] + fn test_parent() { + assert_eq!(Loc::bare(Path::new("foo")).parent(), Some(Path::new(""))); + assert_eq!(Loc::bare(Path::new("")).parent(), None); + + assert_eq!(Loc::bare(UnixPath::new("foo")).parent(), Some(UnixPath::new(""))); + assert_eq!(Loc::bare(UnixPath::new("")).parent(), None); + } + #[test] fn test_with() -> Result<()> { let cases = [ @@ -268,11 +280,11 @@ mod tests { ]; for (path, uri, urn, expect_uri, expect_urn) in cases { - let loc = Loc::with(std::path::Path::new(path), uri, urn)?; + let loc = Loc::with(Path::new(path), uri, urn)?; assert_eq!(loc.uri().to_str().unwrap(), expect_uri); assert_eq!(loc.urn().to_str().unwrap(), expect_urn); - let loc = Loc::with(typed_path::UnixPath::new(path), uri, urn)?; + let loc = Loc::with(UnixPath::new(path), uri, urn)?; assert_eq!(loc.uri().to_str().unwrap(), expect_uri); assert_eq!(loc.urn().to_str().unwrap(), expect_urn); } diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index b0e0f20a..70815f9f 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use yazi_codegen::FromLuaOwned; use yazi_shim::wtf8::FromWtf8Vec; -use crate::{path::{AsPath, Component, PathDyn, PathDynError, PathKind, SetNameError}, strand::AsStrand}; +use crate::{path::{Component, DynPath, PathDyn, PathDynError, PathKind, SetNameError}, strand::AsStrand}; // --- PathBufDyn #[derive(Clone, Debug, Eq, FromLuaOwned)] @@ -48,24 +48,24 @@ impl TryFrom for typed_path::UnixPathBuf { // --- Hash impl Hash for PathBufDyn { - fn hash(&self, state: &mut H) { self.as_path().hash(state) } + fn hash(&self, state: &mut H) { self.dyn_path().hash(state) } } // --- PartialEq impl PartialEq for PathBufDyn { - fn eq(&self, other: &Self) -> bool { self.as_path() == other.as_path() } + fn eq(&self, other: &Self) -> bool { self.dyn_path() == other.dyn_path() } } impl PartialEq> for PathBufDyn { - fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } + fn eq(&self, other: &PathDyn<'_>) -> bool { self.dyn_path() == *other } } impl PartialEq> for &PathBufDyn { - fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } + fn eq(&self, other: &PathDyn<'_>) -> bool { self.dyn_path() == *other } } impl Equivalent> for PathBufDyn { - fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.as_path() == *key } + fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.dyn_path() == *key } } impl PathBufDyn { @@ -126,7 +126,7 @@ impl PathBufDyn { pub fn try_extend(&mut self, paths: T) -> Result<(), PathDynError> where T: IntoIterator, - T::Item: AsPath, + T::Item: DynPath, { for p in paths { self.try_push(p)?; @@ -136,9 +136,9 @@ impl PathBufDyn { pub fn try_push(&mut self, path: T) -> Result<(), PathDynError> where - T: AsPath, + T: DynPath, { - let path = path.as_path(); + let path = path.dyn_path(); Ok(match self { Self::Os(p) => p.push(path.as_os()?), Self::Unix(p) => p.push(path.encoded_bytes()), @@ -199,7 +199,7 @@ impl Serialize for PathBufDyn { path: &'a [u8], } - let path = self.as_path(); + let path = self.dyn_path(); Shadow { kind: path.kind(), path: path.encoded_bytes() }.serialize(serializer) } } diff --git a/yazi-shared/src/path/component.rs b/yazi-shared/src/path/component.rs index ec60377b..d8dad5bc 100644 --- a/yazi-shared/src/path/component.rs +++ b/yazi-shared/src/path/component.rs @@ -18,6 +18,9 @@ impl<'a> Component<'a> { _ => None, } } + + #[inline] + pub fn has_auth(&self) -> bool { matches!(self, Self::ParentDir | Self::Normal(_)) } } impl<'a> From> for Component<'a> { diff --git a/yazi-shared/src/path/components.rs b/yazi-shared/src/path/components.rs index 37273b3b..3a4b7a56 100644 --- a/yazi-shared/src/path/components.rs +++ b/yazi-shared/src/path/components.rs @@ -10,7 +10,17 @@ pub enum Components<'a> { Unix(typed_path::UnixComponents<'a>), } +impl<'a> From<&'a std::path::Path> for Components<'a> { + fn from(value: &'a std::path::Path) -> Self { Self::Os(value.components()) } +} + +impl<'a> From<&'a typed_path::UnixPath> for Components<'a> { + fn from(value: &'a typed_path::UnixPath) -> Self { Self::Unix(value.components()) } +} + impl<'a> Components<'a> { + pub fn auth_depth(self) -> usize { self.filter(Component::has_auth).count() } + pub fn path(&self) -> PathDyn<'a> { match self { Self::Os(c) => PathDyn::Os(c.as_path()), diff --git a/yazi-shared/src/path/conversion.rs b/yazi-shared/src/path/conversion.rs deleted file mode 100644 index 5478b1ff..00000000 --- a/yazi-shared/src/path/conversion.rs +++ /dev/null @@ -1,58 +0,0 @@ -use super::{PathBufDyn, PathDyn}; -use crate::path::PathCow; - -// --- AsPath -pub trait AsPath { - fn as_path(&self) -> PathDyn<'_>; -} - -impl AsPath for std::path::Path { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } -} - -impl AsPath for std::path::PathBuf { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } -} - -impl AsPath for typed_path::UnixPath { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } -} - -impl AsPath for typed_path::UnixPathBuf { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } -} - -impl AsPath for PathDyn<'_> { - fn as_path(&self) -> PathDyn<'_> { *self } -} - -impl AsPath for PathBufDyn { - fn as_path(&self) -> PathDyn<'_> { - match self { - Self::Os(p) => PathDyn::Os(p), - Self::Unix(p) => PathDyn::Unix(p), - } - } -} - -impl AsPath for PathCow<'_> { - fn as_path(&self) -> PathDyn<'_> { - match self { - PathCow::Borrowed(p) => *p, - PathCow::Owned(p) => p.as_path(), - } - } -} - -impl AsPath for super::Components<'_> { - fn as_path(&self) -> PathDyn<'_> { self.path() } -} - -// --- AsPathRef -pub trait AsPathRef<'a> { - fn as_path_ref(self) -> PathDyn<'a>; -} - -impl<'a> AsPathRef<'a> for PathDyn<'a> { - fn as_path_ref(self) -> Self { self } -} diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs index 96ac3053..48033d4f 100644 --- a/yazi-shared/src/path/cow.rs +++ b/yazi-shared/src/path/cow.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use anyhow::Result; -use crate::path::{AsPath, PathBufDyn, PathDyn, PathDynError, PathKind}; +use crate::path::{DynPath, PathBufDyn, PathDyn, PathDynError, PathKind}; #[derive(Debug)] pub enum PathCow<'a> { @@ -23,7 +23,7 @@ impl<'a> From for PathCow<'a> { } impl<'a> From<&'a PathCow<'_>> for PathCow<'a> { - fn from(value: &'a PathCow<'_>) -> Self { Self::Borrowed(value.as_path()) } + fn from(value: &'a PathCow<'_>) -> Self { Self::Borrowed(value.dyn_path()) } } impl From> for PathBufDyn { @@ -31,14 +31,14 @@ impl From> for PathBufDyn { } impl PartialEq for PathCow<'_> { - fn eq(&self, other: &Self) -> bool { self.as_path() == other.as_path() } + fn eq(&self, other: &Self) -> bool { self.dyn_path() == other.dyn_path() } } impl PartialEq<&str> for PathCow<'_> { fn eq(&self, other: &&str) -> bool { match self { - Self::Borrowed(s) => s.as_path() == *other, - Self::Owned(s) => s.as_path() == *other, + Self::Borrowed(s) => s.dyn_path() == *other, + Self::Owned(s) => s.dyn_path() == *other, } } } diff --git a/yazi-shared/src/path/dyn_path.rs b/yazi-shared/src/path/dyn_path.rs new file mode 100644 index 00000000..fbe1e783 --- /dev/null +++ b/yazi-shared/src/path/dyn_path.rs @@ -0,0 +1,66 @@ +use super::{PathBufDyn, PathDyn}; +use crate::path::PathCow; + +// --- DynPath +pub trait DynPath { + fn dyn_path(&self) -> PathDyn<'_>; +} + +impl DynPath for std::path::Path { + fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } +} + +impl DynPath for std::path::PathBuf { + fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } +} + +impl DynPath for typed_path::UnixPath { + fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } +} + +impl DynPath for typed_path::UnixPathBuf { + fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } +} + +impl DynPath for PathDyn<'_> { + fn dyn_path(&self) -> PathDyn<'_> { *self } +} + +impl DynPath for PathBufDyn { + fn dyn_path(&self) -> PathDyn<'_> { + match self { + Self::Os(p) => PathDyn::Os(p), + Self::Unix(p) => PathDyn::Unix(p), + } + } +} + +impl DynPath for PathCow<'_> { + fn dyn_path(&self) -> PathDyn<'_> { + match self { + PathCow::Borrowed(p) => *p, + PathCow::Owned(p) => p.dyn_path(), + } + } +} + +impl DynPath for super::Components<'_> { + fn dyn_path(&self) -> PathDyn<'_> { self.path() } +} + +// --- DynPathRef +pub trait DynPathRef<'a> { + fn dyn_path_ref(self) -> PathDyn<'a>; +} + +impl<'a> DynPathRef<'a> for PathDyn<'a> { + fn dyn_path_ref(self) -> Self { self } +} + +impl<'a> DynPathRef<'a> for &'a std::path::Path { + fn dyn_path_ref(self) -> PathDyn<'a> { PathDyn::Os(self) } +} + +impl<'a> DynPathRef<'a> for &'a typed_path::UnixPath { + fn dyn_path_ref(self) -> PathDyn<'a> { PathDyn::Unix(self) } +} diff --git a/yazi-shared/src/path/kind.rs b/yazi-shared/src/path/kind.rs index 68b59865..7e64828f 100644 --- a/yazi-shared/src/path/kind.rs +++ b/yazi-shared/src/path/kind.rs @@ -15,6 +15,7 @@ impl From for PathKind { AuthKind::Regular => Self::Os, AuthKind::Search => Self::Os, AuthKind::Mount => Self::Os, + AuthKind::Hub => Self::Os, AuthKind::Scope => Self::Unix, AuthKind::Sftp => Self::Unix, } diff --git a/yazi-shared/src/path/like.rs b/yazi-shared/src/path/like.rs index 2c5ef1f2..0036e38e 100644 --- a/yazi-shared/src/path/like.rs +++ b/yazi-shared/src/path/like.rs @@ -2,95 +2,97 @@ use std::borrow::Cow; use anyhow::Result; -use crate::{Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand}}; +use crate::{Utf8BytePredictor, path::{Components, Display, DynPath, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand}}; -pub trait PathLike: AsPath { - fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.as_path().as_os() } +pub trait PathLike: DynPath { + fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.dyn_path().as_os() } - fn as_unix(&self) -> Result<&typed_path::UnixPath, PathDynError> { self.as_path().as_unix() } + fn as_unix(&self) -> Result<&typed_path::UnixPath, PathDynError> { self.dyn_path().as_unix() } - fn components(&self) -> Components<'_> { self.as_path().components() } + fn components(&self) -> Components<'_> { self.dyn_path().components() } - fn display(&self) -> Display<'_> { self.as_path().display() } + fn display(&self) -> Display<'_> { self.dyn_path().display() } - fn encoded_bytes(&self) -> &[u8] { self.as_path().encoded_bytes() } + fn encoded_bytes(&self) -> &[u8] { self.dyn_path().encoded_bytes() } - fn ext(&self) -> Option> { self.as_path().ext() } + fn ext(&self) -> Option> { self.dyn_path().ext() } - fn has_root(&self) -> bool { self.as_path().has_root() } + fn has_root(&self) -> bool { self.dyn_path().has_root() } - fn is_absolute(&self) -> bool { self.as_path().is_absolute() } + fn is_absolute(&self) -> bool { self.dyn_path().is_absolute() } - fn is_empty(&self) -> bool { self.as_path().is_empty() } + fn is_empty(&self) -> bool { self.dyn_path().is_empty() } #[cfg(unix)] - fn is_hidden(&self) -> bool { self.as_path().is_hidden() } + fn is_hidden(&self) -> bool { self.dyn_path().is_hidden() } - fn kind(&self) -> PathKind { self.as_path().kind() } + fn kind(&self) -> PathKind { self.dyn_path().kind() } - fn len(&self) -> usize { self.as_path().len() } + fn len(&self) -> usize { self.dyn_path().len() } - fn name(&self) -> Option> { self.as_path().name() } + fn name(&self) -> Option> { self.dyn_path().name() } - fn parent(&self) -> Option> { self.as_path().parent() } + fn parent(&self) -> Option> { self.dyn_path().parent() } fn rsplit_pred(&self, pred: T) -> Option<(PathDyn<'_>, PathDyn<'_>)> where T: Utf8BytePredictor, { - self.as_path().rsplit_pred(pred) + self.dyn_path().rsplit_pred(pred) } - fn stem(&self) -> Option> { self.as_path().stem() } + fn stem(&self) -> Option> { self.dyn_path().stem() } - fn to_os_owned(&self) -> Result { self.as_path().to_os_owned() } + fn to_os_owned(&self) -> Result { + self.dyn_path().to_os_owned() + } - fn to_owned(&self) -> PathBufDyn { self.as_path().to_owned() } + fn to_owned(&self) -> PathBufDyn { self.dyn_path().to_owned() } - fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.as_path().to_str() } + fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.dyn_path().to_str() } - fn to_string_lossy(&self) -> Cow<'_, str> { self.as_path().to_string_lossy() } + fn to_string_lossy(&self) -> Cow<'_, str> { self.dyn_path().to_string_lossy() } fn try_ends_with(&self, child: T) -> Result where T: AsStrand, { - self.as_path().try_ends_with(child) + self.dyn_path().try_ends_with(child) } fn try_join(&self, path: T) -> Result where T: AsStrand, { - self.as_path().try_join(path) + self.dyn_path().try_join(path) } fn try_rsplit_seq(&self, pat: T) -> Result<(PathDyn<'_>, PathDyn<'_>), RsplitOnceError> where T: AsStrand, { - self.as_path().try_rsplit_seq(pat) + self.dyn_path().try_rsplit_seq(pat) } fn try_starts_with(&self, base: T) -> Result where T: AsStrand, { - self.as_path().try_starts_with(base) + self.dyn_path().try_starts_with(base) } fn try_strip_prefix(&self, base: T) -> Result, StripPrefixError> where T: AsStrand, { - self.as_path().try_strip_prefix(base) + self.dyn_path().try_strip_prefix(base) } fn try_strip_suffix(&self, suffix: T) -> Result, StripSuffixError> where T: AsStrand, { - self.as_path().try_strip_suffix(suffix) + self.dyn_path().try_strip_suffix(suffix) } } diff --git a/yazi-shared/src/path/mod.rs b/yazi-shared/src/path/mod.rs index fe574952..a7d430fb 100644 --- a/yazi-shared/src/path/mod.rs +++ b/yazi-shared/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(buf component components conversion cow display error kind like lua path view); +yazi_macro::mod_flat!(buf component components cow display dyn_path error kind like lua path view); diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index d01c63a5..f8cb1b83 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -4,7 +4,7 @@ use anyhow::Result; use hashbrown::Equivalent; use super::{RsplitOnceError, StartsWithError}; -use crate::{BytesExt, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand, StrandError}}; +use crate::{BytesExt, Utf8BytePredictor, path::{Components, Display, DynPath, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand, StrandError}}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum PathDyn<'p> { @@ -21,11 +21,11 @@ impl<'a> From<&'a typed_path::UnixPath> for PathDyn<'a> { } impl<'a> From<&'a PathBufDyn> for PathDyn<'a> { - fn from(value: &'a PathBufDyn) -> Self { value.as_path() } + fn from(value: &'a PathBufDyn) -> Self { value.dyn_path() } } impl PartialEq for PathDyn<'_> { - fn eq(&self, other: &PathBufDyn) -> bool { *self == other.as_path() } + fn eq(&self, other: &PathBufDyn) -> bool { *self == other.dyn_path() } } impl PartialEq> for &std::path::Path { @@ -36,6 +36,12 @@ impl PartialEq<&std::path::Path> for PathDyn<'_> { fn eq(&self, other: &&std::path::Path) -> bool { matches!(*self, PathDyn::Os(p) if p == *other) } } +impl PartialEq<&typed_path::UnixPath> for PathDyn<'_> { + fn eq(&self, other: &&typed_path::UnixPath) -> bool { + matches!(*self, PathDyn::Unix(p) if p == *other) + } +} + impl PartialEq<&str> for PathDyn<'_> { fn eq(&self, other: &&str) -> bool { match *self { @@ -46,7 +52,7 @@ impl PartialEq<&str> for PathDyn<'_> { } impl Equivalent for PathDyn<'_> { - fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.as_path() } + fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.dyn_path() } } impl<'p> PathDyn<'p> { @@ -139,8 +145,8 @@ impl<'p> PathDyn<'p> { pub fn parent(self) -> Option { Some(match self { - Self::Os(p) => Self::Os(p.parent().filter(|p| !p.as_os_str().is_empty())?), - Self::Unix(p) => Self::Unix(p.parent().filter(|p| !p.as_bytes().is_empty())?), + Self::Os(p) => Self::Os(p.parent()?), + Self::Unix(p) => Self::Unix(p.parent()?), }) } @@ -296,3 +302,34 @@ impl<'p> PathDyn<'p> { } } } + +#[cfg(test)] +mod tests { + use std::path::Path; + + use typed_path::UnixPath; + + use super::*; + + #[test] + fn test_parent() { + let path = Path::new("foo").dyn_path(); + assert_eq!(path.parent(), Some(Path::new("").dyn_path())); + assert_eq!(path.parent().unwrap().parent(), None); + + let path = UnixPath::new("foo").dyn_path(); + assert_eq!(path.parent(), Some(UnixPath::new("").dyn_path())); + assert_eq!(path.parent().unwrap().parent(), None); + } + + #[cfg(windows)] + #[test] + fn test_windows_parent() { + let path = Path::new("foo").dyn_path(); + assert_eq!(path.parent(), Some(Path::new("").dyn_path())); + + let path = Path::new("C:foo").dyn_path(); + assert_eq!(path.parent(), Some(Path::new("C:").dyn_path())); + assert_eq!(path.parent().unwrap().parent(), None); + } +} diff --git a/yazi-shared/src/path/view.rs b/yazi-shared/src/path/view.rs index 31a2fdf0..a2478259 100644 --- a/yazi-shared/src/path/view.rs +++ b/yazi-shared/src/path/view.rs @@ -1,20 +1,20 @@ -// --- AsPathView -pub trait AsPathView<'a, T> { - fn as_path_view(self) -> T; +// --- PathView +pub trait PathView<'a, T> { + fn path_view(self) -> T; } -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { - fn as_path_view(self) -> &'a std::path::Path { self } +impl<'a> PathView<'a, &'a std::path::Path> for &'a std::path::Path { + fn path_view(self) -> &'a std::path::Path { self } } -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { - fn as_path_view(self) -> &'a std::path::Path { self } +impl<'a> PathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { + fn path_view(self) -> &'a std::path::Path { self } } -impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPath { - fn as_path_view(self) -> &'a typed_path::UnixPath { self } +impl<'a> PathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPath { + fn path_view(self) -> &'a typed_path::UnixPath { self } } -impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPathBuf { - fn as_path_view(self) -> &'a typed_path::UnixPath { self } +impl<'a> PathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPathBuf { + fn path_view(self) -> &'a typed_path::UnixPath { self } } diff --git a/yazi-shared/src/spec/encode.rs b/yazi-shared/src/spec/encode.rs index 79728da1..798f70a9 100644 --- a/yazi-shared/src/spec/encode.rs +++ b/yazi-shared/src/spec/encode.rs @@ -1,17 +1,17 @@ use std::fmt::{self, Display}; -use crate::{auth::{AuthKind, Encode as EncodeAuth}, url::Url}; +use crate::{auth::{AuthKind, EncodeAuth, EncodePrefix}, url::Url}; #[derive(Clone, Copy)] -pub struct Encode<'a>(pub Url<'a>); +pub struct EncodeSpec<'a>(pub Url<'a>); -impl<'a> From> for Encode<'a> { +impl<'a> From> for EncodeSpec<'a> { fn from(value: crate::url::Encode<'a>) -> Self { Self(value.0) } } -impl<'a> Encode<'a> { +impl<'a> EncodeSpec<'a> { pub(crate) fn ports(self) -> impl Display { - struct D<'a>(Encode<'a>); + struct D<'a>(EncodeSpec<'a>); impl Display for D<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -30,7 +30,7 @@ impl<'a> Encode<'a> { match self.0.0.kind() { AuthKind::Regular => Ok(()), AuthKind::Search | AuthKind::Mount => w!(0, 0), - AuthKind::Scope | AuthKind::Sftp => { + AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { w!(self.0.0.loc().name().is_some() as usize, self.0.0.loc().name().is_some() as usize) } } @@ -41,15 +41,16 @@ impl<'a> Encode<'a> { } } -impl Display for Encode<'_> { +impl Display for EncodeSpec<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { Url::Regular(_) => write!(f, "regular://"), Url::Search { auth, .. } | Url::Mount { auth, .. } + | Url::Hub { auth, .. } | Url::Scope { auth, .. } | Url::Sftp { auth, .. } => { - write!(f, "{}{}/", EncodeAuth(auth, false), self.ports()) + write!(f, "{}{}{}", EncodeAuth(auth, false), self.ports(), EncodePrefix(auth)) } } } diff --git a/yazi-shared/src/spec/spec.rs b/yazi-shared/src/spec/spec.rs index b33ad015..24b0b5f2 100644 --- a/yazi-shared/src/spec/spec.rs +++ b/yazi-shared/src/spec/spec.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, ensure}; use percent_encoding::percent_decode; use serde::Deserialize; -use crate::{auth::{Auth, AuthKind, Encode, Scheme}, path::{PathCow, PathLike}, spec::ParsedSpec, url::Url}; +use crate::{auth::{Auth, AuthKind, Domain, EncodeAuth, Scheme}, path::{PathCow, PathLike}, spec::ParsedSpec, url::Url}; #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] pub struct Spec { @@ -28,18 +28,23 @@ impl Spec { // Decode domain and ports let mut skip = 0; let (domain, uri, urn) = match &parsed.scheme { - Scheme::Regular => (Cow::Borrowed(""), None, None), + Scheme::Regular => (Domain::default(), None, None), _ => Self::decode_param(rest, &mut skip)?, }; // Resolve authority - let auth = Auth::get(&parsed.scheme, &domain) - .ok_or_else(|| anyhow!("unknown VFS authority: {parsed}://{}", Encode::domain(&domain)))?; + let auth = Auth::get(&parsed.scheme, &domain).ok_or_else(|| { + anyhow!("unknown VFS authority: {parsed}://{}", EncodeAuth::domain(&domain)) + })?; // Decode path - let path = Self::decode_path(auth.kind, parsed.tilde, &rest[skip..])?; - let (uri, urn) = Self::normalize_ports(auth.kind, uri, urn, &path)?; + let (path, auth) = if auth.kind == AuthKind::Hub { + Self::decode_hub(auth, parsed.tilde, &rest[skip..])? + } else { + (Self::decode_path(auth.kind, parsed.tilde, &rest[skip..])?, auth) + }; + let (uri, urn) = Self::normalize_ports(auth.kind, uri, urn, &path)?; Ok((Self { auth, uri, urn }, path)) } @@ -54,16 +59,13 @@ impl Spec { fn decode_param<'a>( bytes: &'a [u8], skip: &mut usize, - ) -> Result<(Cow<'a, str>, Option, Option)> { + ) -> Result<(Domain<'a>, Option, Option)> { let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count(); let slash = bytes.get(len).is_some_and(|&b| b == b'/'); *skip += len + slash as usize; let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?; - let domain = match Cow::from(percent_decode(&bytes[..len])) { - Cow::Borrowed(b) => str::from_utf8(b)?.into(), - Cow::Owned(b) => String::from_utf8(b)?.into(), - }; + let domain = percent_decode(&bytes[..len]).decode_utf8()?.into(); Ok((domain, uri, urn)) } @@ -90,6 +92,41 @@ impl Spec { PathCow::with(kind, bytes) } + fn decode_hub<'a>( + mut auth: Arc, + tilde: bool, + bytes: &'a [u8], + ) -> Result<(PathCow<'a>, Arc)> { + ensure!(bytes.first() == Some(&b'@'), "Hub URL requires an `@` parent marker"); + let end = bytes[1..] + .iter() + .position(|&b| b == b'/') + .map(|i| i + 1) + .ok_or_else(|| anyhow!("Hub URL requires a path delimiter"))?; + + let path = Self::decode_path(AuthKind::Hub, tilde, &bytes[end + 1..])?; + let depth = path.components().auth_depth(); + if depth == 0 { + ensure!(bytes[1..end].is_empty(), "Hub URL has too many parent domains"); + return Ok((path, auth)); + } + + let (mut count, mut parent) = (0, None); + for domain in bytes[1..end].rsplit(|&b| b == b',') { + count += 1; + parent = Some(Arc::new(Auth { + kind: auth.kind, + scheme: auth.scheme.clone(), + domain: Domain::from(percent_decode(domain).decode_utf8()?).into_owned(), + parent, + })); + } + + ensure!(count == depth, "Hub URL parent depth does not match its path"); + Arc::make_mut(&mut auth).parent = parent; + Ok((path, auth)) + } + fn normalize_ports( kind: AuthKind, uri: Option, @@ -107,7 +144,7 @@ impl Spec { (uri, urn) } AuthKind::Mount => (uri.unwrap_or(0), urn.unwrap_or(0)), - AuthKind::Scope | AuthKind::Sftp => { + AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => { let uri = uri.unwrap_or(path.name().is_some() as usize); let urn = urn.unwrap_or(path.name().is_some() as usize); (uri, urn) @@ -118,7 +155,7 @@ impl Spec { pub fn retrieve_ports(url: Url) -> (usize, usize) { match url { Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize), - Url::Search { loc, .. } | Url::Mount { loc, .. } => { + Url::Search { loc, .. } | Url::Mount { loc, .. } | Url::Hub { loc, .. } => { (loc.uri().components().count(), loc.urn().components().count()) } Url::Scope { loc, .. } | Url::Sftp { loc, .. } => { diff --git a/yazi-shared/src/strand/kind.rs b/yazi-shared/src/strand/kind.rs index 7ed18e20..637ea9a2 100644 --- a/yazi-shared/src/strand/kind.rs +++ b/yazi-shared/src/strand/kind.rs @@ -22,6 +22,7 @@ impl From for StrandKind { AuthKind::Regular => Self::Os, AuthKind::Search => Self::Os, AuthKind::Mount => Self::Os, + AuthKind::Hub => Self::Os, AuthKind::Scope => Self::Bytes, AuthKind::Sftp => Self::Bytes, } diff --git a/yazi-shared/src/tests.rs b/yazi-shared/src/tests.rs index 0ac25d82..e3360402 100644 --- a/yazi-shared/src/tests.rs +++ b/yazi-shared/src/tests.rs @@ -10,8 +10,9 @@ pub fn init_tests() { inventory::submit! { AuthInventory { - get: |scheme, domain| match (scheme.as_str(), domain) { + get: |scheme, domain| match (scheme.as_str(), domain.as_ref()) { ("test-mount", "7z") => Some(Auth::new(AuthKind::Mount, scheme.clone(), "7z")), + ("test-hub", _) => Some(Auth::new(AuthKind::Hub, scheme.clone(), domain.clone())), ("test-scope", "aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")), ("sftp", "vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")), _ => None, diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index b86f4f4f..14ba6c83 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -5,13 +5,14 @@ use serde::{Deserialize, Serialize, de::{self, IntoDeserializer}}; use yazi_codegen::FromLuaOwned; use yazi_macro::impl_data_any; -use crate::{auth::Auth, loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, spec::Spec, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlDeserializer, UrlLike}}; +use crate::{auth::{Auth, Domain}, loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, spec::Spec, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlDeserializer, UrlLike}}; #[derive(Clone, Eq, FromLuaOwned)] pub enum UrlBuf { Regular(LocBuf), Search { loc: LocBuf, auth: Arc }, Mount { loc: LocBuf, auth: Arc }, + Hub { loc: LocBuf, auth: Arc }, Scope { loc: LocBuf, auth: Arc }, Sftp { loc: LocBuf, auth: Arc }, } @@ -33,6 +34,7 @@ impl From> for UrlBuf { Url::Regular(loc) => Self::Regular(loc.into()), Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() }, Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() }, + Url::Hub { loc, auth } => Self::Hub { loc: loc.into(), auth: auth.clone() }, Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() }, Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() }, } @@ -140,6 +142,7 @@ impl UrlBuf { Self::Regular(loc) => loc.into_inner().into(), Self::Search { loc, .. } => loc.into_inner().into(), Self::Mount { loc, .. } => loc.into_inner().into(), + Self::Hub { loc, .. } => loc.into_inner().into(), Self::Scope { loc, .. } => loc.into_inner().into(), Self::Sftp { loc, .. } => loc.into_inner().into(), } @@ -156,6 +159,7 @@ impl UrlBuf { Self::Regular(loc) => loc.try_set_name(name.as_os()?)?, Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?, Self::Mount { loc, .. } => loc.try_set_name(name.as_os()?)?, + Self::Hub { loc, .. } => loc.try_set_name(name.as_os()?)?, Self::Scope { loc, .. } => loc.try_set_name(name.encoded_bytes())?, Self::Sftp { loc, .. } => loc.try_set_name(name.encoded_bytes())?, }) @@ -166,6 +170,7 @@ impl UrlBuf { Self::Regular(loc) => Self::Regular(loc.rebase(base)), Self::Search { loc, auth } => Self::Search { loc: loc.rebase(base), auth: auth.clone() }, Self::Mount { loc, auth } => Self::Mount { loc: loc.rebase(base), auth: auth.clone() }, + Self::Hub { .. } => todo!(), Self::Scope { .. } => todo!(), Self::Sftp { .. } => todo!(), } @@ -188,6 +193,18 @@ impl UrlBuf { auth: Auth::search(query.as_ref()), }) } + + pub fn into_domain<'a>(mut self, domain: impl Into>) -> Self { + match &mut self { + Self::Regular(_) => {} + Self::Search { auth, .. } + | Self::Mount { auth, .. } + | Self::Hub { auth, .. } + | Self::Scope { auth, .. } + | Self::Sftp { auth, .. } => Arc::make_mut(auth).domain = domain.into().into_owned(), + } + self + } } impl Debug for UrlBuf { @@ -265,10 +282,14 @@ impl<'de> IntoDeserializer<'de, de::value::Error> for &'de UrlBuf { // --- Tests #[cfg(test)] mod tests { + use std::fmt::Debug; + use anyhow::Result; use super::*; - use crate::url::UrlLike; + use crate::{path::PathKind, url::UrlLike}; + + fn debug(value: impl Debug) -> String { format!("{value:?}").replace(r"\", "/") } #[test] fn test_join() -> anyhow::Result<()> { @@ -293,13 +314,7 @@ mod tests { for (base, path, expected) in cases { let base: UrlBuf = base.parse()?; - #[cfg(unix)] - assert_eq!(format!("{:?}", base.try_join(path)?), expected); - #[cfg(windows)] - assert_eq!( - format!("{:?}", base.try_join(path)?).replace(r"\", "/"), - expected.replace(r"\", "/") - ); + assert_eq!(debug(base.try_join(path)?), expected); } Ok(()) @@ -312,6 +327,8 @@ mod tests { // Regular ("/a", Some("/")), ("/", None), + ("a", Some("")), + ("", None), // Search ("search://kw:2:2//a/b/c", Some("search://kw:1:1//a/b")), ("search://kw:1:1//a/b", Some("search://kw//a")), @@ -329,13 +346,16 @@ mod tests { ("sftp://vps//", None), // Relative ("search://kw:2:2/a/b", Some("search://kw:1:1/a")), - ("search://kw:1:1/a", None), + ("search://kw:1:1/a", Some("search://kw/")), ("search://kw/", None), + ("test-mount://7z:1:1/a", Some("test-mount://7z/")), + ("test-scope://aws/a", Some("test-scope://aws/")), + ("sftp://vps/a", Some("sftp://vps/")), ]; for (path, expected) in cases { let path: UrlBuf = path.parse()?; - assert_eq!(path.parent().map(|u| format!("{u:?}")).as_deref(), expected); + assert_eq!(path.parent().map(debug).as_deref(), expected); } Ok(()) @@ -344,29 +364,166 @@ mod tests { #[test] fn test_into_search() -> Result<()> { crate::init_tests(); - const S: char = std::path::MAIN_SEPARATOR; let u: UrlBuf = "/root".parse()?; - assert_eq!(format!("{u:?}"), "/root"); + assert_eq!(debug(&u), "/root"); let u = u.into_search("kw")?; - assert_eq!(format!("{u:?}"), "search://kw//root"); - assert_eq!(format!("{:?}", u.parent().unwrap()), "/"); + assert_eq!(debug(&u), "search://kw//root"); + assert_eq!(debug(u.parent().unwrap()), "/"); let u = u.try_join("examples")?; - assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); + assert_eq!(debug(&u), "search://kw:1:1//root/examples"); let u = u.try_join("README.md")?; - assert_eq!(format!("{u:?}"), format!("search://kw:2:2//root{S}examples{S}README.md")); + assert_eq!(debug(&u), "search://kw:2:2//root/examples/README.md"); let u = u.parent().unwrap(); - assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); + assert_eq!(debug(u), "search://kw:1:1//root/examples"); let u = u.parent().unwrap(); - assert_eq!(format!("{u:?}"), "search://kw//root"); + assert_eq!(debug(u), "search://kw//root"); let u = u.parent().unwrap(); - assert_eq!(format!("{u:?}"), "/"); + assert_eq!(debug(u), "/"); + + Ok(()) + } + + #[test] + fn test_hub_parse() -> Result<()> { + crate::init_tests(); + + let root: UrlBuf = "test-hub://root/@/".parse()?; + assert_eq!(debug(&root), "test-hub://root/@/"); + assert_eq!(root.loc().kind(), PathKind::Os); + + let encoded: UrlBuf = "test-hub://%252C/@/".parse()?; + assert_eq!(debug(encoded), "test-hub://%252C/@/"); + let encoded: UrlBuf = "test-hub://b1/@a%2Cb%40c%25d%2Fe,root/foo/bar".parse()?; + assert_eq!(debug(encoded), "test-hub://b1/@a%2Cb%40c%25d%2Fe,root/foo/bar"); + + Ok(()) + } + + #[test] + fn test_hub_domain() -> Result<()> { + crate::init_tests(); + + let root: UrlBuf = "test-hub://root/@/".parse()?; + let foo = root.try_join("foo")?.into_domain("a1"); + assert_eq!(debug(&foo), "test-hub://a1/@root/foo"); + + let bar = foo.try_join("bar")?.into_domain("b1"); + assert_eq!(bar.entry_key(), "b1"); + assert_eq!(debug(&bar), "test-hub://b1/@a1,root/foo/bar"); + assert_eq!(debug(bar.parent().unwrap()), "test-hub://a1/@root/foo"); + assert_eq!(debug(bar.parent().unwrap().parent().unwrap()), "test-hub://root/@/"); + + let relative = UrlCow::try_from("test-hub://a1/@/@abc")?; + assert_eq!(debug(relative.as_url()), "test-hub://a1/@/@abc"); + assert_eq!(debug(relative.parent().unwrap().as_url()), "test-hub:///@/"); + assert_eq!(relative.entry_key(), "a1"); + assert!(!relative.is_owned()); + assert!(relative.parent().unwrap().entry_key().is_empty()); + + Ok(()) + } + + #[test] + fn test_hub_join() -> Result<()> { + crate::init_tests(); + + let root: UrlBuf = "test-hub://root/@/".parse()?; + let bar: UrlBuf = "test-hub://b1/@a1,root/foo/bar".parse()?; + + assert_eq!(debug(bar.try_join(".")?), "test-hub://b1/@a1,root/foo/bar"); + assert_eq!(debug(bar.try_join("..")?), "test-hub:///@b1,a1,root/foo/bar/.."); + + assert_eq!(debug(bar.try_join("/x/y")?), "test-hub:///@,//x/y"); + assert_eq!(debug(root.try_join("../../..")?), "test-hub:///@,,root/../../.."); + + let absolute = root.try_join("/foo")?; + assert_eq!(debug(&absolute), "test-hub:///@//foo"); + let absolute = absolute.into_domain("a1"); + assert_eq!(debug(&absolute), "test-hub://a1/@//foo"); + assert_eq!(debug(absolute.parent().unwrap().try_join("..")?), "test-hub:///@//.."); + + Ok(()) + } + + #[test] + fn test_hub_ports() -> Result<()> { + crate::init_tests(); + + let ports: UrlBuf = "test-hub://b1:2:1/@a1,root/foo/bar".parse()?; + assert_eq!(debug(ports.base()), "test-hub://root/@/"); + assert_eq!(debug(ports.trail()), "test-hub://a1/@root/foo"); + + let ports: UrlBuf = "test-hub://b1:3:1/@a1,root//foo/bar".parse()?; + assert_eq!(debug(ports.base()), "test-hub://root/@/"); + assert_eq!(debug(ports.trail()), "test-hub://a1/@root//foo"); + + let zeroed: UrlBuf = "test-hub://b1:0:0/@a1,root/foo/bar".parse()?; + assert_eq!(debug(zeroed.base()), "test-hub://b1/@a1,root/foo/bar"); + assert_eq!(debug(zeroed.trail()), "test-hub://b1/@a1,root/foo/bar"); + + Ok(()) + } + + #[test] + fn test_hub_invalid() { + crate::init_tests(); + + assert!("test-hub://a1/foo".parse::().is_err()); + assert!("test-hub://b1/@a1/foo/bar".parse::().is_err()); + } + + #[test] + fn test_hub_replace() -> Result<()> { + crate::init_tests(); + + let url: UrlBuf = "test-hub://b1/@a1,root/foo/bar".parse()?; + assert_eq!( + debug(url.try_replace(2, Path::new("baz/qux"))?.as_url()), + "test-hub://b1:2:2/@,a1,root/foo/baz/qux" + ); + assert_eq!(debug(url.try_replace(1, Path::new("qux"))?.as_url()), "test-hub://b1/@root/qux"); + + Ok(()) + } + + #[cfg(windows)] + #[test] + fn test_hub_windows() -> Result<()> { + crate::init_tests(); + + let root: UrlBuf = "test-hub://root/@/".parse()?; + let c = root.try_join(r"C:\")?.into_domain("c-root"); + assert_eq!(c.entry_key(), "c-root"); + assert_eq!(c.auth().parent_depth(), 0); + + let drive = c.try_join(r"Users\file.txt")?.into_domain("file"); + assert_eq!(drive.loc(), Path::new(r"C:\Users\file.txt")); + assert_eq!(drive.auth().parent_depth(), 2); + + let parent = drive.parent().unwrap(); + assert_eq!(parent.loc(), Path::new(r"C:\Users")); + + let parent = parent.parent().unwrap(); + assert_eq!(parent.loc(), Path::new(r"C:\")); + assert_eq!(parent.entry_key(), "c-root"); + assert!(parent.parent().is_none()); + + let relative = root.try_join(r"C:foo")?; + assert!(!relative.is_absolute()); + assert_eq!(relative.parent().unwrap().loc(), Path::new("C:")); + assert!(relative.parent().unwrap().parent().is_none()); + + let unc = root.try_join(r"\\server\share\dir\file")?; + assert!(unc.is_absolute()); + assert_eq!(unc.loc(), Path::new(r"\\server\share\dir\file")); + assert_eq!(unc.auth().parent_depth(), 2); Ok(()) } diff --git a/yazi-shared/src/url/components.rs b/yazi-shared/src/url/components.rs index ed7f2cfe..5bdaf627 100644 --- a/yazi-shared/src/url/components.rs +++ b/yazi-shared/src/url/components.rs @@ -1,11 +1,12 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not}; -use crate::{auth::Auth, loc::Loc, path, spec::{Encode as EncodeSpec, Spec}, strand::{StrandBuf, StrandCow}, url::{Component, Encode as EncodeUrl, Url}}; +use crate::{auth::Auth, loc::Loc, path, spec::{EncodeSpec, Spec}, strand::{StrandBuf, StrandCow}, url::{Component, Encode as EncodeUrl, Url}}; #[derive(Clone)] pub struct Components<'a> { inner: path::Components<'a>, url: Url<'a>, + auth_yields: usize, back_yields: usize, scheme_yielded: bool, } @@ -15,6 +16,7 @@ impl<'a> From> for Components<'a> { Self { inner: value.loc().components(), url: value, + auth_yields: 0, back_yields: 0, scheme_yielded: false, } @@ -83,6 +85,10 @@ impl<'a> Components<'a> { Url::Mount { auth, .. } => { Url::Mount { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), auth } } + Url::Hub { auth, .. } => Url::Hub { + loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), + auth: auth.parent_at(self.auth_yields), + }, Url::Scope { auth, .. } => { Url::Scope { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), auth } } @@ -116,6 +122,7 @@ impl<'a> Iterator for Components<'a> { impl<'a> DoubleEndedIterator for Components<'a> { fn next_back(&mut self) -> Option { if let Some(c) = self.inner.next_back() { + self.auth_yields += c.has_auth() as usize; self.back_yields += 1; Some(c.into()) } else if !self.scheme_yielded { diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index 4b09a528..37f22647 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -1,16 +1,17 @@ use std::{borrow::Cow, hash::{Hash, Hasher}, path::PathBuf, sync::Arc}; -use anyhow::Result; +use anyhow::{Result, ensure}; use serde::{Deserialize, Deserializer, Serialize}; use typed_path::{UnixPath, UnixPathBuf}; -use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf, LocCow}, path::{PathBufDyn, PathCow, PathDyn}, spec::Spec, url::{AsUrl, Url, UrlBuf}}; +use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf, LocCow}, path::{DynPath, PathBufDyn, PathCow, PathDyn}, spec::Spec, url::{AsUrl, Url, UrlBuf}}; #[derive(Clone, Debug)] pub enum UrlCow<'a> { Regular(LocCow<'a>), Search { loc: LocCow<'a>, auth: Arc }, Mount { loc: LocCow<'a>, auth: Arc }, + Hub { loc: LocCow<'a>, auth: Arc }, Scope { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc }, Sftp { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc }, } @@ -21,6 +22,7 @@ impl<'a> From> for UrlCow<'a> { Url::Regular(loc) => Self::Regular(loc.into()), Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() }, Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() }, + Url::Hub { loc, auth } => Self::Hub { loc: loc.into(), auth: auth.clone() }, Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() }, Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() }, } @@ -40,6 +42,7 @@ impl From for UrlCow<'_> { UrlBuf::Regular(loc) => Self::Regular(loc.into()), UrlBuf::Search { loc, auth } => Self::Search { loc: loc.into(), auth }, UrlBuf::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth }, + UrlBuf::Hub { loc, auth } => Self::Hub { loc: loc.into(), auth }, UrlBuf::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth }, UrlBuf::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth }, } @@ -113,10 +116,13 @@ impl<'a> TryFrom<(Spec, PathDyn<'a>)> for UrlCow<'a> { fn try_from((spec, path): (Spec, PathDyn<'a>)) -> Result { let Spec { auth, uri, urn } = spec; + validate_auth_depth(&auth, path)?; + Ok(match auth.kind { AuthKind::Regular => Self::Regular(Loc::bare(path.as_os()?).into()), AuthKind::Search => Self::Search { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, AuthKind::Mount => Self::Mount { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, + AuthKind::Hub => Self::Hub { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, AuthKind::Scope => Self::Scope { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth }, AuthKind::Sftp => Self::Sftp { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth }, }) @@ -128,6 +134,8 @@ impl<'a> TryFrom<(Spec, PathBufDyn)> for UrlCow<'a> { fn try_from((spec, path): (Spec, PathBufDyn)) -> Result { let Spec { auth, uri, urn } = spec; + validate_auth_depth(&auth, path.dyn_path())?; + Ok(match auth.kind { AuthKind::Regular => { Self::Regular(LocBuf::::from(path.into_os()?).into()) @@ -140,6 +148,9 @@ impl<'a> TryFrom<(Spec, PathBufDyn)> for UrlCow<'a> { loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), auth, }, + AuthKind::Hub => { + Self::Hub { loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), auth } + } AuthKind::Scope => { Self::Scope { loc: LocBuf::::with(path.try_into()?, uri, urn)?.into(), auth } } @@ -172,6 +183,7 @@ impl<'a> UrlCow<'a> { Self::Regular(loc) => loc.is_owned(), Self::Search { loc, .. } => loc.is_owned(), Self::Mount { loc, .. } => loc.is_owned(), + Self::Hub { loc, .. } => loc.is_owned(), Self::Scope { loc, .. } => loc.is_owned(), Self::Sftp { loc, .. } => loc.is_owned(), } @@ -182,6 +194,7 @@ impl<'a> UrlCow<'a> { Self::Regular(loc) => UrlBuf::Regular(loc.into_owned()), Self::Search { loc, auth } => UrlBuf::Search { loc: loc.into_owned(), auth }, Self::Mount { loc, auth } => UrlBuf::Mount { loc: loc.into_owned(), auth }, + Self::Hub { loc, auth } => UrlBuf::Hub { loc: loc.into_owned(), auth }, Self::Scope { loc, auth } => UrlBuf::Scope { loc: loc.into_owned(), auth }, Self::Sftp { loc, auth } => UrlBuf::Sftp { loc: loc.into_owned(), auth }, } @@ -191,7 +204,9 @@ impl<'a> UrlCow<'a> { let (uri, urn) = Spec::retrieve_ports(self.as_url()); let (auth, path) = match self { Self::Regular(loc) => (Auth::default_arc(), loc.into_path()), - Self::Search { loc, auth } | Self::Mount { loc, auth } => (auth, loc.into_path()), + Self::Search { loc, auth } | Self::Mount { loc, auth } | Self::Hub { loc, auth } => { + (auth, loc.into_path()) + } Self::Scope { loc, auth } | Self::Sftp { loc, auth } => (auth, loc.into_path()), }; (Spec { auth, uri, urn }, path) @@ -222,6 +237,16 @@ impl<'de> Deserialize<'de> for UrlCow<'_> { } } +fn validate_auth_depth(auth: &Auth, path: PathDyn) -> Result<()> { + if auth.kind == AuthKind::Hub { + ensure!( + auth.parent_depth() == path.components().auth_depth(), + "Hub URL parent depth does not match its path" + ); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/yazi-shared/src/url/de.rs b/yazi-shared/src/url/de.rs index d3449501..db494024 100644 --- a/yazi-shared/src/url/de.rs +++ b/yazi-shared/src/url/de.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}}; -use crate::{data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; +use crate::{auth::Domain, data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; pub struct UrlDeserializer<'a>(pub(super) UrlCow<'a>); @@ -33,7 +33,7 @@ impl<'de, 'a: 'de> Deserializer<'de> for UrlDeserializer<'a> { struct MapDeserializer<'a> { kind: Option<&'static str>, scheme: Option>, - domain: Option>, + domain: Option>, uri: Option, urn: Option, path: Option>, @@ -47,7 +47,7 @@ impl<'a> MapDeserializer<'a> { Self { kind: Some(spec.kind.into()), scheme: Some(spec.scheme.intern()), - domain: Some(spec.domain.intern()), + domain: Some(spec.domain.clone()), uri: Some(uri), urn: Some(urn), path: Some(path.into_encoded_bytes()), diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs index 85e43a76..82ecb7e7 100644 --- a/yazi-shared/src/url/display.rs +++ b/yazi-shared/src/url/display.rs @@ -1,4 +1,4 @@ -use crate::{spec::Encode, url::Url}; +use crate::{spec::EncodeSpec, url::Url}; pub struct Display<'a>(pub Url<'a>); @@ -6,7 +6,7 @@ impl std::fmt::Display for Display<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (kind, loc) = (self.0.kind(), self.0.loc()); if kind.is_virtual() { - Encode(self.0).fmt(f)?; + EncodeSpec(self.0).fmt(f)?; } loc.display().fmt(f) } diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index 8969547d..52ef4191 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -1,24 +1,36 @@ use std::fmt::{self, Display}; -use percent_encoding::{CONTROLS, percent_encode}; +use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; -use crate::{auth::Encode as EncodeAuth, url::Url}; +use crate::{auth::{EncodeAuth, EncodePrefix}, spec::EncodeSpec, url::Url}; #[derive(Clone, Copy)] pub struct Encode<'a>(pub Url<'a>); +impl Encode<'_> { + pub fn loc(b: &[u8]) -> PercentEncode<'_> { + const SET: &AsciiSet = &CONTROLS.add(b'%'); + percent_encode(b, SET) + } +} + impl Display for Encode<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use crate::spec::Encode as E; - - let loc = percent_encode(self.0.loc().encoded_bytes(), CONTROLS); + let loc = Self::loc(self.0.loc().encoded_bytes()); match self.0 { Url::Regular(_) => write!(f, "regular~://{loc}"), Url::Search { auth, .. } | Url::Mount { auth, .. } + | Url::Hub { auth, .. } | Url::Scope { auth, .. } | Url::Sftp { auth, .. } => { - write!(f, "{}{}/{loc}", EncodeAuth(auth, true), E::ports((*self).into())) + write!( + f, + "{}{}{}{loc}", + EncodeAuth(auth, true), + EncodeSpec::ports((*self).into()), + EncodePrefix(auth) + ) } } } diff --git a/yazi-shared/src/url/like.rs b/yazi-shared/src/url/like.rs index 471c808b..ccaaebb9 100644 --- a/yazi-shared/src/url/like.rs +++ b/yazi-shared/src/url/like.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, path::Path}; use anyhow::Result; -use crate::{auth::{Auth, AuthKind}, path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, spec::Spec, strand::{AsStrand, Strand}, url::{AsUrl, Components, Display, Url, UrlBuf, UrlCow}}; +use crate::{auth::{Auth, AuthKind}, path::{DynPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, spec::Spec, strand::{AsStrand, Strand}, url::{AsUrl, Components, Display, Url, UrlBuf, UrlCow}}; pub trait UrlLike where @@ -22,6 +22,8 @@ where fn ext(&self) -> Option> { self.as_url().ext() } + fn entry_key(&self) -> PathDyn<'_> { self.as_url().entry_key() } + fn has_base(&self) -> bool { self.as_url().has_base() } fn has_root(&self) -> bool { self.as_url().has_root() } @@ -44,6 +46,8 @@ where fn pair(&self) -> Option<(Url<'_>, PathDyn<'_>)> { self.as_url().pair() } + fn pair2(&self) -> Option<(Url<'_>, PathDyn<'_>)> { self.as_url().pair2() } + fn parent(&self) -> Option> { self.as_url().parent() } fn spec(&self) -> Spec { self.as_url().spec() } @@ -62,8 +66,8 @@ where self.as_url().try_join(path) } - fn try_replace<'a>(&self, take: usize, path: impl AsPathRef<'a>) -> Result> { - self.as_url().try_replace(take, path) + fn try_replace<'a>(&self, take: usize, to: impl DynPathRef<'a>) -> Result> { + self.as_url().try_replace(take, to) } fn try_starts_with(&self, base: impl AsUrl) -> Result { diff --git a/yazi-shared/src/url/lua.rs b/yazi-shared/src/url/lua.rs index 65ef2086..2ad5777c 100644 --- a/yazi-shared/src/url/lua.rs +++ b/yazi-shared/src/url/lua.rs @@ -121,6 +121,9 @@ impl UserData for UrlBuf { methods.add_method_once("into_search", |_, me, domain: LuaString| { me.into_search(domain.to_str()?).into_lua_err() }); + methods.add_method_once("into_domain", |_, me, domain: LuaString| { + Ok(me.into_domain(domain.to_str()?.to_owned())) + }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(*me == *other)); methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| { diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index 547b5824..c5a30b76 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -42,6 +42,7 @@ impl AsUrl for UrlBuf { Self::Regular(loc) => Url::Regular(loc.as_loc()), Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth }, Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth }, + Self::Hub { loc, auth } => Url::Hub { loc: loc.as_loc(), auth }, Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth }, Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth }, } @@ -64,6 +65,7 @@ impl AsUrl for UrlCow<'_> { Self::Regular(loc) => Url::Regular(loc.as_loc()), Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth }, Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth }, + Self::Hub { loc, auth } => Url::Hub { loc: loc.as_loc(), auth }, Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth }, Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth }, } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 07fdbb8c..db3d1dbe 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -5,13 +5,14 @@ use hashbrown::Equivalent; use serde::Serialize; use super::Encode as EncodeUrl; -use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf}, path::{AsPath, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError, StripSuffixError}, spec::{Encode as EncodeSpec, ParsedSpec, Spec}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; +use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf}, path::{DynPath, DynPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError, StripSuffixError}, spec::{EncodeSpec, ParsedSpec, Spec}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub enum Url<'a> { Regular(Loc<'a>), Search { loc: Loc<'a>, auth: &'a Arc }, Mount { loc: Loc<'a>, auth: &'a Arc }, + Hub { loc: Loc<'a>, auth: &'a Arc }, Scope { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc }, Sftp { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc }, } @@ -60,11 +61,22 @@ impl<'a> Url<'a> { Self::Regular(_) => &Auth::DEFAULT, Self::Search { auth, .. } | Self::Mount { auth, .. } + | Self::Hub { auth, .. } | Self::Scope { auth, .. } | Self::Sftp { auth, .. } => auth, } } + fn auth_at(self, base: Self) -> Option<&'a Auth> { + let (Self::Hub { auth, .. }, Self::Hub { .. }) = (self, base) else { + return self.auth().covariant(base.auth()).then_some(self.auth()); + }; + + let depth = + self.loc().components().auth_depth().checked_sub(base.loc().components().auth_depth())?; + Some(auth.parent_at(depth)) + } + #[inline] pub fn as_regular(self) -> Result { Ok(Self::Regular(Loc::bare(self.loc().as_os()?))) @@ -75,6 +87,10 @@ impl<'a> Url<'a> { Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())), Self::Search { loc, auth } => Self::Search { loc: Loc::zeroed(loc.base()), auth }, Self::Mount { loc, auth } => Self::Mount { loc: Loc::zeroed(loc.base()), auth }, + Self::Hub { loc, auth } => Self::Hub { + loc: Loc::bare(loc.base()), + auth: auth.parent_at(loc.uri().dyn_path().components().auth_depth()), + }, Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.base()), auth }, Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.base()), auth }, } @@ -95,6 +111,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.extension()?.as_strand(), Self::Search { loc, .. } => loc.extension()?.as_strand(), Self::Mount { loc, .. } => loc.extension()?.as_strand(), + Self::Hub { loc, .. } => loc.extension()?.as_strand(), Self::Scope { loc, .. } => loc.extension()?.as_strand(), Self::Sftp { loc, .. } => loc.extension()?.as_strand(), }) @@ -106,6 +123,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.has_base(), Self::Search { loc, .. } => loc.has_base(), Self::Mount { loc, .. } => loc.has_base(), + Self::Hub { loc, .. } => loc.has_base(), Self::Scope { loc, .. } => loc.has_base(), Self::Sftp { loc, .. } => loc.has_base(), } @@ -120,6 +138,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.has_trail(), Self::Search { loc, .. } => loc.has_trail(), Self::Mount { loc, .. } => loc.has_trail(), + Self::Hub { loc, .. } => loc.has_trail(), Self::Scope { loc, .. } => loc.has_trail(), Self::Sftp { loc, .. } => loc.has_trail(), } @@ -140,11 +159,12 @@ impl<'a> Url<'a> { #[inline] pub fn loc(self) -> PathDyn<'a> { match self { - Self::Regular(loc) => loc.as_path(), - Self::Search { loc, .. } => loc.as_path(), - Self::Mount { loc, .. } => loc.as_path(), - Self::Scope { loc, .. } => loc.as_path(), - Self::Sftp { loc, .. } => loc.as_path(), + Self::Regular(loc) => loc.dyn_path(), + Self::Search { loc, .. } => loc.dyn_path(), + Self::Mount { loc, .. } => loc.dyn_path(), + Self::Hub { loc, .. } => loc.dyn_path(), + Self::Scope { loc, .. } => loc.dyn_path(), + Self::Sftp { loc, .. } => loc.dyn_path(), } } @@ -154,6 +174,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.file_name()?.as_strand(), Self::Search { loc, .. } => loc.file_name()?.as_strand(), Self::Mount { loc, .. } => loc.file_name()?.as_strand(), + Self::Hub { loc, .. } => loc.file_name()?.as_strand(), Self::Scope { loc, .. } => loc.file_name()?.as_strand(), Self::Sftp { loc, .. } => loc.file_name()?.as_strand(), }) @@ -165,6 +186,19 @@ impl<'a> Url<'a> { #[inline] pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.urn())) } + #[inline] + pub fn pair2(self) -> Option<(Self, PathDyn<'a>)> { + Some((self.parent()?, Some(self.entry_key()).filter(|k| !k.is_empty())?)) + } + + #[inline] + pub fn entry_key(self) -> PathDyn<'a> { + match self { + Self::Hub { auth, .. } => PathDyn::Unix(typed_path::UnixPath::new(auth.domain.as_bytes())), + _ => self.urn(), + } + } + pub fn parent(self) -> Option { let uri = self.uri(); @@ -187,6 +221,11 @@ impl<'a> Url<'a> { Self::Mount { loc: Loc::floated(loc.parent()?, loc.base()), auth } } + // Hub + Self::Hub { loc, auth } => { + Self::Hub { loc: Loc::bare(loc.parent()?), auth: auth.parent.as_ref()? } + } + // Scope Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.parent()?), auth }, @@ -205,6 +244,7 @@ impl<'a> Url<'a> { Self::Regular(_) => Auth::default_arc(), Self::Search { auth, .. } | Self::Mount { auth, .. } + | Self::Hub { auth, .. } | Self::Scope { auth, .. } | Self::Sftp { auth, .. } => auth.clone(), }; @@ -219,6 +259,7 @@ impl<'a> Url<'a> { Self::Regular(loc) => loc.file_stem()?.as_strand(), Self::Search { loc, .. } => loc.file_stem()?.as_strand(), Self::Mount { loc, .. } => loc.file_stem()?.as_strand(), + Self::Hub { loc, .. } => loc.file_stem()?.as_strand(), Self::Scope { loc, .. } => loc.file_stem()?.as_strand(), Self::Sftp { loc, .. } => loc.file_stem()?.as_strand(), }) @@ -253,6 +294,11 @@ impl<'a> Url<'a> { Self::Mount { loc: Loc::new(loc.trail(), loc.base(), loc.base()), auth } } + Self::Hub { loc, auth } => Self::Hub { + loc: Loc::bare(loc.trail()), + auth: auth.parent_at(loc.urn().dyn_path().components().auth_depth()), + }, + Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.trail()), auth }, Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.trail()), auth }, @@ -261,13 +307,16 @@ impl<'a> Url<'a> { pub fn triple(self) -> (PathDyn<'a>, PathDyn<'a>, PathDyn<'a>) { match self { - Self::Regular(loc) | Self::Search { loc, .. } | Self::Mount { loc, .. } => { + Self::Regular(loc) + | Self::Search { loc, .. } + | Self::Mount { loc, .. } + | Self::Hub { loc, .. } => { let (base, rest, urn) = loc.triple(); - (base.as_path(), rest.as_path(), urn.as_path()) + (base.dyn_path(), rest.dyn_path(), urn.dyn_path()) } Self::Scope { loc, .. } | Self::Sftp { loc, .. } => { let (base, rest, urn) = loc.triple(); - (base.as_path(), rest.as_path(), urn.as_path()) + (base.dyn_path(), rest.dyn_path(), urn.dyn_path()) } } } @@ -279,6 +328,7 @@ impl<'a> Url<'a> { } pub fn try_join(self, path: impl AsStrand) -> Result { + let path = path.as_strand(); let joined = self.loc().try_join(path)?; Ok(match self { @@ -300,6 +350,11 @@ impl<'a> Url<'a> { UrlBuf::Mount { loc: LocBuf::::zeroed(joined.into_os()?), auth: auth.clone() } } + Self::Hub { auth, .. } => UrlBuf::Hub { + loc: joined.into_os()?.into(), + auth: auth.clone().descend(path.as_os_path()?), + }, + Self::Scope { auth, .. } => { UrlBuf::Scope { loc: joined.into_unix()?.into(), auth: auth.clone() } } @@ -310,8 +365,8 @@ impl<'a> Url<'a> { }) } - pub fn try_replace<'b>(self, take: usize, to: impl AsPathRef<'b>) -> Result> { - self.try_replace_impl(take, to.as_path_ref()) + pub fn try_replace<'b>(self, take: usize, to: impl DynPathRef<'b>) -> Result> { + self.try_replace_impl(take, to.dyn_path_ref()) } fn try_replace_impl<'b>(self, take: usize, rep: PathDyn<'b>) -> Result> { @@ -337,6 +392,10 @@ impl<'a> Url<'a> { loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), auth: auth.clone(), }, + Self::Hub { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Hub { + auth: auth.clone().with_parent_depth(path.components().auth_depth()), + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + }, Self::Scope { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Scope { loc: LocBuf::::new(path.into_unix()?, loc.base(), loc.trail()), auth: auth.clone(), @@ -354,6 +413,10 @@ impl<'a> Url<'a> { loc: LocBuf::::saturated(path.into_os()?, self.kind()), auth: auth.clone(), }, + Self::Hub { auth, .. } => UrlBuf::Hub { + auth: auth.clone().with_parent_depth(path.components().auth_depth()), + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + }, Self::Scope { auth, .. } => UrlBuf::Scope { loc: LocBuf::::saturated(path.into_unix()?, self.kind()), auth: auth.clone(), @@ -367,10 +430,12 @@ impl<'a> Url<'a> { Ok(url.into()) } - #[inline] pub fn try_starts_with(self, base: impl AsUrl) -> Result { let base = base.as_url(); - Ok(self.loc().try_starts_with(base.loc())? && self.auth().covariant(base.auth())) + Ok( + self.loc().try_starts_with(base.loc())? + && self.auth_at(base).is_some_and(|a| a == base.auth()), + ) } pub fn try_strip_prefix(self, base: impl AsUrl) -> Result, StripPrefixError> { @@ -379,7 +444,7 @@ impl<'a> Url<'a> { let base = base.as_url(); let prefix = self.loc().try_strip_prefix(base.loc())?; - if self.auth().covariant(base.auth()) { + if self.auth_at(base).is_some_and(|a| a == base.auth()) { return Ok(prefix); } @@ -420,22 +485,24 @@ impl<'a> Url<'a> { #[inline] pub fn uri(self) -> PathDyn<'a> { match self { - Self::Regular(loc) => loc.uri().as_path(), - Self::Search { loc, .. } => loc.uri().as_path(), - Self::Mount { loc, .. } => loc.uri().as_path(), - Self::Scope { loc, .. } => loc.uri().as_path(), - Self::Sftp { loc, .. } => loc.uri().as_path(), + Self::Regular(loc) => loc.uri().dyn_path(), + Self::Search { loc, .. } => loc.uri().dyn_path(), + Self::Mount { loc, .. } => loc.uri().dyn_path(), + Self::Hub { loc, .. } => loc.uri().dyn_path(), + Self::Scope { loc, .. } => loc.uri().dyn_path(), + Self::Sftp { loc, .. } => loc.uri().dyn_path(), } } #[inline] pub fn urn(self) -> PathDyn<'a> { match self { - Self::Regular(loc) => loc.urn().as_path(), - Self::Search { loc, .. } => loc.urn().as_path(), - Self::Mount { loc, .. } => loc.urn().as_path(), - Self::Scope { loc, .. } => loc.urn().as_path(), - Self::Sftp { loc, .. } => loc.urn().as_path(), + Self::Regular(loc) => loc.urn().dyn_path(), + Self::Search { loc, .. } => loc.urn().dyn_path(), + Self::Mount { loc, .. } => loc.urn().dyn_path(), + Self::Hub { loc, .. } => loc.urn().dyn_path(), + Self::Scope { loc, .. } => loc.urn().dyn_path(), + Self::Sftp { loc, .. } => loc.urn().dyn_path(), } } } diff --git a/yazi-vfs/src/engine/demand.rs b/yazi-vfs/src/engine/demand.rs index ecbbf140..02120a53 100644 --- a/yazi-vfs/src/engine/demand.rs +++ b/yazi-vfs/src/engine/demand.rs @@ -40,7 +40,7 @@ impl FileBuilder for Demand { AuthKind::Regular | AuthKind::Search => { self.0.build::().open(url).await?.into() } - AuthKind::Mount | AuthKind::Scope => { + AuthKind::Mount | AuthKind::Hub | AuthKind::Scope => { self.0.build::().open(url).await?.into() } AuthKind::Sftp => self.0.build::().open(url).await?.into(), diff --git a/yazi-vfs/src/engine/engine.rs b/yazi-vfs/src/engine/engine.rs index 9a9ce8e9..b727b682 100644 --- a/yazi-vfs/src/engine/engine.rs +++ b/yazi-vfs/src/engine/engine.rs @@ -55,7 +55,7 @@ where match (from.kind().is_local(), to.kind().is_local()) { (true, true) => Local::new(from).await?.copy(to.loc(), attrs).await, - (false, false) if from.auth().covariant(to.auth()) => { + (false, false) if from.auth().same_service(to.auth()) => { Engines::new(from).await?.copy(to.loc(), attrs).await } (true, false) | (false, true) | (false, false) => super::copy_impl(from, to, attrs).await, @@ -75,7 +75,7 @@ where let (from, to) = (from.as_url(), to.as_url()); let attrs = attrs.into(); - if from.auth().covariant(to.auth()) { + if from.auth().same_service(to.auth()) { let engine = Engines::new(from).await?; if engine.capabilities().await?.copy_progressive { return engine.copy_progressive(to.loc(), attrs); @@ -119,7 +119,7 @@ where V: AsUrl, { let (original, link) = (original.as_url(), link.as_url()); - if original.auth().covariant(link.auth()) { + if original.auth().same_service(link.auth()) { Engines::new(original).await?.hard_link(link.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) @@ -208,7 +208,7 @@ where V: AsUrl, { let (from, to) = (from.as_url(), to.as_url()); - if from.auth().covariant(to.auth()) { + if from.auth().same_service(to.auth()) { Engines::new(from).await?.rename(to.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) @@ -268,7 +268,9 @@ where let url = url.into(); match url.as_url() { Url::Regular(_) | Url::Search { .. } => yazi_fs::engine::local::try_absolute(url), - Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => super::try_absolute_impl(url), + Url::Mount { .. } | Url::Hub { .. } | Url::Scope { .. } | Url::Sftp { .. } => { + super::try_absolute_impl(url) + } } } diff --git a/yazi-vfs/src/engine/engines.rs b/yazi-vfs/src/engine/engines.rs index cace26f6..36a8d722 100644 --- a/yazi-vfs/src/engine/engines.rs +++ b/yazi-vfs/src/engine/engines.rs @@ -2,7 +2,7 @@ use std::io; use tokio::sync::mpsc; use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}}; -use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] pub(super) enum Engines<'a> { @@ -52,7 +52,7 @@ impl<'a> Engine for Engines<'a> { async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsPath, + P: DynPath, { match self { Self::Local(p) => p.copy(to, attrs).await, @@ -63,7 +63,7 @@ impl<'a> Engine for Engines<'a> { fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into, { match self { @@ -107,7 +107,7 @@ impl<'a> Engine for Engines<'a> { async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { match self { Self::Local(p) => p.hard_link(to).await, @@ -129,7 +129,7 @@ impl<'a> Engine for Engines<'a> { Ok(match url.kind() { K::Regular | K::Search => Self::Me::Local(yazi_fs::engine::local::Local::new(url).await?), - K::Mount | K::Scope => Self::Me::Lua(super::lua::Lua::new(url).await?), + K::Mount | K::Hub | K::Scope => Self::Me::Lua(super::lua::Lua::new(url).await?), K::Sftp => Self::Me::Sftp(super::sftp::Sftp::new(url).await?), }) } @@ -184,7 +184,7 @@ impl<'a> Engine for Engines<'a> { async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { match self { Self::Local(p) => p.rename(to).await, diff --git a/yazi-vfs/src/engine/lua/lua.rs b/yazi-vfs/src/engine/lua/lua.rs index e9810f63..81373ec9 100644 --- a/yazi-vfs/src/engine/lua/lua.rs +++ b/yazi-vfs/src/engine/lua/lua.rs @@ -6,7 +6,7 @@ use yazi_binding::MpscTx; use yazi_config::vfs::{ServiceLua, Vfs}; use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::Files}; use yazi_runner::{RUNNER, provider::{ProvideResult, ProviderJob}}; -use yazi_shared::{event::Cmd, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{event::Cmd, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; use crate::engine::lua::ReadDir; @@ -48,23 +48,23 @@ impl<'a> Engine for Lua<'a> { async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsPath, + P: DynPath, { let from = self.url.to_owned(); - let to = to.as_path().to_owned(); + let to = to.dyn_path().to_owned(); Ok(self.call(ProviderJob::Copy { from, to, attrs }).await?.0?) } fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into, { let (tx, rx) = mpsc::channel(20); let job = ProviderJob::CopyProgressive { from: self.url.to_owned(), - to: to.as_path().to_owned(), + to: to.dyn_path().to_owned(), attrs: attrs.into(), tx: MpscTx::map(tx.clone(), Ok), }; @@ -88,10 +88,10 @@ impl<'a> Engine for Lua<'a> { async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { let from = self.url.to_owned(); - let to = to.as_path().to_owned(); + let to = to.dyn_path().to_owned(); Ok(self.call(ProviderJob::HardLink { from, to }).await?.ok()?) } @@ -103,7 +103,7 @@ impl<'a> Engine for Lua<'a> { } async fn new<'b>(url: Url<'b>) -> io::Result> { - let (Url::Mount { auth, .. } | Url::Scope { auth, .. }) = url else { + let (Url::Mount { auth, .. } | Url::Hub { auth, .. } | Url::Scope { auth, .. }) = url else { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Not a custom VFS URL: {url:?}"), @@ -141,10 +141,10 @@ impl<'a> Engine for Lua<'a> { async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { let from = self.url.to_owned(); - let to = to.as_path().to_owned(); + let to = to.dyn_path().to_owned(); Ok(self.call(ProviderJob::Rename { from, to }).await?.ok()?) } diff --git a/yazi-vfs/src/engine/sftp/sftp.rs b/yazi-vfs/src/engine/sftp/sftp.rs index fca991df..303f1639 100644 --- a/yazi-vfs/src/engine/sftp/sftp.rs +++ b/yazi-vfs/src/engine/sftp/sftp.rs @@ -4,7 +4,7 @@ use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; use yazi_config::vfs::{ServiceSftp, Vfs}; use yazi_fs::engine::{Capabilities, DirReader, Engine, FileHolder}; use yazi_sftp::fs::{Attrs, Flags}; -use yazi_shared::{auth::AuthKind, loc::LocBuf, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{auth::AuthKind, loc::LocBuf, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; use super::Cha; use crate::engine::sftp::Conn; @@ -83,9 +83,9 @@ impl<'a> Engine for Sftp<'a> { async fn copy

(&self, to: P, attrs: yazi_fs::engine::Attrs) -> io::Result where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_unix()?; + let to = to.dyn_path().as_unix()?; let attrs = super::Attrs(attrs).try_into().unwrap_or_default(); let op = self.op().await?; @@ -107,12 +107,12 @@ impl<'a> Engine for Sftp<'a> { fn copy_progressive(&self, to: P, attrs: A) -> io::Result>> where - P: AsPath, + P: DynPath, A: Into, { let to = UrlBuf::Sftp { loc: LocBuf::::saturated( - to.as_path().to_unix_owned()?, + to.dyn_path().to_unix_owned()?, AuthKind::Sftp, ), auth: self.config.auth.clone(), @@ -138,9 +138,9 @@ impl<'a> Engine for Sftp<'a> { async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_unix()?; + let to = to.dyn_path().as_unix()?; Ok(self.op().await?.hardlink(self.path, to).await?) } @@ -176,9 +176,9 @@ impl<'a> Engine for Sftp<'a> { async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPath, + P: DynPath, { - let to = to.as_path().as_unix()?; + let to = to.dyn_path().as_unix()?; let op = self.op().await?; match op.rename_posix(self.path, &to).await { diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index 6c817473..131e0af7 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -97,20 +97,20 @@ impl Local { let mut ops = Vec::with_capacity(urls.len()); for u in urls { - let Some((parent, urn)) = u.pair() else { continue }; + let Some((parent, key)) = u.pair2() else { continue }; let Ok(file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); continue; }; if let Some(p) = file.url.as_local() && !engine::local::match_name_case(p).await { - ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); continue; } - ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); + ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into())); } FilesOp::mutate(ops); diff --git a/yazi-watcher/src/remote/remote.rs b/yazi-watcher/src/remote/remote.rs index 336f470f..6a94b9d1 100644 --- a/yazi-watcher/src/remote/remote.rs +++ b/yazi-watcher/src/remote/remote.rs @@ -39,16 +39,16 @@ impl Remote { let mut ups = Vec::with_capacity(urls.len()); for (u, upload) in urls { - let Some((parent, urn)) = u.pair() else { continue }; + let Some((parent, key)) = u.pair2() else { continue }; let Ok(mut file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); + ops.push(FilesOp::Deleting(parent.into(), [key.into()].into())); continue; }; let is_file = file.is_file(); file.cha.ctime = Some(SystemTime::now()); - ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); + ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into())); if upload && is_file { ups.push(u); } diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index 1c4c5336..97718f8d 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -21,7 +21,7 @@ impl Reporter { for url in urls.into_iter().map(Into::into) { match url.as_url().kind() { AuthKind::Regular | AuthKind::Search => self.report_local(url), - AuthKind::Mount => {} // TODO: mounted VFS cache invalidation + AuthKind::Mount | AuthKind::Hub => {} // TODO: mounted VFS cache invalidation AuthKind::Scope | AuthKind::Sftp => self.report_remote(url), } } diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index 6628191b..2c035378 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -59,7 +59,7 @@ impl Watched { AuthKind::Mount => UrlBuf::Mount { loc: path.into_os().ok()?.into(), auth }, AuthKind::Scope => UrlBuf::Scope { loc: path.into_unix().ok()?.into(), auth }, AuthKind::Sftp => UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), auth }, - AuthKind::Regular | AuthKind::Search => return None, + AuthKind::Regular | AuthKind::Search | AuthKind::Hub => return None, }; if self.contains_url(&url) { Some(url) } else { None } }