fix: track the latest file changes for the selected, yanked state when available (#791)

This commit is contained in:
三咲雅 · Misaki Masa 2024-03-08 15:37:53 +08:00 committed by GitHub
parent 33782f1224
commit d96af54574
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 370 additions and 309 deletions

View file

@ -66,6 +66,7 @@ keymap = [
{ on = [ "y" ], run = "yank", desc = "Copy the selected files" },
{ on = [ "Y" ], run = "unyank", desc = "Cancel the yank status of 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" },

View file

@ -3,9 +3,7 @@ use std::{borrow::Cow, process};
use validator::{ValidationErrors, ValidationErrorsKind};
pub fn check_validation(res: Result<(), ValidationErrors>) {
let Err(errors) = res else {
return;
};
let Err(errors) = res else { return };
for (field, kind) in errors.into_errors() {
match kind {

View file

@ -1,12 +1,13 @@
use std::{cell::RefCell, ffi::OsString};
use std::ffi::OsString;
use parking_lot::Mutex;
use yazi_shared::RoCell;
pub static CLIPBOARD: RoCell<Clipboard> = RoCell::new();
#[derive(Default)]
pub struct Clipboard {
content: RefCell<OsString>,
content: Mutex<OsString>,
}
impl Clipboard {
@ -18,11 +19,11 @@ impl Clipboard {
use yazi_shared::in_ssh_connection;
if in_ssh_connection() {
return self.content.borrow().clone();
return self.content.lock().clone();
}
let all = [
("pbpaste", &[] as &[&str]),
("pbpaste", &[][..]),
("wl-paste", &[]),
("xclip", &["-o", "-selection", "clipboard"]),
("xsel", &["-ob"]),
@ -36,7 +37,7 @@ impl Clipboard {
return OsString::from_vec(output.stdout);
}
}
self.content.borrow().clone()
self.content.lock().clone()
}
#[cfg(windows)]
@ -48,7 +49,7 @@ impl Clipboard {
return s.into();
}
self.content.borrow().clone()
self.content.lock().clone()
}
#[cfg(unix)]
@ -59,13 +60,13 @@ impl Clipboard {
use tokio::{io::AsyncWriteExt, process::Command};
use yazi_shared::in_ssh_connection;
*self.content.borrow_mut() = s.as_ref().to_owned();
*self.content.lock() = s.as_ref().to_owned();
if in_ssh_connection() {
execute!(stdout(), osc52::SetClipboard::new(s.as_ref())).ok();
}
let all = [
("pbcopy", &[] as &[&str]),
("pbcopy", &[][..]),
("wl-copy", &[]),
("xclip", &["-selection", "clipboard"]),
("xsel", &["-ib"]),
@ -80,9 +81,7 @@ impl Clipboard {
.kill_on_drop(true)
.spawn();
let Ok(mut child) = cmd else {
continue;
};
let Ok(mut child) = cmd else { continue };
let mut stdin = child.stdin.take().unwrap();
if stdin.write_all(s.as_ref().as_encoded_bytes()).await.is_err() {
@ -101,7 +100,7 @@ impl Clipboard {
use clipboard_win::{formats, set_clipboard};
let s = s.as_ref().to_owned();
*self.content.borrow_mut() = s.clone();
*self.content.lock() = s.clone();
tokio::task::spawn_blocking(move || set_clipboard(formats::Unicode, s.to_string_lossy()))
.await

View file

@ -40,9 +40,7 @@ impl Completion {
let mut dir = fs::read_dir(&parent).await?;
let mut cache = vec![];
while let Ok(Some(f)) = dir.next_entry().await {
let Ok(meta) = f.metadata().await else {
continue;
};
let Ok(meta) = f.metadata().await else { continue };
cache.push(format!(
"{}{}",

View file

@ -5,9 +5,7 @@ use crate::input::Input;
impl Input {
pub fn show(&mut self, opt: impl TryInto<InputOpt>) {
let Ok(opt) = opt.try_into() else {
return;
};
let Ok(opt) = opt.try_into() else { return };
self.close(false);
self.visible = true;

View file

@ -0,0 +1,130 @@
use std::{collections::HashMap, ffi::{OsStr, OsString}, io::{stdout, BufWriter, Write}, path::PathBuf};
use anyhow::{anyhow, Result};
use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}};
use yazi_config::{OPEN, PREVIEW};
use yazi_plugin::external::{self, ShellOpt};
use yazi_proxy::AppProxy;
use yazi_scheduler::{HIDER, WATCHER};
use yazi_shared::{fs::{accessible, max_common_root, File, FilesOp, Url}, term::Term, Defer};
use crate::manager::Manager;
impl Manager {
pub(super) fn bulk_rename(&self) {
let Some(opener) = OPEN.block_opener("bulk.txt", "text/plain") else {
return AppProxy::notify_warn("Bulk rename", "No text opener found");
};
let cwd = self.cwd().clone();
let old: Vec<_> = self.selected_or_hovered();
let root = max_common_root(&old);
let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect();
tokio::spawn(async move {
let tmp = PREVIEW.tmpfile("bulk");
let s = old.iter().map(|o| o.as_os_str()).collect::<Vec<_>>().join(OsStr::new("\n"));
OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.await?
.write_all(s.as_encoded_bytes())
.await?;
let _permit = HIDER.acquire().await.unwrap();
let _defer1 = Defer::new(AppProxy::resume);
let _defer2 = Defer::new(|| tokio::spawn(fs::remove_file(tmp.clone())));
AppProxy::stop().await;
let mut child = external::shell(ShellOpt {
cmd: (*opener.run).into(),
args: vec![OsString::new(), tmp.to_owned().into()],
piped: false,
orphan: false,
})?;
child.wait().await?;
let new: Vec<_> = fs::read_to_string(&tmp).await?.lines().map(PathBuf::from).collect();
Self::bulk_rename_do(cwd, root, old, new).await
});
}
async fn bulk_rename_do(
cwd: Url,
root: PathBuf,
old: Vec<PathBuf>,
new: Vec<PathBuf>,
) -> Result<()> {
Term::clear(&mut stdout())?;
if old.len() != new.len() {
println!("Number of old and new differ, press ENTER to exit");
stdin().read_exact(&mut [0]).await?;
return Ok(());
}
let todo: Vec<_> = old.into_iter().zip(new).filter(|(o, n)| o != n).collect();
if todo.is_empty() {
return Ok(());
}
{
let mut stdout = BufWriter::new(stdout().lock());
for (o, n) in &todo {
writeln!(stdout, "{} -> {}", o.display(), n.display())?;
}
write!(stdout, "Continue to rename? (y/N): ")?;
stdout.flush()?;
}
let mut buf = [0; 10];
_ = stdin().read(&mut buf).await?;
if buf[0] != b'y' && buf[0] != b'Y' {
return Ok(());
}
let _permit = WATCHER.acquire().await.unwrap();
let (mut failed, mut succeeded) = (Vec::new(), HashMap::with_capacity(todo.len()));
for (o, n) in todo {
let (old, new) = (root.join(&o), root.join(&n));
if accessible(&new).await {
failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = fs::rename(&old, &new).await {
failed.push((o, n, e.into()));
} else if let Ok(f) = File::from(new.into()).await {
succeeded.insert(Url::from(old), f);
} else {
failed.push((o, n, anyhow!("Failed to retrieve file info")));
}
}
if !succeeded.is_empty() {
FilesOp::Upserting(cwd, succeeded).emit();
}
drop(_permit);
if !failed.is_empty() {
Self::output_failed(failed).await?;
}
Ok(())
}
async fn output_failed(failed: Vec<(PathBuf, PathBuf, anyhow::Error)>) -> Result<()> {
Term::clear(&mut stdout())?;
{
let mut stdout = BufWriter::new(stdout().lock());
writeln!(stdout, "Failed to rename:")?;
for (o, n, e) in failed {
writeln!(stdout, "{} -> {}: {e}", o.display(), n.display())?;
}
writeln!(stdout, "\nPress ENTER to exit")?;
stdout.flush()?;
}
stdin().read_exact(&mut [0]).await?;
Ok(())
}
}

View file

@ -3,7 +3,7 @@ use std::path::PathBuf;
use tokio::fs;
use yazi_config::popup::InputCfg;
use yazi_proxy::{InputProxy, ManagerProxy};
use yazi_shared::{event::Cmd, fs::{File, FilesOp, Url}};
use yazi_shared::{event::Cmd, fs::{accessible, File, FilesOp, Url}};
use crate::manager::Manager;
@ -26,7 +26,7 @@ impl Manager {
};
let path = cwd.join(&name);
if !opt.force && fs::symlink_metadata(&path).await.is_ok() {
if !opt.force && accessible(&path).await {
match InputProxy::show(InputCfg::overwrite()).recv().await {
Some(Ok(c)) if c == "y" || c == "Y" => (),
_ => return Ok(()),

View file

@ -1,3 +1,4 @@
mod bulk_rename;
mod close;
mod create;
mod hover;

View file

@ -16,13 +16,15 @@ impl From<Cmd> for Opt {
impl Manager {
pub fn paste(&mut self, opt: impl Into<Opt>, tasks: &Tasks) {
let opt = opt.into() as Opt;
let (src, dest) = (self.yanked.iter().collect::<Vec<_>>(), self.cwd());
let dest = self.cwd();
if self.yanked.cut {
tasks.file_cut(&self.yanked, dest, opt.force);
tasks.file_cut(&src, dest, opt.force);
self.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(&src, false));
self.unyank(());
} else {
tasks.file_copy(&self.yanked, dest, opt.force, opt.follow);
tasks.file_copy(&src, dest, opt.force, opt.follow);
}
}
}

View file

@ -52,8 +52,13 @@ impl Manager {
pub fn remove_do(&mut self, opt: impl Into<Opt>, tasks: &Tasks) {
let opt = opt.into() as Opt;
self.tabs.iter_mut().for_each(|t| {
t.selected.remove_many(&opt.targets, false);
});
for u in &opt.targets {
self.active_mut().selected.remove(u);
self.yanked.remove(u);
}
tasks.file_remove(opt.targets, opt.permanently);

View file

@ -1,12 +1,11 @@
use std::{collections::HashMap, ffi::{OsStr, OsString}, io::{stdout, BufWriter, Write}, path::PathBuf};
use std::collections::HashMap;
use anyhow::{anyhow, bail, Result};
use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}};
use yazi_config::{popup::InputCfg, OPEN, PREVIEW};
use yazi_plugin::external::{self, ShellOpt};
use yazi_proxy::{AppProxy, InputProxy, ManagerProxy};
use yazi_scheduler::BLOCKER;
use yazi_shared::{event::Cmd, fs::{max_common_root, File, FilesOp, Url}, term::Term, Defer};
use anyhow::Result;
use tokio::fs;
use yazi_config::popup::InputCfg;
use yazi_proxy::{InputProxy, ManagerProxy};
use yazi_scheduler::WATCHER;
use yazi_shared::{event::Cmd, fs::{accessible, File, FilesOp, Url}};
use crate::manager::Manager;
@ -27,32 +26,6 @@ impl From<Cmd> for Opt {
}
impl Manager {
fn empty_url_part(url: &Url, by: &str) -> String {
if by == "all" {
return String::new();
}
let ext = url.extension();
match by {
"stem" => ext.map_or_else(String::new, |s| format!(".{}", s.to_string_lossy().into_owned())),
"ext" if ext.is_some() => format!("{}.", url.file_stem().unwrap().to_string_lossy()),
"dot_ext" if ext.is_some() => url.file_stem().unwrap().to_string_lossy().into_owned(),
_ => url.file_name().map_or_else(String::new, |s| s.to_string_lossy().into_owned()),
}
}
async fn rename_and_hover(old: Url, new: Url) -> Result<()> {
fs::rename(&old, &new).await?;
if old.parent() != new.parent() {
return Ok(());
}
let file = File::from(new.clone()).await?;
FilesOp::Deleting(file.parent().unwrap(), vec![new.clone()]).emit();
FilesOp::Upserting(file.parent().unwrap(), HashMap::from_iter([(old, file)])).emit();
Ok(ManagerProxy::hover(Some(new)))
}
pub fn rename(&mut self, opt: impl Into<Opt>) {
if !self.active_mut().try_escape_visual() {
return;
@ -84,117 +57,45 @@ impl Manager {
};
let new = hovered.parent().unwrap().join(name);
if opt.force || fs::symlink_metadata(&new).await.is_err() {
Self::rename_and_hover(hovered, Url::from(new)).await.ok();
if opt.force || !accessible(&new).await {
Self::rename_do(hovered, Url::from(new)).await.ok();
return;
}
let mut result = InputProxy::show(InputCfg::overwrite());
if let Some(Ok(choice)) = result.recv().await {
if choice == "y" || choice == "Y" {
Self::rename_and_hover(hovered, Url::from(new)).await.ok();
Self::rename_do(hovered, Url::from(new)).await.ok();
}
};
});
}
fn bulk_rename(&self) {
let old: Vec<_> = self.selected_or_hovered();
async fn rename_do(old: Url, new: Url) -> Result<()> {
let _permit = WATCHER.acquire().await.unwrap();
let root = max_common_root(&old);
let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect();
fs::rename(&old, &new).await?;
if old.parent() != new.parent() {
return Ok(());
}
let tmp = PREVIEW.tmpfile("bulk");
tokio::spawn(async move {
let Some(opener) = OPEN.block_opener("bulk.txt", "text/plain") else {
bail!("No opener for bulk rename");
};
{
let s = old.iter().map(|o| o.as_os_str()).collect::<Vec<_>>().join(OsStr::new("\n"));
OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.await?
.write_all(s.as_encoded_bytes())
.await?;
}
let _guard = BLOCKER.acquire().await.unwrap();
let _defer = Defer::new(|| {
AppProxy::resume();
tokio::spawn(fs::remove_file(tmp.clone()))
});
AppProxy::stop().await;
let mut child = external::shell(ShellOpt {
cmd: (*opener.run).into(),
args: vec![OsString::new(), tmp.to_owned().into()],
piped: false,
orphan: false,
})?;
child.wait().await?;
let new: Vec<_> = fs::read_to_string(&tmp).await?.lines().map(PathBuf::from).collect();
Self::bulk_rename_do(root, old, new).await
});
let file = File::from(new.clone()).await?;
FilesOp::Deleting(file.parent().unwrap(), vec![new.clone()]).emit();
FilesOp::Upserting(file.parent().unwrap(), HashMap::from_iter([(old, file)])).emit();
Ok(ManagerProxy::hover(Some(new)))
}
async fn bulk_rename_do(root: PathBuf, old: Vec<PathBuf>, new: Vec<PathBuf>) -> Result<()> {
Term::clear(&mut stdout())?;
if old.len() != new.len() {
println!("Number of old and new differ, press ENTER to exit");
stdin().read_exact(&mut [0]).await?;
return Ok(());
fn empty_url_part(url: &Url, by: &str) -> String {
if by == "all" {
return String::new();
}
let todo: Vec<_> = old.into_iter().zip(new).filter(|(o, n)| o != n).collect();
if todo.is_empty() {
return Ok(());
let ext = url.extension();
match by {
"stem" => ext.map_or_else(String::new, |s| format!(".{}", s.to_string_lossy().into_owned())),
"ext" if ext.is_some() => format!("{}.", url.file_stem().unwrap().to_string_lossy()),
"dot_ext" if ext.is_some() => url.file_stem().unwrap().to_string_lossy().into_owned(),
_ => url.file_name().map_or_else(String::new, |s| s.to_string_lossy().into_owned()),
}
{
let mut stdout = BufWriter::new(stdout().lock());
for (o, n) in &todo {
writeln!(stdout, "{} -> {}", o.display(), n.display())?;
}
write!(stdout, "Continue to rename? (y/N): ")?;
stdout.flush()?;
}
let mut buf = [0; 10];
_ = stdin().read(&mut buf).await?;
if buf[0] != b'y' && buf[0] != b'Y' {
return Ok(());
}
let mut failed = vec![];
for (o, n) in todo {
if fs::symlink_metadata(&n).await.is_ok() {
failed.push((o, n, anyhow!("Destination already exists")));
continue;
}
if let Err(e) = fs::rename(root.join(&o), root.join(&n)).await {
failed.push((o, n, e.into()));
}
}
if failed.is_empty() {
return Ok(());
}
Term::clear(&mut stdout())?;
{
let mut stdout = BufWriter::new(stdout().lock());
writeln!(stdout, "Failed to rename:")?;
for (o, n, e) in failed {
writeln!(stdout, "{} -> {}: {e}", o.display(), n.display())?;
}
writeln!(stdout, "\nPress ENTER to exit")?;
stdout.flush()?;
}
stdin().read_exact(&mut [0]).await?;
Ok(())
}
}

View file

@ -16,8 +16,33 @@ impl TryFrom<Cmd> for Opt {
}
impl Manager {
pub fn update_files(&mut self, opt: impl TryInto<Opt>, tasks: &Tasks) {
let Ok(opt) = opt.try_into() else {
return;
};
let mut ops = vec![opt.op];
for u in self.watcher.linked.read().from_dir(ops[0].url()) {
ops.push(ops[0].chroot(u));
}
for op in ops {
let idx = self.tabs.idx;
self.yanked.apply_op(&op);
for (_, tab) in self.tabs.iter_mut().enumerate().filter(|(i, _)| *i != idx) {
Self::update_tab(tab, Cow::Borrowed(&op), tasks);
}
Self::update_tab(self.active_mut(), Cow::Owned(op), tasks);
}
self.active_mut().apply_files_attrs();
}
fn update_tab(tab: &mut Tab, op: Cow<FilesOp>, tasks: &Tasks) {
let url = op.url();
tab.selected.apply_op(&op);
if tab.current.cwd == *url {
Self::update_current(tab, op, tasks);
} else if matches!(&tab.parent, Some(p) if p.cwd == *url) {
@ -92,26 +117,4 @@ impl Manager {
tab.leave(());
}
}
pub fn update_files(&mut self, opt: impl TryInto<Opt>, tasks: &Tasks) {
let Ok(opt) = opt.try_into() else {
return;
};
let mut ops = vec![opt.op];
for u in self.watcher.linked.read().from_dir(ops[0].url()) {
ops.push(ops[0].chroot(u));
}
for op in ops {
let idx = self.tabs.idx;
for (_, tab) in self.tabs.iter_mut().enumerate().filter(|(i, _)| *i != idx) {
Self::update_tab(tab, Cow::Borrowed(&op), tasks);
}
Self::update_tab(self.active_mut(), Cow::Owned(op), tasks);
}
self.active_mut().apply_files_attrs();
}
}

View file

@ -7,6 +7,7 @@ use tokio::{fs, pin, sync::mpsc::{self, UnboundedReceiver}};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use tracing::error;
use yazi_plugin::isolate;
use yazi_scheduler::WATCHER;
use yazi_shared::fs::{File, FilesOp, Url};
use super::Linked;
@ -25,9 +26,7 @@ impl Watcher {
{
let tx = tx.clone();
move |res: Result<notify::Event, notify::Error>| {
let Ok(event) = res else {
return;
};
let Ok(event) = res else { return };
match event.kind {
EventKind::Create(_) => {}
@ -143,17 +142,15 @@ impl Watcher {
async fn on_changed(rx: UnboundedReceiver<Url>) {
// TODO: revert this once a new notification is implemented
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(100, Duration::from_millis(20));
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(50));
pin!(rx);
while let Some(urls) = rx.next().await {
let urls: HashSet<_> = urls.into_iter().collect();
let _permit = WATCHER.acquire().await.unwrap();
let mut reload = Vec::with_capacity(urls.len());
for u in urls {
let Some(parent) = u.parent_url() else {
continue;
};
for u in urls.into_iter().collect::<HashSet<_>>() {
let Some(parent) = u.parent_url() else { continue };
let Ok(file) = File::from(u.clone()).await else {
FilesOp::Deleting(parent, vec![u]).emit();

View file

@ -1,6 +1,6 @@
use std::{collections::HashSet, ops::Deref};
use std::{collections::HashSet, ops::{Deref, DerefMut}};
use yazi_shared::fs::Url;
use yazi_shared::fs::{FilesOp, Url};
#[derive(Default)]
pub struct Yanked {
@ -13,3 +13,22 @@ impl Deref for Yanked {
fn deref(&self) -> &Self::Target { &self.urls }
}
impl DerefMut for Yanked {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.urls }
}
impl Yanked {
pub fn apply_op(&mut self, op: &FilesOp) {
let (removal, addition) = match op {
FilesOp::Deleting(_, urls) => (urls.iter().collect(), vec![]),
FilesOp::Updating(_, urls) | FilesOp::Upserting(_, urls) => {
urls.iter().filter(|(u, _)| self.contains(u)).map(|(u, f)| (u, f.url())).unzip()
}
_ => (vec![], vec![]),
};
self.urls.retain(|u| !removal.contains(&u));
self.urls.extend(addition);
}
}

View file

@ -36,9 +36,7 @@ impl Tab {
while let Some(result) = rx.next().await {
let done = result.is_ok();
let (Ok(s) | Err(InputError::Typed(s))) = result else {
continue;
};
let (Ok(s) | Err(InputError::Typed(s))) = result else { continue };
emit!(Call(
Cmd::args("filter_do", vec![s])

View file

@ -1,6 +1,6 @@
use yazi_plugin::external::{self, FzfOpt, ZoxideOpt};
use yazi_proxy::{AppProxy, TabProxy};
use yazi_scheduler::BLOCKER;
use yazi_scheduler::HIDER;
use yazi_shared::{event::Cmd, fs::ends_with_slash, Defer};
use crate::tab::Tab;
@ -37,7 +37,7 @@ impl Tab {
let cwd = self.current.cwd.clone();
tokio::spawn(async move {
let _guard = BLOCKER.acquire().await.unwrap();
let _permit = HIDER.acquire().await.unwrap();
let _defer = Defer::new(AppProxy::resume);
AppProxy::stop().await;

View file

@ -1,6 +1,6 @@
use std::{collections::{HashMap, HashSet}, ops::Deref};
use yazi_shared::fs::Url;
use yazi_shared::fs::{FilesOp, Url};
#[derive(Default)]
pub struct Selected {
@ -63,27 +63,27 @@ impl Selected {
#[inline]
pub fn remove(&mut self, url: &Url) -> bool { self.remove_same(&[url]) == 1 }
pub fn remove_many(&mut self, urls: &[&Url], same: bool) -> usize {
pub fn remove_many(&mut self, urls: &[impl AsRef<Url>], same: bool) -> usize {
if same {
return self.remove_same(urls);
}
let mut grouped: HashMap<_, Vec<_>> = Default::default();
for &u in urls {
if let Some(p) = u.parent_url() {
for u in urls {
if let Some(p) = u.as_ref().parent_url() {
grouped.entry(p).or_default().push(u);
}
}
grouped.into_values().map(|v| self.remove_same(&v)).sum()
}
fn remove_same(&mut self, urls: &[&Url]) -> usize {
let count = urls.iter().map(|&u| self.inner.remove(u)).filter(|&b| b).count();
fn remove_same(&mut self, urls: &[impl AsRef<Url>]) -> usize {
let count = urls.iter().map(|u| self.inner.remove(u.as_ref())).filter(|&b| b).count();
if count == 0 {
return 0;
}
let mut parent = urls[0].parent_url();
let mut parent = urls[0].as_ref().parent_url();
while let Some(u) = parent {
let n = self.parents.get_mut(&u).unwrap();
@ -101,6 +101,23 @@ impl Selected {
self.inner.clear();
self.parents.clear();
}
pub fn apply_op(&mut self, op: &FilesOp) {
let (removal, addition) = match op {
FilesOp::Deleting(_, urls) => (urls.iter().collect(), vec![]),
FilesOp::Updating(_, urls) | FilesOp::Upserting(_, urls) => {
urls.iter().filter(|(u, _)| self.contains(u)).map(|(u, f)| (u, &f.url)).unzip()
}
_ => (vec![], vec![]),
};
if !removal.is_empty() {
self.remove_many(&removal, !op.url().is_search());
}
if !addition.is_empty() {
self.add_many(&addition, !op.url().is_search());
}
}
}
#[cfg(test)]

View file

@ -4,7 +4,7 @@ use crate::tasks::Tasks;
impl Tasks {
pub fn cancel(&mut self, _: Cmd) {
let id = self.scheduler.running.lock().get_id(self.cursor);
let id = self.scheduler.ongoing.lock().get_id(self.cursor);
if id.map(|id| self.scheduler.cancel(id)) != Some(true) {
return;
}

View file

@ -3,25 +3,25 @@ use std::io::{stdout, Write};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tokio::{io::{stdin, AsyncReadExt}, select, sync::mpsc, time};
use yazi_proxy::AppProxy;
use yazi_scheduler::BLOCKER;
use yazi_scheduler::HIDER;
use yazi_shared::{event::Cmd, term::Term, Defer};
use crate::tasks::Tasks;
impl Tasks {
pub fn inspect(&self, _: Cmd) {
let Some(id) = self.scheduler.running.lock().get_id(self.cursor) else {
let Some(id) = self.scheduler.ongoing.lock().get_id(self.cursor) else {
return;
};
let scheduler = self.scheduler.clone();
tokio::spawn(async move {
let _guard = BLOCKER.acquire().await.unwrap();
let _permit = HIDER.acquire().await.unwrap();
let (tx, mut rx) = mpsc::unbounded_channel();
let buffered = {
let mut running = scheduler.running.lock();
let Some(task) = running.get_mut(id) else { return };
let mut ongoing = scheduler.ongoing.lock();
let Some(task) = ongoing.get_mut(id) else { return };
task.logger = Some(tx);
task.logs.clone()
@ -47,7 +47,7 @@ impl Tasks {
stdout.write_all(b"\r\n").ok();
}
_ = time::sleep(time::Duration::from_millis(500)) => {
if scheduler.running.lock().get(id).is_none() {
if scheduler.ongoing.lock().get(id).is_none() {
stdout().write_all(b"Task finished, press `q` to quit\r\n").ok();
break;
}
@ -61,7 +61,7 @@ impl Tasks {
}
}
if let Some(task) = scheduler.running.lock().get_mut(id) {
if let Some(task) = scheduler.ongoing.lock().get_mut(id) {
task.logger = None;
}
while answer != b'q' {

View file

@ -1,5 +1,5 @@
use serde::Serialize;
use yazi_scheduler::Running;
use yazi_scheduler::Ongoing;
#[derive(Clone, Copy, Default, Eq, PartialEq, Serialize)]
pub struct TasksProgress {
@ -11,14 +11,14 @@ pub struct TasksProgress {
pub processed: u64,
}
impl From<&Running> for TasksProgress {
fn from(running: &Running) -> Self {
impl From<&Ongoing> for TasksProgress {
fn from(ongoing: &Ongoing) -> Self {
let mut progress = Self::default();
if running.is_empty() {
if ongoing.is_empty() {
return progress;
}
for task in running.values() {
for task in ongoing.values() {
progress.total += task.total;
progress.succ += task.succ;
progress.fail += task.fail;

View file

@ -29,13 +29,13 @@ impl Tasks {
summaries: Default::default(),
};
let running = tasks.scheduler.running.clone();
let ongoing = tasks.scheduler.ongoing.clone();
tokio::spawn(async move {
let mut last = TasksProgress::default();
loop {
sleep(Duration::from_millis(500)).await;
let new = TasksProgress::from(&*running.lock());
let new = TasksProgress::from(&*ongoing.lock());
if last != new {
last = new;
emit!(Call(Cmd::new("update_progress").with_data(new), Layer::App));
@ -52,8 +52,8 @@ impl Tasks {
}
pub fn paginate(&self) -> Vec<TaskSummary> {
let running = self.scheduler.running.lock();
running.values().take(Self::limit()).map(Into::into).collect()
let ongoing = self.scheduler.ongoing.lock();
ongoing.values().take(Self::limit()).map(Into::into).collect()
}
pub fn file_open(&self, hovered: &Url, targets: &[(Url, String)]) {
@ -78,10 +78,10 @@ impl Tasks {
}
}
pub fn file_cut(&self, src: &HashSet<Url>, dest: &Url, force: bool) {
for u in src {
pub fn file_cut(&self, src: &[&Url], dest: &Url, force: bool) {
for &u in src {
let to = dest.join(u.file_name().unwrap());
if force && u == &to {
if force && *u == to {
debug!("file_cut: same file, skipping {:?}", to);
} else {
self.scheduler.file_cut(u.clone(), to, force);
@ -89,10 +89,10 @@ impl Tasks {
}
}
pub fn file_copy(&self, src: &HashSet<Url>, dest: &Url, force: bool, follow: bool) {
for u in src {
pub fn file_copy(&self, src: &[&Url], dest: &Url, force: bool, follow: bool) {
for &u in src {
let to = dest.join(u.file_name().unwrap());
if force && u == &to {
if force && *u == to {
debug!("file_copy: same file, skipping {:?}", to);
} else {
self.scheduler.file_copy(u.clone(), to, force, follow);
@ -219,5 +219,5 @@ impl Tasks {
impl Tasks {
#[inline]
pub fn len(&self) -> usize { self.scheduler.running.lock().len() }
pub fn len(&self) -> usize { self.scheduler.ongoing.lock().len() }
}

View file

@ -14,9 +14,7 @@ impl Progress {
let mut f = || {
let comp: Table = LUA.globals().raw_get("Progress")?;
for widget in comp.call_method::<_, Vec<AnyUserData>>("partial_render", ())? {
let Some(w) = cast_to_renderable(widget) else {
continue;
};
let Some(w) = cast_to_renderable(widget) else { continue };
let area = w.area();
w.render(buf);

View file

@ -44,7 +44,7 @@ impl Signals {
fn spawn_system_task(&self) -> Result<JoinHandle<()>> {
use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM};
use yazi_proxy::AppProxy;
use yazi_scheduler::BLOCKER;
use yazi_scheduler::HIDER;
let mut signals = signal_hook_tokio::Signals::new([
// Terminating signals
@ -56,7 +56,7 @@ impl Signals {
let tx = self.tx.clone();
Ok(tokio::spawn(async move {
while let Some(signal) = signals.next().await {
if BLOCKER.try_acquire().is_err() {
if HIDER.try_acquire().is_err() {
continue;
}

View file

@ -39,7 +39,7 @@ impl Widget for Which<'_> {
let chunks = {
use Constraint::*;
layout::Layout::horizontal(match cols {
1 => &[Ratio(1, 1)] as &[Constraint],
1 => &[Ratio(1, 1)][..],
2 => &[Ratio(1, 2), Ratio(1, 2)],
_ => &[Ratio(1, 3), Ratio(1, 3), Ratio(1, 3)],
})

View file

@ -1,6 +0,0 @@
use tokio::sync::Semaphore;
use yazi_shared::RoCell;
pub static BLOCKER: RoCell<Semaphore> = RoCell::new();
pub(super) fn init_blocker() { BLOCKER.init(Semaphore::new(1)) }

View file

@ -5,7 +5,7 @@ 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, path_relative_to, Url};
use yazi_shared::fs::{accessible, calculate_size, copy_with_progress, path_relative_to, Url};
use super::{FileOp, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash};
use crate::{TaskOp, TaskProg, LOW, NORMAL};
@ -107,7 +107,7 @@ impl File {
}
FileOp::Delete(task) => {
if let Err(e) = fs::remove_file(&task.target).await {
if e.kind() != NotFound && fs::symlink_metadata(&task.target).await.is_ok() {
if e.kind() != NotFound && accessible(&task.target).await {
self.fail(task.id, format!("Delete task failed: {:?}, {e}", task))?;
Err(e)?
}
@ -225,16 +225,10 @@ impl File {
let mut dirs = VecDeque::from([task.target]);
while let Some(target) = dirs.pop_front() {
let mut it = match fs::read_dir(target).await {
Ok(it) => it,
Err(_) => continue,
};
let Ok(mut it) = fs::read_dir(target).await else { continue };
while let Ok(Some(entry)) = it.next_entry().await {
let meta = match entry.metadata().await {
Ok(m) => m,
Err(_) => continue,
};
let Ok(meta) = entry.metadata().await else { continue };
if meta.is_dir() {
dirs.push_front(Url::from(entry.path()));

View file

@ -1,23 +1,23 @@
#![allow(clippy::option_map_unit_fn, clippy::unit_arg)]
mod blocker;
mod file;
mod ongoing;
mod op;
mod plugin;
mod preload;
mod process;
mod running;
mod scheduler;
mod semaphore;
mod task;
pub use blocker::*;
pub use ongoing::*;
pub use op::*;
pub use running::*;
pub use scheduler::*;
pub use semaphore::*;
pub use task::*;
const LOW: u8 = yazi_config::Priority::Low as u8;
const NORMAL: u8 = yazi_config::Priority::Normal as u8;
const HIGH: u8 = yazi_config::Priority::High as u8;
pub fn init() { init_blocker(); }
pub fn init() { init_semaphore(); }

View file

@ -7,14 +7,14 @@ use super::{Task, TaskStage};
use crate::TaskKind;
#[derive(Default)]
pub struct Running {
pub struct Ongoing {
incr: usize,
pub(super) hooks: HashMap<usize, Box<dyn (FnOnce(bool) -> BoxFuture<'static, ()>) + Send + Sync>>,
pub(super) all: HashMap<usize, Task>,
}
impl Running {
impl Ongoing {
pub fn add(&mut self, kind: TaskKind, name: String) -> usize {
self.incr += 1;
self.all.insert(self.incr, Task::new(self.incr, kind, name));

View file

@ -5,7 +5,7 @@ use yazi_proxy::AppProxy;
use yazi_shared::Defer;
use super::ProcessOpOpen;
use crate::{TaskProg, BLOCKER};
use crate::{TaskProg, HIDER};
pub struct Process {
prog: mpsc::UnboundedSender<TaskProg>,
@ -63,7 +63,7 @@ impl Process {
}
async fn open_block(&self, task: ProcessOpOpen) -> Result<()> {
let _guard = BLOCKER.acquire().await.unwrap();
let _permit = HIDER.acquire().await.unwrap();
let _defer = Defer::new(AppProxy::resume);
AppProxy::stop().await;

View file

@ -7,7 +7,7 @@ use yazi_config::{open::Opener, plugin::PluginRule, TASKS};
use yazi_plugin::ValueSendable;
use yazi_shared::{fs::{unique_path, Url}, Throttle};
use super::{Running, TaskProg, TaskStage};
use super::{Ongoing, TaskProg, TaskStage};
use crate::{file::{File, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, preload::{Preload, PreloadOpRule, PreloadOpSize}, process::{Process, ProcessOpOpen}, TaskKind, TaskOp, HIGH, LOW, NORMAL};
pub struct Scheduler {
@ -18,8 +18,7 @@ pub struct Scheduler {
micro: async_priority_channel::Sender<BoxFuture<'static, ()>, u8>,
prog: mpsc::UnboundedSender<TaskProg>,
// FIXME
pub running: Arc<Mutex<Running>>,
pub ongoing: Arc<Mutex<Ongoing>>,
}
impl Scheduler {
@ -36,7 +35,7 @@ impl Scheduler {
micro: micro_tx,
prog: prog_tx,
running: Default::default(),
ongoing: Default::default(),
};
for _ in 0..TASKS.micro_workers {
@ -69,7 +68,7 @@ impl Scheduler {
let preload = self.preload.clone();
let prog = self.prog.clone();
let running = self.running.clone();
let ongoing = self.ongoing.clone();
tokio::spawn(async move {
loop {
@ -79,7 +78,7 @@ impl Scheduler {
}
Ok((op, _)) = macro_.recv() => {
let id = op.id();
if !running.lock().exists(id) {
if !ongoing.lock().exists(id) {
continue;
}
@ -100,36 +99,36 @@ impl Scheduler {
fn progress(&self, mut rx: UnboundedReceiver<TaskProg>) {
let micro = self.micro.clone();
let running = self.running.clone();
let ongoing = self.ongoing.clone();
tokio::spawn(async move {
while let Some(op) = rx.recv().await {
match op {
TaskProg::New(id, size) => {
if let Some(task) = running.lock().get_mut(id) {
if let Some(task) = ongoing.lock().get_mut(id) {
task.total += 1;
task.found += size;
}
}
TaskProg::Adv(id, succ, processed) => {
let mut running = running.lock();
if let Some(task) = running.get_mut(id) {
let mut ongoing = ongoing.lock();
if let Some(task) = ongoing.get_mut(id) {
task.succ += succ;
task.processed += processed;
}
if succ > 0 {
if let Some(fut) = running.try_remove(id, TaskStage::Pending) {
if let Some(fut) = ongoing.try_remove(id, TaskStage::Pending) {
micro.try_send(fut, NORMAL).ok();
}
}
}
TaskProg::Succ(id) => {
if let Some(fut) = running.lock().try_remove(id, TaskStage::Dispatched) {
if let Some(fut) = ongoing.lock().try_remove(id, TaskStage::Dispatched) {
micro.try_send(fut, NORMAL).ok();
}
}
TaskProg::Fail(id, reason) => {
if let Some(task) = running.lock().get_mut(id) {
if let Some(task) = ongoing.lock().get_mut(id) {
task.fail += 1;
task.logs.push_str(&reason);
task.logs.push('\n');
@ -140,7 +139,7 @@ impl Scheduler {
}
}
TaskProg::Log(id, line) => {
if let Some(task) = running.lock().get_mut(id) {
if let Some(task) = ongoing.lock().get_mut(id) {
task.logs.push_str(&line);
task.logs.push('\n');
@ -155,29 +154,29 @@ impl Scheduler {
}
pub fn cancel(&self, id: usize) -> bool {
let mut running = self.running.lock();
let b = running.all.remove(&id).is_some();
let mut ongoing = self.ongoing.lock();
let b = ongoing.all.remove(&id).is_some();
if let Some(hook) = running.hooks.remove(&id) {
if let Some(hook) = ongoing.hooks.remove(&id) {
self.micro.try_send(hook(true), HIGH).ok();
}
b
}
pub fn file_cut(&self, from: Url, mut to: Url, force: bool) {
let mut running = self.running.lock();
let id = running.add(TaskKind::User, format!("Cut {:?} to {:?}", from, to));
let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, format!("Cut {:?} to {:?}", from, to));
running.hooks.insert(id, {
ongoing.hooks.insert(id, {
let from = from.clone();
let running = self.running.clone();
let ongoing = self.ongoing.clone();
Box::new(move |canceled: bool| {
async move {
if !canceled {
File::remove_empty_dirs(&from).await;
}
running.lock().try_remove(id, TaskStage::Hooked);
ongoing.lock().try_remove(id, TaskStage::Hooked);
}
.boxed()
})
@ -198,7 +197,7 @@ impl Scheduler {
pub fn file_copy(&self, from: Url, mut to: Url, force: bool, follow: bool) {
let name = format!("Copy {:?} to {:?}", from, to);
let id = self.running.lock().add(TaskKind::User, name);
let id = self.ongoing.lock().add(TaskKind::User, name);
let file = self.file.clone();
_ = self.micro.try_send(
@ -215,7 +214,7 @@ impl Scheduler {
pub fn file_link(&self, from: Url, mut to: Url, relative: bool, force: bool) {
let name = format!("Link {from:?} to {to:?}");
let id = self.running.lock().add(TaskKind::User, name);
let id = self.ongoing.lock().add(TaskKind::User, name);
let file = self.file.clone();
_ = self.micro.try_send(
@ -234,19 +233,19 @@ impl Scheduler {
}
pub fn file_delete(&self, target: Url) {
let mut running = self.running.lock();
let id = running.add(TaskKind::User, format!("Delete {:?}", target));
let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, format!("Delete {:?}", target));
running.hooks.insert(id, {
ongoing.hooks.insert(id, {
let target = target.clone();
let running = self.running.clone();
let ongoing = self.ongoing.clone();
Box::new(move |canceled: bool| {
async move {
if !canceled {
fs::remove_dir_all(target).await.ok();
}
running.lock().try_remove(id, TaskStage::Hooked);
ongoing.lock().try_remove(id, TaskStage::Hooked);
}
.boxed()
})
@ -264,7 +263,7 @@ impl Scheduler {
pub fn file_trash(&self, target: Url) {
let name = format!("Trash {:?}", target);
let id = self.running.lock().add(TaskKind::User, name);
let id = self.ongoing.lock().add(TaskKind::User, name);
let file = self.file.clone();
_ = self.micro.try_send(
@ -277,7 +276,7 @@ impl Scheduler {
}
pub fn plugin_micro(&self, name: String, args: Vec<ValueSendable>) {
let id = self.running.lock().add(TaskKind::User, format!("Run micro plugin `{name}`"));
let id = self.ongoing.lock().add(TaskKind::User, format!("Run micro plugin `{name}`"));
let plugin = self.plugin.clone();
_ = self.micro.try_send(
@ -290,13 +289,13 @@ impl Scheduler {
}
pub fn plugin_macro(&self, name: String, args: Vec<ValueSendable>) {
let id = self.running.lock().add(TaskKind::User, format!("Run macro plugin `{name}`"));
let id = self.ongoing.lock().add(TaskKind::User, format!("Run macro plugin `{name}`"));
self.plugin.macro_(PluginOpEntry { id, name, args }).ok();
}
pub fn preload_paged(&self, rule: &PluginRule, targets: Vec<&yazi_shared::fs::File>) {
let id = self.running.lock().add(
let id = self.ongoing.lock().add(
TaskKind::Preload,
format!("Run preloader `{}` with {} target(s)", rule.cmd.name, targets.len()),
);
@ -315,10 +314,10 @@ impl Scheduler {
pub fn preload_size(&self, targets: Vec<&Url>) {
let throttle = Arc::new(Throttle::new(targets.len(), Duration::from_millis(300)));
let mut running = self.running.lock();
let mut ongoing = self.ongoing.lock();
for target in targets {
let id = running.add(TaskKind::Preload, format!("Calculate the size of {:?}", target));
let id = ongoing.add(TaskKind::Preload, format!("Calculate the size of {:?}", target));
let target = target.clone();
let throttle = throttle.clone();
@ -340,18 +339,18 @@ impl Scheduler {
if args.is_empty() { s } else { format!("{s} with `{args}`") }
};
let mut running = self.running.lock();
let id = running.add(TaskKind::User, name);
let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, name);
let (cancel_tx, mut cancel_rx) = oneshot::channel();
running.hooks.insert(id, {
let running = self.running.clone();
ongoing.hooks.insert(id, {
let ongoing = self.ongoing.clone();
Box::new(move |canceled: bool| {
async move {
if canceled {
cancel_rx.close();
}
running.lock().try_remove(id, TaskStage::Hooked);
ongoing.lock().try_remove(id, TaskStage::Hooked);
}
.boxed()
})

View file

@ -0,0 +1,11 @@
use tokio::sync::Semaphore;
use yazi_shared::RoCell;
pub static HIDER: RoCell<Semaphore> = RoCell::new();
pub static WATCHER: RoCell<Semaphore> = RoCell::new();
pub(super) fn init_semaphore() {
HIDER.init(Semaphore::new(1));
WATCHER.init(Semaphore::new(1));
}

View file

@ -3,27 +3,26 @@ use std::{collections::VecDeque, path::{Path, PathBuf}};
use anyhow::Result;
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
pub async fn accessible(path: &Path) -> bool {
match fs::symlink_metadata(path).await {
Ok(_) => true,
Err(e) => e.kind() != io::ErrorKind::NotFound,
}
}
pub async fn calculate_size(path: &Path) -> u64 {
let mut total = 0;
let mut stack = VecDeque::from([path.to_path_buf()]);
while let Some(path) = stack.pop_front() {
let Ok(meta) = fs::symlink_metadata(&path).await else {
continue;
};
let Ok(meta) = fs::symlink_metadata(&path).await else { continue };
if !meta.is_dir() {
total += meta.len();
continue;
}
let Ok(mut it) = fs::read_dir(path).await else {
continue;
};
let Ok(mut it) = fs::read_dir(path).await else { continue };
while let Ok(Some(entry)) = it.next_entry().await {
let Ok(meta) = entry.metadata().await else {
continue;
};
let Ok(meta) = entry.metadata().await else { continue };
if meta.is_dir() {
stack.push_back(entry.path());

View file

@ -1,7 +1,6 @@
use std::{borrow::Cow, env, ffi::OsString, path::{Component, Path, PathBuf, MAIN_SEPARATOR}};
use tokio::fs;
use super::accessible;
use crate::fs::Url;
#[inline]
@ -68,7 +67,7 @@ pub async fn unique_path(mut p: Url) -> Url {
.unwrap_or_default();
let mut i = 0;
while fs::symlink_metadata(&p).await.is_ok() {
while accessible(&p).await {
i += 1;
let mut name = OsString::with_capacity(stem.len() + ext.len() + 5);