mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: URL schemas (#3019)
This commit is contained in:
parent
c2883f1e05
commit
0e3cd8545c
35 changed files with 924 additions and 505 deletions
|
|
@ -81,13 +81,16 @@ impl UserData for File {
|
|||
})?
|
||||
});
|
||||
methods.add_method("prefix", |lua, me, ()| {
|
||||
if !me.folder.url.is_search() {
|
||||
if !me.url.has_base() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(path) = me.url.as_path() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut p = me.url.strip_prefix(&me.folder.url).unwrap_or(&me.url).components();
|
||||
p.next_back();
|
||||
Some(lua.create_string(p.as_path().as_os_str().as_encoded_bytes())).transpose()
|
||||
let mut comp = path.strip_prefix(me.url.loc.base()).unwrap_or(path).components();
|
||||
comp.next_back();
|
||||
Some(lua.create_string(comp.as_path().as_os_str().as_encoded_bytes())).transpose()
|
||||
});
|
||||
methods.add_method("style", |lua, me, ()| {
|
||||
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, io::{Read, Write}, path::PathBuf};
|
||||
use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, hash::Hash, io::{Read, Write}, ops::Deref};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use crossterm::{execute, style::Print};
|
||||
use scopeguard::defer;
|
||||
use tokio::{fs::{self, OpenOptions}, io::AsyncWriteExt};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use yazi_config::YAZI;
|
||||
use yazi_dds::Pubsub;
|
||||
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file};
|
||||
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file, services::{self, Local}, skip_url};
|
||||
use yazi_macro::{err, succ};
|
||||
use yazi_parser::VoidOpt;
|
||||
use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER};
|
||||
use yazi_shared::{event::Data, terminal_clear, url::Url};
|
||||
use yazi_shared::{OsStrJoin, event::Data, terminal_clear, url::{Component, Url}};
|
||||
use yazi_term::tty::TTY;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
|
@ -22,30 +22,33 @@ impl Actor for BulkRename {
|
|||
|
||||
const NAME: &str = "bulk_rename";
|
||||
|
||||
// FIXME: VFS
|
||||
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
|
||||
let Some(opener) = YAZI.opener.block(YAZI.open.all("bulk-rename.txt", "text/plain")) else {
|
||||
succ!(AppProxy::notify_warn("Bulk rename", "No text opener found"));
|
||||
};
|
||||
|
||||
let old: Vec<_> = cx.tab().selected_or_hovered().collect();
|
||||
let selected: Vec<_> = cx.tab().selected_or_hovered().cloned().collect();
|
||||
if selected.is_empty() {
|
||||
succ!(AppProxy::notify_warn("Bulk rename", "No files selected"));
|
||||
}
|
||||
|
||||
let root = max_common_root(&old);
|
||||
let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect();
|
||||
let root = max_common_root(&selected);
|
||||
let old: Vec<_> =
|
||||
selected.iter().enumerate().map(|(i, u)| Tuple::new(i, skip_url(u, root))).collect();
|
||||
|
||||
let cwd = cx.cwd().clone();
|
||||
tokio::spawn(async move {
|
||||
let tmp = YAZI.preview.tmpfile("bulk");
|
||||
let s = old.iter().map(|o| o.as_os_str()).collect::<Vec<_>>().join(OsStr::new("\n"));
|
||||
OpenOptions::new()
|
||||
// TODO: pull `OpenOptions` into `yazi_fs`
|
||||
tokio::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&tmp)
|
||||
.await?
|
||||
.write_all(s.as_encoded_bytes())
|
||||
.write_all(old.join(OsStr::new("\n")).as_encoded_bytes())
|
||||
.await?;
|
||||
|
||||
defer! { tokio::spawn(fs::remove_file(tmp.clone())); }
|
||||
defer! { tokio::spawn(Local::remove_file(tmp.clone())); }
|
||||
TasksProxy::process_exec(Cow::Borrowed(opener), cwd, vec![
|
||||
OsString::new(),
|
||||
tmp.to_owned().into(),
|
||||
|
|
@ -56,16 +59,22 @@ impl Actor for BulkRename {
|
|||
defer!(AppProxy::resume());
|
||||
AppProxy::stop().await;
|
||||
|
||||
let new: Vec<_> =
|
||||
fs::read_to_string(&tmp).await?.lines().take(old.len()).map(PathBuf::from).collect();
|
||||
Self::r#do(root, old, new).await
|
||||
let new: Vec<_> = Local::read_to_string(&tmp)
|
||||
.await?
|
||||
.lines()
|
||||
.take(old.len())
|
||||
.enumerate()
|
||||
.map(|(i, s)| Tuple::new(i, s))
|
||||
.collect();
|
||||
|
||||
Self::r#do(root, old, new, selected).await
|
||||
});
|
||||
succ!();
|
||||
}
|
||||
}
|
||||
|
||||
impl BulkRename {
|
||||
async fn r#do(root: PathBuf, old: Vec<PathBuf>, new: Vec<PathBuf>) -> Result<()> {
|
||||
async fn r#do(root: usize, old: Vec<Tuple>, new: Vec<Tuple>, selected: Vec<Url>) -> Result<()> {
|
||||
terminal_clear(TTY.writer())?;
|
||||
if old.len() != new.len() {
|
||||
#[rustfmt::skip]
|
||||
|
|
@ -100,14 +109,17 @@ impl BulkRename {
|
|||
let permit = WATCHER.acquire().await.unwrap();
|
||||
let (mut failed, mut succeeded) = (Vec::new(), HashMap::with_capacity(todo.len()));
|
||||
for (o, n) in todo {
|
||||
let (old, new) = (root.join(&o), root.join(&n));
|
||||
let (old, new): (Url, Url) = (
|
||||
selected[o.0].components().take(root).chain([Component::Normal(&o)]).collect(),
|
||||
selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(),
|
||||
);
|
||||
|
||||
if maybe_exists(&new).await && !paths_to_same_file(&old, &new).await {
|
||||
failed.push((o, n, anyhow!("Destination already exists")));
|
||||
} else if let Err(e) = fs::rename(&old, &new).await {
|
||||
} else if let Err(e) = services::rename(&old, &new).await {
|
||||
failed.push((o, n, e.into()));
|
||||
} else if let Ok(f) = File::new(new.into()).await {
|
||||
succeeded.insert(Url::from(old), f);
|
||||
} else if let Ok(f) = File::new(new).await {
|
||||
succeeded.insert(old, f);
|
||||
} else {
|
||||
failed.push((o, n, anyhow!("Failed to retrieve file info")));
|
||||
}
|
||||
|
|
@ -126,7 +138,7 @@ impl BulkRename {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn output_failed(failed: Vec<(PathBuf, PathBuf, anyhow::Error)>) -> Result<()> {
|
||||
async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> {
|
||||
let mut stdout = TTY.lockout();
|
||||
terminal_clear(&mut *stdout)?;
|
||||
|
||||
|
|
@ -141,9 +153,9 @@ impl BulkRename {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn prioritized_paths(old: Vec<PathBuf>, new: Vec<PathBuf>) -> Vec<(PathBuf, PathBuf)> {
|
||||
let orders: HashMap<_, _> = old.iter().enumerate().map(|(i, p)| (p, i)).collect();
|
||||
let mut incomes: HashMap<_, _> = old.iter().map(|p| (p, false)).collect();
|
||||
fn prioritized_paths(old: Vec<Tuple>, new: Vec<Tuple>) -> Vec<(Tuple, Tuple)> {
|
||||
let orders: HashMap<_, _> = old.iter().enumerate().map(|(i, t)| (t, i)).collect();
|
||||
let mut incomes: HashMap<_, _> = old.iter().map(|t| (t, false)).collect();
|
||||
let mut todos: HashMap<_, _> = old
|
||||
.iter()
|
||||
.zip(new)
|
||||
|
|
@ -156,7 +168,7 @@ impl BulkRename {
|
|||
let mut sorted = Vec::with_capacity(old.len());
|
||||
while !todos.is_empty() {
|
||||
// Paths that are non-incomes and don't need to be prioritized in this round
|
||||
let mut outcomes: Vec<_> = incomes.iter().filter(|&(_, b)| !b).map(|(&p, _)| p).collect();
|
||||
let mut outcomes: Vec<_> = incomes.iter().filter(|&(_, b)| !b).map(|(&t, _)| t).collect();
|
||||
outcomes.sort_unstable_by(|a, b| orders[b].cmp(&orders[a]));
|
||||
|
||||
// If there're no outcomes, it means there are cycles in the renaming
|
||||
|
|
@ -180,6 +192,35 @@ impl BulkRename {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Tuple
|
||||
#[derive(Clone, Debug)]
|
||||
struct Tuple(usize, OsString);
|
||||
|
||||
impl Deref for Tuple {
|
||||
type Target = OsStr;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.1 }
|
||||
}
|
||||
|
||||
impl PartialEq for Tuple {
|
||||
fn eq(&self, other: &Self) -> bool { self.1 == other.1 }
|
||||
}
|
||||
|
||||
impl Eq for Tuple {}
|
||||
|
||||
impl Hash for Tuple {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.1.hash(state); }
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for Tuple {
|
||||
fn as_ref(&self) -> &OsStr { &self.1 }
|
||||
}
|
||||
|
||||
impl Tuple {
|
||||
fn new(index: usize, inner: impl Into<OsString>) -> Self { Self(index, inner.into()) }
|
||||
}
|
||||
|
||||
// --- Tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -188,8 +229,8 @@ mod tests {
|
|||
fn test_sort() {
|
||||
fn cmp(input: &[(&str, &str)], expected: &[(&str, &str)]) {
|
||||
let sorted = BulkRename::prioritized_paths(
|
||||
input.iter().map(|&(o, _)| o.into()).collect(),
|
||||
input.iter().map(|&(_, n)| n.into()).collect(),
|
||||
input.iter().map(|&(o, _)| Tuple::new(0, o)).collect(),
|
||||
input.iter().map(|&(_, n)| Tuple::new(0, n)).collect(),
|
||||
);
|
||||
let sorted: Vec<_> =
|
||||
sorted.iter().map(|(o, n)| (o.to_str().unwrap(), n.to_str().unwrap())).collect();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// FIXME: VFS
|
||||
use std::{ffi::OsString, path::Path};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
|
|
@ -41,7 +42,7 @@ impl Actor for Copy {
|
|||
|
||||
// Copy the CWD path regardless even if the directory is empty
|
||||
if s.is_empty() && opt.r#type == "dirname" {
|
||||
s.push(cx.cwd());
|
||||
s.push(opt.separator.transform(cx.cwd()));
|
||||
}
|
||||
|
||||
futures::executor::block_on(CLIPBOARD.set(s));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use anyhow::Result;
|
|||
use yazi_dds::Pubsub;
|
||||
use yazi_macro::{act, err, render, succ};
|
||||
use yazi_parser::mgr::{HoverDoOpt, HoverOpt};
|
||||
use yazi_shared::{event::Data, url::Urn};
|
||||
use yazi_shared::event::Data;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -37,8 +37,8 @@ impl Actor for HoverDo {
|
|||
|
||||
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
|
||||
// Hover on the file
|
||||
if let Ok(p) = opt.url.strip_prefix(cx.cwd()) {
|
||||
render!(cx.current_mut().hover(Urn::new(p)));
|
||||
if let Some(u) = opt.url.strip_prefix(cx.cwd()) {
|
||||
render!(cx.current_mut().hover(u.urn()));
|
||||
}
|
||||
|
||||
// Turn on tracing
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{ops::Deref, path::Path};
|
||||
use std::{borrow::Cow, ops::Deref};
|
||||
|
||||
use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value};
|
||||
use yazi_shared::IntoOsStr;
|
||||
use yazi_shared::url::Scheme;
|
||||
|
||||
use crate::{Urn, cached_field};
|
||||
|
||||
|
|
@ -29,15 +29,18 @@ impl AsRef<yazi_shared::url::Url> for Url {
|
|||
fn as_ref(&self) -> &yazi_shared::url::Url { &self.inner }
|
||||
}
|
||||
|
||||
// FIXME: remove
|
||||
impl AsRef<Path> for Url {
|
||||
fn as_ref(&self) -> &Path { self.inner.as_path() }
|
||||
}
|
||||
|
||||
impl From<Url> for yazi_shared::url::Url {
|
||||
fn from(value: Url) -> Self { value.inner }
|
||||
}
|
||||
|
||||
impl From<Url> for Cow<'_, yazi_shared::url::Url> {
|
||||
fn from(value: Url) -> Self { Cow::Owned(value.inner) }
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Url> for Cow<'a, yazi_shared::url::Url> {
|
||||
fn from(value: &'a Url) -> Self { Cow::Borrowed(&value.inner) }
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Url {
|
||||
type Error = mlua::Error;
|
||||
|
||||
|
|
@ -103,7 +106,14 @@ impl UserData for Url {
|
|||
cached_field!(fields, base, |_, me| {
|
||||
Ok(if me.base().as_os_str().is_empty() { None } else { Some(Self::new(me.base())) })
|
||||
});
|
||||
cached_field!(fields, frag, |lua, me| lua.create_string(me.frag().as_encoded_bytes()));
|
||||
// TODO: remove
|
||||
cached_field!(fields, frag, |lua, me| {
|
||||
if let Scheme::Search(kw) = &me.scheme {
|
||||
Some(lua.create_string(kw)).transpose()
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
});
|
||||
|
||||
fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular()));
|
||||
fields.add_field_method_get("is_search", |_, me| Ok(me.is_search()));
|
||||
|
|
@ -135,16 +145,16 @@ impl UserData for Url {
|
|||
})
|
||||
});
|
||||
methods.add_method("strip_prefix", |_, me, base: Value| {
|
||||
let path = match base {
|
||||
Value::String(s) => me.strip_prefix(s.to_str()?.as_ref()),
|
||||
let url = match base {
|
||||
Value::String(s) => me.strip_prefix(Self::try_from(s.as_bytes().as_ref())?),
|
||||
Value::UserData(ud) => me.strip_prefix(&ud.borrow::<Self>()?.inner),
|
||||
_ => Err("must be a string or a Url".into_lua_err())?,
|
||||
};
|
||||
Ok(path.ok().map(Self::new))
|
||||
Ok(url.map(Self::new))
|
||||
});
|
||||
|
||||
methods.add_function_mut("into_search", |_, (ud, frag): (AnyUserData, mlua::String)| {
|
||||
Ok(Self::new(ud.take::<Self>()?.inner.into_search(frag.as_bytes().into_os_str()?)))
|
||||
methods.add_function_mut("into_search", |_, (ud, frag): (AnyUserData, String)| {
|
||||
Ok(Self::new(ud.take::<Self>()?.inner.into_search(frag)))
|
||||
});
|
||||
|
||||
methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
yazi_macro::mod_pub!(package);
|
||||
yazi_macro::mod_pub!(package shared);
|
||||
|
||||
yazi_macro::mod_flat!(args);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use yazi_fs::must_exists;
|
||||
|
||||
use super::{Dependency, Git};
|
||||
use crate::shared::must_exists;
|
||||
|
||||
impl Dependency {
|
||||
pub(super) async fn add(&mut self) -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
use anyhow::{Context, Result};
|
||||
use yazi_fs::{maybe_exists, ok_or_not_found, remove_dir_clean, remove_sealed, services::Local};
|
||||
use yazi_fs::{ok_or_not_found, remove_dir_clean, services::Local};
|
||||
use yazi_macro::outln;
|
||||
use yazi_shared::url::Url;
|
||||
|
||||
use super::Dependency;
|
||||
use crate::shared::{maybe_exists, remove_sealed};
|
||||
|
||||
impl Dependency {
|
||||
pub(super) async fn delete(&self) -> Result<()> {
|
||||
|
|
@ -34,7 +36,7 @@ impl Dependency {
|
|||
Err(e) => Err(e).context(format!("failed to read `{}`", assets.display()))?,
|
||||
};
|
||||
|
||||
remove_dir_clean(&assets).await;
|
||||
remove_dir_clean(&assets.into()).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use yazi_fs::{copy_and_seal, maybe_exists, remove_dir_clean, services::Local};
|
||||
use yazi_fs::{remove_dir_clean, services::Local};
|
||||
use yazi_macro::outln;
|
||||
|
||||
use super::Dependency;
|
||||
use crate::shared::{copy_and_seal, maybe_exists};
|
||||
|
||||
impl Dependency {
|
||||
pub(super) async fn deploy(&mut self) -> Result<()> {
|
||||
|
|
@ -29,7 +30,7 @@ impl Dependency {
|
|||
self.delete_sources().await?;
|
||||
}
|
||||
|
||||
remove_dir_clean(&to).await;
|
||||
remove_dir_clean(&to.into()).await;
|
||||
self.hash = self.hash().await?;
|
||||
res2?;
|
||||
res1?;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use yazi_fs::must_exists;
|
||||
|
||||
use super::{Dependency, Git};
|
||||
use crate::shared::must_exists;
|
||||
|
||||
impl Dependency {
|
||||
pub(super) async fn install(&mut self) -> Result<()> {
|
||||
|
|
|
|||
1
yazi-cli/src/shared/mod.rs
Normal file
1
yazi-cli/src/shared/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(shared);
|
||||
45
yazi-cli/src/shared/shared.rs
Normal file
45
yazi-cli/src/shared/shared.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use std::{io, path::Path};
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use yazi_fs::{ok_or_not_found, services::Local};
|
||||
|
||||
#[inline]
|
||||
pub async fn must_exists(path: impl AsRef<Path>) -> bool {
|
||||
Local::symlink_metadata(path).await.is_ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn maybe_exists(path: impl AsRef<Path>) -> bool {
|
||||
match Local::symlink_metadata(path).await {
|
||||
Ok(_) => true,
|
||||
Err(e) => e.kind() != std::io::ErrorKind::NotFound,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use `yazi_fs` instead of `tokio::fs`
|
||||
pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> {
|
||||
let b = Local::read(from).await?;
|
||||
ok_or_not_found(remove_sealed(to).await)?;
|
||||
|
||||
let mut file =
|
||||
tokio::fs::OpenOptions::new().create_new(true).write(true).truncate(true).open(to).await?;
|
||||
file.write_all(&b).await?;
|
||||
|
||||
let mut perm = file.metadata().await?.permissions();
|
||||
perm.set_readonly(true);
|
||||
file.set_permissions(perm).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: use `yazi_fs` instead of `tokio::fs`
|
||||
pub async fn remove_sealed(p: &Path) -> io::Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut perm = tokio::fs::metadata(p).await?.permissions();
|
||||
perm.set_readonly(false);
|
||||
tokio::fs::set_permissions(p, perm).await?;
|
||||
}
|
||||
|
||||
tokio::fs::remove_file(p).await
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#![allow(clippy::module_inception)]
|
||||
|
||||
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which);
|
||||
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview scheme tasks theme which);
|
||||
|
||||
yazi_macro::mod_flat!(color icon layout pattern platform preset priority style yazi);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use anyhow::{Result, bail};
|
|||
use serde::Deserialize;
|
||||
use yazi_codegen::DeserializeOver2;
|
||||
use yazi_fs::{CWD, SortBy};
|
||||
use yazi_shared::SyncCell;
|
||||
use yazi_shared::{SyncCell, url::Url};
|
||||
|
||||
use super::{MgrRatio, MouseEvents};
|
||||
|
||||
|
|
@ -32,9 +32,9 @@ impl Mgr {
|
|||
return None;
|
||||
}
|
||||
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
let cwd = if let Ok(p) = CWD.load().strip_prefix(home) {
|
||||
format!("~{}{}", std::path::MAIN_SEPARATOR, p.display())
|
||||
let home = Url::from(dirs::home_dir().unwrap_or_default());
|
||||
let cwd = if let Some(u) = CWD.load().strip_prefix(home) {
|
||||
format!("~{}{}", std::path::MAIN_SEPARATOR, u.display())
|
||||
} else {
|
||||
format!("{}", CWD.load().display())
|
||||
};
|
||||
|
|
|
|||
1
yazi-config/src/scheme/mod.rs
Normal file
1
yazi-config/src/scheme/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(sftp);
|
||||
9
yazi-config/src/scheme/sftp.rs
Normal file
9
yazi-config/src/scheme/sftp.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
pub struct Sftp {
|
||||
pub host: String,
|
||||
pub user: String,
|
||||
pub port: u16,
|
||||
pub password: Option<String>,
|
||||
pub key_file: Option<PathBuf>,
|
||||
}
|
||||
|
|
@ -1,22 +1,14 @@
|
|||
use std::{borrow::Cow, collections::HashMap, path::PathBuf};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use yazi_fs::File;
|
||||
use yazi_shared::{MIME_DIR, SStr, url::{Scheme, Url}};
|
||||
use yazi_shared::{MIME_DIR, SStr, url::Url};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Mimetype(HashMap<PathBuf, String>);
|
||||
pub struct Mimetype(HashMap<Url, String>);
|
||||
|
||||
impl Mimetype {
|
||||
#[inline]
|
||||
pub fn by_url(&self, url: &Url) -> Option<&str> {
|
||||
match url.scheme() {
|
||||
Scheme::Regular => self.0.get(url.as_path()),
|
||||
Scheme::Search => None,
|
||||
Scheme::SearchItem => self.0.get(url.as_path()),
|
||||
Scheme::Archive => None,
|
||||
}
|
||||
.map(|s| s.as_str())
|
||||
}
|
||||
pub fn by_url(&self, url: &Url) -> Option<&str> { self.0.get(url).map(|s| s.as_str()) }
|
||||
|
||||
#[inline]
|
||||
pub fn by_url_owned(&self, url: &Url) -> Option<SStr> {
|
||||
|
|
@ -34,26 +26,8 @@ impl Mimetype {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn contains(&self, url: &Url) -> bool {
|
||||
match url.scheme() {
|
||||
Scheme::Regular => self.0.contains_key(url.as_path()),
|
||||
Scheme::Search => false,
|
||||
Scheme::SearchItem => self.0.contains_key(url.as_path()),
|
||||
Scheme::Archive => false,
|
||||
}
|
||||
}
|
||||
pub fn contains(&self, url: &Url) -> bool { self.0.contains_key(url) }
|
||||
|
||||
pub fn extend(&mut self, iter: impl IntoIterator<Item = (Url, String)>) {
|
||||
self.0.extend(iter.into_iter().filter_map(|(u, s)| {
|
||||
Some((
|
||||
match u.scheme() {
|
||||
Scheme::Regular => u.into_path(),
|
||||
Scheme::Search => None?,
|
||||
Scheme::SearchItem => u.into_path(),
|
||||
Scheme::Archive => None?,
|
||||
},
|
||||
s,
|
||||
))
|
||||
}))
|
||||
}
|
||||
#[inline]
|
||||
pub fn extend(&mut self, iter: impl IntoIterator<Item = (Url, String)>) { self.0.extend(iter) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ impl Yanked {
|
|||
pub fn contains_in(&self, dir: &Url) -> bool {
|
||||
self.urls.iter().any(|u| {
|
||||
let mut it = u.components();
|
||||
// FIXME
|
||||
it.next_back().is_some() && it == dir.components() && u.parent_url().as_ref() == Some(dir)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use std::{collections::VecDeque, fs::ReadDir, future::poll_fn, mem, path::{Path, PathBuf}, pin::Pin, task::{Poll, ready}, time::{Duration, Instant}};
|
||||
use std::{collections::VecDeque, fs::ReadDir, future::poll_fn, io, mem, pin::Pin, task::{Poll, ready}, time::{Duration, Instant}};
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
use yazi_shared::Either;
|
||||
use yazi_shared::{Either, url::Url};
|
||||
|
||||
type Task = Either<PathBuf, ReadDir>;
|
||||
use crate::services;
|
||||
|
||||
type Task = Either<Url, ReadDir>;
|
||||
|
||||
pub enum SizeCalculator {
|
||||
Idle((VecDeque<Task>, Option<u64>)),
|
||||
|
|
@ -11,23 +13,23 @@ pub enum SizeCalculator {
|
|||
}
|
||||
|
||||
impl SizeCalculator {
|
||||
pub async fn new(path: impl AsRef<Path>) -> std::io::Result<Self> {
|
||||
let p = path.as_ref().to_owned();
|
||||
pub async fn new(url: &Url) -> io::Result<Self> {
|
||||
let u = url.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let meta = std::fs::symlink_metadata(&p)?;
|
||||
let meta = services::symlink_metadata_sync(&u)?;
|
||||
if !meta.is_dir() {
|
||||
return Ok(Self::Idle((VecDeque::new(), Some(meta.len()))));
|
||||
}
|
||||
|
||||
let mut buf = VecDeque::from([Either::Right(std::fs::read_dir(p)?)]);
|
||||
let mut buf = VecDeque::from([Either::Right(services::read_dir_sync(u)?)]);
|
||||
let size = Self::next_chunk(&mut buf);
|
||||
Ok(Self::Idle((buf, size)))
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
pub async fn total(path: impl AsRef<Path>) -> std::io::Result<u64> {
|
||||
let mut it = Self::new(path).await?;
|
||||
pub async fn total(url: &Url) -> io::Result<u64> {
|
||||
let mut it = Self::new(url).await?;
|
||||
let mut total = 0;
|
||||
while let Some(n) = it.next().await? {
|
||||
total += n;
|
||||
|
|
@ -35,7 +37,7 @@ impl SizeCalculator {
|
|||
Ok(total)
|
||||
}
|
||||
|
||||
pub async fn next(&mut self) -> std::io::Result<Option<u64>> {
|
||||
pub async fn next(&mut self) -> io::Result<Option<u64>> {
|
||||
poll_fn(|cx| {
|
||||
loop {
|
||||
match self {
|
||||
|
|
@ -61,7 +63,7 @@ impl SizeCalculator {
|
|||
.await
|
||||
}
|
||||
|
||||
fn next_chunk(buf: &mut VecDeque<Either<PathBuf, ReadDir>>) -> Option<u64> {
|
||||
fn next_chunk(buf: &mut VecDeque<Either<Url, ReadDir>>) -> Option<u64> {
|
||||
let (mut i, mut size, now) = (0, 0, Instant::now());
|
||||
macro_rules! pop_and_continue {
|
||||
() => {{
|
||||
|
|
@ -77,8 +79,8 @@ impl SizeCalculator {
|
|||
i += 1;
|
||||
let front = buf.front_mut()?;
|
||||
|
||||
if let Either::Left(p) = front {
|
||||
*front = match std::fs::read_dir(p) {
|
||||
if let Either::Left(u) = front {
|
||||
*front = match services::read_dir_sync(u) {
|
||||
Ok(it) => Either::Right(it),
|
||||
Err(_) => pop_and_continue!(),
|
||||
};
|
||||
|
|
@ -91,7 +93,7 @@ impl SizeCalculator {
|
|||
let Ok(ent) = next else { continue };
|
||||
let Ok(ft) = ent.file_type() else { continue };
|
||||
if ft.is_dir() {
|
||||
buf.push_back(Either::Left(ent.path()));
|
||||
buf.push_back(Either::Left(ent.path().into()));
|
||||
} else if let Ok(meta) = ent.metadata() {
|
||||
size += meta.len();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{env::{current_dir, set_current_dir}, ops::Deref, path::PathBuf, sync::{Arc, atomic::{self, AtomicBool}}};
|
||||
use std::{env::{current_dir, set_current_dir}, ops::Deref, path::PathBuf, sync::{Arc, atomic::{AtomicBool, Ordering}}};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use yazi_shared::{RoCell, url::Url};
|
||||
|
|
@ -32,25 +32,32 @@ impl Cwd {
|
|||
}
|
||||
|
||||
self.store(Arc::new(url.clone()));
|
||||
unsafe { std::env::set_var("PWD", url) };
|
||||
if let Some(p) = url.as_path() {
|
||||
unsafe { std::env::set_var("PWD", p) };
|
||||
Self::sync_cwd();
|
||||
}
|
||||
|
||||
Self::sync_cwd();
|
||||
true
|
||||
}
|
||||
|
||||
fn sync_cwd() {
|
||||
static SYNCING: AtomicBool = AtomicBool::new(false);
|
||||
if SYNCING.swap(true, atomic::Ordering::Relaxed) {
|
||||
if SYNCING.swap(true, Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
_ = set_current_dir(CWD.load().as_ref());
|
||||
let p = current_dir().unwrap_or_default();
|
||||
if let Some(p) = CWD.load().as_path() {
|
||||
_ = set_current_dir(p);
|
||||
}
|
||||
|
||||
SYNCING.store(false, atomic::Ordering::Relaxed);
|
||||
if p != CWD.load().as_path() {
|
||||
set_current_dir(CWD.load().as_ref()).ok();
|
||||
let cur = current_dir().unwrap_or_default();
|
||||
SYNCING.store(false, Ordering::Relaxed);
|
||||
|
||||
if let Some(p) = CWD.load().as_path()
|
||||
&& cur != p
|
||||
{
|
||||
set_current_dir(p).ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,24 +3,22 @@
|
|||
use std::{borrow::Cow, collections::{HashMap, HashSet}, ffi::{OsStr, OsString}, path::{Path, PathBuf}};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use tokio::{fs, io::{self, AsyncWriteExt}, select, sync::{mpsc, oneshot}, time};
|
||||
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
|
||||
use yazi_shared::url::{Component, Url};
|
||||
|
||||
use crate::cha::Cha;
|
||||
use crate::{cha::Cha, services};
|
||||
|
||||
#[inline]
|
||||
pub async fn must_exists(p: impl AsRef<Path>) -> bool { fs::symlink_metadata(p).await.is_ok() }
|
||||
|
||||
#[inline]
|
||||
pub async fn maybe_exists(p: impl AsRef<Path>) -> bool {
|
||||
match fs::symlink_metadata(p).await {
|
||||
pub async fn maybe_exists(u: impl AsRef<Url>) -> bool {
|
||||
match services::symlink_metadata(u).await {
|
||||
Ok(_) => true,
|
||||
Err(e) => e.kind() != io::ErrorKind::NotFound,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn must_be_dir(p: impl AsRef<Path>) -> bool {
|
||||
fs::metadata(p).await.is_ok_and(|m| m.is_dir())
|
||||
pub async fn must_be_dir(u: impl AsRef<Url>) -> bool {
|
||||
services::metadata(u).await.is_ok_and(|m| m.is_dir())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -85,39 +83,13 @@ async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result<bool> {
|
|||
Ok(final_name(a).await? == final_name(b).await?)
|
||||
}
|
||||
|
||||
pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> {
|
||||
let b = fs::read(from).await?;
|
||||
ok_or_not_found(remove_sealed(to).await)?;
|
||||
|
||||
let mut file =
|
||||
fs::OpenOptions::new().create_new(true).write(true).truncate(true).open(to).await?;
|
||||
file.write_all(&b).await?;
|
||||
|
||||
let mut perm = file.metadata().await?.permissions();
|
||||
perm.set_readonly(true);
|
||||
file.set_permissions(perm).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_sealed(p: &Path) -> io::Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut perm = fs::metadata(p).await?.permissions();
|
||||
perm.set_readonly(false);
|
||||
fs::set_permissions(p, perm).await?;
|
||||
}
|
||||
|
||||
fs::remove_file(p).await
|
||||
}
|
||||
|
||||
pub async fn realname(p: &Path) -> Option<OsString> {
|
||||
let name = p.file_name()?;
|
||||
if p == fs::canonicalize(p).await.ok()? {
|
||||
pub async fn realname(u: &Url) -> Option<OsString> {
|
||||
let name = u.file_name()?;
|
||||
if *u == services::canonicalize(u).await.ok()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
realname_unchecked(p, &mut HashMap::new())
|
||||
realname_unchecked(u, &mut HashMap::new())
|
||||
.await
|
||||
.ok()
|
||||
.filter(|s| s != name)
|
||||
|
|
@ -127,13 +99,15 @@ pub async fn realname(p: &Path) -> Option<OsString> {
|
|||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_realname_unchecked() {
|
||||
fs::remove_dir_all("/tmp/issue-1173").await.ok();
|
||||
fs::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap();
|
||||
fs::File::create("/tmp/issue-1173/A").await.unwrap();
|
||||
fs::File::create("/tmp/issue-1173/b").await.unwrap();
|
||||
fs::File::create("/tmp/issue-1173/real-dir/C").await.unwrap();
|
||||
fs::symlink("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap();
|
||||
fs::symlink("real-dir", "/tmp/issue-1173/link-dir").await.unwrap();
|
||||
use crate::services::Local;
|
||||
|
||||
Local::remove_dir_all("/tmp/issue-1173").await.ok();
|
||||
Local::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap();
|
||||
Local::create("/tmp/issue-1173/A").await.unwrap();
|
||||
Local::create("/tmp/issue-1173/b").await.unwrap();
|
||||
Local::create("/tmp/issue-1173/real-dir/C").await.unwrap();
|
||||
Local::symlink_file("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap();
|
||||
Local::symlink_dir("real-dir", "/tmp/issue-1173/link-dir").await.unwrap();
|
||||
|
||||
let c = &mut HashMap::new();
|
||||
async fn check(a: &str, b: &str, c: &mut HashMap<PathBuf, HashSet<OsString>>) {
|
||||
|
|
@ -184,25 +158,22 @@ pub async fn realname_unchecked<'a>(
|
|||
}
|
||||
|
||||
pub fn copy_with_progress(
|
||||
from: &Path,
|
||||
to: &Path,
|
||||
from: &Url,
|
||||
to: &Url,
|
||||
cha: Cha,
|
||||
) -> mpsc::Receiver<Result<u64, io::Error>> {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
let (tick_tx, mut tick_rx) = oneshot::channel();
|
||||
|
||||
tokio::spawn({
|
||||
let (from, to) = (from.to_owned(), to.to_owned());
|
||||
|
||||
let (from, to) = (from.clone(), to.clone());
|
||||
async move {
|
||||
tick_tx.send(_copy_with_progress(from, to, cha).await).ok();
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn({
|
||||
let tx = tx.clone();
|
||||
let to = to.to_path_buf();
|
||||
|
||||
let (tx, to) = (tx.clone(), to.clone());
|
||||
async move {
|
||||
let mut last = 0;
|
||||
let mut exit = None;
|
||||
|
|
@ -228,7 +199,7 @@ pub fn copy_with_progress(
|
|||
None => {}
|
||||
}
|
||||
|
||||
let len = fs::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0);
|
||||
let len = services::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0);
|
||||
if len > last {
|
||||
tx.send(Ok(len - last)).await.ok();
|
||||
last = len;
|
||||
|
|
@ -240,7 +211,7 @@ pub fn copy_with_progress(
|
|||
rx
|
||||
}
|
||||
|
||||
async fn _copy_with_progress(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result<u64> {
|
||||
async fn _copy_with_progress(from: Url, to: Url, cha: Cha) -> io::Result<u64> {
|
||||
let mut ft = std::fs::FileTimes::new();
|
||||
cha.atime.map(|t| ft = ft.set_accessed(t));
|
||||
cha.mtime.map(|t| ft = ft.set_modified(t));
|
||||
|
|
@ -288,18 +259,18 @@ async fn _copy_with_progress(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn remove_dir_clean(dir: &Path) {
|
||||
let Ok(mut it) = fs::read_dir(dir).await else { return };
|
||||
pub async fn remove_dir_clean(dir: &Url) {
|
||||
let Ok(mut it) = services::read_dir(dir).await else { return };
|
||||
|
||||
while let Ok(Some(entry)) = it.next_entry().await {
|
||||
if entry.file_type().await.is_ok_and(|t| t.is_dir()) {
|
||||
let path = entry.path();
|
||||
let path = entry.path().into();
|
||||
Box::pin(remove_dir_clean(&path)).await;
|
||||
fs::remove_dir(path).await.ok();
|
||||
services::remove_dir(path).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_dir(dir).await.ok();
|
||||
services::remove_dir(dir).await.ok();
|
||||
}
|
||||
|
||||
// Convert a file mode to a string representation
|
||||
|
|
@ -355,50 +326,61 @@ pub fn permissions(m: libc::mode_t, dummy: bool) -> String {
|
|||
s
|
||||
}
|
||||
|
||||
// Find the max common root in a list of files
|
||||
// Find the max common root in a list of urls
|
||||
// e.g. /a/b/c, /a/b/d -> /a/b
|
||||
// /aa/bb/cc, /aa/dd/ee -> /aa
|
||||
pub fn max_common_root(files: &[impl AsRef<Path>]) -> PathBuf {
|
||||
if files.is_empty() {
|
||||
return PathBuf::new();
|
||||
pub fn max_common_root(urls: &[Url]) -> usize {
|
||||
if urls.is_empty() {
|
||||
return 0;
|
||||
} else if urls.len() == 1 {
|
||||
return urls[0].components().count().saturating_sub(1);
|
||||
}
|
||||
|
||||
let mut it = files.iter().map(|p| p.as_ref().parent().unwrap_or(Path::new("")).components());
|
||||
let mut root = it.next().unwrap().collect::<PathBuf>();
|
||||
for components in it {
|
||||
let mut new_root = PathBuf::new();
|
||||
for (a, b) in root.components().zip(components) {
|
||||
if a != b {
|
||||
break;
|
||||
}
|
||||
new_root.push(a);
|
||||
}
|
||||
root = new_root;
|
||||
let mut it = urls.iter().map(|u| u.parent_url());
|
||||
let Some(first) = it.next().unwrap() else {
|
||||
return 0; // The first URL has no parent
|
||||
};
|
||||
|
||||
let mut min = first.components().count();
|
||||
for parent in it {
|
||||
let Some(parent) = parent else {
|
||||
return 0; // One of the URLs has no parent
|
||||
};
|
||||
|
||||
min = first
|
||||
.components()
|
||||
.zip(parent.components())
|
||||
.take_while(|(a, b)| match (a, b) {
|
||||
(Component::Scheme(a), Component::Scheme(b)) => a.covariant(b),
|
||||
(a, b) => a == b,
|
||||
})
|
||||
.count()
|
||||
.min(min);
|
||||
}
|
||||
root
|
||||
|
||||
min
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_max_common_root() {
|
||||
assert_eq!(max_common_root(&[] as &[PathBuf]).as_os_str(), "");
|
||||
assert_eq!(max_common_root(&["".into()] as &[PathBuf]).as_os_str(), "");
|
||||
assert_eq!(max_common_root(&["a".into()] as &[PathBuf]).as_os_str(), "");
|
||||
assert_eq!(max_common_root(&["/a".into()] as &[PathBuf]).as_os_str(), "/");
|
||||
assert_eq!(max_common_root(&["/a/b".into()] as &[PathBuf]).as_os_str(), "/a");
|
||||
assert_eq!(
|
||||
max_common_root(&["/a/b/c".into(), "/a/b/d".into()] as &[PathBuf]).as_os_str(),
|
||||
"/a/b"
|
||||
);
|
||||
assert_eq!(
|
||||
max_common_root(&["/aa/bb/cc".into(), "/aa/dd/ee".into()] as &[PathBuf]).as_os_str(),
|
||||
"/aa"
|
||||
);
|
||||
assert_eq!(
|
||||
max_common_root(
|
||||
&["/aa/bb/cc".into(), "/aa/bb/cc/dd/ee".into(), "/aa/bb/cc/ff".into()] as &[PathBuf]
|
||||
)
|
||||
.as_os_str(),
|
||||
"/aa/bb"
|
||||
);
|
||||
fn assert(input: &[&str], expected: &str) {
|
||||
let urls: Vec<_> = input.iter().copied().map(Url::try_from).collect::<Result<_>>().unwrap();
|
||||
|
||||
let mut comp = urls[0].components();
|
||||
for _ in 0..comp.clone().count() - max_common_root(&urls) {
|
||||
comp.next_back();
|
||||
}
|
||||
assert_eq!(comp.os_str(), OsStr::new(expected));
|
||||
}
|
||||
|
||||
assert_eq!(max_common_root(&[]), 0);
|
||||
assert(&[""], "");
|
||||
assert(&["a"], "");
|
||||
|
||||
assert(&["/a"], "/");
|
||||
assert(&["/a/b"], "/a");
|
||||
assert(&["/a/b/c", "/a/b/d"], "/a/b");
|
||||
assert(&["/aa/bb/cc", "/aa/dd/ee"], "/aa");
|
||||
assert(&["/aa/bb/cc", "/aa/bb/cc/dd/ee", "/aa/bb/cc/ff"], "/aa/bb");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{Component, Path, PathBuf}};
|
||||
use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{Path, PathBuf}};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use yazi_shared::url::Url;
|
||||
|
||||
use crate::{CWD, services};
|
||||
|
|
@ -11,17 +12,16 @@ pub fn clean_url(url: &Url) -> Url { Url::from(clean_path(url)) }
|
|||
pub fn clean_path(path: impl AsRef<Path>) -> PathBuf { _clean_path(path.as_ref()) }
|
||||
|
||||
fn _clean_path(path: &Path) -> PathBuf {
|
||||
use std::path::Component::*;
|
||||
|
||||
let mut out = vec![];
|
||||
for c in path.components() {
|
||||
match c {
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => match out.last() {
|
||||
Some(Component::RootDir) => {}
|
||||
Some(Component::Normal(_)) => _ = out.pop(),
|
||||
None
|
||||
| Some(Component::CurDir)
|
||||
| Some(Component::ParentDir)
|
||||
| Some(Component::Prefix(_)) => out.push(c),
|
||||
CurDir => {}
|
||||
ParentDir => match out.last() {
|
||||
Some(RootDir) => {}
|
||||
Some(Normal(_)) => _ = out.pop(),
|
||||
None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => out.push(c),
|
||||
},
|
||||
c => out.push(c),
|
||||
}
|
||||
|
|
@ -33,6 +33,12 @@ fn _clean_path(path: &Path) -> PathBuf {
|
|||
#[inline]
|
||||
pub fn expand_path(p: impl AsRef<Path>) -> PathBuf { _expand_path(p.as_ref()) }
|
||||
|
||||
#[inline]
|
||||
pub fn expand_url<'a>(url: impl Into<Cow<'a, Url>>) -> Cow<'a, Url> {
|
||||
let u: Cow<'a, Url> = url.into();
|
||||
if let Some(p) = u.as_path() { Url::from(_expand_path(p)).into() } else { u }
|
||||
}
|
||||
|
||||
fn _expand_path(p: &Path) -> PathBuf {
|
||||
// ${HOME} or $HOME
|
||||
#[cfg(unix)]
|
||||
|
|
@ -68,14 +74,14 @@ fn _expand_path(p: &Path) -> PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn skip_path(p: &Path, u: usize) -> &Path {
|
||||
let mut it = p.components();
|
||||
for _ in 0..u {
|
||||
pub fn skip_url(url: &Url, n: usize) -> Cow<'_, OsStr> {
|
||||
let mut it = url.components();
|
||||
for _ in 0..n {
|
||||
if it.next().is_none() {
|
||||
return Path::new("");
|
||||
return OsStr::new("").into();
|
||||
}
|
||||
}
|
||||
it.as_path()
|
||||
it.os_str()
|
||||
}
|
||||
|
||||
pub async fn unique_name<F>(u: Url, append: F) -> io::Result<Url>
|
||||
|
|
@ -128,34 +134,29 @@ async fn _unique_name(mut url: Url, append: bool) -> io::Result<Url> {
|
|||
Ok(url)
|
||||
}
|
||||
|
||||
// Parameters
|
||||
// * `path`: The absolute path(contains no `/./`) to get relative path.
|
||||
// * `root`: The absolute path(contains no `/./`) to be compared.
|
||||
//
|
||||
// Return
|
||||
// * Unix: The relative format to `root` of `path`.
|
||||
// * Windows: The relative format to `root` of `path`; or `path` itself when
|
||||
// `path` and `root` are both under different disk drives.
|
||||
pub fn path_relative_to<'a>(path: &'a Path, root: &Path) -> Cow<'a, Path> {
|
||||
assert!(path.is_absolute());
|
||||
assert!(root.is_absolute());
|
||||
let mut p_comps = path.components();
|
||||
let mut r_comps = root.components();
|
||||
pub fn url_relative_to<'a>(from: &Url, to: &'a Url) -> Result<Cow<'a, Url>> {
|
||||
use yazi_shared::url::Component::*;
|
||||
|
||||
// 1. Ensure that the two paths have the same prefix.
|
||||
// 2. Strips any common prefix the two paths do have.
|
||||
//
|
||||
// NOTE:
|
||||
// Prefixes are platform dependent,
|
||||
// but different prefixes would for example indicate paths for different drives
|
||||
// on Windows.
|
||||
let (p_head, r_head) = loop {
|
||||
use std::path::Component::*;
|
||||
match (p_comps.next(), r_comps.next()) {
|
||||
(Some(RootDir), Some(RootDir)) => (),
|
||||
(Some(Prefix(a)), Some(Prefix(b))) if a == b => (),
|
||||
(Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => {
|
||||
return Cow::from(path);
|
||||
if from.is_absolute() != to.is_absolute() {
|
||||
return if to.is_absolute() {
|
||||
Ok(to.into())
|
||||
} else {
|
||||
bail!("Urls must be both absolute or both relative: {from:?} and {to:?}");
|
||||
};
|
||||
}
|
||||
|
||||
if from.covariant(to) {
|
||||
return Ok(Url { loc: Path::new(".").into(), scheme: to.scheme.clone() }.into());
|
||||
}
|
||||
|
||||
let (mut f_it, mut t_it) = (from.components(), to.components());
|
||||
let (f_head, t_head) = loop {
|
||||
match (f_it.next(), t_it.next()) {
|
||||
(Some(Scheme(a)), Some(Scheme(b))) if a.covariant(b) => {}
|
||||
(Some(RootDir), Some(RootDir)) => {}
|
||||
(Some(Prefix(a)), Some(Prefix(b))) if a == b => {}
|
||||
(Some(Scheme(_) | Prefix(_) | RootDir), _) | (_, Some(Scheme(_) | Prefix(_) | RootDir)) => {
|
||||
return Ok(to.into());
|
||||
}
|
||||
(None, None) => break (None, None),
|
||||
(a, b) if a != b => break (a, b),
|
||||
|
|
@ -163,14 +164,11 @@ pub fn path_relative_to<'a>(path: &'a Path, root: &Path) -> Cow<'a, Path> {
|
|||
}
|
||||
};
|
||||
|
||||
let p_comps = p_head.into_iter().chain(p_comps);
|
||||
let walk_up = r_head.into_iter().chain(r_comps).map(|_| Component::ParentDir);
|
||||
let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir);
|
||||
let rest = t_head.into_iter().chain(t_it);
|
||||
|
||||
let mut buf = PathBuf::new();
|
||||
buf.extend(walk_up);
|
||||
buf.extend(p_comps);
|
||||
|
||||
Cow::from(buf)
|
||||
let buf: PathBuf = dots.chain(rest).collect();
|
||||
Ok(Url { loc: buf.into(), scheme: to.scheme.clone() }.into())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
|
@ -196,35 +194,60 @@ pub fn backslash_to_slash(p: &Path) -> Cow<'_, Path> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{borrow::Cow, path::Path};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::path_relative_to;
|
||||
use yazi_shared::url::Url;
|
||||
|
||||
use super::url_relative_to;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_path_relative_to() {
|
||||
fn assert(path: &str, root: &str, res: &str) {
|
||||
assert_eq!(path_relative_to(Path::new(path), Path::new(root)), Cow::Borrowed(Path::new(res)));
|
||||
fn assert(from: &str, to: &str, ret: &str) {
|
||||
assert_eq!(
|
||||
url_relative_to(&Url::try_from(from).unwrap(), &Url::try_from(to).unwrap()).unwrap(),
|
||||
Cow::Owned(Url::try_from(ret).unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
assert("/a/b", "/a/b/c", "../");
|
||||
assert("/a/b/c", "/a/b", "c");
|
||||
assert("/a/b/c", "/a/b/d", "../c");
|
||||
assert("/a", "/a/b/c", "../../");
|
||||
assert("/a/a/b", "/a/b/b", "../../a/b");
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// Same urls
|
||||
assert("", "", ".");
|
||||
assert(".", ".", ".");
|
||||
assert("/a", "/a", ".");
|
||||
assert("regular:///", "/", ".");
|
||||
assert("regular://", "regular://", ".");
|
||||
assert("regular://", "search://kw/", "search://kw/.");
|
||||
assert("regular:///b", "search://kw//b", "search://kw/.");
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn test_path_relative_to() {
|
||||
fn assert(path: &str, root: &str, res: &str) {
|
||||
assert_eq!(path_relative_to(Path::new(path), Path::new(root)), Cow::Borrowed(Path::new(res)));
|
||||
// Relative urls
|
||||
assert("foo", "bar", "../bar");
|
||||
|
||||
// Absolute urls
|
||||
assert("/a/b/c", "/a/b", "../");
|
||||
assert("/a/b", "/a/b/c", "c");
|
||||
assert("/a/b/d", "/a/b/c", "../c");
|
||||
assert("/a/b/c", "/a", "../../");
|
||||
assert("/a/b/b", "/a/a/b", "../../a/b");
|
||||
|
||||
assert("regular:///a/b", "regular:///a/b/c", "c");
|
||||
assert("/a/b/c/", "search://kw//a/d/", "search://kw/../../d");
|
||||
assert("search://kw//a/b/c", "search://kw//a/b", "search://kw/../");
|
||||
|
||||
// Different schemes
|
||||
assert("", "sftp://test/", "sftp://test/");
|
||||
assert("a", "sftp://test/", "sftp://test/");
|
||||
assert("a", "sftp://test/b", "sftp://test/b");
|
||||
assert("/a", "sftp://test//b", "sftp://test//b");
|
||||
assert("sftp://test//a/b", "sftp://test//a/d", "sftp://test/../d");
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
assert(r"C:\a\b\c", r"C:\a\b", r"..\");
|
||||
assert(r"C:\a\b", r"C:\a\b\c", "c");
|
||||
assert(r"C:\a\b\d", r"C:\a\b\c", r"..\c");
|
||||
assert(r"C:\a\b\c", r"C:\a", r"..\..\");
|
||||
assert(r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b");
|
||||
}
|
||||
|
||||
assert("C:\\a\\b", "C:\\a\\b\\c", "..\\");
|
||||
assert("C:\\a\\b\\c", "C:\\a\\b", "c");
|
||||
assert("C:\\a\\b\\c", "C:\\a\\b\\d", "..\\c");
|
||||
assert("C:\\a", "C:\\a\\b\\c", "..\\..\\");
|
||||
assert("C:\\a\\a\\b", "C:\\a\\b\\b", "..\\..\\a\\b");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ impl Local {
|
|||
tokio::fs::create_dir_all(path).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn hard_link(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
|
||||
tokio::fs::hard_link(original, link).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn metadata(url: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
|
||||
tokio::fs::metadata(url).await
|
||||
|
|
@ -41,6 +46,11 @@ impl Local {
|
|||
tokio::fs::read_dir(path).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read_dir_sync(path: impl AsRef<Path>) -> io::Result<std::fs::ReadDir> {
|
||||
std::fs::read_dir(path)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn read_link(url: impl AsRef<Path>) -> io::Result<PathBuf> {
|
||||
tokio::fs::read_link(url).await
|
||||
|
|
@ -71,11 +81,40 @@ impl Local {
|
|||
tokio::fs::rename(from, to).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn symlink_dir(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
tokio::fs::symlink(original, link).await
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tokio::fs::symlink_dir(original, link).await
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn symlink_file(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
tokio::fs::symlink(original, link).await
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tokio::fs::symlink_file(original, link).await
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn symlink_metadata(path: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
|
||||
tokio::fs::symlink_metadata(path).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn symlink_metadata_sync(path: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
|
||||
std::fs::symlink_metadata(path)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
||||
tokio::fs::write(path, contents).await
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ pub async fn create_dir_all(url: impl AsRef<Url>) -> io::Result<()> {
|
|||
Local::create_dir_all(url.as_ref()).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn hard_link(original: impl AsRef<Url>, link: impl AsRef<Url>) -> io::Result<()> {
|
||||
Local::hard_link(original.as_ref(), link.as_ref()).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn metadata(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
|
||||
Local::metadata(url.as_ref()).await
|
||||
|
|
@ -39,6 +44,11 @@ pub async fn read_dir(url: impl AsRef<Url>) -> io::Result<tokio::fs::ReadDir> {
|
|||
Local::read_dir(url.as_ref()).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read_dir_sync(url: impl AsRef<Url>) -> io::Result<std::fs::ReadDir> {
|
||||
Local::read_dir_sync(url.as_ref())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn read_link(url: impl AsRef<Url>) -> io::Result<Url> {
|
||||
Local::read_link(url.as_ref()).await.map(Into::into)
|
||||
|
|
@ -64,11 +74,25 @@ pub async fn rename(from: impl AsRef<Url>, to: impl AsRef<Url>) -> io::Result<()
|
|||
Local::rename(from.as_ref(), to.as_ref()).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn symlink_dir(original: impl AsRef<Url>, link: impl AsRef<Url>) -> io::Result<()> {
|
||||
Local::symlink_dir(original.as_ref(), link.as_ref()).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn symlink_file(original: impl AsRef<Url>, link: impl AsRef<Url>) -> io::Result<()> {
|
||||
Local::symlink_file(original.as_ref(), link.as_ref()).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn symlink_metadata(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
|
||||
Local::symlink_metadata(url.as_ref()).await
|
||||
}
|
||||
|
||||
pub fn symlink_metadata_sync(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
|
||||
Local::symlink_metadata_sync(url.as_ref())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn write(url: impl AsRef<Url>, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
||||
Local::write(url.as_ref(), contents).await
|
||||
|
|
|
|||
9
yazi-plugin/src/external/fd.rs
vendored
9
yazi-plugin/src/external/fd.rs
vendored
|
|
@ -30,9 +30,16 @@ pub fn fd(opt: FdOpt) -> Result<UnboundedReceiver<File>> {
|
|||
}
|
||||
|
||||
fn spawn(program: &str, opt: &FdOpt) -> std::io::Result<Child> {
|
||||
let Some(path) = opt.cwd.as_path() else {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"fd can only search local filesystem",
|
||||
));
|
||||
};
|
||||
|
||||
Command::new(program)
|
||||
.arg("--base-directory")
|
||||
.arg(&opt.cwd)
|
||||
.arg(path)
|
||||
.arg("--regex")
|
||||
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
|
||||
.args(&opt.args)
|
||||
|
|
|
|||
8
yazi-plugin/src/external/rg.rs
vendored
8
yazi-plugin/src/external/rg.rs
vendored
|
|
@ -1,6 +1,6 @@
|
|||
use std::process::Stdio;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, bail};
|
||||
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}};
|
||||
use yazi_fs::File;
|
||||
use yazi_shared::url::Url;
|
||||
|
|
@ -13,12 +13,16 @@ pub struct RgOpt {
|
|||
}
|
||||
|
||||
pub fn rg(opt: RgOpt) -> Result<UnboundedReceiver<File>> {
|
||||
let Some(path) = opt.cwd.as_path() else {
|
||||
bail!("rg can only search local filesystem");
|
||||
};
|
||||
|
||||
let mut child = Command::new("rg")
|
||||
.args(["--color=never", "--files-with-matches", "--smart-case"])
|
||||
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
|
||||
.args(opt.args)
|
||||
.arg(opt.subject)
|
||||
.arg(&opt.cwd)
|
||||
.arg(path)
|
||||
.kill_on_drop(true)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
|
|
|
|||
8
yazi-plugin/src/external/rga.rs
vendored
8
yazi-plugin/src/external/rga.rs
vendored
|
|
@ -1,6 +1,6 @@
|
|||
use std::process::Stdio;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, bail};
|
||||
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}};
|
||||
use yazi_fs::File;
|
||||
use yazi_shared::url::Url;
|
||||
|
|
@ -13,12 +13,16 @@ pub struct RgaOpt {
|
|||
}
|
||||
|
||||
pub fn rga(opt: RgaOpt) -> Result<UnboundedReceiver<File>> {
|
||||
let Some(path) = opt.cwd.as_path() else {
|
||||
bail!("rga can only search local filesystem");
|
||||
};
|
||||
|
||||
let mut child = Command::new("rga")
|
||||
.args(["--color=never", "--files-with-matches", "--smart-case"])
|
||||
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
|
||||
.args(opt.args)
|
||||
.arg(opt.subject)
|
||||
.arg(&opt.cwd)
|
||||
.arg(path)
|
||||
.kill_on_drop(true)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use globset::GlobBuilder;
|
||||
use mlua::{ExternalError, ExternalResult, Function, IntoLua, IntoLuaMulti, Lua, Table, Value};
|
||||
use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef};
|
||||
|
|
@ -159,7 +161,7 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
|
|||
|
||||
fn calc_size(lua: &Lua) -> mlua::Result<Function> {
|
||||
lua.create_async_function(|lua, url: UrlRef| async move {
|
||||
match yazi_fs::SizeCalculator::new(&*url).await {
|
||||
match yazi_fs::SizeCalculator::new(&url).await {
|
||||
Ok(it) => SizeCalculator(it).into_lua_multi(&lua),
|
||||
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
|
||||
}
|
||||
|
|
@ -167,13 +169,16 @@ fn calc_size(lua: &Lua) -> mlua::Result<Function> {
|
|||
}
|
||||
|
||||
fn expand_url(lua: &Lua) -> mlua::Result<Function> {
|
||||
lua.create_function(|_, value: Value| {
|
||||
use yazi_fs::expand_path;
|
||||
Ok(Url::new(match value {
|
||||
Value::String(s) => expand_path(s.to_str()?.as_ref()),
|
||||
Value::UserData(ud) => expand_path(&*ud.borrow::<yazi_binding::Url>()?),
|
||||
_ => Err("must be a string or a Url".into_lua_err())?,
|
||||
}))
|
||||
lua.create_function(|lua, value: Value| {
|
||||
use yazi_fs::expand_url;
|
||||
match &value {
|
||||
Value::String(s) => Url::new(expand_url(Url::try_from(s.as_bytes().as_ref())?)).into_lua(lua),
|
||||
Value::UserData(ud) => match expand_url(&*ud.borrow::<yazi_binding::Url>()?) {
|
||||
Cow::Borrowed(_) => Ok(value),
|
||||
Cow::Owned(u) => Url::new(u).into_lua(lua),
|
||||
},
|
||||
_ => Err("must be a string or a Url".into_lua_err()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
// FIXME: VFS, depends on yazi_fs::fns
|
||||
use std::{borrow::Cow, collections::VecDeque};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use tokio::{fs::{self, DirEntry}, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
|
||||
use tokio::{fs::DirEntry, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
|
||||
use tracing::warn;
|
||||
use yazi_config::YAZI;
|
||||
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to, services, skip_path};
|
||||
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, services, skip_url, url_relative_to};
|
||||
use yazi_shared::{Id, url::Url};
|
||||
|
||||
use super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash};
|
||||
|
|
@ -27,14 +26,14 @@ impl File {
|
|||
pub async fn work(&self, r#in: FileIn) -> Result<()> {
|
||||
match r#in {
|
||||
FileIn::Paste(mut task) => {
|
||||
ok_or_not_found(fs::remove_file(&task.to).await)?;
|
||||
ok_or_not_found(services::remove_file(&task.to).await)?;
|
||||
let mut it = copy_with_progress(&task.from, &task.to, task.cha.unwrap());
|
||||
|
||||
while let Some(res) = it.recv().await {
|
||||
match res {
|
||||
Ok(0) => {
|
||||
if task.cut {
|
||||
fs::remove_file(&task.from).await.ok();
|
||||
services::remove_file(&task.from).await.ok();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -63,8 +62,8 @@ impl File {
|
|||
let cha = task.cha.unwrap();
|
||||
|
||||
let src = if task.resolve {
|
||||
match fs::read_link(&task.from).await {
|
||||
Ok(p) => Cow::Owned(p),
|
||||
match services::read_link(&task.from).await {
|
||||
Ok(u) => Cow::Owned(u),
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
warn!("Link task partially done: {task:?}");
|
||||
return Ok(self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?);
|
||||
|
|
@ -72,46 +71,39 @@ impl File {
|
|||
Err(e) => Err(e)?,
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(task.from.as_path())
|
||||
Cow::Borrowed(&task.from)
|
||||
};
|
||||
|
||||
let src = if task.relative {
|
||||
path_relative_to(&src, &fs::canonicalize(task.to.parent().unwrap()).await?)
|
||||
url_relative_to(&services::canonicalize(task.to.parent_url().unwrap()).await?, &src)?
|
||||
} else {
|
||||
src
|
||||
};
|
||||
|
||||
ok_or_not_found(fs::remove_file(&task.to).await)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
fs::symlink(src, &task.to).await?;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if cha.is_dir() {
|
||||
fs::symlink_dir(src, &task.to).await?;
|
||||
} else {
|
||||
fs::symlink_file(src, &task.to).await?;
|
||||
}
|
||||
ok_or_not_found(services::remove_file(&task.to).await)?;
|
||||
if cha.is_dir() {
|
||||
services::symlink_dir(src, &task.to).await?;
|
||||
} else {
|
||||
services::symlink_file(src, &task.to).await?;
|
||||
}
|
||||
|
||||
if task.delete {
|
||||
fs::remove_file(&task.from).await.ok();
|
||||
services::remove_file(&task.from).await.ok();
|
||||
}
|
||||
self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?;
|
||||
}
|
||||
FileIn::Hardlink(task) => {
|
||||
let cha = task.cha.unwrap();
|
||||
let src = if !task.follow {
|
||||
Cow::Borrowed(task.from.as_path())
|
||||
} else if let Ok(p) = fs::canonicalize(&task.from).await {
|
||||
Cow::Borrowed(&task.from)
|
||||
} else if let Ok(p) = services::canonicalize(&task.from).await {
|
||||
Cow::Owned(p)
|
||||
} else {
|
||||
Cow::Borrowed(task.from.as_path())
|
||||
Cow::Borrowed(&task.from)
|
||||
};
|
||||
|
||||
ok_or_not_found(fs::remove_file(&task.to).await)?;
|
||||
match fs::hard_link(src, &task.to).await {
|
||||
ok_or_not_found(services::remove_file(&task.to).await)?;
|
||||
match services::hard_link(src, &task.to).await {
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
warn!("Hardlink task partially done: {task:?}");
|
||||
}
|
||||
|
|
@ -121,7 +113,7 @@ impl File {
|
|||
self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?;
|
||||
}
|
||||
FileIn::Delete(task) => {
|
||||
if let Err(e) = fs::remove_file(&task.target).await
|
||||
if let Err(e) = services::remove_file(&task.target).await
|
||||
&& e.kind() != NotFound
|
||||
&& maybe_exists(&task.target).await
|
||||
{
|
||||
|
|
@ -153,7 +145,7 @@ impl File {
|
|||
}
|
||||
|
||||
pub async fn paste(&self, mut task: FileInPaste) -> Result<()> {
|
||||
if task.cut && ok_or_not_found(fs::rename(&task.from, &task.to).await).is_ok() {
|
||||
if task.cut && ok_or_not_found(services::rename(&task.from, &task.to).await).is_ok() {
|
||||
return self.succ(task.id);
|
||||
}
|
||||
|
||||
|
|
@ -192,13 +184,13 @@ impl File {
|
|||
let mut dirs = VecDeque::from([task.from.clone()]);
|
||||
|
||||
while let Some(src) = dirs.pop_front() {
|
||||
let dest = root.join(skip_path(&src, skip));
|
||||
continue_unless_ok!(match fs::create_dir(&dest).await {
|
||||
let dest = root.join(skip_url(&src, skip));
|
||||
continue_unless_ok!(match services::create_dir(&dest).await {
|
||||
Err(e) if e.kind() != AlreadyExists => Err(e),
|
||||
_ => Ok(()),
|
||||
});
|
||||
|
||||
let mut it = continue_unless_ok!(fs::read_dir(&src).await);
|
||||
let mut it = continue_unless_ok!(services::read_dir(&src).await);
|
||||
while let Ok(Some(entry)) = it.next_entry().await {
|
||||
let from = Url::from(entry.path());
|
||||
let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await);
|
||||
|
|
@ -263,13 +255,13 @@ impl File {
|
|||
let mut dirs = VecDeque::from([task.from.clone()]);
|
||||
|
||||
while let Some(src) = dirs.pop_front() {
|
||||
let dest = root.join(skip_path(&src, skip));
|
||||
continue_unless_ok!(match fs::create_dir(&dest).await {
|
||||
let dest = root.join(skip_url(&src, skip));
|
||||
continue_unless_ok!(match services::create_dir(&dest).await {
|
||||
Err(e) if e.kind() != AlreadyExists => Err(e),
|
||||
_ => Ok(()),
|
||||
});
|
||||
|
||||
let mut it = continue_unless_ok!(fs::read_dir(&src).await);
|
||||
let mut it = continue_unless_ok!(services::read_dir(&src).await);
|
||||
while let Ok(Some(entry)) = it.next_entry().await {
|
||||
let from = Url::from(entry.path());
|
||||
let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await);
|
||||
|
|
@ -288,7 +280,7 @@ impl File {
|
|||
}
|
||||
|
||||
pub async fn delete(&self, mut task: FileInDelete) -> Result<()> {
|
||||
let meta = fs::symlink_metadata(&task.target).await?;
|
||||
let meta = services::symlink_metadata(&task.target).await?;
|
||||
if !meta.is_dir() {
|
||||
let id = task.id;
|
||||
task.length = meta.len();
|
||||
|
|
@ -299,7 +291,7 @@ impl File {
|
|||
|
||||
let mut dirs = VecDeque::from([task.target]);
|
||||
while let Some(target) = dirs.pop_front() {
|
||||
let Ok(mut it) = fs::read_dir(target).await else { continue };
|
||||
let Ok(mut it) = services::read_dir(target).await else { continue };
|
||||
|
||||
while let Ok(Some(entry)) = it.next_entry().await {
|
||||
let Ok(meta) = entry.metadata().await else { continue };
|
||||
|
|
|
|||
|
|
@ -33,3 +33,28 @@ impl<'a> IntoOsStr<'a> for &'a [u8] {
|
|||
|
||||
fn into_os_str(self) -> Result<Cow<'a, OsStr>, Self::Error> { Cow::Borrowed(self).into_os_str() }
|
||||
}
|
||||
|
||||
// --- OsStrJoin
|
||||
pub trait OsStrJoin {
|
||||
fn join(&self, sep: &OsStr) -> OsString;
|
||||
}
|
||||
|
||||
impl<T> OsStrJoin for Vec<T>
|
||||
where
|
||||
T: AsRef<OsStr>,
|
||||
{
|
||||
fn join(&self, sep: &OsStr) -> OsString {
|
||||
if self.is_empty() {
|
||||
return OsString::new();
|
||||
}
|
||||
|
||||
let mut result = OsString::new();
|
||||
for (i, item) in self.iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push(sep);
|
||||
}
|
||||
result.push(item.as_ref());
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
157
yazi-shared/src/url/component.rs
Normal file
157
yazi-shared/src/url/component.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, path::{self, PathBuf, PrefixComponent}};
|
||||
|
||||
use crate::url::{Scheme, Url};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum Component<'a> {
|
||||
Scheme(&'a Scheme),
|
||||
Prefix(PrefixComponent<'a>),
|
||||
RootDir,
|
||||
CurDir,
|
||||
ParentDir,
|
||||
Normal(&'a OsStr),
|
||||
}
|
||||
|
||||
impl<'a> From<path::Component<'a>> for Component<'a> {
|
||||
fn from(comp: path::Component<'a>) -> Self {
|
||||
match comp {
|
||||
path::Component::Prefix(p) => Component::Prefix(p),
|
||||
path::Component::RootDir => Component::RootDir,
|
||||
path::Component::CurDir => Component::CurDir,
|
||||
path::Component::ParentDir => Component::ParentDir,
|
||||
path::Component::Normal(s) => Component::Normal(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<Component<'a>> for Url {
|
||||
fn from_iter<I: IntoIterator<Item = Component<'a>>>(iter: I) -> Self {
|
||||
let mut scheme = Scheme::Regular;
|
||||
let mut buf = PathBuf::new();
|
||||
iter.into_iter().for_each(|c| match c {
|
||||
Component::Scheme(s) => scheme = s.clone(),
|
||||
Component::Prefix(p) => buf.push(path::Component::Prefix(p)),
|
||||
Component::RootDir => buf.push(path::Component::RootDir),
|
||||
Component::CurDir => buf.push(path::Component::CurDir),
|
||||
Component::ParentDir => buf.push(path::Component::ParentDir),
|
||||
Component::Normal(s) => buf.push(path::Component::Normal(s)),
|
||||
});
|
||||
|
||||
Self { loc: buf.into(), scheme }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<Component<'a>> for PathBuf {
|
||||
fn from_iter<I: IntoIterator<Item = Component<'a>>>(iter: I) -> Self {
|
||||
let mut buf = PathBuf::new();
|
||||
iter.into_iter().for_each(|c| match c {
|
||||
Component::Scheme(_) => {}
|
||||
Component::Prefix(p) => buf.push(path::Component::Prefix(p)),
|
||||
Component::RootDir => buf.push(path::Component::RootDir),
|
||||
Component::CurDir => buf.push(path::Component::CurDir),
|
||||
Component::ParentDir => buf.push(path::Component::ParentDir),
|
||||
Component::Normal(s) => buf.push(path::Component::Normal(s)),
|
||||
});
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
// --- Components
|
||||
#[derive(Clone)]
|
||||
pub struct Components<'a> {
|
||||
inner: path::Components<'a>,
|
||||
scheme: &'a Scheme,
|
||||
scheme_yielded: bool,
|
||||
}
|
||||
|
||||
impl<'a> Components<'a> {
|
||||
pub fn new(url: &'a Url) -> Self {
|
||||
Self {
|
||||
inner: url.loc.components(),
|
||||
scheme: &url.scheme,
|
||||
scheme_yielded: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn os_str(&self) -> Cow<'a, OsStr> {
|
||||
if !self.scheme.is_virtual() || self.scheme_yielded {
|
||||
return Cow::Borrowed(self.inner.as_path().as_os_str());
|
||||
}
|
||||
|
||||
let mut oss = OsString::from(format!("{}", self.scheme));
|
||||
oss.push(self.inner.as_path());
|
||||
Cow::Owned(oss)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Components<'a> {
|
||||
type Item = Component<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.scheme_yielded {
|
||||
self.scheme_yielded = true;
|
||||
Some(Component::Scheme(self.scheme))
|
||||
} else {
|
||||
self.inner.next().map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let (min, max) = self.inner.size_hint();
|
||||
let scheme = self.scheme_yielded.not() as usize;
|
||||
|
||||
(min + scheme, max.map(|n| n + scheme))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DoubleEndedIterator for Components<'a> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if let Some(comp) = self.inner.next_back() {
|
||||
Some(comp.into())
|
||||
} else if !self.scheme_yielded {
|
||||
self.scheme_yielded = true;
|
||||
Some(Component::Scheme(self.scheme))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FusedIterator for Components<'a> {}
|
||||
|
||||
impl<'a> PartialEq for Components<'a> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Some(self.scheme).filter(|_| !self.scheme_yielded)
|
||||
== Some(other.scheme).filter(|_| !other.scheme_yielded)
|
||||
&& self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_collect() {
|
||||
let search = Url::try_from("search://keyword//root/projects/yazi").unwrap();
|
||||
assert_eq!(search.scheme, Scheme::Search("keyword".to_owned()));
|
||||
|
||||
let item = search.join("main.rs");
|
||||
assert_eq!(item.scheme, Scheme::SearchItem);
|
||||
|
||||
let u: Url = item.components().take(4).collect();
|
||||
assert_eq!(u.scheme, Scheme::SearchItem);
|
||||
assert_eq!(u.loc.as_path(), Path::new("/root/projects"));
|
||||
|
||||
let u: Url = item
|
||||
.components()
|
||||
.take(5)
|
||||
.chain([Component::Normal(OsStr::new("target/release/yazi"))])
|
||||
.collect();
|
||||
assert_eq!(u.scheme, Scheme::SearchItem);
|
||||
assert_eq!(u.loc.as_path(), Path::new("/root/projects/yazi/target/release/yazi"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,28 @@
|
|||
use std::{cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
|
||||
use std::{borrow::Cow, cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
|
||||
|
||||
use crate::url::{Urn, UrnBuf};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Loc {
|
||||
path: PathBuf,
|
||||
urn: usize,
|
||||
name: usize,
|
||||
inner: PathBuf,
|
||||
urn: usize,
|
||||
name: usize,
|
||||
}
|
||||
|
||||
unsafe impl Send for Loc {}
|
||||
|
||||
unsafe impl Sync for Loc {}
|
||||
|
||||
impl Deref for Loc {
|
||||
type Target = PathBuf;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.path }
|
||||
fn deref(&self) -> &Self::Target { &self.inner }
|
||||
}
|
||||
|
||||
impl PartialEq for Loc {
|
||||
fn eq(&self, other: &Self) -> bool { self.path == other.path }
|
||||
fn eq(&self, other: &Self) -> bool { self.inner == other.inner }
|
||||
}
|
||||
|
||||
impl Eq for Loc {}
|
||||
|
||||
impl Ord for Loc {
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering { self.path.cmp(&other.path) }
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering { self.inner.cmp(&other.inner) }
|
||||
}
|
||||
|
||||
impl PartialOrd for Loc {
|
||||
|
|
@ -34,24 +30,36 @@ impl PartialOrd for Loc {
|
|||
}
|
||||
|
||||
impl Hash for Loc {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) { self.path.hash(state) }
|
||||
fn hash<H: Hasher>(&self, state: &mut H) { self.inner.hash(state) }
|
||||
}
|
||||
|
||||
impl Debug for Loc {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.debug_struct("Loc")
|
||||
.field("path", &self.path)
|
||||
.field("path", &self.inner)
|
||||
.field("urn", &self.urn())
|
||||
.field("name", &self.name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Loc {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
impl From<&Path> for Loc {
|
||||
fn from(value: &Path) -> Self { Self::from(value.to_path_buf()) }
|
||||
}
|
||||
|
||||
impl From<Cow<'_, Path>> for Loc {
|
||||
fn from(value: Cow<'_, Path>) -> Self { Self::from(value.into_owned()) }
|
||||
}
|
||||
|
||||
impl From<Cow<'_, OsStr>> for Loc {
|
||||
fn from(value: Cow<'_, OsStr>) -> Self { Self::from(PathBuf::from(value.into_owned())) }
|
||||
}
|
||||
|
||||
impl From<PathBuf> for Loc {
|
||||
fn from(path: PathBuf) -> Self {
|
||||
let Some(name) = path.file_name() else {
|
||||
let urn = path.as_os_str().len();
|
||||
return Self { path, urn, name: 0 };
|
||||
return Self { inner: path, urn, name: 0 };
|
||||
};
|
||||
|
||||
let name_len = name.len();
|
||||
|
|
@ -62,15 +70,17 @@ impl Loc {
|
|||
let mut bytes = path.into_os_string().into_encoded_bytes();
|
||||
bytes.truncate(name_len + prefix_len as usize);
|
||||
Self {
|
||||
path: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }),
|
||||
urn: name_len,
|
||||
name: name_len,
|
||||
inner: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }),
|
||||
urn: name_len,
|
||||
name: name_len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from(base: &Path, path: PathBuf) -> Self {
|
||||
let mut loc = Self::new(path);
|
||||
loc.urn = loc.path.strip_prefix(base).unwrap_or(&loc.path).as_os_str().len();
|
||||
impl Loc {
|
||||
pub fn with(base: &Path, path: PathBuf) -> Self {
|
||||
let mut loc = Self::from(path);
|
||||
loc.urn = loc.inner.strip_prefix(base).unwrap_or(&loc.inner).as_os_str().len();
|
||||
loc
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +118,7 @@ impl Loc {
|
|||
}
|
||||
|
||||
self.name = name.len();
|
||||
self.path.set_file_name(name);
|
||||
self.inner.set_file_name(name);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -120,20 +130,23 @@ impl Loc {
|
|||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_base(&self) -> bool { self.bytes().len() != self.urn }
|
||||
|
||||
#[inline]
|
||||
pub fn rebase(&self, parent: &Path) -> Self {
|
||||
debug_assert!(self.urn == self.name);
|
||||
let path = parent.join(self.name());
|
||||
|
||||
debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name));
|
||||
Self { path, urn: self.name, name: self.name }
|
||||
Self { inner: path, urn: self.name, name: self.name }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_path(self) -> PathBuf { self.path }
|
||||
pub fn into_path(self) -> PathBuf { self.inner }
|
||||
|
||||
#[inline]
|
||||
fn bytes(&self) -> &[u8] { self.path.as_os_str().as_encoded_bytes() }
|
||||
fn bytes(&self) -> &[u8] { self.inner.as_os_str().as_encoded_bytes() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -144,17 +157,17 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
let loc = Loc::new("/".into());
|
||||
let loc: Loc = Path::new("/").into();
|
||||
assert_eq!(loc.urn(), Urn::new("/"));
|
||||
assert_eq!(loc.name(), OsStr::new(""));
|
||||
assert_eq!(loc.base(), Path::new(""));
|
||||
|
||||
let loc = Loc::new("/root".into());
|
||||
let loc: Loc = Path::new("/root").into();
|
||||
assert_eq!(loc.urn(), Urn::new("root"));
|
||||
assert_eq!(loc.name(), OsStr::new("root"));
|
||||
assert_eq!(loc.base(), Path::new("/"));
|
||||
|
||||
let loc = Loc::new("/root/code/foo/".into());
|
||||
let loc: Loc = Path::new("/root/code/foo/").into();
|
||||
assert_eq!(loc.urn(), Urn::new("foo"));
|
||||
assert_eq!(loc.name(), OsStr::new("foo"));
|
||||
assert_eq!(loc.base(), Path::new("/root/code/"));
|
||||
|
|
@ -162,17 +175,17 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
let loc = Loc::from(Path::new("/"), "/".into());
|
||||
let loc = Loc::with(Path::new("/"), "/".into());
|
||||
assert_eq!(loc.urn().as_os_str(), OsStr::new(""));
|
||||
assert_eq!(loc.name(), OsStr::new(""));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new("/"));
|
||||
|
||||
let loc = Loc::from(Path::new("/root/"), "/root/code/".into());
|
||||
let loc = Loc::with(Path::new("/root/"), "/root/code/".into());
|
||||
assert_eq!(loc.urn().as_os_str(), OsStr::new("code"));
|
||||
assert_eq!(loc.name(), OsStr::new("code"));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
|
||||
|
||||
let loc = Loc::from(Path::new("/root//"), "/root/code/foo//".into());
|
||||
let loc = Loc::with(Path::new("/root//"), "/root/code/foo//".into());
|
||||
assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo"));
|
||||
assert_eq!(loc.name(), OsStr::new("foo"));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
|
||||
|
|
@ -180,7 +193,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_set_name() {
|
||||
let mut loc = Loc::from(Path::new("/root"), "/root/code/foo/".into());
|
||||
let mut loc = Loc::with(Path::new("/root"), "/root/code/foo/".into());
|
||||
assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo"));
|
||||
assert_eq!(loc.name(), OsStr::new("foo"));
|
||||
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(loc scheme url urn);
|
||||
yazi_macro::mod_flat!(component loc scheme url urn);
|
||||
|
|
|
|||
|
|
@ -1,36 +1,92 @@
|
|||
use std::fmt::Display;
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::{Result, bail};
|
||||
use percent_encoding::{CONTROLS, PercentEncode, percent_decode, percent_encode};
|
||||
|
||||
#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
use crate::BytesExt;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum Scheme {
|
||||
#[default]
|
||||
Regular,
|
||||
Search,
|
||||
|
||||
Search(String),
|
||||
SearchItem,
|
||||
Archive,
|
||||
|
||||
Archive(String),
|
||||
|
||||
Sftp(String),
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Scheme {
|
||||
type Error = anyhow::Error;
|
||||
impl Scheme {
|
||||
pub(super) fn parse(bytes: &[u8]) -> Result<(Self, usize)> {
|
||||
let Some((protocol, rest)) = bytes.split_by_seq(b"://") else {
|
||||
return Ok((Self::Regular, 0));
|
||||
};
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
b"regular" => Scheme::Regular,
|
||||
b"search" => Scheme::Search,
|
||||
b"archive" => Scheme::Archive,
|
||||
_ => bail!("Unknown URL scheme: {}", String::from_utf8_lossy(value)),
|
||||
Ok(match protocol {
|
||||
b"regular" => (Self::Regular, 10),
|
||||
b"search" => {
|
||||
let (name, skip) = Self::decode_param(rest)?;
|
||||
(Self::Search(name), 9 + skip)
|
||||
}
|
||||
b"archive" => {
|
||||
let (name, skip) = Self::decode_param(rest)?;
|
||||
(Self::Archive(name), 10 + skip)
|
||||
}
|
||||
b"sftp" => {
|
||||
let (name, skip) = Self::decode_param(rest)?;
|
||||
(Self::Sftp(name), 7 + skip)
|
||||
}
|
||||
_ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn covariant(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
// Local files
|
||||
(
|
||||
Self::Regular | Self::Search(_) | Self::SearchItem,
|
||||
Self::Regular | Self::Search(_) | Self::SearchItem,
|
||||
) => true,
|
||||
|
||||
// Virtual files within the same namespace
|
||||
(a, b) => a == b,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn is_virtual(&self) -> bool {
|
||||
match self {
|
||||
Self::Regular | Self::Search(_) | Self::SearchItem => false,
|
||||
Self::Archive(_) | Self::Sftp(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_param(bytes: &[u8]) -> Result<(String, usize)> {
|
||||
let len = bytes.iter().copied().take_while(|&b| b != b'/').count();
|
||||
|
||||
let s = match Cow::from(percent_decode(&bytes[..len])) {
|
||||
Cow::Borrowed(b) => str::from_utf8(b)?.to_owned(),
|
||||
Cow::Owned(b) => String::from_utf8(b)?,
|
||||
};
|
||||
|
||||
let slash = bytes.get(len).is_some_and(|&b| b == b'/') as usize;
|
||||
Ok((s, len + slash))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn encode_param<'a>(s: &'a str) -> PercentEncode<'a> { percent_encode(s.as_bytes(), CONTROLS) }
|
||||
}
|
||||
|
||||
impl Display for Scheme {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Scheme::Regular => write!(f, "regular"),
|
||||
Scheme::Search => write!(f, "search"),
|
||||
Scheme::SearchItem => write!(f, "search_item"),
|
||||
Scheme::Archive => write!(f, "archive"),
|
||||
Self::Regular => write!(f, "regular://"),
|
||||
Self::Search(kw) => write!(f, "search://{}/", Self::encode_param(kw)),
|
||||
Self::SearchItem => write!(f, "search_item://"),
|
||||
Self::Archive(id) => write!(f, "archive://{}/", Self::encode_param(id)),
|
||||
Self::Sftp(id) => write!(f, "sftp://{}/", Self::encode_param(id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
use std::{borrow::Cow, ffi::{OsStr, OsString}, fmt::{Debug, Display, Formatter}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
|
||||
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Display, Formatter}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
|
||||
|
||||
use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode};
|
||||
use percent_encoding::{AsciiSet, CONTROLS, percent_encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::UrnBuf;
|
||||
use crate::{BytesExt, IntoOsStr, url::{Loc, Scheme}};
|
||||
use crate::{IntoOsStr, url::{Components, Loc, Scheme}};
|
||||
|
||||
const ENCODE_SET: &AsciiSet = &CONTROLS.add(b'#');
|
||||
|
||||
#[derive(Clone, Default, Eq, Ord, PartialOrd)]
|
||||
pub struct Url {
|
||||
loc: Loc,
|
||||
scheme: Scheme,
|
||||
frag: OsString,
|
||||
pub loc: Loc,
|
||||
pub scheme: Scheme,
|
||||
}
|
||||
|
||||
impl Deref for Url {
|
||||
|
|
@ -23,21 +22,23 @@ impl Deref for Url {
|
|||
|
||||
impl Debug for Url {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self.scheme {
|
||||
Scheme::Regular => write!(f, "Regular({:?})", self.loc),
|
||||
Scheme::Search => write!(f, "Search({:?}, {})", self.loc, self.frag.display()),
|
||||
Scheme::SearchItem => write!(f, "SearchItem({:?})", self.loc),
|
||||
Scheme::Archive => write!(f, "Archive({:?})", self.loc),
|
||||
let Self { scheme, loc } = self;
|
||||
match scheme {
|
||||
Scheme::Regular => write!(f, "{scheme}{}", loc.display()),
|
||||
Scheme::Search(_) => write!(f, "{scheme}{}", loc.display()),
|
||||
Scheme::SearchItem => write!(f, "{scheme}{}", loc.display()),
|
||||
Scheme::Archive(_) => write!(f, "{scheme}{}", loc.display()),
|
||||
Scheme::Sftp(_) => write!(f, "{scheme}{}", loc.display()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Loc> for Url {
|
||||
fn from(loc: Loc) -> Self { Self { loc, ..Default::default() } }
|
||||
fn from(loc: Loc) -> Self { Self { loc, scheme: Scheme::Regular } }
|
||||
}
|
||||
|
||||
impl From<PathBuf> for Url {
|
||||
fn from(path: PathBuf) -> Self { Loc::new(path).into() }
|
||||
fn from(path: PathBuf) -> Self { Loc::from(path).into() }
|
||||
}
|
||||
|
||||
impl From<&PathBuf> for Url {
|
||||
|
|
@ -45,35 +46,16 @@ impl From<&PathBuf> for Url {
|
|||
}
|
||||
|
||||
impl From<&Path> for Url {
|
||||
fn from(path: &Path) -> Self { path.to_owned().into() }
|
||||
fn from(path: &Path) -> Self { path.to_path_buf().into() }
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Url {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
let mut url = Url::default();
|
||||
let Some((scheme, rest)) = value.split_by_seq(b"://") else {
|
||||
url.loc = Loc::new(value.into_os_str()?.into_owned().into());
|
||||
return Ok(url);
|
||||
};
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let (scheme, skip) = Scheme::parse(bytes)?;
|
||||
|
||||
url.scheme = Scheme::try_from(scheme)?;
|
||||
if url.scheme == Scheme::Regular {
|
||||
url.loc = Loc::new(rest.into_os_str()?.into_owned().into());
|
||||
return Ok(url);
|
||||
}
|
||||
|
||||
let (loc, frag) = match rest.split_by_seq(b"#") {
|
||||
None => (rest, OsString::new()),
|
||||
Some((a, b)) => (a, b.into_os_str()?.into_owned()),
|
||||
};
|
||||
|
||||
// FIXME: use `Loc::from(base, path)` instead
|
||||
url.loc = Loc::new(Cow::from(percent_decode(loc)).into_os_str()?.into_owned().into());
|
||||
url.frag = frag;
|
||||
|
||||
Ok(url)
|
||||
Ok(Url { loc: bytes[skip..].into_os_str()?.into(), scheme })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,14 +75,12 @@ impl AsRef<Url> for Url {
|
|||
fn as_ref(&self) -> &Url { self }
|
||||
}
|
||||
|
||||
// FIXME: remove
|
||||
impl AsRef<Path> for Url {
|
||||
fn as_ref(&self) -> &Path { &self.loc }
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for Url {
|
||||
fn as_ref(&self) -> &OsStr { self.loc.as_os_str() }
|
||||
}
|
||||
|
||||
// FIXME: remove
|
||||
impl Display for Url {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if matches!(self.scheme, Scheme::Regular | Scheme::SearchItem) {
|
||||
|
|
@ -108,16 +88,13 @@ impl Display for Url {
|
|||
}
|
||||
|
||||
let loc = percent_encode(self.loc.as_os_str().as_encoded_bytes(), ENCODE_SET);
|
||||
write!(f, "{}://{loc}", self.scheme)?;
|
||||
|
||||
if !self.frag.is_empty() {
|
||||
write!(f, "#{}", percent_encode(self.frag.as_encoded_bytes(), ENCODE_SET))?;
|
||||
}
|
||||
write!(f, "{}{loc}", self.scheme)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: remove
|
||||
impl From<&Url> for String {
|
||||
fn from(url: &Url) -> Self { url.to_string() }
|
||||
}
|
||||
|
|
@ -136,37 +113,77 @@ impl From<Cow<'_, Url>> for Url {
|
|||
|
||||
impl Url {
|
||||
#[inline]
|
||||
pub fn join(&self, path: impl AsRef<Path>) -> Self {
|
||||
match self.scheme {
|
||||
Scheme::Regular => Self::from(self.loc.join(path)),
|
||||
Scheme::Search => {
|
||||
let loc = Loc::from(&self.loc, self.loc.join(path));
|
||||
Self::from(loc).into_search_item()
|
||||
}
|
||||
Scheme::SearchItem => {
|
||||
let loc = Loc::from(self.loc.base(), self.loc.join(path));
|
||||
Self::from(loc).into_search_item()
|
||||
}
|
||||
Scheme::Archive => Self::from(self.loc.join(path)).into_archive(),
|
||||
pub fn base(&self) -> Url {
|
||||
let loc: Loc = self.loc.base().into();
|
||||
match &self.scheme {
|
||||
Scheme::Regular => Self { loc, scheme: Scheme::Regular },
|
||||
Scheme::Search(_) => Self { loc, scheme: self.scheme.clone() },
|
||||
Scheme::SearchItem => Self { loc, scheme: Scheme::Search(String::new()) },
|
||||
Scheme::Archive(_) => Self { loc, scheme: self.scheme.clone() },
|
||||
Scheme::Sftp(_) => Self { loc, scheme: self.scheme.clone() },
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn parent_url(&self) -> Option<Url> {
|
||||
let p = self.loc.parent()?;
|
||||
Some(match self.scheme {
|
||||
Scheme::Regular | Scheme::Search => Self::from(p),
|
||||
Scheme::SearchItem => {
|
||||
if p == self.loc.base() {
|
||||
Self::from(p).into_search("")
|
||||
} else {
|
||||
Self::from(p).into_search_item()
|
||||
}
|
||||
pub fn join(&self, path: impl AsRef<Path>) -> Self {
|
||||
match self.scheme {
|
||||
Scheme::Regular => Self { loc: self.loc.join(path).into(), scheme: Scheme::Regular },
|
||||
Scheme::Search(_) => {
|
||||
Self { loc: Loc::with(&self.loc, self.loc.join(path)), scheme: Scheme::SearchItem }
|
||||
}
|
||||
Scheme::Archive => Self::from(p),
|
||||
Scheme::SearchItem => {
|
||||
Self { loc: Loc::with(self.loc.base(), self.loc.join(path)), scheme: Scheme::SearchItem }
|
||||
}
|
||||
Scheme::Archive(_) => {
|
||||
Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() }
|
||||
}
|
||||
Scheme::Sftp(_) => Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() },
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: check usages
|
||||
#[inline]
|
||||
pub fn components(&self) -> Components<'_> { Components::new(self) }
|
||||
|
||||
#[inline]
|
||||
pub fn covariant(&self, other: &Self) -> bool {
|
||||
self.scheme.covariant(&other.scheme) && self.loc == other.loc
|
||||
}
|
||||
|
||||
pub fn parent_url(&self) -> Option<Url> {
|
||||
let parent = self.loc.parent()?;
|
||||
let base = self.loc.base();
|
||||
|
||||
Some(match &self.scheme {
|
||||
Scheme::Regular => Self { loc: parent.into(), scheme: Scheme::Regular },
|
||||
Scheme::Search(_) => Self { loc: parent.into(), scheme: Scheme::Regular },
|
||||
Scheme::SearchItem if parent == base => {
|
||||
Self { loc: parent.into(), scheme: Scheme::Search(String::new()) }
|
||||
}
|
||||
Scheme::SearchItem => {
|
||||
Self { loc: Loc::with(base, parent.to_owned()), scheme: Scheme::SearchItem }
|
||||
}
|
||||
Scheme::Archive(_) => Self { loc: parent.into(), scheme: self.scheme.clone() },
|
||||
Scheme::Sftp(_) => Self { loc: parent.into(), scheme: self.scheme.clone() },
|
||||
})
|
||||
}
|
||||
|
||||
pub fn strip_prefix(&self, base: impl AsRef<Url>) -> Option<Self> {
|
||||
let base = base.as_ref();
|
||||
if !self.scheme.covariant(&base.scheme) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
loc: self.loc.strip_prefix(base.loc.as_path()).ok()?.into(),
|
||||
scheme: self.scheme.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_path(&self) -> Option<&Path> {
|
||||
Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_name(&mut self, name: impl AsRef<OsStr>) { self.loc.set_name(name); }
|
||||
|
||||
|
|
@ -189,82 +206,53 @@ impl Url {
|
|||
pub fn is_regular(&self) -> bool { self.scheme == Scheme::Regular }
|
||||
|
||||
#[inline]
|
||||
pub fn to_regular(&self) -> Self { Self { loc: self.loc.clone(), ..Default::default() } }
|
||||
pub fn to_regular(&self) -> Self { Self { loc: self.loc.clone(), scheme: Scheme::Regular } }
|
||||
|
||||
#[inline]
|
||||
pub fn into_regular(mut self) -> Self {
|
||||
self.scheme = Scheme::Regular;
|
||||
self.frag = OsString::new();
|
||||
self
|
||||
}
|
||||
|
||||
// --- Search
|
||||
#[inline]
|
||||
pub fn is_search(&self) -> bool { self.scheme == Scheme::Search }
|
||||
pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) }
|
||||
|
||||
#[inline]
|
||||
pub fn to_search(&self, frag: impl AsRef<OsStr>) -> Self {
|
||||
Self { loc: self.loc.clone(), scheme: Scheme::Search, frag: frag.as_ref().to_owned() }
|
||||
pub fn to_search(&self, frag: impl AsRef<str>) -> Self {
|
||||
Self { loc: self.loc.clone(), scheme: Scheme::Search(frag.as_ref().to_owned()) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_search(mut self, frag: impl AsRef<OsStr>) -> Self {
|
||||
self.scheme = Scheme::Search;
|
||||
self.frag = frag.as_ref().to_owned();
|
||||
pub fn into_search(mut self, frag: impl AsRef<str>) -> Self {
|
||||
self.scheme = Scheme::Search(frag.as_ref().to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
// --- Archive
|
||||
#[inline]
|
||||
pub fn is_search_item(&self) -> bool { self.scheme == Scheme::SearchItem }
|
||||
|
||||
#[inline]
|
||||
pub fn into_search_item(mut self) -> Self {
|
||||
self.scheme = Scheme::SearchItem;
|
||||
self.frag = OsString::new();
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_archive(&self) -> bool { self.scheme == Scheme::Archive }
|
||||
|
||||
#[inline]
|
||||
pub fn to_archive(&self) -> Self {
|
||||
Self { loc: self.loc.clone(), scheme: Scheme::Archive, ..Default::default() }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_archive(mut self) -> Self {
|
||||
self.scheme = Scheme::Archive;
|
||||
self.frag = OsString::new();
|
||||
self
|
||||
}
|
||||
|
||||
// --- Loc
|
||||
#[inline]
|
||||
pub fn set_loc(&mut self, loc: Loc) { self.loc = loc; }
|
||||
pub fn is_archive(&self) -> bool { matches!(self.scheme, Scheme::Archive(_)) }
|
||||
|
||||
// FIXME: remove
|
||||
#[inline]
|
||||
pub fn into_path(self) -> PathBuf { self.loc.into_path() }
|
||||
|
||||
// --- Scheme
|
||||
#[inline]
|
||||
pub fn scheme(&self) -> Scheme { self.scheme }
|
||||
|
||||
// --- Frag
|
||||
#[inline]
|
||||
pub fn frag(&self) -> &OsStr { &self.frag }
|
||||
}
|
||||
|
||||
impl Hash for Url {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.loc.hash(state);
|
||||
|
||||
match self.scheme {
|
||||
Scheme::Regular | Scheme::SearchItem => {}
|
||||
Scheme::Search | Scheme::Archive => {
|
||||
match &self.scheme {
|
||||
Scheme::Regular => {}
|
||||
Scheme::Search(_) => {
|
||||
self.scheme.hash(state);
|
||||
}
|
||||
Scheme::SearchItem => {}
|
||||
Scheme::Archive(_) => {
|
||||
self.scheme.hash(state);
|
||||
}
|
||||
Scheme::Sftp(_) => {
|
||||
self.scheme.hash(state);
|
||||
self.frag.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -272,11 +260,13 @@ impl Hash for Url {
|
|||
|
||||
impl PartialEq for Url {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self.scheme, other.scheme) {
|
||||
(Scheme::Regular | Scheme::SearchItem, Scheme::Regular | Scheme::SearchItem) => {
|
||||
self.loc == other.loc
|
||||
}
|
||||
_ => self.loc == other.loc && self.scheme == other.scheme,
|
||||
if self.loc != other.loc {
|
||||
return false;
|
||||
}
|
||||
|
||||
match (&self.scheme, &other.scheme) {
|
||||
(Scheme::Regular | Scheme::SearchItem, Scheme::Regular | Scheme::SearchItem) => true,
|
||||
_ => self.scheme == other.scheme,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -293,6 +283,6 @@ impl<'de> Deserialize<'de> for Url {
|
|||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Url::try_from(s).map_err(serde::de::Error::custom)
|
||||
Self::try_from(s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue