feat: URL covariance for the mimetype agency (#3030)

This commit is contained in:
三咲雅 misaki masa 2025-07-31 00:48:54 +08:00 committed by GitHub
parent 9adc00c3a9
commit da97e5a8b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 198 additions and 127 deletions

35
Cargo.lock generated
View file

@ -391,9 +391,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.41" version = "4.5.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -401,9 +401,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.41" version = "4.5.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -1335,9 +1335,9 @@ dependencies = [
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.6" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" checksum = "360e552c93fa0e8152ab463bc4c4837fce76a225df11dfaeea66c313de5e61f7"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"libc", "libc",
@ -2155,9 +2155,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.16" version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
] ]
@ -2751,9 +2751,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.9.2" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -3310,7 +3310,7 @@ version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [ dependencies = [
"windows-targets 0.53.2", "windows-targets 0.53.3",
] ]
[[package]] [[package]]
@ -3331,10 +3331,11 @@ dependencies = [
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.53.2" version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [ dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.0", "windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0", "windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0", "windows_i686_gnu 0.53.0",
@ -3578,7 +3579,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"toml 0.9.2", "toml 0.9.4",
"twox-hash", "twox-hash",
"vergen-gitcl", "vergen-gitcl",
"yazi-boot", "yazi-boot",
@ -3609,7 +3610,7 @@ dependencies = [
"ratatui", "ratatui",
"regex", "regex",
"serde", "serde",
"toml 0.9.2", "toml 0.9.4",
"tracing", "tracing",
"yazi-codegen", "yazi-codegen",
"yazi-fs", "yazi-fs",
@ -3946,9 +3947,9 @@ dependencies = [
[[package]] [[package]]
name = "zune-jpeg" name = "zune-jpeg"
version = "0.4.19" version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089"
dependencies = [ dependencies = [
"zune-core", "zune-core",
] ]

View file

@ -24,7 +24,7 @@ ansi-to-tui = "7.0.0"
anyhow = "1.0.98" anyhow = "1.0.98"
base64 = "0.22.1" base64 = "0.22.1"
bitflags = { version = "2.9.1", features = [ "serde" ] } 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" core-foundation-sys = "0.8.7"
crossterm = { version = "0.29.0", features = [ "event-stream" ] } crossterm = { version = "0.29.0", features = [ "event-stream" ] }
dirs = "6.0.0" 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 = { version = "1.47.0", features = [ "full" ] }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
tokio-util = "0.7.15" 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" ] } 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" ] } twox-hash = { version = "2.1.1", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
unicode-width = { version = "0.2.0", default-features = false } unicode-width = { version = "0.2.0", default-features = false }

View file

@ -1,4 +1,4 @@
use std::{mem, ops::ControlFlow}; use std::{ffi::OsStr, mem, ops::ControlFlow};
use anyhow::Result; use anyhow::Result;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
@ -30,7 +30,7 @@ impl Actor for Show {
}; };
cmp.ticket = opt.ticket; 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() { if cmp.cands.is_empty() {
succ!(render!(mem::replace(&mut cmp.visible, false))); succ!(render!(mem::replace(&mut cmp.visible, false)));
} }
@ -43,8 +43,8 @@ impl Actor for Show {
} }
impl Show { impl Show {
fn match_candidates(word: &str, cache: &[CmpItem]) -> Vec<CmpItem> { fn match_candidates(word: &OsStr, cache: &[CmpItem]) -> Vec<CmpItem> {
let smart = !word.bytes().any(|c| c.is_ascii_uppercase()); 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| { let flow = cache.iter().try_fold((Vec::new(), Vec::new()), |(mut exact, mut fuzzy), item| {
if osstr_starts_with(&item.name, word, smart) { if osstr_starts_with(&item.name, word, smart) {

View file

@ -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 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_macro::{act, render, succ};
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
use yazi_proxy::CmpProxy; use yazi_proxy::CmpProxy;
use yazi_shared::{event::Data, natsort}; use yazi_shared::{OsStrSplit, event::Data, natsort, url::{Url, UrnBuf}};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -25,13 +25,13 @@ impl Actor for Trigger {
cmp.ticket = t; 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); return act!(cmp:close, cx, false);
}; };
if cmp.caches.contains_key(&parent) { if cmp.caches.contains_key(&parent) {
let ticket = cmp.ticket; 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; let ticket = cmp.ticket;
@ -42,7 +42,7 @@ impl Actor for Trigger {
// "/" is both a directory separator and the root directory per se // "/" 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 // 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 }); cache.push(CmpItem { name: OsString::new(), is_dir: true });
} }
@ -56,7 +56,7 @@ impl Actor for Trigger {
cache.sort_unstable_by(|a, b| { cache.sort_unstable_by(|a, b| {
natsort(a.name.as_encoded_bytes(), b.name.as_encoded_bytes(), false) 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>(()) Ok::<_, anyhow::Error>(())
@ -67,34 +67,36 @@ impl Actor for Trigger {
} }
impl Trigger { impl Trigger {
fn split_path(s: &str) -> Option<(PathBuf, String)> { fn split_url(s: &str) -> Option<(Url, UrnBuf)> {
if s == "~" { 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 `~/` return None; // We don't autocomplete a `~`, but `~/`
} }
#[cfg(windows)] #[cfg(windows)]
const SEP: [char; 2] = ['/', '\\']; const SEP: &[char] = &['/', '\\'];
#[cfg(not(windows))] #[cfg(not(windows))]
const SEP: char = std::path::MAIN_SEPARATOR; const SEP: char = std::path::MAIN_SEPARATOR;
Some(match s.rsplit_once(SEP) { Some(match path.as_os_str().rsplit_once(SEP) {
Some(("", c)) => (PathBuf::from(MAIN_SEPARATOR_STR), c.to_owned()), Some((p, c)) if p.is_empty() => (Url { loc: MAIN_SEPARATOR_STR.into(), scheme }, c.into()),
Some((p, c)) => (expand_path(p), c.to_owned()), Some((p, c)) => (expand_url(Url { loc: p.into(), scheme }).into_owned(), c.into()),
None => (CWD.load().to_path_buf(), s.to_owned()), None => (CWD.load().as_ref().clone(), path.into()),
}) })
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::Path; use yazi_shared::url::Urn;
use super::*; use super::*;
fn compare(s: &str, parent: &str, child: &str) { fn compare(s: &str, parent: &str, child: &str) {
let (p, c) = Trigger::split_path(s).unwrap(); let (p, c) = Trigger::split_url(s).unwrap();
let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(&p); let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(p);
assert_eq!((p, c.as_str()), (Path::new(parent), child)); assert_eq!((p, c.as_urn()), (parent.try_into().unwrap(), Urn::new(child)));
} }
#[cfg(unix)] #[cfg(unix)]

View file

@ -20,7 +20,7 @@ impl Actor for Follow {
if link_to.is_absolute() { if link_to.is_absolute() {
act!(mgr:reveal, cx, link_to.to_owned()) act!(mgr:reveal, cx, link_to.to_owned())
} else if let Some(p) = file.url.parent_url() { } 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 { } else {
succ!() succ!()
} }

View file

@ -1,12 +1,12 @@
use std::{collections::HashMap, path::PathBuf}; use std::collections::HashMap;
use yazi_parser::cmp::CmpItem; use yazi_parser::cmp::CmpItem;
use yazi_shared::Id; use yazi_shared::{Id, url::Url};
use yazi_widgets::Scrollable; use yazi_widgets::Scrollable;
#[derive(Default)] #[derive(Default)]
pub struct Cmp { pub struct Cmp {
pub caches: HashMap<PathBuf, Vec<CmpItem>>, pub caches: HashMap<Url, Vec<CmpItem>>,
pub cands: Vec<CmpItem>, pub cands: Vec<CmpItem>,
pub offset: usize, pub offset: usize,
pub cursor: usize, pub cursor: usize,

View file

@ -1,14 +1,16 @@
use std::{borrow::Cow, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
use yazi_fs::File; use yazi_fs::File;
use yazi_shared::{MIME_DIR, SStr, url::Url}; use yazi_shared::{MIME_DIR, SStr, url::{CovUrl, Url}};
#[derive(Default)] #[derive(Default)]
pub struct Mimetype(HashMap<Url, String>); pub struct Mimetype(HashMap<CovUrl, String>);
impl Mimetype { impl Mimetype {
#[inline] #[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] #[inline]
pub fn by_url_owned(&self, url: &Url) -> Option<SStr> { pub fn by_url_owned(&self, url: &Url) -> Option<SStr> {
@ -26,8 +28,10 @@ impl Mimetype {
} }
#[inline] #[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] #[inline]
pub fn extend(&mut self, iter: impl IntoIterator<Item = (Url, String)>) { self.0.extend(iter) } pub fn extend(&mut self, iter: impl IntoIterator<Item = (Url, String)>) {
self.0.extend(iter.into_iter().map(|(u, m)| (CovUrl(u), m)));
}
} }

View file

@ -333,7 +333,7 @@ pub fn max_common_root(urls: &[Url]) -> usize {
if urls.is_empty() { if urls.is_empty() {
return 0; return 0;
} else if urls.len() == 1 { } 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()); 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 return 0; // The first URL has no parent
}; };
let mut min = first.components().count(); let mut common = first.components().count();
for parent in it { for parent in it {
let Some(parent) = parent else { let Some(parent) = parent else {
return 0; // One of the URLs has no parent return 0; // One of the URLs has no parent
}; };
min = first common = first
.components() .components()
.zip(parent.components()) .zip(parent.components())
.take_while(|(a, b)| match (a, b) { .take_while(|(a, b)| match (a, b) {
@ -355,10 +355,14 @@ pub fn max_common_root(urls: &[Url]) -> usize {
(a, b) => a == b, (a, b) => a == b,
}) })
.count() .count()
.min(min); .min(common);
if common == 0 {
break; // No common root found
}
} }
min common
} }
#[cfg(unix)] #[cfg(unix)]

View file

@ -5,14 +5,18 @@ use yazi_shared::url::{Loc, Url};
use crate::{CWD, services}; use crate::{CWD, services};
#[inline] pub fn clean_url<'a>(url: impl Into<Cow<'a, Url>>) -> Cow<'a, Url> {
pub fn clean_url(url: &Url) -> Url { Url::from(clean_path(url)) } let url = url.into();
let path = clean_path(&url.loc);
// FIXME: VFS if path.as_os_str() == url.loc.as_os_str() {
#[inline] url
pub fn clean_path(path: impl AsRef<Path>) -> PathBuf { _clean_path(path.as_ref()) } } 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::*; use std::path::Component::*;
let mut out = vec![]; let mut out = vec![];
@ -33,17 +37,20 @@ fn _clean_path(path: &Path) -> PathBuf {
// FIXME: VFS // FIXME: VFS
#[inline] #[inline]
pub fn expand_path(p: impl AsRef<Path>) -> PathBuf { _expand_path(p.as_ref()) } pub fn expand_path(p: impl AsRef<Path>) -> PathBuf {
expand_url(Url::from(p.as_ref())).into_owned().loc.into_path()
}
#[inline] #[inline]
pub fn expand_url<'a>(url: impl Into<Cow<'a, Url>>) -> Cow<'a, Url> { pub fn expand_url<'a>(url: impl Into<Cow<'a, Url>>) -> Cow<'a, Url> {
let u: Cow<'a, Url> = url.into(); let cow: Cow<'a, Url> = url.into();
let Some(p) = u.as_path() else { return u }; match _expand_url(&cow) {
Cow::Borrowed(_) => cow,
Url { loc: Loc::with(u.loc.base(), _expand_path(p)), scheme: u.scheme.clone() }.into() Cow::Owned(url) => url.into(),
}
} }
fn _expand_path(p: &Path) -> PathBuf { fn _expand_url(url: &Url) -> Cow<'_, Url> {
// ${HOME} or $HOME // ${HOME} or $HOME
#[cfg(unix)] #[cfg(unix)]
let re = regex::bytes::Regex::new(r"\$(?:\{([^}]+)\}|([a-zA-Z\d_]+))").unwrap(); let re = regex::bytes::Regex::new(r"\$(?:\{([^}]+)\}|([a-zA-Z\d_]+))").unwrap();
@ -52,7 +59,16 @@ fn _expand_path(p: &Path) -> PathBuf {
#[cfg(windows)] #[cfg(windows)]
let re = regex::bytes::Regex::new(r"%([^%]+)%").unwrap(); let re = regex::bytes::Regex::new(r"%([^%]+)%").unwrap();
let b = re.replace_all(p.as_os_str().as_encoded_bytes(), |caps: &regex::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: &regex::bytes::Captures| {
let name = caps.get(2).or_else(|| caps.get(1)).unwrap(); let name = caps.get(2).or_else(|| caps.get(1)).unwrap();
str::from_utf8(name.as_bytes()) str::from_utf8(name.as_bytes())
.ok() .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()) .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:" let path: Cow<_> = unsafe {
#[cfg(windows)] match b {
if b.len() == 2 { Cow::Borrowed(b) => Path::new(OsStr::from_encoded_bytes_unchecked(b)).into(),
if b[1] == b':' && b[0].is_ascii_alphabetic() { Cow::Owned(b) => PathBuf::from(OsString::from_encoded_bytes_unchecked(b)).into(),
return PathBuf::from(format!("{}:\\", b[0].to_ascii_uppercase() as char));
} }
} };
let p = unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(b.as_ref())) }; if let Some(rest) = path.strip_prefix("~").ok().filter(|_| local) {
if let Ok(rest) = p.strip_prefix("~") { url.with(clean_path(&dirs::home_dir().unwrap_or_default().join(rest))).into()
clean_path(dirs::home_dir().unwrap_or_default().join(rest)) } else if path.is_absolute() {
} else if p.is_absolute() { url.with(clean_path(&path)).into()
clean_path(p)
} else { } 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<Cow<'a, Url>> {
} }
if from.covariant(to) { 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()); 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<Cow<'a, Url>> {
let rest = t_head.into_iter().chain(t_it); let rest = t_head.into_iter().chain(t_it);
let buf: PathBuf = dots.chain(rest).collect(); let buf: PathBuf = dots.chain(rest).collect();
Ok(Url { loc: buf.into(), scheme: to.scheme.clone() }.into()) Ok(to.with(buf).into())
} }
#[cfg(windows)] #[cfg(windows)]

View file

@ -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)] #[derive(Default)]
pub struct ShowOpt { pub struct ShowOpt {
pub cache: Vec<CmpItem>, pub cache: Vec<CmpItem>,
pub cache_name: PathBuf, pub cache_name: Url,
pub word: SStr, pub word: UrnBuf,
pub ticket: Id, pub ticket: Id,
} }
@ -15,15 +16,10 @@ impl TryFrom<CmdCow> for ShowOpt {
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> { fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
if let Some(opt) = c.take_any2("opt") { 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(),
})
} }
} }

View file

@ -58,3 +58,38 @@ where
result result
} }
} }
// --- OsStrSplit
pub trait OsStrSplit {
fn rsplit_once<P: Pattern>(&self, predicate: P) -> Option<(&Self, &Self)>;
}
impl OsStrSplit for OsStr {
fn rsplit_once<P: Pattern>(&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)) }
}

View file

@ -47,16 +47,12 @@ impl Debug for Loc {
} }
} }
impl From<&Path> for Loc { impl From<OsString> for Loc {
fn from(value: &Path) -> Self { Self::from(value.to_path_buf()) } fn from(value: OsString) -> Self { Self::from(PathBuf::from(value)) }
} }
impl From<Cow<'_, Path>> for Loc { impl From<String> for Loc {
fn from(value: Cow<'_, Path>) -> Self { Self::from(value.into_owned()) } fn from(value: String) -> Self { Self::from(PathBuf::from(value)) }
}
impl From<Cow<'_, OsStr>> for Loc {
fn from(value: Cow<'_, OsStr>) -> Self { Self::from(PathBuf::from(value.into_owned())) }
} }
impl From<PathBuf> for Loc { impl From<PathBuf> for Loc {
@ -81,6 +77,14 @@ impl From<PathBuf> for Loc {
} }
} }
impl From<Cow<'_, Path>> for Loc {
fn from(value: Cow<'_, Path>) -> Self { Self::from(value.into_owned()) }
}
impl<T: ?Sized + AsRef<OsStr>> From<&T> for Loc {
fn from(value: &T) -> Self { Self::from(value.as_ref().to_os_string()) }
}
impl Loc { impl Loc {
pub fn with(base: &Path, path: PathBuf) -> Self { pub fn with(base: &Path, path: PathBuf) -> Self {
let mut loc = Self::from(path); let mut loc = Self::from(path);

View file

@ -53,7 +53,7 @@ impl Scheme {
} }
#[inline] #[inline]
pub(super) fn is_virtual(&self) -> bool { pub fn is_virtual(&self) -> bool {
match self { match self {
Self::Regular | Self::Search(_) | Self::SearchItem => false, Self::Regular | Self::Search(_) | Self::SearchItem => false,
Self::Archive(_) | Self::Sftp(_) => true, Self::Archive(_) | Self::Sftp(_) => true,

View file

@ -1,5 +1,6 @@
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}}; use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}};
use anyhow::Result;
use percent_encoding::percent_decode; use percent_encoding::percent_decode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -38,15 +39,8 @@ impl TryFrom<&[u8]> for Url {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let (scheme, skip, tilde) = Scheme::parse(bytes)?; let (scheme, path) = Self::parse(bytes)?;
Ok(Self { loc: path.into(), scheme })
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 }
})
} }
} }
@ -84,15 +78,22 @@ impl From<Cow<'_, Url>> for Url {
} }
impl Url { impl Url {
#[inline]
pub fn with(&self, loc: impl Into<Loc>) -> Self {
let loc: Loc = loc.into();
// FIXME: simplify this
Self { loc: Loc::with(self.loc.base(), loc.into_path()), scheme: self.scheme.clone() }
}
#[inline] #[inline]
pub fn base(&self) -> Url { pub fn base(&self) -> Url {
let loc: Loc = self.loc.base().into(); let loc: Loc = self.loc.base().into();
match &self.scheme { match &self.scheme {
Scheme::Regular => Self { loc, scheme: Scheme::Regular }, 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::SearchItem => Self { loc, scheme: Scheme::Search(String::new()) },
Scheme::Archive(_) => Self { loc, scheme: self.scheme.clone() }, Scheme::Archive(_) => self.with(loc),
Scheme::Sftp(_) => Self { loc, scheme: self.scheme.clone() }, Scheme::Sftp(_) => self.with(loc),
} }
} }
@ -105,14 +106,11 @@ impl Url {
Scheme::SearchItem => { Scheme::SearchItem => {
Self { loc: Loc::with(self.loc.base(), self.loc.join(path)), scheme: Scheme::SearchItem } Self { loc: Loc::with(self.loc.base(), self.loc.join(path)), scheme: Scheme::SearchItem }
} }
Scheme::Archive(_) => { Scheme::Archive(_) => self.with(self.loc.join(path)),
Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() } Scheme::Sftp(_) => self.with(self.loc.join(path)),
}
Scheme::Sftp(_) => Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() },
} }
} }
// FIXME: check usages
#[inline] #[inline]
pub fn components(&self) -> Components<'_> { Components::new(self) } pub fn components(&self) -> Components<'_> { Components::new(self) }
@ -140,8 +138,8 @@ impl Url {
Scheme::SearchItem => { Scheme::SearchItem => {
Self { loc: Loc::with(base, parent.to_owned()), scheme: Scheme::SearchItem } Self { loc: Loc::with(base, parent.to_owned()), scheme: Scheme::SearchItem }
} }
Scheme::Archive(_) => Self { loc: parent.into(), scheme: self.scheme.clone() }, Scheme::Archive(_) => self.with(parent),
Scheme::Sftp(_) => Self { loc: parent.into(), scheme: self.scheme.clone() }, Scheme::Sftp(_) => self.with(parent),
}) })
} }
@ -151,10 +149,7 @@ impl Url {
return None; return None;
} }
Some(Self { Some(self.with(self.loc.strip_prefix(&base.loc).ok()?))
loc: self.loc.strip_prefix(&base.loc).ok()?.into(),
scheme: self.scheme.clone(),
})
} }
#[inline] #[inline]
@ -176,6 +171,19 @@ impl Url {
debug_assert!(self.is_regular()); debug_assert!(self.is_regular());
self.loc.rebase(parent).into() 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 { impl Url {

View file

@ -55,7 +55,7 @@ impl PartialEq<Cow<'_, OsStr>> for &Urn {
} }
// --- UrnBuf // --- UrnBuf
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize)] #[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize)]
pub struct UrnBuf(PathBuf); pub struct UrnBuf(PathBuf);
impl Borrow<Urn> for UrnBuf { impl Borrow<Urn> for UrnBuf {
@ -81,4 +81,7 @@ impl<T: Into<PathBuf>> From<T> for UrnBuf {
impl UrnBuf { impl UrnBuf {
#[inline] #[inline]
pub fn as_urn(&self) -> &Urn { self.borrow() } pub fn as_urn(&self) -> &Urn { self.borrow() }
#[inline]
pub fn as_os_str(&self) -> &OsStr { self.0.as_os_str() }
} }