mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat(symlink): create symlinks to the yanked files on unix
This commit is contained in:
parent
f7fdda9d9b
commit
4a8cf212f9
12 changed files with 153 additions and 29 deletions
|
|
@ -108,6 +108,17 @@ impl Executor {
|
||||||
cx.tasks.file_copy(src, dest, force, exec.named.contains_key("follow"))
|
cx.tasks.file_copy(src, dest, force, exec.named.contains_key("follow"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"symlink" => {
|
||||||
|
let dest = cx.manager.cwd().to_owned();
|
||||||
|
let (_, src) = cx.manager.yanked();
|
||||||
|
|
||||||
|
cx.tasks.file_symlink(
|
||||||
|
src,
|
||||||
|
dest,
|
||||||
|
exec.named.contains_key("force"),
|
||||||
|
exec.named.contains_key("relative"),
|
||||||
|
)
|
||||||
|
}
|
||||||
"remove" => {
|
"remove" => {
|
||||||
let targets = cx.manager.selected().into_iter().map(|f| f.url_owned()).collect();
|
let targets = cx.manager.selected().into_iter().map(|f| f.url_owned()).collect();
|
||||||
let force = exec.named.contains_key("force");
|
let force = exec.named.contains_key("force");
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,12 @@ keymap = [
|
||||||
{ on = [ "<C-Enter>" ], exec = "open --interactive", desc = "Open the selected files interactively" }, # It's cool if you're using a terminal that supports CSI u
|
{ on = [ "<C-Enter>" ], exec = "open --interactive", desc = "Open the selected files interactively" }, # It's cool if you're using a terminal that supports CSI u
|
||||||
{ on = [ "y" ], exec = "yank", desc = "Copy the selected files" },
|
{ on = [ "y" ], exec = "yank", desc = "Copy the selected files" },
|
||||||
{ on = [ "x" ], exec = "yank --cut", desc = "Cut the selected files" },
|
{ on = [ "x" ], exec = "yank --cut", desc = "Cut the selected files" },
|
||||||
{ on = [ "p" ], exec = "paste", desc = "Paste the files" },
|
{ on = [ "p" , "p" ], exec = "paste", desc = "Paste the files" },
|
||||||
{ on = [ "P" ], exec = "paste --force", desc = "Paste the files (overwrite if the destination exists)" },
|
{ on = [ "P" ], exec = "paste --force", desc = "Paste the files (overwrite if the destination exists)" },
|
||||||
{ on = [ "k" ], exec = "paste --follow", desc = "Paste the files (follow the symlinks)" },
|
{ on = [ "k" ], exec = "paste --follow", desc = "Paste the files (follow the symlinks)" },
|
||||||
{ on = [ "K" ], exec = "paste --follow --force", desc = "Paste the files (overwrite + follow)" },
|
{ on = [ "K" ], exec = "paste --follow --force", desc = "Paste the files (overwrite + follow)" },
|
||||||
|
{ on = [ "p", "l" ], exec = "symlink", desc = "Symlink the absolute path of files" },
|
||||||
|
{ on = [ "p", "L" ], exec = "symlink --relative", desc = "Symlink the relative path of files" },
|
||||||
{ on = [ "d" ], exec = "remove", desc = "Move the files to the trash" },
|
{ on = [ "d" ], exec = "remove", desc = "Move the files to the trash" },
|
||||||
{ on = [ "D" ], exec = "remove --permanently", desc = "Permanently delete the files" },
|
{ on = [ "D" ], exec = "remove --permanently", desc = "Permanently delete the files" },
|
||||||
{ on = [ "a" ], exec = "create", desc = "Create a file or directory (ends with / for directories)" },
|
{ on = [ "a" ], exec = "create", desc = "Create a file or directory (ends with / for directories)" },
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,7 @@ impl File {
|
||||||
pub fn url(&self) -> &Url { &self.url }
|
pub fn url(&self) -> &Url { &self.url }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_url(&mut self, url: Url) { self.url = url; }
|
pub fn url_mut(&mut self) -> &mut Url { &mut self.url }
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn url_owned(&self) -> Url { self.url.clone() }
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn url_os_str(&self) -> &OsStr { self.url.as_os_str() }
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn name(&self) -> Option<&OsStr> { self.url.file_name() }
|
pub fn name(&self) -> Option<&OsStr> { self.url.file_name() }
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ impl Files {
|
||||||
self.items.iter().any(|f| !self.selected.contains(&f.url))
|
self.items.iter().any(|f| !self.selected.contains(&f.url))
|
||||||
};
|
};
|
||||||
|
|
||||||
self.selected = self.iter().map(|f| f.url_owned()).collect();
|
self.selected = self.iter().map(|f| f.url().to_owned()).collect();
|
||||||
b
|
b
|
||||||
}
|
}
|
||||||
Some(false) => {
|
Some(false) => {
|
||||||
|
|
@ -106,7 +106,7 @@ impl Files {
|
||||||
if self.selected.contains(&item.url) {
|
if self.selected.contains(&item.url) {
|
||||||
self.selected.remove(&item.url);
|
self.selected.remove(&item.url);
|
||||||
} else {
|
} else {
|
||||||
self.selected.insert(item.url_owned());
|
self.selected.insert(item.url().to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
!self.items.is_empty()
|
!self.items.is_empty()
|
||||||
|
|
@ -116,7 +116,7 @@ impl Files {
|
||||||
|
|
||||||
pub fn select_index(&mut self, indices: &BTreeSet<usize>, state: Option<bool>) -> bool {
|
pub fn select_index(&mut self, indices: &BTreeSet<usize>, state: Option<bool>) -> bool {
|
||||||
let mut applied = false;
|
let mut applied = false;
|
||||||
let paths: Vec<_> = self.pick(indices).iter().map(|f| f.url_owned()).collect();
|
let paths: Vec<_> = self.pick(indices).iter().map(|f| f.url().to_owned()).collect();
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
applied |= self.select(&path, state);
|
applied |= self.select(&path, state);
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ impl Finder {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.matched.insert(file.url_owned(), i);
|
self.matched.insert(file.url().to_owned(), i);
|
||||||
if self.matched.len() > 99 {
|
if self.matched.len() > 99 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ impl Folder {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hover_repos(&mut self) -> bool {
|
pub fn hover_repos(&mut self) -> bool {
|
||||||
self.hover(&self.hovered.as_ref().map(|h| h.url_owned()).unwrap_or_default())
|
self.hover(&self.hovered.as_ref().map(|h| h.url().to_owned()).unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hover_force(&mut self, file: File) -> bool {
|
pub fn hover_force(&mut self, file: File) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ impl Manager {
|
||||||
|
|
||||||
pub fn yank(&mut self, cut: bool) -> bool {
|
pub fn yank(&mut self, cut: bool) -> bool {
|
||||||
self.yanked.0 = cut;
|
self.yanked.0 = cut;
|
||||||
self.yanked.1 = self.selected().into_iter().map(|f| f.url_owned()).collect();
|
self.yanked.1 = self.selected().into_iter().map(|f| f.url().to_owned()).collect();
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ impl Manager {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|f| {
|
.map(|f| {
|
||||||
(
|
(
|
||||||
f.url_owned(),
|
f.url().to_owned(),
|
||||||
f.is_dir().then(|| MIME_DIR.to_owned()).or_else(|| self.mimetype.get(f.url()).cloned()),
|
f.is_dir().then(|| MIME_DIR.to_owned()).or_else(|| self.mimetype.get(f.url()).cloned()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -208,7 +208,7 @@ impl Manager {
|
||||||
return self.bulk_rename();
|
return self.bulk_rename();
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(hovered) = self.hovered().map(|h| h.url_owned()) else {
|
let Some(hovered) = self.hovered().map(|h| h.url().to_owned()) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -333,7 +333,7 @@ impl Manager {
|
||||||
pub fn update_read(&mut self, op: FilesOp) -> bool {
|
pub fn update_read(&mut self, op: FilesOp) -> bool {
|
||||||
let url = op.url().clone();
|
let url = op.url().clone();
|
||||||
let cwd = self.cwd().to_owned();
|
let cwd = self.cwd().to_owned();
|
||||||
let hovered = self.hovered().map(|h| h.url_owned());
|
let hovered = self.hovered().map(|h| h.url().to_owned());
|
||||||
|
|
||||||
let mut b = if cwd == url {
|
let mut b = if cwd == url {
|
||||||
self.current_mut().update(op)
|
self.current_mut().update(op)
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ impl Tab {
|
||||||
let mut it = self.selected().into_iter().peekable();
|
let mut it = self.selected().into_iter().peekable();
|
||||||
while let Some(f) = it.next() {
|
while let Some(f) = it.next() {
|
||||||
s.push(match type_ {
|
s.push(match type_ {
|
||||||
"path" => f.url_os_str(),
|
"path" => f.url().as_os_str(),
|
||||||
"dirname" => f.url().parent().map_or(OsStr::new(""), |p| p.as_os_str()),
|
"dirname" => f.url().parent().map_or(OsStr::new(""), |p| p.as_os_str()),
|
||||||
"filename" => f.name().unwrap_or(OsStr::new("")),
|
"filename" => f.name().unwrap_or(OsStr::new("")),
|
||||||
"name_without_ext" => f.stem().unwrap_or(OsStr::new("")),
|
"name_without_ext" => f.stem().unwrap_or(OsStr::new("")),
|
||||||
|
|
@ -356,7 +356,7 @@ impl Tab {
|
||||||
let selected: Vec<_> = self
|
let selected: Vec<_> = self
|
||||||
.selected()
|
.selected()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|f| (f.url_os_str().to_owned(), Default::default()))
|
.map(|f| (f.url().as_os_str().to_owned(), Default::default()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut exec = exec.to_owned();
|
let mut exec = exec.to_owned();
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ impl Watcher {
|
||||||
let mut new = Vec::with_capacity(files.len());
|
let mut new = Vec::with_capacity(files.len());
|
||||||
for file in files {
|
for file in files {
|
||||||
let mut file = file.clone();
|
let mut file = file.clone();
|
||||||
file.set_url(ori.join(file.url().strip_prefix(url).unwrap()));
|
*file.url_mut() = ori.join(file.url().strip_prefix(url).unwrap());
|
||||||
new.push(file);
|
new.push(file);
|
||||||
}
|
}
|
||||||
new
|
new
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use shared::{unique_path, Throttle, Url};
|
||||||
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep};
|
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep};
|
||||||
use tracing::{info, trace};
|
use tracing::{info, trace};
|
||||||
|
|
||||||
use super::{workers::{File, FileOpDelete, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage};
|
use super::{workers::{File, FileOpDelete, FileOpPaste, FileOpSymlink, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage};
|
||||||
use crate::emit;
|
use crate::emit;
|
||||||
|
|
||||||
pub struct Scheduler {
|
pub struct Scheduler {
|
||||||
|
|
@ -234,6 +234,22 @@ impl Scheduler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn file_symlink(&self, from: Url, mut to: Url, force: bool) {
|
||||||
|
let name = format!("Symlink {from:?} to {to:?}");
|
||||||
|
let id = self.running.write().add(name);
|
||||||
|
|
||||||
|
let _ = self.todo.send_blocking({
|
||||||
|
let file = self.file.clone();
|
||||||
|
async move {
|
||||||
|
if !force {
|
||||||
|
to = unique_path(to).await;
|
||||||
|
}
|
||||||
|
file.symlink(FileOpSymlink { id, from, to }).await.ok();
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn file_delete(&self, target: Url) {
|
pub(super) fn file_delete(&self, target: Url) {
|
||||||
let mut running = self.running.write();
|
let mut running = self.running.write();
|
||||||
let id = running.add(format!("Delete {:?}", target));
|
let id = running.add(format!("Delete {:?}", target));
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ pub(crate) struct File {
|
||||||
pub(crate) enum FileOp {
|
pub(crate) enum FileOp {
|
||||||
Paste(FileOpPaste),
|
Paste(FileOpPaste),
|
||||||
Link(FileOpLink),
|
Link(FileOpLink),
|
||||||
|
Symlink(FileOpSymlink),
|
||||||
Delete(FileOpDelete),
|
Delete(FileOpDelete),
|
||||||
Trash(FileOpTrash),
|
Trash(FileOpTrash),
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +44,13 @@ pub(crate) struct FileOpLink {
|
||||||
pub length: u64,
|
pub length: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct FileOpSymlink {
|
||||||
|
pub id: usize,
|
||||||
|
pub from: Url,
|
||||||
|
pub to: Url,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct FileOpDelete {
|
pub(crate) struct FileOpDelete {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
|
|
@ -68,6 +76,7 @@ impl File {
|
||||||
Ok(match self.rx.recv().await? {
|
Ok(match self.rx.recv().await? {
|
||||||
FileOp::Paste(t) => (t.id, FileOp::Paste(t)),
|
FileOp::Paste(t) => (t.id, FileOp::Paste(t)),
|
||||||
FileOp::Link(t) => (t.id, FileOp::Link(t)),
|
FileOp::Link(t) => (t.id, FileOp::Link(t)),
|
||||||
|
FileOp::Symlink(t) => (t.id, FileOp::Symlink(t)),
|
||||||
FileOp::Delete(t) => (t.id, FileOp::Delete(t)),
|
FileOp::Delete(t) => (t.id, FileOp::Delete(t)),
|
||||||
FileOp::Trash(t) => (t.id, FileOp::Trash(t)),
|
FileOp::Trash(t) => (t.id, FileOp::Trash(t)),
|
||||||
})
|
})
|
||||||
|
|
@ -113,6 +122,17 @@ impl File {
|
||||||
}
|
}
|
||||||
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
|
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
|
||||||
}
|
}
|
||||||
|
FileOp::Symlink(task) => {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
fs::symlink(&task.from, &task.to).await?;
|
||||||
|
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
FileOp::Link(task) => {
|
FileOp::Link(task) => {
|
||||||
let src = match fs::read_link(&task.from).await {
|
let src = match fs::read_link(&task.from).await {
|
||||||
Ok(src) => src,
|
Ok(src) => src,
|
||||||
|
|
@ -126,14 +146,14 @@ impl File {
|
||||||
match fs::remove_file(&task.to).await {
|
match fs::remove_file(&task.to).await {
|
||||||
Err(e) if e.kind() != NotFound => Err(e)?,
|
Err(e) if e.kind() != NotFound => Err(e)?,
|
||||||
_ => {
|
_ => {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
fs::symlink(src, &task.to).await?
|
||||||
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
fs::symlink_file(src, &task.to).await?
|
fs::symlink_file(src, &task.to).await?
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
fs::symlink(src, &task.to).await?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,7 +225,7 @@ impl File {
|
||||||
let dest = root.join(src.components().skip(skip).collect::<PathBuf>());
|
let dest = root.join(src.components().skip(skip).collect::<PathBuf>());
|
||||||
match fs::create_dir(&dest).await {
|
match fs::create_dir(&dest).await {
|
||||||
Err(e) if e.kind() != AlreadyExists => {
|
Err(e) if e.kind() != AlreadyExists => {
|
||||||
self.log(task.id, format!("Create dir failed: {:?}, {e}", dest))?;
|
self.log(task.id, format!("Create dir failed: {dest:?}, {e}"))?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -214,7 +234,7 @@ impl File {
|
||||||
let mut it = match fs::read_dir(&src).await {
|
let mut it = match fs::read_dir(&src).await {
|
||||||
Ok(it) => it,
|
Ok(it) => it,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.log(task.id, format!("Read dir failed: {:?}, {e}", src))?;
|
self.log(task.id, format!("Read dir failed: {src:?}, {e}"))?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -244,6 +264,13 @@ impl File {
|
||||||
self.done(task.id)
|
self.done(task.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn symlink(&self, task: FileOpSymlink) -> Result<()> {
|
||||||
|
let id = task.id;
|
||||||
|
self.sch.send(TaskOp::New(id, 0))?;
|
||||||
|
self.tx.send(FileOp::Symlink(task)).await?;
|
||||||
|
self.done(id)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn delete(&self, mut task: FileOpDelete) -> Result<()> {
|
pub(crate) async fn delete(&self, mut task: FileOpDelete) -> Result<()> {
|
||||||
let meta = fs::symlink_metadata(&task.target).await?;
|
let meta = fs::symlink_metadata(&task.target).await?;
|
||||||
if !meta.is_dir() {
|
if !meta.is_dir() {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{env, path::{Path, PathBuf}};
|
use std::{borrow::Cow, env, path::{Component, Path, PathBuf}};
|
||||||
|
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ pub fn readable_size(size: u64) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unique_path(mut p: Url) -> Url {
|
pub async fn unique_path(mut p: Url) -> Url {
|
||||||
let Some(name) = p.file_name().map(|n| n.to_os_string()) else {
|
let Some(name) = p.file_name().map(|n| n.to_owned()) else {
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -65,6 +65,52 @@ pub async fn unique_path(mut p: Url) -> Url {
|
||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parmaters
|
||||||
|
// * `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<P: AsRef<Path>>(path: &Path, root: P) -> Cow<'_, Path> {
|
||||||
|
let root = root.as_ref();
|
||||||
|
assert!(path.is_absolute());
|
||||||
|
assert!(root.is_absolute());
|
||||||
|
let mut p_comps = path.components();
|
||||||
|
let mut r_comps = root.components();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
(None, None) => break (None, None),
|
||||||
|
(a, b) if a != b => break (a, b),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let p_comps = p_head.into_iter().chain(p_comps);
|
||||||
|
let walk_up = r_head.into_iter().chain(r_comps).map(|_| Component::ParentDir);
|
||||||
|
|
||||||
|
let mut buf = PathBuf::new();
|
||||||
|
buf.extend(walk_up);
|
||||||
|
buf.extend(p_comps);
|
||||||
|
|
||||||
|
Cow::from(buf)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn optional_bool(s: &str) -> Option<bool> {
|
pub fn optional_bool(s: &str) -> Option<bool> {
|
||||||
if s == "true" {
|
if s == "true" {
|
||||||
|
|
@ -75,3 +121,31 @@ pub fn optional_bool(s: &str) -> Option<bool> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{borrow::Cow, path::Path};
|
||||||
|
|
||||||
|
use super::path_relative_to;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_path_relative_to() {
|
||||||
|
let foo = "/foo";
|
||||||
|
let bar = "/foo/bar";
|
||||||
|
let baz = "/foo/bar/baz";
|
||||||
|
let qux = "/foo/bar/qux";
|
||||||
|
let aha = "/foo/aha/bar";
|
||||||
|
|
||||||
|
assert_path_relate_to_root(bar, baz, "../"); // self reflection
|
||||||
|
assert_path_relate_to_root(baz, bar, "baz"); // child entry
|
||||||
|
assert_path_relate_to_root(qux, baz, "../qux"); // sibling entry
|
||||||
|
assert_path_relate_to_root(baz, qux, "../baz"); // sibling entry
|
||||||
|
assert_path_relate_to_root(foo, baz, "../../"); // ancestor entry
|
||||||
|
assert_path_relate_to_root(aha, baz, "../../aha/bar"); // ancestor's child entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_path_relate_to_root(path: &str, root: &str, res: &str) {
|
||||||
|
assert_eq!(path_relative_to(Path::new(path), Path::new(root)), Cow::Borrowed(Path::new(res)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue