diff --git a/Cargo.lock b/Cargo.lock index 9d6d1b63..3187a76e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,9 +391,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" dependencies = [ "clap_builder", "clap_derive", @@ -401,9 +401,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" dependencies = [ "anstream", "anstyle", @@ -1335,9 +1335,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +checksum = "360e552c93fa0e8152ab463bc4c4837fce76a225df11dfaeea66c313de5e61f7" dependencies = [ "bitflags 2.9.1", "libc", @@ -2155,9 +2155,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags 2.9.1", ] @@ -2751,9 +2751,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1" dependencies = [ "indexmap", "serde", @@ -3310,7 +3310,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -3331,10 +3331,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -3578,7 +3579,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "toml 0.9.2", + "toml 0.9.4", "twox-hash", "vergen-gitcl", "yazi-boot", @@ -3609,7 +3610,7 @@ dependencies = [ "ratatui", "regex", "serde", - "toml 0.9.2", + "toml 0.9.4", "tracing", "yazi-codegen", "yazi-fs", @@ -3946,9 +3947,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" +checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 958f0899..7be4ade9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ ansi-to-tui = "7.0.0" anyhow = "1.0.98" base64 = "0.22.1" bitflags = { version = "2.9.1", features = [ "serde" ] } -clap = { version = "4.5.41", features = [ "derive" ] } +clap = { version = "4.5.42", features = [ "derive" ] } core-foundation-sys = "0.8.7" crossterm = { version = "0.29.0", features = [ "event-stream" ] } dirs = "6.0.0" @@ -48,7 +48,7 @@ syntect = { version = "5.2.0", default-features = false, features = tokio = { version = "1.47.0", features = [ "full" ] } tokio-stream = "0.1.17" tokio-util = "0.7.15" -toml = { version = "0.9.2" } +toml = { version = "0.9.4" } tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.1", default-features = false, features = [ "std", "random", "xxhash3_128" ] } unicode-width = { version = "0.2.0", default-features = false } diff --git a/yazi-actor/src/cmp/show.rs b/yazi-actor/src/cmp/show.rs index 64a3343c..43c410a8 100644 --- a/yazi-actor/src/cmp/show.rs +++ b/yazi-actor/src/cmp/show.rs @@ -1,4 +1,4 @@ -use std::{mem, ops::ControlFlow}; +use std::{ffi::OsStr, mem, ops::ControlFlow}; use anyhow::Result; use yazi_macro::{render, succ}; @@ -30,7 +30,7 @@ impl Actor for Show { }; cmp.ticket = opt.ticket; - cmp.cands = Self::match_candidates(&opt.word, cache); + cmp.cands = Self::match_candidates(opt.word.as_os_str(), cache); if cmp.cands.is_empty() { succ!(render!(mem::replace(&mut cmp.visible, false))); } @@ -43,8 +43,8 @@ impl Actor for Show { } impl Show { - fn match_candidates(word: &str, cache: &[CmpItem]) -> Vec { - let smart = !word.bytes().any(|c| c.is_ascii_uppercase()); + fn match_candidates(word: &OsStr, cache: &[CmpItem]) -> Vec { + let smart = !word.as_encoded_bytes().iter().any(|c| c.is_ascii_uppercase()); let flow = cache.iter().try_fold((Vec::new(), Vec::new()), |(mut exact, mut fuzzy), item| { if osstr_starts_with(&item.name, word, smart) { diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index 29f435b5..532774c7 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -1,11 +1,11 @@ -use std::{ffi::OsString, mem, path::{MAIN_SEPARATOR_STR, Path, PathBuf}}; +use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR}; use anyhow::Result; -use yazi_fs::{CWD, expand_path, services::Local}; +use yazi_fs::{CWD, expand_url, services::Local}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_proxy::CmpProxy; -use yazi_shared::{event::Data, natsort}; +use yazi_shared::{OsStrSplit, event::Data, natsort, url::{Url, UrnBuf}}; use crate::{Actor, Ctx}; @@ -25,13 +25,13 @@ impl Actor for Trigger { cmp.ticket = t; } - let Some((parent, word)) = Self::split_path(&opt.word) else { + let Some((parent, word)) = Self::split_url(&opt.word) else { return act!(cmp:close, cx, false); }; if cmp.caches.contains_key(&parent) { let ticket = cmp.ticket; - return act!(cmp:show, cx, ShowOpt { cache_name: parent, word: word.into(), ticket, ..Default::default() }); + return act!(cmp:show, cx, ShowOpt { cache_name: parent, word, ticket, ..Default::default() }); } let ticket = cmp.ticket; @@ -42,7 +42,7 @@ impl Actor for Trigger { // "/" is both a directory separator and the root directory per se // As there's no parent directory for the FS root, it is a special case - if parent == Path::new("/") { + if parent.loc.as_os_str() == "/" { cache.push(CmpItem { name: OsString::new(), is_dir: true }); } @@ -56,7 +56,7 @@ impl Actor for Trigger { cache.sort_unstable_by(|a, b| { natsort(a.name.as_encoded_bytes(), b.name.as_encoded_bytes(), false) }); - CmpProxy::show(ShowOpt { cache, cache_name: parent, word: word.into(), ticket }); + CmpProxy::show(ShowOpt { cache, cache_name: parent, word, ticket }); } Ok::<_, anyhow::Error>(()) @@ -67,34 +67,36 @@ impl Actor for Trigger { } impl Trigger { - fn split_path(s: &str) -> Option<(PathBuf, String)> { - if s == "~" { + fn split_url(s: &str) -> Option<(Url, UrnBuf)> { + let (scheme, path) = Url::parse(s.as_bytes()).ok()?; + + if !scheme.is_virtual() && path.as_os_str() == "~" { return None; // We don't autocomplete a `~`, but `~/` } #[cfg(windows)] - const SEP: [char; 2] = ['/', '\\']; + const SEP: &[char] = &['/', '\\']; #[cfg(not(windows))] const SEP: char = std::path::MAIN_SEPARATOR; - Some(match s.rsplit_once(SEP) { - Some(("", c)) => (PathBuf::from(MAIN_SEPARATOR_STR), c.to_owned()), - Some((p, c)) => (expand_path(p), c.to_owned()), - None => (CWD.load().to_path_buf(), s.to_owned()), + Some(match path.as_os_str().rsplit_once(SEP) { + Some((p, c)) if p.is_empty() => (Url { loc: MAIN_SEPARATOR_STR.into(), scheme }, c.into()), + Some((p, c)) => (expand_url(Url { loc: p.into(), scheme }).into_owned(), c.into()), + None => (CWD.load().as_ref().clone(), path.into()), }) } } #[cfg(test)] mod tests { - use std::path::Path; + use yazi_shared::url::Urn; use super::*; fn compare(s: &str, parent: &str, child: &str) { - let (p, c) = Trigger::split_path(s).unwrap(); - let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(&p); - assert_eq!((p, c.as_str()), (Path::new(parent), child)); + let (p, c) = Trigger::split_url(s).unwrap(); + let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(p); + assert_eq!((p, c.as_urn()), (parent.try_into().unwrap(), Urn::new(child))); } #[cfg(unix)] diff --git a/yazi-actor/src/mgr/follow.rs b/yazi-actor/src/mgr/follow.rs index 02914f78..82b6f9ae 100644 --- a/yazi-actor/src/mgr/follow.rs +++ b/yazi-actor/src/mgr/follow.rs @@ -20,7 +20,7 @@ impl Actor for Follow { if link_to.is_absolute() { act!(mgr:reveal, cx, link_to.to_owned()) } else if let Some(p) = file.url.parent_url() { - act!(mgr:reveal, cx, clean_url(&p.join(link_to))) + act!(mgr:reveal, cx, clean_url(p.join(link_to)).into_owned()) } else { succ!() } diff --git a/yazi-core/src/cmp/cmp.rs b/yazi-core/src/cmp/cmp.rs index ac0d61a0..28f69c49 100644 --- a/yazi-core/src/cmp/cmp.rs +++ b/yazi-core/src/cmp/cmp.rs @@ -1,12 +1,12 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::collections::HashMap; use yazi_parser::cmp::CmpItem; -use yazi_shared::Id; +use yazi_shared::{Id, url::Url}; use yazi_widgets::Scrollable; #[derive(Default)] pub struct Cmp { - pub caches: HashMap>, + pub caches: HashMap>, pub cands: Vec, pub offset: usize, pub cursor: usize, diff --git a/yazi-core/src/mgr/mimetype.rs b/yazi-core/src/mgr/mimetype.rs index 16320c34..a83d6e9c 100644 --- a/yazi-core/src/mgr/mimetype.rs +++ b/yazi-core/src/mgr/mimetype.rs @@ -1,14 +1,16 @@ use std::{borrow::Cow, collections::HashMap}; use yazi_fs::File; -use yazi_shared::{MIME_DIR, SStr, url::Url}; +use yazi_shared::{MIME_DIR, SStr, url::{CovUrl, Url}}; #[derive(Default)] -pub struct Mimetype(HashMap); +pub struct Mimetype(HashMap); impl Mimetype { #[inline] - pub fn by_url(&self, url: &Url) -> Option<&str> { self.0.get(url).map(|s| s.as_str()) } + pub fn by_url(&self, url: &Url) -> Option<&str> { + self.0.get(CovUrl::new(url)).map(|s| s.as_str()) + } #[inline] pub fn by_url_owned(&self, url: &Url) -> Option { @@ -26,8 +28,10 @@ impl Mimetype { } #[inline] - pub fn contains(&self, url: &Url) -> bool { self.0.contains_key(url) } + pub fn contains(&self, url: &Url) -> bool { self.0.contains_key(CovUrl::new(url)) } #[inline] - pub fn extend(&mut self, iter: impl IntoIterator) { self.0.extend(iter) } + pub fn extend(&mut self, iter: impl IntoIterator) { + self.0.extend(iter.into_iter().map(|(u, m)| (CovUrl(u), m))); + } } diff --git a/yazi-fs/src/fns.rs b/yazi-fs/src/fns.rs index 2be87d02..f433fe44 100644 --- a/yazi-fs/src/fns.rs +++ b/yazi-fs/src/fns.rs @@ -333,7 +333,7 @@ pub fn max_common_root(urls: &[Url]) -> usize { if urls.is_empty() { return 0; } else if urls.len() == 1 { - return urls[0].components().count().saturating_sub(1); + return urls[0].components().count() - 1; } let mut it = urls.iter().map(|u| u.parent_url()); @@ -341,13 +341,13 @@ pub fn max_common_root(urls: &[Url]) -> usize { return 0; // The first URL has no parent }; - let mut min = first.components().count(); + let mut common = first.components().count(); for parent in it { let Some(parent) = parent else { return 0; // One of the URLs has no parent }; - min = first + common = first .components() .zip(parent.components()) .take_while(|(a, b)| match (a, b) { @@ -355,10 +355,14 @@ pub fn max_common_root(urls: &[Url]) -> usize { (a, b) => a == b, }) .count() - .min(min); + .min(common); + + if common == 0 { + break; // No common root found + } } - min + common } #[cfg(unix)] diff --git a/yazi-fs/src/path.rs b/yazi-fs/src/path.rs index 69f152b0..ac8290d7 100644 --- a/yazi-fs/src/path.rs +++ b/yazi-fs/src/path.rs @@ -5,14 +5,18 @@ use yazi_shared::url::{Loc, Url}; use crate::{CWD, services}; -#[inline] -pub fn clean_url(url: &Url) -> Url { Url::from(clean_path(url)) } +pub fn clean_url<'a>(url: impl Into>) -> Cow<'a, Url> { + let url = url.into(); + let path = clean_path(&url.loc); -// FIXME: VFS -#[inline] -pub fn clean_path(path: impl AsRef) -> PathBuf { _clean_path(path.as_ref()) } + if path.as_os_str() == url.loc.as_os_str() { + url + } else { + url.with(Loc::with(&clean_path(url.loc.base()), path)).into() + } +} -fn _clean_path(path: &Path) -> PathBuf { +fn clean_path(path: &Path) -> PathBuf { use std::path::Component::*; let mut out = vec![]; @@ -33,17 +37,20 @@ fn _clean_path(path: &Path) -> PathBuf { // FIXME: VFS #[inline] -pub fn expand_path(p: impl AsRef) -> PathBuf { _expand_path(p.as_ref()) } +pub fn expand_path(p: impl AsRef) -> PathBuf { + expand_url(Url::from(p.as_ref())).into_owned().loc.into_path() +} #[inline] pub fn expand_url<'a>(url: impl Into>) -> Cow<'a, Url> { - let u: Cow<'a, Url> = url.into(); - let Some(p) = u.as_path() else { return u }; - - Url { loc: Loc::with(u.loc.base(), _expand_path(p)), scheme: u.scheme.clone() }.into() + let cow: Cow<'a, Url> = url.into(); + match _expand_url(&cow) { + Cow::Borrowed(_) => cow, + Cow::Owned(url) => url.into(), + } } -fn _expand_path(p: &Path) -> PathBuf { +fn _expand_url(url: &Url) -> Cow<'_, Url> { // ${HOME} or $HOME #[cfg(unix)] let re = regex::bytes::Regex::new(r"\$(?:\{([^}]+)\}|([a-zA-Z\d_]+))").unwrap(); @@ -52,7 +59,16 @@ fn _expand_path(p: &Path) -> PathBuf { #[cfg(windows)] let re = regex::bytes::Regex::new(r"%([^%]+)%").unwrap(); - let b = re.replace_all(p.as_os_str().as_encoded_bytes(), |caps: ®ex::bytes::Captures| { + let b = url.loc.as_os_str().as_encoded_bytes(); + let local = !url.scheme.is_virtual(); + + // Windows paths that only have a drive letter but no root, e.g. "D:" + #[cfg(windows)] + if local && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() { + return url.with(format!(r"{}:\", b[0].to_ascii_uppercase() as char)).into(); + } + + let b = re.replace_all(b, |caps: ®ex::bytes::Captures| { let name = caps.get(2).or_else(|| caps.get(1)).unwrap(); str::from_utf8(name.as_bytes()) .ok() @@ -60,21 +76,19 @@ fn _expand_path(p: &Path) -> PathBuf { .map_or_else(|| caps.get(0).unwrap().as_bytes().to_owned(), |s| s.into_encoded_bytes()) }); - // Windows paths that only have a drive letter but no root, e.g. "D:" - #[cfg(windows)] - if b.len() == 2 { - if b[1] == b':' && b[0].is_ascii_alphabetic() { - return PathBuf::from(format!("{}:\\", b[0].to_ascii_uppercase() as char)); + let path: Cow<_> = unsafe { + match b { + Cow::Borrowed(b) => Path::new(OsStr::from_encoded_bytes_unchecked(b)).into(), + Cow::Owned(b) => PathBuf::from(OsString::from_encoded_bytes_unchecked(b)).into(), } - } + }; - let p = unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(b.as_ref())) }; - if let Ok(rest) = p.strip_prefix("~") { - clean_path(dirs::home_dir().unwrap_or_default().join(rest)) - } else if p.is_absolute() { - clean_path(p) + if let Some(rest) = path.strip_prefix("~").ok().filter(|_| local) { + url.with(clean_path(&dirs::home_dir().unwrap_or_default().join(rest))).into() + } else if path.is_absolute() { + url.with(clean_path(&path)).into() } else { - clean_path(CWD.load().join(p)) + clean_url(CWD.load().join(path)) } } @@ -150,7 +164,7 @@ pub fn url_relative_to<'a>(from: &Url, to: &'a Url) -> Result> { } if from.covariant(to) { - return Ok(Url { loc: Path::new(".").into(), scheme: to.scheme.clone() }.into()); + return Ok(to.with(Path::new(".")).into()); } let (mut f_it, mut t_it) = (from.components(), to.components()); @@ -172,7 +186,7 @@ pub fn url_relative_to<'a>(from: &Url, to: &'a Url) -> Result> { let rest = t_head.into_iter().chain(t_it); let buf: PathBuf = dots.chain(rest).collect(); - Ok(Url { loc: buf.into(), scheme: to.scheme.clone() }.into()) + Ok(to.with(buf).into()) } #[cfg(windows)] diff --git a/yazi-parser/src/cmp/show.rs b/yazi-parser/src/cmp/show.rs index 57da8865..bb7954c3 100644 --- a/yazi-parser/src/cmp/show.rs +++ b/yazi-parser/src/cmp/show.rs @@ -1,12 +1,13 @@ -use std::{ffi::OsString, path::{MAIN_SEPARATOR_STR, PathBuf}}; +use std::{ffi::OsString, path::MAIN_SEPARATOR_STR}; -use yazi_shared::{Id, SStr, event::CmdCow}; +use anyhow::bail; +use yazi_shared::{Id, event::CmdCow, url::{Url, UrnBuf}}; #[derive(Default)] pub struct ShowOpt { pub cache: Vec, - pub cache_name: PathBuf, - pub word: SStr, + pub cache_name: Url, + pub word: UrnBuf, pub ticket: Id, } @@ -15,15 +16,10 @@ impl TryFrom for ShowOpt { fn try_from(mut c: CmdCow) -> Result { if let Some(opt) = c.take_any2("opt") { - return opt; + opt + } else { + bail!("missing 'opt' argument"); } - - Ok(Self { - cache: c.take_any("cache").unwrap_or_default(), - cache_name: c.take_any("cache-name").unwrap_or_default(), - word: c.take_str("word").unwrap_or_default(), - ticket: c.id("ticket").unwrap_or_default(), - }) } } diff --git a/yazi-shared/src/osstr.rs b/yazi-shared/src/osstr.rs index 7fc560af..9e5a6fd2 100644 --- a/yazi-shared/src/osstr.rs +++ b/yazi-shared/src/osstr.rs @@ -58,3 +58,38 @@ where result } } + +// --- OsStrSplit +pub trait OsStrSplit { + fn rsplit_once(&self, predicate: P) -> Option<(&Self, &Self)>; +} + +impl OsStrSplit for OsStr { + fn rsplit_once(&self, pat: P) -> Option<(&Self, &Self)> { + let bytes = self.as_encoded_bytes(); + for (i, &byte) in bytes.iter().enumerate().rev() { + if !pat.predicate(byte) { + continue; + } + + let (a, b) = bytes.split_at(i); + // SAFETY: These substrings were separated by a UTF-8 string. + return Some(unsafe { + (OsStr::from_encoded_bytes_unchecked(a), OsStr::from_encoded_bytes_unchecked(&b[1..])) + }); + } + None + } +} + +pub trait Pattern { + fn predicate(&self, byte: u8) -> bool; +} + +impl Pattern for char { + fn predicate(&self, byte: u8) -> bool { *self == byte as char } +} + +impl Pattern for &[char] { + fn predicate(&self, byte: u8) -> bool { self.contains(&(byte as char)) } +} diff --git a/yazi-shared/src/url/loc.rs b/yazi-shared/src/url/loc.rs index 7c48d651..00d4c467 100644 --- a/yazi-shared/src/url/loc.rs +++ b/yazi-shared/src/url/loc.rs @@ -47,16 +47,12 @@ impl Debug for Loc { } } -impl From<&Path> for Loc { - fn from(value: &Path) -> Self { Self::from(value.to_path_buf()) } +impl From for Loc { + fn from(value: OsString) -> Self { Self::from(PathBuf::from(value)) } } -impl From> for Loc { - fn from(value: Cow<'_, Path>) -> Self { Self::from(value.into_owned()) } -} - -impl From> for Loc { - fn from(value: Cow<'_, OsStr>) -> Self { Self::from(PathBuf::from(value.into_owned())) } +impl From for Loc { + fn from(value: String) -> Self { Self::from(PathBuf::from(value)) } } impl From for Loc { @@ -81,6 +77,14 @@ impl From for Loc { } } +impl From> for Loc { + fn from(value: Cow<'_, Path>) -> Self { Self::from(value.into_owned()) } +} + +impl> From<&T> for Loc { + fn from(value: &T) -> Self { Self::from(value.as_ref().to_os_string()) } +} + impl Loc { pub fn with(base: &Path, path: PathBuf) -> Self { let mut loc = Self::from(path); diff --git a/yazi-shared/src/url/scheme.rs b/yazi-shared/src/url/scheme.rs index ccc86cdc..ec62e184 100644 --- a/yazi-shared/src/url/scheme.rs +++ b/yazi-shared/src/url/scheme.rs @@ -53,7 +53,7 @@ impl Scheme { } #[inline] - pub(super) fn is_virtual(&self) -> bool { + pub fn is_virtual(&self) -> bool { match self { Self::Regular | Self::Search(_) | Self::SearchItem => false, Self::Archive(_) | Self::Sftp(_) => true, diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 1540624f..0ddc9557 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -1,5 +1,6 @@ use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}}; +use anyhow::Result; use percent_encoding::percent_decode; use serde::{Deserialize, Serialize}; @@ -38,15 +39,8 @@ impl TryFrom<&[u8]> for Url { type Error = anyhow::Error; fn try_from(bytes: &[u8]) -> Result { - let (scheme, skip, tilde) = Scheme::parse(bytes)?; - - let rest = &bytes[skip + tilde as usize..]; - - Ok(if tilde { - Self { loc: Cow::from(percent_decode(rest)).into_os_str()?.into(), scheme } - } else { - Self { loc: rest.into_os_str()?.into(), scheme } - }) + let (scheme, path) = Self::parse(bytes)?; + Ok(Self { loc: path.into(), scheme }) } } @@ -84,15 +78,22 @@ impl From> for Url { } impl Url { + #[inline] + pub fn with(&self, loc: impl Into) -> Self { + let loc: Loc = loc.into(); + // FIXME: simplify this + Self { loc: Loc::with(self.loc.base(), loc.into_path()), scheme: self.scheme.clone() } + } + #[inline] pub fn base(&self) -> Url { let loc: Loc = self.loc.base().into(); match &self.scheme { Scheme::Regular => Self { loc, scheme: Scheme::Regular }, - Scheme::Search(_) => Self { loc, scheme: self.scheme.clone() }, + Scheme::Search(_) => self.with(loc), Scheme::SearchItem => Self { loc, scheme: Scheme::Search(String::new()) }, - Scheme::Archive(_) => Self { loc, scheme: self.scheme.clone() }, - Scheme::Sftp(_) => Self { loc, scheme: self.scheme.clone() }, + Scheme::Archive(_) => self.with(loc), + Scheme::Sftp(_) => self.with(loc), } } @@ -105,14 +106,11 @@ impl Url { Scheme::SearchItem => { Self { loc: Loc::with(self.loc.base(), self.loc.join(path)), scheme: Scheme::SearchItem } } - Scheme::Archive(_) => { - Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() } - } - Scheme::Sftp(_) => Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() }, + Scheme::Archive(_) => self.with(self.loc.join(path)), + Scheme::Sftp(_) => self.with(self.loc.join(path)), } } - // FIXME: check usages #[inline] pub fn components(&self) -> Components<'_> { Components::new(self) } @@ -140,8 +138,8 @@ impl Url { Scheme::SearchItem => { Self { loc: Loc::with(base, parent.to_owned()), scheme: Scheme::SearchItem } } - Scheme::Archive(_) => Self { loc: parent.into(), scheme: self.scheme.clone() }, - Scheme::Sftp(_) => Self { loc: parent.into(), scheme: self.scheme.clone() }, + Scheme::Archive(_) => self.with(parent), + Scheme::Sftp(_) => self.with(parent), }) } @@ -151,10 +149,7 @@ impl Url { return None; } - Some(Self { - loc: self.loc.strip_prefix(&base.loc).ok()?.into(), - scheme: self.scheme.clone(), - }) + Some(self.with(self.loc.strip_prefix(&base.loc).ok()?)) } #[inline] @@ -176,6 +171,19 @@ impl Url { debug_assert!(self.is_regular()); self.loc.rebase(parent).into() } + + pub fn parse(bytes: &[u8]) -> Result<(Scheme, Cow<'_, Path>)> { + let (scheme, skip, tilde) = Scheme::parse(bytes)?; + + let rest = &bytes[skip + tilde as usize..]; + let rest = + if tilde { Cow::from(percent_decode(rest)).into_os_str()? } else { rest.into_os_str()? }; + + Ok((scheme, match rest { + Cow::Borrowed(s) => Path::new(s).into(), + Cow::Owned(s) => PathBuf::from(s).into(), + })) + } } impl Url { diff --git a/yazi-shared/src/url/urn.rs b/yazi-shared/src/url/urn.rs index ca928cb0..746403c6 100644 --- a/yazi-shared/src/url/urn.rs +++ b/yazi-shared/src/url/urn.rs @@ -55,7 +55,7 @@ impl PartialEq> for &Urn { } // --- UrnBuf -#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize)] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize)] pub struct UrnBuf(PathBuf); impl Borrow for UrnBuf { @@ -81,4 +81,7 @@ impl> From for UrnBuf { impl UrnBuf { #[inline] pub fn as_urn(&self) -> &Urn { self.borrow() } + + #[inline] + pub fn as_os_str(&self) -> &OsStr { self.0.as_os_str() } }