From 4a8cf212f96885c867fc70acbead6f180893db5a Mon Sep 17 00:00:00 2001 From: TD-Sky Date: Sun, 17 Sep 2023 01:31:21 +0800 Subject: [PATCH] feat(symlink): create symlinks to the yanked files on unix --- app/src/executor.rs | 11 +++++ config/preset/keymap.toml | 4 +- core/src/files/file.rs | 8 +--- core/src/files/files.rs | 6 +-- core/src/manager/finder.rs | 2 +- core/src/manager/folder.rs | 2 +- core/src/manager/manager.rs | 8 ++-- core/src/manager/tab.rs | 4 +- core/src/manager/watcher.rs | 2 +- core/src/tasks/scheduler.rs | 18 +++++++- core/src/tasks/workers/file.rs | 39 ++++++++++++++--- shared/src/fns.rs | 78 +++++++++++++++++++++++++++++++++- 12 files changed, 153 insertions(+), 29 deletions(-) diff --git a/app/src/executor.rs b/app/src/executor.rs index 51e928db..a1890d44 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -108,6 +108,17 @@ impl Executor { 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" => { let targets = cx.manager.selected().into_iter().map(|f| f.url_owned()).collect(); let force = exec.named.contains_key("force"); diff --git a/config/preset/keymap.toml b/config/preset/keymap.toml index d21b91cc..8957e041 100644 --- a/config/preset/keymap.toml +++ b/config/preset/keymap.toml @@ -49,10 +49,12 @@ keymap = [ { on = [ "" ], 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 = [ "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 = [ "k" ], exec = "paste --follow", desc = "Paste the files (follow the symlinks)" }, { 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 --permanently", desc = "Permanently delete the files" }, { on = [ "a" ], exec = "create", desc = "Create a file or directory (ends with / for directories)" }, diff --git a/core/src/files/file.rs b/core/src/files/file.rs index 3e01c076..f63d7335 100644 --- a/core/src/files/file.rs +++ b/core/src/files/file.rs @@ -42,13 +42,7 @@ impl File { pub fn url(&self) -> &Url { &self.url } #[inline] - pub fn set_url(&mut self, url: Url) { self.url = url; } - - #[inline] - pub fn url_owned(&self) -> Url { self.url.clone() } - - #[inline] - pub fn url_os_str(&self) -> &OsStr { self.url.as_os_str() } + pub fn url_mut(&mut self) -> &mut Url { &mut self.url } #[inline] pub fn name(&self) -> Option<&OsStr> { self.url.file_name() } diff --git a/core/src/files/files.rs b/core/src/files/files.rs index 2781740f..f6ec4cbf 100644 --- a/core/src/files/files.rs +++ b/core/src/files/files.rs @@ -89,7 +89,7 @@ impl Files { 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 } Some(false) => { @@ -106,7 +106,7 @@ impl Files { if self.selected.contains(&item.url) { self.selected.remove(&item.url); } else { - self.selected.insert(item.url_owned()); + self.selected.insert(item.url().to_owned()); } } !self.items.is_empty() @@ -116,7 +116,7 @@ impl Files { pub fn select_index(&mut self, indices: &BTreeSet, state: Option) -> bool { 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 { applied |= self.select(&path, state); diff --git a/core/src/manager/finder.rs b/core/src/manager/finder.rs index 01a6b85a..fb6e310b 100644 --- a/core/src/manager/finder.rs +++ b/core/src/manager/finder.rs @@ -84,7 +84,7 @@ impl Finder { continue; } - self.matched.insert(file.url_owned(), i); + self.matched.insert(file.url().to_owned(), i); if self.matched.len() > 99 { break; } diff --git a/core/src/manager/folder.rs b/core/src/manager/folder.rs index 6899d1f7..6d04b56e 100644 --- a/core/src/manager/folder.rs +++ b/core/src/manager/folder.rs @@ -114,7 +114,7 @@ impl Folder { #[inline] 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 { diff --git a/core/src/manager/manager.rs b/core/src/manager/manager.rs index 373c418e..7bc16758 100644 --- a/core/src/manager/manager.rs +++ b/core/src/manager/manager.rs @@ -84,7 +84,7 @@ impl Manager { pub fn yank(&mut self, cut: bool) -> bool { 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 } @@ -131,7 +131,7 @@ impl Manager { .into_iter() .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()), ) }) @@ -208,7 +208,7 @@ impl Manager { 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; }; @@ -333,7 +333,7 @@ impl Manager { pub fn update_read(&mut self, op: FilesOp) -> bool { let url = op.url().clone(); 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 { self.current_mut().update(op) diff --git a/core/src/manager/tab.rs b/core/src/manager/tab.rs index 5f141b7c..295e8c65 100644 --- a/core/src/manager/tab.rs +++ b/core/src/manager/tab.rs @@ -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(); diff --git a/core/src/manager/watcher.rs b/core/src/manager/watcher.rs index f4bf91b1..d7538095 100644 --- a/core/src/manager/watcher.rs +++ b/core/src/manager/watcher.rs @@ -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 diff --git a/core/src/tasks/scheduler.rs b/core/src/tasks/scheduler.rs index d48f1002..48269b9a 100644 --- a/core/src/tasks/scheduler.rs +++ b/core/src/tasks/scheduler.rs @@ -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, FileOpPaste, FileOpSymlink, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage}; use crate::emit; 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) { let mut running = self.running.write(); let id = running.add(format!("Delete {:?}", target)); diff --git a/core/src/tasks/workers/file.rs b/core/src/tasks/workers/file.rs index 52d8f77d..a0a904ff 100644 --- a/core/src/tasks/workers/file.rs +++ b/core/src/tasks/workers/file.rs @@ -20,6 +20,7 @@ pub(crate) struct File { pub(crate) enum FileOp { Paste(FileOpPaste), Link(FileOpLink), + Symlink(FileOpSymlink), Delete(FileOpDelete), Trash(FileOpTrash), } @@ -43,6 +44,13 @@ pub(crate) struct FileOpLink { pub length: u64, } +#[derive(Clone, Debug)] +pub(crate) struct FileOpSymlink { + pub id: usize, + pub from: Url, + pub to: Url, +} + #[derive(Clone, Debug)] pub(crate) struct FileOpDelete { pub id: usize, @@ -68,6 +76,7 @@ impl File { Ok(match self.rx.recv().await? { FileOp::Paste(t) => (t.id, FileOp::Paste(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::Trash(t) => (t.id, FileOp::Trash(t)), }) @@ -113,6 +122,17 @@ impl File { } 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) => { let src = match fs::read_link(&task.from).await { Ok(src) => src, @@ -126,14 +146,14 @@ impl File { match fs::remove_file(&task.to).await { Err(e) if e.kind() != NotFound => Err(e)?, _ => { + #[cfg(unix)] + { + fs::symlink(src, &task.to).await? + } #[cfg(target_os = "windows")] { 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::()); 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 +234,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; } }; @@ -244,6 +264,13 @@ impl File { 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<()> { let meta = fs::symlink_metadata(&task.target).await?; if !meta.is_dir() { diff --git a/shared/src/fns.rs b/shared/src/fns.rs index 5b915a98..21e4ef43 100644 --- a/shared/src/fns.rs +++ b/shared/src/fns.rs @@ -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,52 @@ 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>(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] pub fn optional_bool(s: &str) -> Option { if s == "true" { @@ -75,3 +121,31 @@ pub fn optional_bool(s: &str) -> Option { 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))); + } +}