mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: new link command, which creates symlinks to the yanked files (#167)
This commit is contained in:
parent
f7fdda9d9b
commit
782f88b965
10 changed files with 200 additions and 46 deletions
|
|
@ -98,16 +98,26 @@ impl Executor {
|
|||
"open" => cx.manager.open(exec.named.contains_key("interactive")),
|
||||
"yank" => cx.manager.yank(exec.named.contains_key("cut")),
|
||||
"paste" => {
|
||||
let dest = cx.manager.cwd().to_owned();
|
||||
let dest = cx.manager.cwd();
|
||||
let (cut, src) = cx.manager.yanked();
|
||||
|
||||
let force = exec.named.contains_key("force");
|
||||
if *cut {
|
||||
cx.tasks.file_cut(src, dest, force)
|
||||
} else {
|
||||
cx.tasks.file_copy(src, dest, force, exec.named.contains_key("follow"))
|
||||
cx.tasks.file_copy(src, dest, force)
|
||||
}
|
||||
}
|
||||
"link" => {
|
||||
let (cut, src) = cx.manager.yanked();
|
||||
!cut
|
||||
&& cx.tasks.file_link(
|
||||
src,
|
||||
cx.manager.cwd(),
|
||||
exec.named.contains_key("relative"),
|
||||
exec.named.contains_key("force"),
|
||||
)
|
||||
}
|
||||
"remove" => {
|
||||
let targets = cx.manager.selected().into_iter().map(|f| f.url_owned()).collect();
|
||||
let force = exec.named.contains_key("force");
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@
|
|||
- paste: Paste the files.
|
||||
|
||||
- `--force`: Overwrite the destination file if it exists.
|
||||
- `--follow`: Copy the file pointed to by a symbolic link, rather than the link itself. Only valid during copying.
|
||||
|
||||
- remove: Move the files to the trash/recycle bin.
|
||||
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ keymap = [
|
|||
{ on = [ "x" ], exec = "yank --cut", desc = "Cut the selected files" },
|
||||
{ on = [ "p" ], exec = "paste", desc = "Paste the files" },
|
||||
{ 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 --force", desc = "Paste the files (overwrite + follow)" },
|
||||
{ on = [ "n" ], exec = "link", desc = "Symlink the absolute path of files" },
|
||||
{ on = [ "N" ], exec = "link --relative", desc = "Symlink the relative path of files" },
|
||||
{ on = [ "d" ], exec = "remove", desc = "Move the files to the trash" },
|
||||
{ on = [ "D" ], exec = "remove --permanently", desc = "Permanently delete the files" },
|
||||
{ on = [ "a" ], exec = "create", desc = "Create a file or directory (ends with / for directories)" },
|
||||
|
|
|
|||
|
|
@ -42,14 +42,11 @@ impl File {
|
|||
pub fn url(&self) -> &Url { &self.url }
|
||||
|
||||
#[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]
|
||||
pub fn name(&self) -> Option<&OsStr> { self.url.file_name() }
|
||||
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ impl Tab {
|
|||
let mut it = self.selected().into_iter().peekable();
|
||||
while let Some(f) = it.next() {
|
||||
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()),
|
||||
"filename" => f.name().unwrap_or(OsStr::new("")),
|
||||
"name_without_ext" => f.stem().unwrap_or(OsStr::new("")),
|
||||
|
|
@ -356,7 +356,7 @@ impl Tab {
|
|||
let selected: Vec<_> = self
|
||||
.selected()
|
||||
.into_iter()
|
||||
.map(|f| (f.url_os_str().to_owned(), Default::default()))
|
||||
.map(|f| (f.url().as_os_str().to_owned(), Default::default()))
|
||||
.collect();
|
||||
|
||||
let mut exec = exec.to_owned();
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ impl Watcher {
|
|||
let mut new = Vec::with_capacity(files.len());
|
||||
for file in files {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use shared::{unique_path, Throttle, Url};
|
|||
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep};
|
||||
use tracing::{info, trace};
|
||||
|
||||
use super::{workers::{File, FileOpDelete, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage};
|
||||
use super::{workers::{File, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage};
|
||||
use crate::emit;
|
||||
|
||||
pub struct Scheduler {
|
||||
|
|
@ -218,7 +218,7 @@ impl Scheduler {
|
|||
});
|
||||
}
|
||||
|
||||
pub(super) fn file_copy(&self, from: Url, mut to: Url, force: bool, follow: bool) {
|
||||
pub(super) fn file_copy(&self, from: Url, mut to: Url, force: bool) {
|
||||
let name = format!("Copy {:?} to {:?}", from, to);
|
||||
let id = self.running.write().add(name);
|
||||
|
||||
|
|
@ -228,7 +228,26 @@ impl Scheduler {
|
|||
if !force {
|
||||
to = unique_path(to).await;
|
||||
}
|
||||
file.paste(FileOpPaste { id, from, to, cut: false, follow, retry: 0 }).await.ok();
|
||||
file.paste(FileOpPaste { id, from, to, cut: false, follow: true, retry: 0 }).await.ok();
|
||||
}
|
||||
.boxed()
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn file_link(&self, from: Url, mut to: Url, relative: bool, force: bool) {
|
||||
let name = format!("Link {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
|
||||
.link(FileOpLink { id, from, to, meta: None, resolve: false, relative, delete: false })
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
.boxed()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ impl Tasks {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn file_cut(&self, src: &HashSet<Url>, dest: Url, force: bool) -> bool {
|
||||
pub fn file_cut(&self, src: &HashSet<Url>, dest: &Url, force: bool) -> bool {
|
||||
for u in src {
|
||||
let to = dest.join(u.file_name().unwrap());
|
||||
if force && u == &to {
|
||||
|
|
@ -167,13 +167,25 @@ impl Tasks {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn file_copy(&self, src: &HashSet<Url>, dest: Url, force: bool, follow: bool) -> bool {
|
||||
pub fn file_copy(&self, src: &HashSet<Url>, dest: &Url, force: bool) -> bool {
|
||||
for u in src {
|
||||
let to = dest.join(u.file_name().unwrap());
|
||||
if force && u == &to {
|
||||
trace!("file_copy: same file, skipping {:?}", to);
|
||||
} else {
|
||||
self.scheduler.file_copy(u.clone(), to, force, follow);
|
||||
self.scheduler.file_copy(u.clone(), to, force);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn file_link(&self, src: &HashSet<Url>, dest: &Url, relative: bool, force: bool) -> bool {
|
||||
for u in src {
|
||||
let to = dest.join(u.file_name().unwrap());
|
||||
if force && *u == to {
|
||||
trace!("file_link: same file, skipping {:?}", to);
|
||||
} else {
|
||||
self.scheduler.file_link(u.clone(), to, relative, force);
|
||||
}
|
||||
}
|
||||
false
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use std::{collections::VecDeque, fs::Metadata, path::{Path, PathBuf}};
|
||||
use std::{borrow::Cow, collections::VecDeque, fs::Metadata, path::{Path, PathBuf}};
|
||||
|
||||
use anyhow::Result;
|
||||
use config::TASKS;
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use shared::{calculate_size, copy_with_progress, Url};
|
||||
use shared::{calculate_size, copy_with_progress, path_relative_to, Url};
|
||||
use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
|
||||
use tracing::trace;
|
||||
|
||||
|
|
@ -36,11 +36,13 @@ pub(crate) struct FileOpPaste {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct FileOpLink {
|
||||
pub id: usize,
|
||||
pub from: Url,
|
||||
pub to: Url,
|
||||
pub cut: bool,
|
||||
pub length: u64,
|
||||
pub id: usize,
|
||||
pub from: Url,
|
||||
pub to: Url,
|
||||
pub meta: Option<Metadata>,
|
||||
pub resolve: bool,
|
||||
pub relative: bool,
|
||||
pub delete: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -114,33 +116,49 @@ impl File {
|
|||
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
|
||||
}
|
||||
FileOp::Link(task) => {
|
||||
let src = match fs::read_link(&task.from).await {
|
||||
Ok(src) => src,
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
self.log(task.id, format!("Link task partially done: {:?}", task))?;
|
||||
return Ok(self.sch.send(TaskOp::Adv(task.id, 1, task.length))?);
|
||||
let meta = task.meta.as_ref().unwrap();
|
||||
|
||||
let src = if task.resolve {
|
||||
match fs::read_link(&task.from).await {
|
||||
Ok(p) => Cow::Owned(p),
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
self.log(task.id, format!("Link task partially done: {:?}", task))?;
|
||||
return Ok(self.sch.send(TaskOp::Adv(task.id, 1, meta.len()))?);
|
||||
}
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
Err(e) => Err(e)?,
|
||||
} else {
|
||||
Cow::Borrowed(task.from.as_path())
|
||||
};
|
||||
|
||||
let src = if task.relative {
|
||||
path_relative_to(&src, &fs::canonicalize(task.to.parent().unwrap()).await?)
|
||||
} else {
|
||||
src
|
||||
};
|
||||
|
||||
match fs::remove_file(&task.to).await {
|
||||
Err(e) if e.kind() != NotFound => Err(e)?,
|
||||
_ => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
fs::symlink_file(src, &task.to).await?
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(unix)]
|
||||
{
|
||||
fs::symlink(src, &task.to).await?
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if meta.is_dir() {
|
||||
fs::symlink_dir(src, &task.to).await?
|
||||
} else {
|
||||
fs::symlink_file(src, &task.to).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if task.cut {
|
||||
if task.delete {
|
||||
fs::remove_file(&task.from).await.ok();
|
||||
}
|
||||
self.sch.send(TaskOp::Adv(task.id, 1, task.length))?;
|
||||
self.sch.send(TaskOp::Adv(task.id, 1, meta.len()))?;
|
||||
}
|
||||
FileOp::Delete(task) => {
|
||||
if let Err(e) = fs::remove_file(&task.target).await {
|
||||
|
|
@ -192,7 +210,7 @@ impl File {
|
|||
if meta.is_file() {
|
||||
self.tx.send(FileOp::Paste(task)).await?;
|
||||
} else if meta.is_symlink() {
|
||||
self.tx.send(FileOp::Link(task.to_link(meta.len()))).await?;
|
||||
self.tx.send(FileOp::Link(task.to_link(meta))).await?;
|
||||
}
|
||||
return self.done(id);
|
||||
}
|
||||
|
|
@ -205,7 +223,7 @@ impl File {
|
|||
let dest = root.join(src.components().skip(skip).collect::<PathBuf>());
|
||||
match fs::create_dir(&dest).await {
|
||||
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;
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -214,7 +232,7 @@ impl File {
|
|||
let mut it = match fs::read_dir(&src).await {
|
||||
Ok(it) => it,
|
||||
Err(e) => {
|
||||
self.log(task.id, format!("Read dir failed: {:?}, {e}", src))?;
|
||||
self.log(task.id, format!("Read dir failed: {src:?}, {e}"))?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
|
@ -237,13 +255,24 @@ impl File {
|
|||
if meta.is_file() {
|
||||
self.tx.send(FileOp::Paste(task.clone())).await?;
|
||||
} else if meta.is_symlink() {
|
||||
self.tx.send(FileOp::Link(task.to_link(meta.len()))).await?;
|
||||
self.tx.send(FileOp::Link(task.to_link(meta))).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.done(task.id)
|
||||
}
|
||||
|
||||
pub(crate) async fn link(&self, mut task: FileOpLink) -> Result<()> {
|
||||
let id = task.id;
|
||||
if task.meta.is_none() {
|
||||
task.meta = Some(fs::symlink_metadata(&task.from).await?);
|
||||
}
|
||||
|
||||
self.sch.send(TaskOp::New(id, task.meta.as_ref().unwrap().len()))?;
|
||||
self.tx.send(FileOp::Link(task)).await?;
|
||||
self.done(id)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete(&self, mut task: FileOpDelete) -> Result<()> {
|
||||
let meta = fs::symlink_metadata(&task.target).await?;
|
||||
if !meta.is_dir() {
|
||||
|
|
@ -321,7 +350,15 @@ impl File {
|
|||
}
|
||||
|
||||
impl FileOpPaste {
|
||||
fn to_link(&self, length: u64) -> FileOpLink {
|
||||
FileOpLink { id: self.id, from: self.from.clone(), to: self.to.clone(), cut: self.cut, length }
|
||||
fn to_link(&self, meta: Metadata) -> FileOpLink {
|
||||
FileOpLink {
|
||||
id: self.id,
|
||||
from: self.from.clone(),
|
||||
to: self.to.clone(),
|
||||
meta: Some(meta),
|
||||
resolve: true,
|
||||
relative: false,
|
||||
delete: self.cut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{env, path::{Path, PathBuf}};
|
||||
use std::{borrow::Cow, env, path::{Component, Path, PathBuf}};
|
||||
|
||||
use tokio::fs;
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ pub fn readable_size(size: u64) -> String {
|
|||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
@ -65,6 +65,51 @@ pub async fn unique_path(mut p: Url) -> Url {
|
|||
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<'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();
|
||||
|
||||
// 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]
|
||||
pub fn optional_bool(s: &str) -> Option<bool> {
|
||||
if s == "true" {
|
||||
|
|
@ -75,3 +120,38 @@ pub fn optional_bool(s: &str) -> Option<bool> {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
use super::path_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)));
|
||||
}
|
||||
|
||||
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(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)));
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue