diff --git a/yazi-actor/src/mgr/bulk_create.rs b/yazi-actor/src/mgr/bulk_create.rs index 920c4e4a..f3d67491 100644 --- a/yazi-actor/src/mgr/bulk_create.rs +++ b/yazi-actor/src/mgr/bulk_create.rs @@ -1,18 +1,19 @@ -use std::{io::{Read, Write}, path::Path, sync::Arc}; +use std::{fmt::{self, Display}, io::{self, Read, Write}, path::{MAIN_SEPARATOR, Path}, sync::Arc}; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Result, anyhow}; use scopeguard::defer; use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRule}; -use yazi_fs::{File, FilesOp, Splatter, provider::{FileBuilder, Provider, local::{Gate, Local}}}; -use yazi_macro::{ok_or_not_found, succ}; +use yazi_fs::{File, FilesOp, Splatter, provider::{Provider, local::Local}}; +use yazi_macro::succ; use yazi_parser::VoidForm; -use yazi_proxy::{MgrProxy, TasksProxy}; +use yazi_proxy::TasksProxy; use yazi_scheduler::{AppProxy, NotifyProxy}; -use yazi_shared::{data::Data, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{data::Data, strand::Strand, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; +use yazi_shim::path::CROSS_SEPARATOR; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::TTY; -use yazi_vfs::{VfsFile, maybe_exists, provider}; +use yazi_vfs::{VfsFile, provider}; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -26,72 +27,79 @@ impl Actor for BulkCreate { let Some(opener) = Self::opener() else { succ!(NotifyProxy::push_warn("Bulk create", "No text opener found")); }; + let cwd = cx.cwd().clone(); tokio::spawn(async move { - let tmp = YAZI.preview.tmpfile("bulk"); - _ = Gate::default().write(true).create_new(true).open(&tmp).await?; + let tmp = YAZI.preview.tmpfile("bulk-create"); + provider::create_new(&tmp).await?; + defer! { let tmp = tmp.clone(); tokio::spawn(async move { Local::regular(&tmp).remove_file().await }); } + TasksProxy::process_exec( - cwd.clone().into(), + cwd.clone(), Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run), vec![UrlCow::default(), UrlBuf::from(&tmp).into()], opener.block, opener.orphan, ) .await; + let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume()); AppProxy::stop().await; - let todo: Vec<_> = - Local::regular(&tmp).read_to_string().await?.lines().filter_map(Entry::parse).collect(); - Self::r#do(cwd, todo).await + + let content = Local::regular(&tmp).read_to_string().await?; + Self::r#do(cwd, content.lines().filter_map(Entry::parse).collect()).await }); succ!() } } + impl BulkCreate { - async fn r#do(cwd: UrlBuf, todo: Vec) -> Result<()> { + async fn r#do(cwd: UrlBuf, todo: Vec>) -> Result<()> { terminal_clear(TTY.writer())?; if todo.is_empty() { return Ok(()); + } else if !Self::ask_continue(&todo, None)? { + return Ok(()); // TODO: support `bulk_exit`? } - { - let mut w = TTY.lockout(); - for entry in &todo { - writeln!(w, "{}", entry.name)?; - } - write!(w, "Continue to create? (y/N): ")?; - w.flush()?; - } - let mut buf = [0; 10]; - _ = TTY.reader().read(&mut buf)?; - if buf[0] != b'y' && buf[0] != b'Y' { - return Ok(()); - } + let _permit = WATCHER.acquire().await.unwrap(); - let mut failed = Vec::new(); - let mut reveal = None; + let (mut failed, mut succeeded) = (vec![], Vec::with_capacity(todo.len())); for entry in todo { - let Ok(new) = cwd.try_join(&entry.name) else { + let Ok(dist) = cwd.try_join(entry.path) else { failed.push((entry, anyhow!("Invalid path"))); continue; }; - if maybe_exists(&new).await { - failed.push((entry, anyhow!("Destination already exists"))); - continue; - } - match Self::create_one(&new, entry.dir).await { - Ok(real) => reveal = Some(real), - Err(e) => failed.push((entry, e)), + + let result: io::Result<()> = if entry.is_dir { + provider::create_dir_all(&dist).await + } else if let Some(parent) = dist.parent() { + provider::create_dir_all(parent).await.ok(); + provider::create_new(&dist).await.map(|_| ()) + } else { + Err(io::Error::other("No parent directory")) + }; + + if let Err(e) = result { + failed.push((entry, e.into())); + } else if let Ok(f) = File::new(dist).await { + succeeded.push(f); + } else { + failed.push((entry, anyhow!("Failed to retrieve file info"))); } } - if let Some(url) = reveal { - MgrProxy::reveal(&url); + + if !succeeded.is_empty() { + // err!(Pubsub::pub_after_bulk_create(it)); // FIXME + FilesOp::create(succeeded); } + drop(_permit); + if !failed.is_empty() { Self::output_failed(failed).await?; } @@ -105,56 +113,64 @@ impl BulkCreate { .and_then(|r| YAZI.opener.block(&r)) } - async fn create_one(new: &UrlBuf, dir: bool) -> Result { - if dir { - provider::create_dir_all(new).await?; - } else if let Ok(real) = provider::casefold(new).await - && let Some((parent, urn)) = real.pair() - { - ok_or_not_found!(provider::remove_file(new).await); - FilesOp::Deleting(parent.into(), [urn.into()].into()).emit(); - provider::create(new).await?; - } else if let Some(parent) = new.parent() { - provider::create_dir_all(parent).await.ok(); - ok_or_not_found!(provider::remove_file(new).await); - provider::create(new).await?; - } else { - bail!("Cannot create file at root"); + fn ask_continue(todo: &[Entry], decision: Option) -> Result { + if let Some(decision) = decision { + return Ok(decision); } - if let Ok(real) = provider::casefold(new).await - && let Some((parent, urn)) = real.pair() + { - let file = File::new(&real).await?; - FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()).emit(); - Ok(real) - } else { - bail!("Failed to retrieve file info"); + let mut w = TTY.lockout(); + for entry in todo { + writeln!(w, "{entry}")?; + } + write!(w, "Continue to create? (y/N): ")?; + w.flush()?; } + + let mut buf = [0; 10]; + _ = TTY.reader().read(&mut buf)?; + Ok(buf[0] == b'y' || buf[0] == b'Y') } - async fn output_failed(failed: Vec<(Entry, anyhow::Error)>) -> Result<()> { + async fn output_failed(failed: Vec<(Entry<'_>, anyhow::Error)>) -> Result<()> { let mut stdout = TTY.lockout(); terminal_clear(&mut *stdout)?; + writeln!(stdout, "Failed to create:")?; for (entry, err) in failed { - writeln!(stdout, "{}: {err}", entry.name)?; + writeln!(stdout, "{entry}: {err}")?; } writeln!(stdout, "\nPress ENTER to exit")?; + stdout.flush()?; TTY.reader().read_exact(&mut [0])?; Ok(()) } } -struct Entry { - name: String, - dir: bool, + +// --- Entry +struct Entry<'a> { + path: Strand<'a>, + is_dir: bool, } -impl Entry { - fn parse(s: &str) -> Option { - let s = s.trim(); - if s.is_empty() { - return None; - } - Some(Self { dir: s.ends_with('/') || s.ends_with('\\'), name: s.to_owned() }) + +impl<'a> Entry<'a> { + fn parse(s: &'a str) -> Option { + let (path, is_dir) = match s.strip_suffix(CROSS_SEPARATOR) { + Some(p) => (p, true), + None => (s, false), + }; + + Some(Self { path: path.into(), is_dir }).filter(|_| !path.is_empty()) + } +} + +impl Display for Entry<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_dir { + write!(f, "{}{MAIN_SEPARATOR}", self.path.display()) + } else { + self.path.display().fmt(f) + } } } diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 9e48fe82..d24cec4c 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -143,7 +143,7 @@ impl BulkRename { if !succeeded.is_empty() { let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url())); - err!(Pubsub::pub_after_bulk(it)); + err!(Pubsub::pub_after_bulk_rename(it)); FilesOp::rename(succeeded); } drop(permit); diff --git a/yazi-dds/src/ember/bulk.rs b/yazi-dds/src/ember/bulk_rename.rs similarity index 77% rename from yazi-dds/src/ember/bulk.rs rename to yazi-dds/src/ember/bulk_rename.rs index 93650d14..fedd25bf 100644 --- a/yazi-dds/src/ember/bulk.rs +++ b/yazi-dds/src/ember/bulk_rename.rs @@ -6,11 +6,11 @@ use yazi_shared::url::{Url, UrlCow}; use super::Ember; #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct EmberBulk<'a> { +pub struct EmberBulkRename<'a> { pub changes: HashMap, UrlCow<'a>>, } -impl<'a> EmberBulk<'a> { +impl<'a> EmberBulkRename<'a> { pub fn borrowed(changes: I) -> Ember<'a> where I: Iterator, Url<'a>)>, @@ -19,7 +19,7 @@ impl<'a> EmberBulk<'a> { } } -impl EmberBulk<'static> { +impl EmberBulkRename<'static> { pub fn owned<'a, I>(changes: I) -> Ember<'static> where I: Iterator, Url<'a>)>, @@ -31,11 +31,11 @@ impl EmberBulk<'static> { } } -impl<'a> From> for Ember<'a> { - fn from(value: EmberBulk<'a>) -> Self { Self::Bulk(value) } +impl<'a> From> for Ember<'a> { + fn from(value: EmberBulkRename<'a>) -> Self { Self::BulkRename(value) } } -impl IntoLua for EmberBulk<'_> { +impl IntoLua for EmberBulkRename<'_> { fn into_lua(self, lua: &Lua) -> mlua::Result { lua .create_table_from( diff --git a/yazi-dds/src/ember/ember.rs b/yazi-dds/src/ember/ember.rs index b2325752..aeafaec1 100644 --- a/yazi-dds/src/ember/ember.rs +++ b/yazi-dds/src/ember/ember.rs @@ -2,7 +2,7 @@ use anyhow::{Result, bail}; use mlua::{ExternalResult, IntoLua, Lua, Value}; use yazi_shared::Id; -use super::{EmberBulk, EmberBye, EmberCd, EmberCustom, EmberDelete, EmberDownload, EmberDuplicate, EmberHey, EmberHi, EmberHover, EmberLoad, EmberMount, EmberMove, EmberRename, EmberTab, EmberTrash, EmberYank}; +use super::{EmberBulkRename, EmberBye, EmberCd, EmberCustom, EmberDelete, EmberDownload, EmberDuplicate, EmberHey, EmberHi, EmberHover, EmberLoad, EmberMount, EmberMove, EmberRename, EmberTab, EmberTrash, EmberYank}; use crate::Payload; #[derive(Clone, Debug)] @@ -15,7 +15,7 @@ pub enum Ember<'a> { Load(EmberLoad<'a>), Hover(EmberHover<'a>), Rename(EmberRename<'a>), - Bulk(EmberBulk<'a>), + BulkRename(EmberBulkRename<'a>), Yank(EmberYank<'a>), Duplicate(EmberDuplicate<'a>), Move(EmberMove<'a>), @@ -37,7 +37,7 @@ impl Ember<'static> { "load" => Self::Load(serde_json::from_str(body)?), "hover" => Self::Hover(serde_json::from_str(body)?), "rename" => Self::Rename(serde_json::from_str(body)?), - "bulk" => Self::Bulk(serde_json::from_str(body)?), + "bulk-rename" => Self::BulkRename(serde_json::from_str(body)?), "@yank" => Self::Yank(serde_json::from_str(body)?), "duplicate" => Self::Duplicate(serde_json::from_str(body)?), "move" => Self::Move(serde_json::from_str(body)?), @@ -65,7 +65,7 @@ impl Ember<'static> { | "load" | "hover" | "rename" - | "bulk" + | "bulk-rename" | "@yank" | "duplicate" | "move" @@ -104,7 +104,7 @@ impl<'a> Ember<'a> { Self::Load(_) => "load", Self::Hover(_) => "hover", Self::Rename(_) => "rename", - Self::Bulk(_) => "bulk", + Self::BulkRename(_) => "bulk-rename", Self::Yank(_) => "@yank", Self::Duplicate(_) => "duplicate", Self::Move(_) => "move", @@ -132,7 +132,7 @@ impl<'a> IntoLua for Ember<'a> { Self::Hover(b) => b.into_lua(lua), Self::Tab(b) => b.into_lua(lua), Self::Rename(b) => b.into_lua(lua), - Self::Bulk(b) => b.into_lua(lua), + Self::BulkRename(b) => b.into_lua(lua), Self::Yank(b) => b.into_lua(lua), Self::Duplicate(b) => b.into_lua(lua), Self::Move(b) => b.into_lua(lua), diff --git a/yazi-dds/src/ember/mod.rs b/yazi-dds/src/ember/mod.rs index 28711e9d..6a83139d 100644 --- a/yazi-dds/src/ember/mod.rs +++ b/yazi-dds/src/ember/mod.rs @@ -1,3 +1,3 @@ yazi_macro::mod_flat!( - bulk bye cd custom delete download duplicate ember hey hi hover load mount r#move rename tab trash yank + bulk_rename bye cd custom delete download duplicate ember hey hi hover load mount r#move rename tab trash yank ); diff --git a/yazi-dds/src/payload.rs b/yazi-dds/src/payload.rs index 1e72f1a6..5d3ffe77 100644 --- a/yazi-dds/src/payload.rs +++ b/yazi-dds/src/payload.rs @@ -94,7 +94,7 @@ impl Display for Payload<'_> { Ember::Hover(b) => serde_json::to_string(b), Ember::Tab(b) => serde_json::to_string(b), Ember::Rename(b) => serde_json::to_string(b), - Ember::Bulk(b) => serde_json::to_string(b), + Ember::BulkRename(b) => serde_json::to_string(b), Ember::Yank(b) => serde_json::to_string(b), Ember::Duplicate(b) => serde_json::to_string(b), Ember::Move(b) => serde_json::to_string(b), diff --git a/yazi-dds/src/pubsub.rs b/yazi-dds/src/pubsub.rs index f8daa8d6..ff012795 100644 --- a/yazi-dds/src/pubsub.rs +++ b/yazi-dds/src/pubsub.rs @@ -8,7 +8,7 @@ use yazi_fs::FolderStage; use yazi_shared::{Id, url::{Url, UrlBuf, UrlBufCov}}; use yazi_shim::cell::RoCell; -use crate::{Client, ID, PEERS, ember::{Ember, EmberBulk, EmberDuplicateItem, EmberHi, EmberMoveItem}}; +use crate::{Client, ID, PEERS, ember::{Ember, EmberBulkRename, EmberDuplicateItem, EmberHi, EmberMoveItem}}; pub static LOCAL: RoCell>>> = RoCell::new(); @@ -128,18 +128,18 @@ impl Pubsub { true } - pub fn pub_after_bulk<'a, I>(changes: I) -> Result<()> + pub fn pub_after_bulk_rename<'a, I>(changes: I) -> Result<()> where I: Iterator, Url<'a>)> + Clone, { - if BOOT.local_events.contains("bulk") { - EmberBulk::borrowed(changes.clone()).with_receiver(*ID).flush()?; + if BOOT.local_events.contains("bulk-rename") { + EmberBulkRename::borrowed(changes.clone()).with_receiver(*ID).flush()?; } - if PEERS.read().values().any(|p| p.able("bulk")) { - Client::push(EmberBulk::borrowed(changes.clone()))?; + if PEERS.read().values().any(|p| p.able("bulk-rename")) { + Client::push(EmberBulkRename::borrowed(changes.clone()))?; } - if LOCAL.read().contains_key("bulk") { - Self::r#pub(EmberBulk::owned(changes))?; + if LOCAL.read().contains_key("bulk-rename") { + Self::r#pub(EmberBulkRename::owned(changes))?; } Ok(()) }