Skip to content

Commit

Permalink
Done
Browse files Browse the repository at this point in the history
  • Loading branch information
sxyazi committed Jul 10, 2024
1 parent 8c28db2 commit e19134f
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 74 deletions.
64 changes: 32 additions & 32 deletions yazi-config/preset/keymap.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,30 @@ keymap = [
{ on = "<C-r>", run = "select_all --state=none", desc = "Inverse selection of all files" },

# Operation
{ on = "o", run = "open", desc = "Open the selected files" },
{ on = "O", run = "open --interactive", desc = "Open the selected files interactively" },
{ on = "<Enter>", run = "open", desc = "Open the selected files" },
{ on = "<S-Enter>", run = "open --interactive", desc = "Open the selected files interactively" },
{ on = "y", run = "yank", desc = "Copy the selected files" },
{ on = "Y", run = "unyank", desc = "Cancel the yank status of files" },
{ on = "o", run = "open", desc = "Open selected files" },
{ on = "O", run = "open --interactive", desc = "Open selected files interactively" },
{ on = "<Enter>", run = "open", desc = "Open selected files" },
{ on = "<S-Enter>", run = "open --interactive", desc = "Open selected files interactively" },
{ on = "y", run = "yank", desc = "Copy selected files" },
{ on = "x", run = "yank --cut", desc = "Cut the selected files" },
{ on = "X", run = "unyank", desc = "Cancel the yank status of files" },
{ on = "p", run = "paste", desc = "Paste the files" },
{ on = "P", run = "paste --force", desc = "Paste the files (overwrite if the destination exists)" },
{ on = "-", run = "link", desc = "Symlink the absolute path of files" },
{ on = "_", run = "link --relative", desc = "Symlink the relative path of files" },
{ on = "d", run = "remove", desc = "Move the files to the trash" },
{ on = "D", run = "remove --permanently", desc = "Permanently delete the files" },
{ on = "a", run = "create", desc = "Create a file or directory (ends with / for directories)" },
{ on = "r", run = "rename --cursor=before_ext", desc = "Rename a file or directory" },
{ on = "Y", run = "unyank", desc = "Cancel the yank status" },
{ on = "X", run = "unyank", desc = "Cancel the yank status" },
{ on = "p", run = "paste", desc = "Paste yanked files" },
{ on = "P", run = "paste --force", desc = "Paste yanked files (overwrite if the destination exists)" },
{ on = "-", run = "link", desc = "Symlink the absolute path of yanked files" },
{ on = "_", run = "link --relative", desc = "Symlink the relative path of yanked files" },
{ on = "d", run = "remove", desc = "Trash selected files" },
{ on = "D", run = "remove --permanently", desc = "Permanently delete selected files" },
{ on = "a", run = "create", desc = "Create a file (ends with / for directories)" },
{ on = "r", run = "rename --cursor=before_ext", desc = "Rename selected file(s)" },
{ on = ";", run = "shell --interactive", desc = "Run a shell command" },
{ on = ":", run = "shell --block --interactive", desc = "Run a shell command (block the UI until the command finishes)" },
{ on = ":", run = "shell --block --interactive", desc = "Run a shell command (block until finishes)" },
{ on = ".", run = "hidden toggle", desc = "Toggle the visibility of hidden files" },
{ on = "s", run = "search fd", desc = "Search files by name using fd" },
{ on = "S", run = "search rg", desc = "Search files by content using ripgrep" },
{ on = "<C-s>", run = "search none", desc = "Cancel the ongoing search" },
{ on = "z", run = "plugin zoxide", desc = "Jump to a directory using zoxide" },
{ on = "Z", run = "plugin fzf", desc = "Jump to a directory, or reveal a file using fzf" },
{ on = "Z", run = "plugin fzf", desc = "Jump to a directory or reveal a file using fzf" },

# Linemode
{ on = [ "m", "s" ], run = "linemode size", desc = "Set linemode to size" },
Expand All @@ -92,19 +92,19 @@ keymap = [
{ on = [ "m", "n" ], run = "linemode none", desc = "Set linemode to none" },

# Copy
{ on = [ "c", "c" ], run = "copy path", desc = "Copy the absolute path" },
{ on = [ "c", "d" ], run = "copy dirname", desc = "Copy the path of the parent directory" },
{ on = [ "c", "f" ], run = "copy filename", desc = "Copy the name of the file" },
{ on = [ "c", "n" ], run = "copy name_without_ext", desc = "Copy the name of the file without the extension" },
{ on = [ "c", "c" ], run = "copy path", desc = "Copy the file path" },
{ on = [ "c", "d" ], run = "copy dirname", desc = "Copy the directory path" },
{ on = [ "c", "f" ], run = "copy filename", desc = "Copy the filename" },
{ on = [ "c", "n" ], run = "copy name_without_ext", desc = "Copy the filename without extension" },

# Filter
{ on = "f", run = "filter --smart", desc = "Filter the files" },
{ on = "f", run = "filter --smart", desc = "Filter files" },

# Find
{ on = "/", run = "find --smart", desc = "Find next file" },
{ on = "?", run = "find --previous --smart", desc = "Find previous file" },
{ on = "n", run = "find_arrow", desc = "Go to next found file" },
{ on = "N", run = "find_arrow --previous", desc = "Go to previous found file" },
{ on = "n", run = "find_arrow", desc = "Go to the next found" },
{ on = "N", run = "find_arrow --previous", desc = "Go to the previous found" },

# Sorting
{ on = [ ",", "m" ], run = "sort modified --reverse=no", desc = "Sort by modified time" },
Expand All @@ -121,7 +121,7 @@ keymap = [
{ on = [ ",", "S" ], run = "sort size --reverse", desc = "Sort by size (reverse)" },

# Tabs
{ on = "t", run = "tab_create --current", desc = "Create a new tab using the current path" },
{ on = "t", run = "tab_create --current", desc = "Create a new tab with CWD" },

{ on = "1", run = "tab_switch 0", desc = "Switch to the first tab" },
{ on = "2", run = "tab_switch 1", desc = "Switch to the second tab" },
Expand All @@ -136,11 +136,11 @@ keymap = [
{ on = "[", run = "tab_switch -1 --relative", desc = "Switch to the previous tab" },
{ on = "]", run = "tab_switch 1 --relative", desc = "Switch to the next tab" },

{ on = "{", run = "tab_swap -1", desc = "Swap the current tab with the previous tab" },
{ on = "}", run = "tab_swap 1", desc = "Swap the current tab with the next tab" },
{ on = "{", run = "tab_swap -1", desc = "Swap current tab with previous tab" },
{ on = "}", run = "tab_swap 1", desc = "Swap current tab with next tab" },

# Tasks
{ on = "w", run = "tasks_show", desc = "Show the tasks manager" },
{ on = "w", run = "tasks_show", desc = "Show task manager" },

# Goto
{ on = [ "g", "h" ], run = "cd ~", desc = "Go to the home directory" },
Expand All @@ -155,10 +155,10 @@ keymap = [
[tasks]

keymap = [
{ on = "<Esc>", run = "close", desc = "Hide the task manager" },
{ on = "<C-[>", run = "close", desc = "Hide the task manager" },
{ on = "<C-c>", run = "close", desc = "Hide the task manager" },
{ on = "w", run = "close", desc = "Hide the task manager" },
{ on = "<Esc>", run = "close", desc = "Close task manager" },
{ on = "<C-[>", run = "close", desc = "Close task manager" },
{ on = "<C-c>", run = "close", desc = "Close task manager" },
{ on = "w", run = "close", desc = "Close task manager" },

{ on = "k", run = "arrow -1", desc = "Move cursor up" },
{ on = "j", run = "arrow 1", desc = "Move cursor down" },
Expand Down
7 changes: 4 additions & 3 deletions yazi-core/src/manager/commands/hardlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use yazi_shared::event::Cmd;
use crate::{manager::Manager, tasks::Tasks};

pub struct Opt {
force: bool,
force: bool,
follow: bool,
}

impl From<Cmd> for Opt {
fn from(c: Cmd) -> Self { Self { force: c.bool("force") } }
fn from(c: Cmd) -> Self { Self { force: c.bool("force"), follow: c.bool("follow") } }
}

impl Manager {
Expand All @@ -17,6 +18,6 @@ impl Manager {
}

let opt = opt.into() as Opt;
tasks.file_hardlink(&self.yanked, self.cwd(), opt.force);
tasks.file_hardlink(&self.yanked, self.cwd(), opt.force, opt.follow);
}
}
4 changes: 2 additions & 2 deletions yazi-core/src/tasks/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ impl Tasks {
}
}

pub fn file_hardlink(&self, src: &HashSet<Url>, dest: &Url, force: bool) {
pub fn file_hardlink(&self, src: &HashSet<Url>, dest: &Url, force: bool, follow: bool) {
for u in src {
let to = dest.join(u.file_name().unwrap());
if force && *u == to {
debug!("file_hardlink: same file, skipping {:?}", to);
} else {
self.scheduler.file_hardlink(u.clone(), to, force);
self.scheduler.file_hardlink(u.clone(), to, force, follow);
}
}
}
Expand Down
123 changes: 88 additions & 35 deletions yazi-scheduler/src/file/file.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use std::{borrow::Cow, collections::VecDeque, fs::Metadata, path::{Path, PathBuf}};

use anyhow::{anyhow, Result};
use futures::{future::BoxFuture, FutureExt};
use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::warn;
use yazi_config::TASKS;
use yazi_shared::fs::{calculate_size, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to, Url};

use super::{FileOp, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash};
use super::{FileOp, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash};
use crate::{TaskOp, TaskProg, LOW, NORMAL};

pub struct File {
Expand Down Expand Up @@ -99,6 +98,25 @@ impl File {
}
self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?;
}
FileOp::Hardlink(task) => {
let meta = task.meta.as_ref().unwrap();
let src = if !task.follow {
Cow::Borrowed(task.from.as_path())
} else {
match fs::read_link(&task.from).await {
Ok(p) => Cow::Owned(p),
Err(e) if e.kind() == NotFound => {
self.log(task.id, format!("Hardlink task partially done: {:?}", task))?;
return Ok(self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?);
}
Err(_) => Cow::Borrowed(task.from.as_path()),
}
};

ok_or_not_found(fs::remove_file(&task.to).await)?;
fs::hard_link(src, &task.to).await?;
self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?;
}
FileOp::Delete(task) => {
if let Err(e) = fs::remove_file(&task.target).await {
if e.kind() != NotFound && maybe_exists(&task.target).await {
Expand Down Expand Up @@ -206,6 +224,61 @@ impl File {
self.succ(id)
}

pub async fn hardlink(&self, mut task: FileOpHardlink) -> Result<()> {
if task.meta.is_none() {
task.meta = Some(Self::metadata(&task.from, task.follow).await?);
}

let meta = task.meta.as_ref().unwrap();
if !meta.is_dir() {
let id = task.id;
self.prog.send(TaskProg::New(id, meta.len()))?;
self.queue(FileOp::Hardlink(task), NORMAL).await?;
return self.succ(id);
}

macro_rules! continue_unless_ok {
($result:expr) => {
match $result {
Ok(v) => v,
Err(e) => {
self.prog.send(TaskProg::New(task.id, 0))?;
self.fail(task.id, format!("An error occurred while hard-linking: {e}"))?;
continue;
}
}
};
}

let root = &task.to;
let skip = task.from.components().count();
let mut dirs = VecDeque::from([task.from.clone()]);

while let Some(src) = dirs.pop_front() {
let dest = root.join(src.components().skip(skip).collect::<PathBuf>());
continue_unless_ok!(match fs::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => Err(e),
_ => Ok(()),
});

let mut it = continue_unless_ok!(fs::read_dir(&src).await);
while let Ok(Some(entry)) = it.next_entry().await {
let from = Url::from(entry.path());
let meta = continue_unless_ok!(Self::metadata(&from, task.follow).await);

if meta.is_dir() {
dirs.push_back(from);
continue;
}

let to = dest.join(from.file_name().unwrap());
self.prog.send(TaskProg::New(task.id, meta.len()))?;
self.queue(FileOp::Hardlink(task.spawn(from, to, meta)), NORMAL).await?;
}
}
self.succ(task.id)
}

pub async fn delete(&self, mut task: FileOpDelete) -> Result<()> {
let meta = fs::symlink_metadata(&task.target).await?;
if !meta.is_dir() {
Expand Down Expand Up @@ -246,6 +319,7 @@ impl File {
self.succ(id)
}

#[inline]
async fn metadata(path: &Path, follow: bool) -> io::Result<Metadata> {
if !follow {
return fs::symlink_metadata(path).await;
Expand All @@ -255,43 +329,22 @@ impl File {
if meta.is_ok() { meta } else { fs::symlink_metadata(path).await }
}

pub(crate) fn remove_empty_dirs(dir: &Path) -> BoxFuture<()> {
async move {
let mut it = match fs::read_dir(dir).await {
Ok(it) => it,
Err(_) => return,
};

while let Ok(Some(entry)) = it.next_entry().await {
if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
let path = entry.path();
Self::remove_empty_dirs(&path).await;
fs::remove_dir(path).await.ok();
}
pub(crate) async fn remove_empty_dirs(dir: &Path) {
let mut it = match fs::read_dir(dir).await {
Ok(it) => it,
Err(_) => return,
};

while let Ok(Some(entry)) = it.next_entry().await {
if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
let path = entry.path();
Box::pin(Self::remove_empty_dirs(&path)).await;
fs::remove_dir(path).await.ok();
}

fs::remove_dir(dir).await.ok();
}
.boxed()
}
}

async fn hard_link_directory(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
fs::create_dir(&dst).await?;

let mut directory = fs::read_dir(src).await?;

while let Some(entry) = directory.next_entry().await? {
let target_path = dst.as_ref().join(entry.file_name());

if entry.file_type().await?.is_dir() {
Box::pin(hard_link_directory(entry.path(), target_path)).await?;
} else {
fs::hard_link(entry.path(), target_path).await?;
}
fs::remove_dir(dir).await.ok();
}

Ok(())
}

impl File {
Expand Down
22 changes: 22 additions & 0 deletions yazi-scheduler/src/file/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use yazi_shared::fs::Url;
pub enum FileOp {
Paste(FileOpPaste),
Link(FileOpLink),
Hardlink(FileOpHardlink),
Delete(FileOpDelete),
Trash(FileOpTrash),
}
Expand All @@ -15,12 +16,14 @@ impl FileOp {
match self {
Self::Paste(op) => op.id,
Self::Link(op) => op.id,
Self::Hardlink(op) => op.id,
Self::Delete(op) => op.id,
Self::Trash(op) => op.id,
}
}
}

// --- Paste
#[derive(Clone, Debug)]
pub struct FileOpPaste {
pub id: usize,
Expand All @@ -46,6 +49,7 @@ impl FileOpPaste {
}
}

// --- Link
#[derive(Clone, Debug)]
pub struct FileOpLink {
pub id: usize,
Expand All @@ -71,13 +75,31 @@ impl From<FileOpPaste> for FileOpLink {
}
}

// --- Hardlink
#[derive(Clone, Debug)]
pub struct FileOpHardlink {
pub id: usize,
pub from: Url,
pub to: Url,
pub meta: Option<Metadata>,
pub follow: bool,
}

impl FileOpHardlink {
pub(super) fn spawn(&self, from: Url, to: Url, meta: Metadata) -> Self {
Self { id: self.id, from, to, meta: Some(meta), follow: self.follow }
}
}

// --- Delete
#[derive(Clone, Debug)]
pub struct FileOpDelete {
pub id: usize,
pub target: Url,
pub length: u64,
}

// --- Trash
#[derive(Clone, Debug)]
pub struct FileOpTrash {
pub id: usize,
Expand Down
Loading

0 comments on commit e19134f

Please sign in to comment.