feat: new target parameter for create action (#4088)

This commit is contained in:
三咲雅 misaki masa 2026-06-27 13:20:28 +08:00 committed by sxyazi
parent 39527f488f
commit 21550a7eb5
No known key found for this signature in database
9 changed files with 90 additions and 23 deletions

View file

@ -66,14 +66,8 @@ impl Actor for Trigger {
impl Trigger {
fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> {
let sep = if cfg!(windows) {
AnyAsciiChar::new(b"/\\").unwrap()
} else {
AnyAsciiChar::new(b"/").unwrap()
};
let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?;
if path.is_empty() && !sep.predicate(s.bytes().last()?) {
if path.is_empty() && !AnyAsciiChar::SEP.predicate(s.bytes().last()?) {
return None; // We don't complete a `sftp://test`, but `sftp://test/`
}
@ -84,7 +78,7 @@ impl Trigger {
}
// Child
let child = path.rsplit_pred(sep).map_or(path.as_path(), |(_, c)| c).to_owned();
let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.as_path(), |(_, c)| c).to_owned();
// Parent
let url = UrlCow::try_from((scheme.clone().zeroed(), path)).ok()?;

View file

@ -1,13 +1,16 @@
use std::pin::Pin;
use anyhow::{Result, bail};
use futures::{Stream, StreamExt};
use tokio_stream::wrappers::UnboundedReceiverStream;
use yazi_config::{YAZI, popup::ConfirmCfg};
use yazi_fs::{FilesOp, file::File};
use yazi_macro::{input, ok_or_not_found, succ};
use yazi_parser::mgr::CreateForm;
use yazi_proxy::{ConfirmProxy, MgrProxy};
use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}};
use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, strand::{StrandBuf, StrandLike}, url::{UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, maybe_exists, provider};
use yazi_watcher::WATCHER;
use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx};
@ -18,28 +21,35 @@ impl Actor for Create {
const NAME: &str = "create";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
fn act(cx: &mut Ctx, CreateForm { target, dir, force }: Self::Form) -> Result<Data> {
let cwd = cx.cwd().to_owned();
let mut input = input!(cx, YAZI.input.create(form.dir))?;
let mut target: Pin<Box<dyn Stream<Item = StrandBuf> + Send>> = if target.is_empty() {
let input = input!(cx, YAZI.input.create(dir))?;
Box::pin(
UnboundedReceiverStream::new(input).filter_map(|event| async { event.map(Into::into) }),
)
} else {
Box::pin(tokio_stream::iter(vec![target]))
};
tokio::spawn(async move {
let Some(InputEvent::Submit(name)) = input.recv().await else { return };
let Some(name) = target.next().await else { return };
if name.is_empty() {
return;
}
let Ok(new) = cwd.try_join(&name) else {
return;
};
let Ok(new) = cwd.try_join(&name) else { return };
if !form.force
if !force
&& maybe_exists(&new).await
&& !ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await
{
return;
}
_ = Self::r#do(new, form.dir || name.ends_with('/') || name.ends_with('\\')).await;
let end_sep = AnyAsciiChar::SEP.predicate(*name.encoded_bytes().last().unwrap());
_ = Self::r#do(new, dir || end_sep).await;
});
succ!();
}

View file

@ -1,13 +1,15 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use serde::Deserialize;
use yazi_shared::event::ActionCow;
use yazi_shared::{event::ActionCow, strand::StrandBuf};
#[derive(Debug, Deserialize)]
pub struct CreateForm {
#[serde(alias = "0", default)]
pub target: StrandBuf,
#[serde(default)]
pub dir: bool,
pub dir: bool,
#[serde(default)]
pub force: bool,
pub force: bool,
}
impl TryFrom<ActionCow> for CreateForm {

View file

@ -12,6 +12,11 @@ pub trait Utf8BytePredictor {
pub struct AnyAsciiChar<'a>(&'a [u8]);
impl<'a> AnyAsciiChar<'a> {
#[cfg(windows)]
pub const SEP: Self = Self(b"/\\");
#[cfg(not(windows))]
pub const SEP: Self = Self(b"/");
pub fn new(chars: &'a [u8]) -> Option<Self> {
if chars.iter().all(|&b| b <= 0x7f) { Some(Self(chars)) } else { None }
}

View file

@ -0,0 +1,40 @@
use std::fmt;
use serde::{Deserialize, Deserializer, de::{self, Visitor}};
use crate::strand::StrandBuf;
impl<'de> Deserialize<'de> for StrandBuf {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct V;
impl<'de> Visitor<'de> for V {
type Value = StrandBuf;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a string or bytes")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(StrandBuf::Utf8(v.to_owned()))
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(StrandBuf::Utf8(v))
}
fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
Ok(StrandBuf::Bytes(v.to_owned()))
}
fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
Ok(StrandBuf::Bytes(v))
}
}
deserializer.deserialize_any(V)
}
}

View file

@ -17,6 +17,8 @@ pub trait StrandLike: AsStrand {
fn encoded_bytes(&self) -> &[u8] { self.as_strand().encoded_bytes() }
fn ends_with(&self, needle: impl AsStrand) -> bool { self.as_strand().ends_with(needle) }
fn eq_ignore_ascii_case(&self, other: impl AsStrand) -> bool {
self.as_strand().eq_ignore_ascii_case(other)
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(buf conversion cow error extensions kind like strand view);
yazi_macro::mod_flat!(buf conversion cow de error extensions kind like strand view);

View file

@ -156,6 +156,10 @@ impl<'a> Strand<'a> {
}
}
pub fn ends_with(self, needle: impl AsStrand) -> bool {
self.encoded_bytes().ends_with(needle.as_strand().encoded_bytes())
}
pub fn eq_ignore_ascii_case(self, other: impl AsStrand) -> bool {
self.encoded_bytes().eq_ignore_ascii_case(other.as_strand().encoded_bytes())
}

View file

@ -12,11 +12,21 @@ pub enum InputEvent {
}
impl InputEvent {
pub fn is_submit(&self) -> bool { matches!(self, Self::Submit(_)) }
pub fn value(&self) -> &str {
match self {
Self::Submit(v) | Self::Cancel(v) | Self::Type(v) | Self::Trigger(v, _) => v.as_str(),
}
}
pub fn is_submit(&self) -> bool { matches!(self, Self::Submit(_)) }
pub fn map<T, F>(self, f: F) -> Option<T>
where
F: FnOnce(String) -> T,
{
match self {
Self::Submit(v) => Some(f(v)),
_ => None,
}
}
}