diff --git a/cspell.json b/cspell.json index b9709634e..cc37ffa45 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","nlink"],"language":"en","version":"0.2","flagWords":[]} \ No newline at end of file +{"flagWords":[],"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking"],"version":"0.2"} \ No newline at end of file diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index 9d9505bbb..daa6b442b 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -59,30 +59,30 @@ keymap = [ { on = "", 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 = "", run = "open", desc = "Open the selected files" }, - { on = "", 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 = "", run = "open", desc = "Open selected files" }, + { on = "", 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 = "", 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" }, @@ -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" }, @@ -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" }, @@ -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" }, @@ -155,10 +155,10 @@ keymap = [ [tasks] keymap = [ - { on = "", run = "close", desc = "Hide the task manager" }, - { on = "", run = "close", desc = "Hide the task manager" }, - { on = "", run = "close", desc = "Hide the task manager" }, - { on = "w", run = "close", desc = "Hide the task manager" }, + { on = "", run = "close", desc = "Close task manager" }, + { on = "", run = "close", desc = "Close task manager" }, + { on = "", 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" }, diff --git a/yazi-core/src/manager/commands/hardlink.rs b/yazi-core/src/manager/commands/hardlink.rs new file mode 100644 index 000000000..c215deeb2 --- /dev/null +++ b/yazi-core/src/manager/commands/hardlink.rs @@ -0,0 +1,23 @@ +use yazi_shared::event::Cmd; + +use crate::{manager::Manager, tasks::Tasks}; + +pub struct Opt { + force: bool, + follow: bool, +} + +impl From for Opt { + fn from(c: Cmd) -> Self { Self { force: c.bool("force"), follow: c.bool("follow") } } +} + +impl Manager { + pub fn hardlink(&mut self, opt: impl Into, tasks: &Tasks) { + if self.yanked.cut { + return; + } + + let opt = opt.into() as Opt; + tasks.file_hardlink(&self.yanked, self.cwd(), opt.force, opt.follow); + } +} diff --git a/yazi-core/src/manager/commands/mod.rs b/yazi-core/src/manager/commands/mod.rs index 9827366ff..d690ce0cb 100644 --- a/yazi-core/src/manager/commands/mod.rs +++ b/yazi-core/src/manager/commands/mod.rs @@ -1,6 +1,7 @@ mod bulk_rename; mod close; mod create; +mod hardlink; mod hover; mod link; mod open; diff --git a/yazi-core/src/tasks/file.rs b/yazi-core/src/tasks/file.rs index 7e2e2fe38..c09068d5d 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -39,6 +39,17 @@ impl Tasks { } } + pub fn file_hardlink(&self, src: &HashSet, 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, follow); + } + } + } + pub fn file_remove(&self, targets: Vec, permanently: bool) { for u in targets { if permanently { diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index e34d4a71e..dcc9f89b2 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -99,6 +99,7 @@ impl<'a> Executor<'a> { on!(MANAGER, unyank); on!(MANAGER, paste, &self.app.cx.tasks); on!(MANAGER, link, &self.app.cx.tasks); + on!(MANAGER, hardlink, &self.app.cx.tasks); on!(MANAGER, remove, &self.app.cx.tasks); on!(MANAGER, remove_do, &self.app.cx.tasks); on!(MANAGER, create); diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index a68f0ac06..0373a59c5 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -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 { @@ -39,7 +38,7 @@ impl File { } Ok(n) => self.prog.send(TaskProg::Adv(task.id, 0, n))?, Err(e) if e.kind() == NotFound => { - warn!("Paste task partially done: {:?}", task); + warn!("Paste task partially done: {task:?}"); break; } // Operation not permitted (os error 1) @@ -65,7 +64,7 @@ impl File { 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))?; + warn!("Link task partially done: {task:?}"); return Ok(self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?); } Err(e) => Err(e)?, @@ -83,14 +82,14 @@ impl File { ok_or_not_found(fs::remove_file(&task.to).await)?; #[cfg(unix)] { - fs::symlink(src, &task.to).await? + fs::symlink(src, &task.to).await?; } #[cfg(windows)] { if meta.is_dir() { - fs::symlink_dir(src, &task.to).await? + fs::symlink_dir(src, &task.to).await?; } else { - fs::symlink_file(src, &task.to).await? + fs::symlink_file(src, &task.to).await?; } } @@ -99,6 +98,26 @@ 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 if let Ok(p) = fs::canonicalize(&task.from).await { + Cow::Owned(p) + } else { + Cow::Borrowed(task.from.as_path()) + }; + + ok_or_not_found(fs::remove_file(&task.to).await)?; + match fs::hard_link(src, &task.to).await { + Err(e) if e.kind() == NotFound => { + warn!("Hardlink task partially done: {task:?}"); + } + v => v?, + } + + 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 { @@ -206,6 +225,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 hardlinking: {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::()); + 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() { @@ -246,6 +320,7 @@ impl File { self.succ(id) } + #[inline] async fn metadata(path: &Path, follow: bool) -> io::Result { if !follow { return fs::symlink_metadata(path).await; @@ -255,24 +330,18 @@ 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, - }; + pub(crate) async fn remove_empty_dirs(dir: &Path) { + let Ok(mut it) = fs::read_dir(dir).await else { 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(); - } + 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(); + Box::pin(Self::remove_empty_dirs(&path)).await; + fs::remove_dir(path).await.ok(); } - - fs::remove_dir(dir).await.ok(); } - .boxed() + + fs::remove_dir(dir).await.ok(); } } diff --git a/yazi-scheduler/src/file/op.rs b/yazi-scheduler/src/file/op.rs index 3eabec69e..7f898fceb 100644 --- a/yazi-scheduler/src/file/op.rs +++ b/yazi-scheduler/src/file/op.rs @@ -6,6 +6,7 @@ use yazi_shared::fs::Url; pub enum FileOp { Paste(FileOpPaste), Link(FileOpLink), + Hardlink(FileOpHardlink), Delete(FileOpDelete), Trash(FileOpTrash), } @@ -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, @@ -46,6 +49,7 @@ impl FileOpPaste { } } +// --- Link #[derive(Clone, Debug)] pub struct FileOpLink { pub id: usize, @@ -71,6 +75,23 @@ impl From for FileOpLink { } } +// --- Hardlink +#[derive(Clone, Debug)] +pub struct FileOpHardlink { + pub id: usize, + pub from: Url, + pub to: Url, + pub meta: Option, + 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, @@ -78,6 +99,7 @@ pub struct FileOpDelete { pub length: u64, } +// --- Trash #[derive(Clone, Debug)] pub struct FileOpTrash { pub id: usize, diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 8159be84b..a4137958c 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -9,7 +9,7 @@ use yazi_dds::Pump; use yazi_shared::{event::Data, fs::{unique_path, Url}, Throttle}; use super::{Ongoing, TaskProg, TaskStage}; -use crate::{file::{File, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, prework::{Prework, PreworkOpFetch, PreworkOpLoad, PreworkOpSize}, process::{Process, ProcessOpBg, ProcessOpBlock, ProcessOpOrphan}, TaskKind, TaskOp, HIGH, LOW, NORMAL}; +use crate::{file::{File, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, prework::{Prework, PreworkOpFetch, PreworkOpLoad, PreworkOpSize}, process::{Process, ProcessOpBg, ProcessOpBlock, ProcessOpOrphan}, TaskKind, TaskOp, HIGH, LOW, NORMAL}; pub struct Scheduler { pub file: Arc, @@ -154,6 +154,28 @@ impl Scheduler { ); } + pub fn file_hardlink(&self, from: Url, mut to: Url, force: bool, follow: bool) { + let name = format!("Hardlink {:?} to {:?}", from, to); + let id = self.ongoing.lock().add(TaskKind::User, name); + + if to.starts_with(&from) && to != from { + self.new_and_fail(id, "Cannot hardlink directory into itself").ok(); + return; + } + + let file = self.file.clone(); + _ = self.micro.try_send( + async move { + if !force { + to = unique_path(to).await; + } + file.hardlink(FileOpHardlink { id, from, to, meta: None, follow }).await.ok(); + } + .boxed(), + LOW, + ); + } + pub fn file_delete(&self, target: Url) { let mut ongoing = self.ongoing.lock(); let id = ongoing.add(TaskKind::User, format!("Delete {:?}", target)); @@ -368,7 +390,7 @@ impl Scheduler { }; if let Err(e) = result { - prog.send(TaskProg::Fail(id, format!("Failed to work on this task: {:?}", e))).ok(); + prog.send(TaskProg::Fail(id, format!("Failed to work on this task: {e:?}"))).ok(); } } }