mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
fix: add preset VFS config file to avoid TOML parsing errors (#3414)
This commit is contained in:
parent
9ae8b90d23
commit
396f634bb8
13 changed files with 184 additions and 52 deletions
1
yazi-config/preset/vfs-default.toml
Normal file
1
yazi-config/preset/vfs-default.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
[services]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{Yazi, keymap::Keymap, theme::Theme};
|
||||
use crate::{Yazi, keymap::Keymap, theme::Theme, vfs::Vfs};
|
||||
|
||||
pub(crate) struct Preset;
|
||||
|
||||
|
|
@ -19,6 +19,10 @@ impl Preset {
|
|||
})
|
||||
}
|
||||
|
||||
pub(super) fn vfs() -> Result<Vfs, toml::de::Error> {
|
||||
toml::from_str(&yazi_macro::config_preset!("vfs"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn mix<E, A, B, C>(a: A, b: B, c: C) -> impl Iterator<Item = E>
|
||||
where
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(service vfs);
|
||||
yazi_macro::mod_flat!(service services vfs);
|
||||
|
|
|
|||
42
yazi-config/src/vfs/services.rs
Normal file
42
yazi-config/src/vfs/services.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::vfs::Service;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Services(HashMap<String, Service>);
|
||||
|
||||
impl Deref for Services {
|
||||
type Target = HashMap<String, Service>;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.0 }
|
||||
}
|
||||
|
||||
impl Services {
|
||||
pub(super) fn reshape(mut self) -> Result<Self> {
|
||||
for (name, service) in &mut self.0 {
|
||||
if name.is_empty() || name.len() > 20 {
|
||||
bail!("VFS name `{name}` must be between 1 and 20 characters");
|
||||
} else if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) {
|
||||
bail!("VFS name `{name}` must be in kebab-case");
|
||||
}
|
||||
|
||||
service.reshape()?;
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn deserialize_over<'de, D>(mut self, deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let map: HashMap<String, Service> = HashMap::deserialize(deserializer)?;
|
||||
self.0.extend(map);
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
use std::io;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::OnceCell;
|
||||
use yazi_fs::Xdg;
|
||||
use yazi_macro::ok_or_not_found;
|
||||
use yazi_codegen::DeserializeOver1;
|
||||
use yazi_fs::{Xdg, ok_or_not_found};
|
||||
|
||||
use super::Service;
|
||||
use crate::{Preset, vfs::Services};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, DeserializeOver1)]
|
||||
pub struct Vfs {
|
||||
pub services: HashMap<String, Service>,
|
||||
pub services: Services,
|
||||
}
|
||||
|
||||
impl Vfs {
|
||||
|
|
@ -19,9 +20,10 @@ impl Vfs {
|
|||
|
||||
async fn init() -> io::Result<Vfs> {
|
||||
tokio::task::spawn_blocking(|| {
|
||||
toml::from_str::<Vfs>(&Vfs::read()?).map_err(io::Error::other)?.reshape()
|
||||
Preset::vfs()?.deserialize_over(toml::Deserializer::parse(&Vfs::read()?)?)?.reshape()
|
||||
})
|
||||
.await?
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
|
||||
LOADED.get_or_try_init(init).await
|
||||
|
|
@ -40,24 +42,11 @@ impl Vfs {
|
|||
}
|
||||
}
|
||||
|
||||
fn read() -> io::Result<String> {
|
||||
fn read() -> Result<String> {
|
||||
let p = Xdg::config_dir().join("vfs.toml");
|
||||
Ok(ok_or_not_found!(std::fs::read_to_string(&p).map_err(|e| {
|
||||
std::io::Error::new(e.kind(), format!("Failed to read VFS config {p:?}: {e}"))
|
||||
})))
|
||||
ok_or_not_found(std::fs::read_to_string(&p))
|
||||
.with_context(|| format!("Failed to read config {p:?}"))
|
||||
}
|
||||
|
||||
fn reshape(mut self) -> io::Result<Self> {
|
||||
for (name, service) in &mut self.services {
|
||||
if name.is_empty() || name.len() > 20 {
|
||||
Err(io::Error::other(format!("VFS name `{name}` must be between 1 and 20 characters")))?;
|
||||
} else if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) {
|
||||
Err(io::Error::other(format!("VFS name `{name}` must be in kebab-case")))?;
|
||||
}
|
||||
|
||||
service.reshape()?;
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
fn reshape(self) -> Result<Self> { Ok(Self { services: self.services.reshape()? }) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,14 +119,12 @@ impl File {
|
|||
.await?;
|
||||
|
||||
if !links.is_empty() {
|
||||
let len = links.len();
|
||||
let (tx, mut rx) = mpsc::channel(len);
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
for task in links {
|
||||
self.queue(task.with_drop(&tx), LOW);
|
||||
}
|
||||
for _ in 0..len {
|
||||
rx.recv().await;
|
||||
}
|
||||
drop(tx);
|
||||
while rx.recv().await.is_some() {}
|
||||
}
|
||||
|
||||
for task in files {
|
||||
|
|
|
|||
|
|
@ -42,11 +42,7 @@ pub(crate) struct FileInCut {
|
|||
}
|
||||
|
||||
impl Drop for FileInCut {
|
||||
fn drop(&mut self) {
|
||||
if let Some(tx) = self.drop.take() {
|
||||
tx.try_send(()).ok();
|
||||
}
|
||||
}
|
||||
fn drop(&mut self) { _ = self.drop.take(); }
|
||||
}
|
||||
|
||||
impl FileInCut {
|
||||
|
|
|
|||
|
|
@ -266,24 +266,24 @@ mod tests {
|
|||
#[test]
|
||||
fn test_new() {
|
||||
let loc: LocBuf = Path::new("/").into();
|
||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("/"));
|
||||
assert_eq!(loc.uri().as_os_str(), OsStr::new(""));
|
||||
assert_eq!(loc.urn().as_os_str(), OsStr::new(""));
|
||||
assert_eq!(loc.file_name(), None);
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new(""));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new("/"));
|
||||
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
|
||||
|
||||
let loc: LocBuf = Path::new("/root").into();
|
||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("/root"));
|
||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("root"));
|
||||
assert_eq!(loc.urn().as_os_str(), OsStr::new("root"));
|
||||
assert_eq!(loc.file_name().unwrap(), OsStr::new("root"));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new(""));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new("/"));
|
||||
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
|
||||
|
||||
let loc: LocBuf = Path::new("/root/code/foo/").into();
|
||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("/root/code/foo"));
|
||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("foo"));
|
||||
assert_eq!(loc.urn().as_os_str(), OsStr::new("foo"));
|
||||
assert_eq!(loc.file_name().unwrap(), OsStr::new("foo"));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new(""));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/"));
|
||||
assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ where
|
|||
let path = path.as_path_view();
|
||||
let Some(name) = path.file_name() else {
|
||||
let p = path.strip_prefix(P::empty()).unwrap();
|
||||
return Self { inner: p, uri: p.len(), urn: 0, _phantom: PhantomData };
|
||||
return Self { inner: p, uri: 0, urn: 0, _phantom: PhantomData };
|
||||
};
|
||||
|
||||
let name_len = name.len();
|
||||
|
|
@ -97,7 +97,7 @@ where
|
|||
let bytes = &path.as_encoded_bytes()[..prefix_len + name_len];
|
||||
Self {
|
||||
inner: unsafe { P::from_encoded_bytes_unchecked(bytes) },
|
||||
uri: bytes.len(),
|
||||
uri: name_len,
|
||||
urn: name_len,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ impl<'a> SchemeCow<'a> {
|
|||
Ok(match kind {
|
||||
SchemeKind::Regular => {
|
||||
ensure!(uri.is_none() && urn.is_none(), "Regular scheme cannot have ports");
|
||||
(path.components().count(), path.name().is_some() as usize)
|
||||
(path.name().is_some() as usize, path.name().is_some() as usize)
|
||||
}
|
||||
SchemeKind::Search => {
|
||||
let (uri, urn) = (uri.unwrap_or(0), urn.unwrap_or(0));
|
||||
|
|
@ -155,7 +155,7 @@ impl<'a> SchemeCow<'a> {
|
|||
}
|
||||
SchemeKind::Archive => (uri.unwrap_or(0), urn.unwrap_or(0)),
|
||||
SchemeKind::Sftp => {
|
||||
let uri = uri.unwrap_or_else(|| path.components().count());
|
||||
let uri = uri.unwrap_or(path.name().is_some() as usize);
|
||||
let urn = urn.unwrap_or(path.name().is_some() as usize);
|
||||
(uri, urn)
|
||||
}
|
||||
|
|
@ -163,7 +163,12 @@ impl<'a> SchemeCow<'a> {
|
|||
}
|
||||
|
||||
pub fn retrieve_ports(url: Url) -> (usize, usize) {
|
||||
(url.uri().components().count(), url.urn().components().count())
|
||||
match url {
|
||||
Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize),
|
||||
Url::Search { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()),
|
||||
Url::Archive { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()),
|
||||
Url::Sftp { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ impl<'a> Encode<'a> {
|
|||
SchemeKind::Regular => Ok(()),
|
||||
SchemeKind::Search | SchemeKind::Archive => w!(0, 0),
|
||||
SchemeKind::Sftp => {
|
||||
w!(self.0.0.loc().components().count(), self.0.0.loc().name().is_some() as usize)
|
||||
w!(self.0.0.loc().name().is_some() as usize, self.0.0.loc().name().is_some() as usize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,3 +266,87 @@ impl<'de> Deserialize<'de> for UrlCow<'_> {
|
|||
UrlBuf::deserialize(deserializer).map(UrlCow::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::url::UrlLike;
|
||||
|
||||
#[test]
|
||||
fn test_parse() -> Result<()> {
|
||||
struct Case {
|
||||
url: &'static str,
|
||||
urn: &'static str,
|
||||
uri: &'static str,
|
||||
trail: &'static str,
|
||||
base: &'static str,
|
||||
}
|
||||
|
||||
let cases = [
|
||||
// Regular
|
||||
Case {
|
||||
url: "/root/music/rock/song.mp3",
|
||||
urn: "song.mp3",
|
||||
uri: "song.mp3",
|
||||
trail: "/root/music/rock/",
|
||||
base: "/root/music/rock/",
|
||||
},
|
||||
// Search portal
|
||||
Case {
|
||||
url: "search://keyword//root/Documents/reports",
|
||||
urn: "",
|
||||
uri: "",
|
||||
trail: "search://keyword//root/Documents/reports",
|
||||
base: "search://keyword//root/Documents/reports",
|
||||
},
|
||||
// Search item
|
||||
Case {
|
||||
url: "search://keyword:2:2//root/Documents/reports/2023/summary.docx",
|
||||
urn: "2023/summary.docx",
|
||||
uri: "2023/summary.docx",
|
||||
trail: "search://keyword//root/Documents/reports/",
|
||||
base: "search://keyword//root/Documents/reports/",
|
||||
},
|
||||
// Archive portal
|
||||
Case {
|
||||
url: "archive://domain//root/Downloads/images.zip",
|
||||
urn: "",
|
||||
uri: "",
|
||||
trail: "archive://domain//root/Downloads/images.zip",
|
||||
base: "archive://domain//root/Downloads/images.zip",
|
||||
},
|
||||
// Archive item
|
||||
Case {
|
||||
url: "archive://domain:2:1//root/Downloads/images.zip/2025/city.jpg",
|
||||
urn: "city.jpg",
|
||||
uri: "2025/city.jpg",
|
||||
trail: "archive://domain:1:1//root/Downloads/images.zip/2025/",
|
||||
base: "archive://domain//root/Downloads/images.zip/",
|
||||
},
|
||||
// SFTP
|
||||
Case {
|
||||
url: "sftp://my-server//root/docs/report.pdf",
|
||||
urn: "report.pdf",
|
||||
uri: "report.pdf",
|
||||
trail: "sftp://my-server//root/docs/",
|
||||
base: "sftp://my-server//root/docs/",
|
||||
},
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let url = UrlCow::try_from(case.url)?;
|
||||
assert_eq!(url.urn().to_str()?, case.urn);
|
||||
assert_eq!(url.uri().to_str()?, case.uri);
|
||||
assert_eq!(
|
||||
format!("{:?}", url.trail()),
|
||||
format!("{:?}", UrlCow::try_from(case.trail)?.as_url())
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", url.base()),
|
||||
format!("{:?}", UrlCow::try_from(case.base)?.as_url())
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ impl<'a> Url<'a> {
|
|||
pub fn base(self) -> Self {
|
||||
match self {
|
||||
Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())),
|
||||
Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.base()), domain },
|
||||
Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.base()), domain },
|
||||
Self::Search { loc, domain } => Self::Search { loc: Loc::zeroed(loc.base()), domain },
|
||||
Self::Archive { loc, domain } => Self::Archive { loc: Loc::zeroed(loc.base()), domain },
|
||||
Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.base()), domain },
|
||||
}
|
||||
}
|
||||
|
|
@ -222,12 +222,25 @@ impl<'a> Url<'a> {
|
|||
#[inline]
|
||||
pub fn to_owned(self) -> UrlBuf { self.into() }
|
||||
|
||||
#[inline]
|
||||
pub fn trail(self) -> Self {
|
||||
let uri = self.uri();
|
||||
match self {
|
||||
Self::Regular(loc) => Self::Regular(Loc::bare(loc.trail())),
|
||||
Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.trail()), domain },
|
||||
Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.trail()), domain },
|
||||
|
||||
Self::Search { loc, domain } if uri.is_empty() => {
|
||||
Self::Search { loc: Loc::zeroed(loc.trail()), domain }
|
||||
}
|
||||
Self::Search { loc, domain } => {
|
||||
Self::Search { loc: Loc::new(loc.trail(), loc.base(), loc.base()), domain }
|
||||
}
|
||||
|
||||
Self::Archive { loc, domain } if uri.is_empty() => {
|
||||
Self::Archive { loc: Loc::zeroed(loc.trail()), domain }
|
||||
}
|
||||
Self::Archive { loc, domain } => {
|
||||
Self::Archive { loc: Loc::new(loc.trail(), loc.base(), loc.base()), domain }
|
||||
}
|
||||
|
||||
Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.trail()), domain },
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue