mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: add support for %@ argument extension on Windows to align with Unix platforms (#1319)
This commit is contained in:
parent
66e83f3e18
commit
a9eb218a5b
14 changed files with 461 additions and 147 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2935,6 +2935,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"uzers",
|
||||
"vergen-gitcl",
|
||||
"yazi-boot",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":"0.2","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","nlink","nlink","linemodes","SIGSTOP"],"language":"en","flagWords":[]}
|
||||
{"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","nlink","nlink","linemodes","SIGSTOP","sevenzip"],"version":"0.2","language":"en","flagWords":[]}
|
||||
|
|
@ -26,6 +26,9 @@ tokio = { version = "1.38.1", features = [ "full" ] }
|
|||
tokio-stream = "0.1.15"
|
||||
tokio-util = "0.7.11"
|
||||
|
||||
# Logging
|
||||
tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] }
|
||||
|
||||
[build-dependencies]
|
||||
vergen-gitcl = { version = "1.0.0", features = [ "build" ] }
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use anyhow::{bail, Context, Result};
|
|||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{io::AsyncWriteExt, select, sync::mpsc, task::JoinHandle, time};
|
||||
use tracing::error;
|
||||
use yazi_shared::RoCell;
|
||||
|
||||
use crate::{body::{Body, BodyBye, BodyHi}, ClientReader, ClientWriter, Payload, Pubsub, Server, Stream};
|
||||
|
|
@ -54,8 +55,8 @@ impl Client {
|
|||
continue;
|
||||
} else if line.starts_with("hey,") {
|
||||
Self::handle_hey(&line);
|
||||
} else {
|
||||
Payload::from_str(&line).map(|p| p.emit()).ok();
|
||||
} else if let Err(e) = Payload::from_str(&line).map(|p| p.emit()) {
|
||||
error!("Could not parse payload:\n{line}\n\nError:\n{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ impl Cha {
|
|||
reg.add_field_method_get("accessed", |_, me| {
|
||||
Ok(me.atime.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok()))
|
||||
});
|
||||
reg.add_method("permissions", |_, me, ()| {
|
||||
reg.add_method("permissions", |_, _me, ()| {
|
||||
Ok(
|
||||
#[cfg(unix)]
|
||||
Some(yazi_shared::fs::permissions(me.perm, me.is_dummy())),
|
||||
Some(yazi_shared::fs::permissions(_me.perm, _me.is_dummy())),
|
||||
#[cfg(windows)]
|
||||
None::<String>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use mlua::{IntoLuaMulti, Lua, Value};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{bindings::Cast, cha::Cha, url::UrlRef};
|
||||
use crate::{bindings::Cast, cha::Cha, url::{Url, UrlRef}};
|
||||
|
||||
pub fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.globals().raw_set(
|
||||
|
|
@ -34,6 +34,13 @@ pub fn install(lua: &Lua) -> mlua::Result<()> {
|
|||
}
|
||||
})?,
|
||||
),
|
||||
(
|
||||
"unique_name",
|
||||
lua.create_async_function(|lua, url: UrlRef| async move {
|
||||
// FIXME: handle errors
|
||||
Url::cast(lua, yazi_shared::fs::unique_name(url.clone()).await)
|
||||
})?,
|
||||
),
|
||||
])?,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ impl Utils {
|
|||
"quote",
|
||||
lua.create_function(|_, (s, unix): (mlua::String, Option<bool>)| {
|
||||
let s = match unix {
|
||||
Some(true) => shell_escape::unix::escape(s.to_str()?.into()),
|
||||
Some(false) => shell_escape::windows::escape(s.to_str()?.into()),
|
||||
None => shell_escape::escape(s.to_str()?.into()),
|
||||
Some(true) => yazi_shared::escape::unix(s.to_str()?),
|
||||
Some(false) => yazi_shared::escape::windows(s.to_str()?),
|
||||
None => yazi_shared::escape::native(s.to_str()?),
|
||||
};
|
||||
Ok(s.into_owned())
|
||||
})?,
|
||||
|
|
|
|||
|
|
@ -46,13 +46,10 @@ pub fn shell(opt: ShellOpt) -> Result<Child> {
|
|||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let args: Vec<String> = opt.args.iter().map(|s| s.to_string_lossy().into_owned()).collect();
|
||||
let args_: Vec<&str> = args.iter().map(|s| s.as_ref()).collect();
|
||||
let expanded = parser::parse(opt.cmd.to_string_lossy().as_ref(), &args_);
|
||||
Ok(
|
||||
Command::new("cmd")
|
||||
.arg("/C")
|
||||
.args(&expanded)
|
||||
Command::new("cmd.exe")
|
||||
.raw_arg("/C")
|
||||
.raw_arg(parser::parse(&opt.cmd, &opt.args))
|
||||
.stdin(opt.stdio())
|
||||
.stdout(opt.stdio())
|
||||
.stderr(opt.stdio())
|
||||
|
|
@ -64,156 +61,173 @@ pub fn shell(opt: ShellOpt) -> Result<Child> {
|
|||
|
||||
#[cfg(windows)]
|
||||
mod parser {
|
||||
use std::{iter::Peekable, str::Chars};
|
||||
use std::{ffi::{OsStr, OsString}, iter::Peekable, os::windows::ffi::{EncodeWide, OsStrExt, OsStringExt}};
|
||||
|
||||
pub(super) fn parse(cmd: &str, args: &[&str]) -> Vec<String> {
|
||||
let mut it = cmd.chars().peekable();
|
||||
let mut expanded = vec![];
|
||||
|
||||
while let Some(c) = it.next() {
|
||||
if c.is_whitespace() {
|
||||
continue;
|
||||
}
|
||||
let mut s = String::new();
|
||||
|
||||
if c == '\'' {
|
||||
while let Some(c) = it.next() {
|
||||
if c == '\'' {
|
||||
break;
|
||||
}
|
||||
next_string(&mut it, args, &mut s, c);
|
||||
}
|
||||
expanded.push(s);
|
||||
} else if c == '"' {
|
||||
while let Some(c) = it.next() {
|
||||
if c == '"' {
|
||||
break;
|
||||
}
|
||||
next_string(&mut it, args, &mut s, c);
|
||||
}
|
||||
expanded.push(s);
|
||||
} else if c == '%' && it.peek().is_some_and(|&c| c == '*') {
|
||||
it.next();
|
||||
expanded.extend(args.iter().skip(1).map(|&s| s.to_owned()));
|
||||
} else {
|
||||
next_string(&mut it, args, &mut s, c);
|
||||
|
||||
while let Some(c) = it.next() {
|
||||
if c.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
next_string(&mut it, args, &mut s, c);
|
||||
}
|
||||
expanded.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
expanded
|
||||
macro_rules! w {
|
||||
($c:literal) => {
|
||||
$c as u16
|
||||
};
|
||||
}
|
||||
|
||||
fn next_string(it: &mut Peekable<Chars<'_>>, args: &[&str], s: &mut String, c: char) {
|
||||
if c == '\\' {
|
||||
match it.next() {
|
||||
Some('\\') => s.push('\\'), // \\ ==> \
|
||||
Some('\'') => s.push('\''), // \' ==> '
|
||||
Some('"') => s.push('"'), // \" ==> "
|
||||
Some('%') => s.push('%'), // \% ==> %
|
||||
Some('n') => s.push('\n'), // \n ==> '\n'
|
||||
Some('t') => s.push('\t'), // \t ==> '\t'
|
||||
Some('r') => s.push('\r'), // \r ==> '\r'
|
||||
Some(c) => {
|
||||
s.push('\\');
|
||||
s.push(c);
|
||||
}
|
||||
None => s.push('\\'),
|
||||
}
|
||||
} else if c == '%' {
|
||||
match it.peek() {
|
||||
Some('*') => {
|
||||
if args.len() > 1 {
|
||||
s.push_str(&args[1..].join(" "));
|
||||
}
|
||||
it.next();
|
||||
}
|
||||
Some(n) if n.is_ascii_digit() => {
|
||||
let mut pos = n.to_string();
|
||||
|
||||
it.next();
|
||||
while let Some(&n) = it.peek() {
|
||||
if n.is_ascii_digit() {
|
||||
pos.push(it.next().unwrap());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(arg) = args.get(pos.parse::<usize>().unwrap()) {
|
||||
s.push_str(arg);
|
||||
}
|
||||
}
|
||||
_ => s.push('%'),
|
||||
}
|
||||
} else {
|
||||
s.push(c);
|
||||
pub(super) fn parse(cmd: &OsStr, args: &[OsString]) -> OsString {
|
||||
if cmd.len() < 2 {
|
||||
return cmd.to_owned();
|
||||
}
|
||||
|
||||
let mut buf = Vec::with_capacity(cmd.len());
|
||||
let mut it = cmd.encode_wide().peekable();
|
||||
|
||||
while let Some(c) = it.next() {
|
||||
if c == w!('%') {
|
||||
let n = visit_percent(it.clone(), &mut buf, &args, false);
|
||||
if n == 0 {
|
||||
buf.push(c);
|
||||
} else {
|
||||
it.by_ref().take(n).for_each(drop);
|
||||
}
|
||||
} else if c == w!('"') && it.peek().is_some_and(|&c| c == w!('%')) {
|
||||
it.next();
|
||||
|
||||
let n = visit_percent(it.clone(), &mut buf, &args, true);
|
||||
if n == 0 {
|
||||
buf.push(c);
|
||||
buf.push(w!('%'));
|
||||
} else {
|
||||
it.by_ref().take(n).for_each(drop);
|
||||
}
|
||||
} else {
|
||||
buf.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
OsString::from_wide(&buf)
|
||||
}
|
||||
|
||||
fn visit_percent(
|
||||
mut it: Peekable<EncodeWide>,
|
||||
buf: &mut Vec<u16>,
|
||||
args: &[OsString],
|
||||
quote: bool,
|
||||
) -> usize {
|
||||
let Some(c) = it.next().and_then(|c| char::from_u32(c as _)) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let mut pos = None;
|
||||
if c.is_ascii_digit() {
|
||||
let mut p = c.to_string();
|
||||
while let Some(n) = it.peek().and_then(|&c| char::from_u32(c as _)) {
|
||||
if n.is_ascii_digit() {
|
||||
it.next();
|
||||
p.push(n);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
pos = Some(p);
|
||||
}
|
||||
|
||||
if quote && !it.next().is_some_and(|e| e == w!('"')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if let Some(p) = pos {
|
||||
if let Some(arg) = args.get(p.parse::<usize>().unwrap()) {
|
||||
if quote {
|
||||
buf.extend(yazi_shared::escape::os_str(arg).encode_wide());
|
||||
} else {
|
||||
buf.extend(arg.encode_wide());
|
||||
}
|
||||
}
|
||||
return p.len() + quote as usize;
|
||||
}
|
||||
|
||||
if c != '*' && c != '@' {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut s = OsString::new();
|
||||
for (i, arg) in args.iter().skip(1).enumerate() {
|
||||
if i > 0 {
|
||||
s.push(" ");
|
||||
}
|
||||
if c == '*' {
|
||||
s.push(yazi_shared::escape::os_str(arg));
|
||||
} else {
|
||||
s.push(arg);
|
||||
}
|
||||
}
|
||||
if quote {
|
||||
buf.extend(yazi_shared::escape::os_str(&s).encode_wide());
|
||||
} else {
|
||||
buf.extend(s.encode_wide());
|
||||
}
|
||||
|
||||
1 + quote as usize
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
|
||||
fn parse(cmd: &str, args: &[&str]) -> String {
|
||||
let cmd = OsString::from(cmd);
|
||||
let args: Vec<_> = args.iter().map(|&s| OsString::from(s)).collect();
|
||||
super::parse(&cmd, &args).to_str().unwrap().to_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_quote() {
|
||||
let args = parse("echo abc xyz %0 %2", &["000", "111", "222"]);
|
||||
assert_eq!(args, ["echo", "abc", "xyz", "000", "222"]);
|
||||
let s = parse("echo abc xyz %0 %2", &["000", "111", "222"]);
|
||||
assert_eq!(s, "echo abc xyz 000 222");
|
||||
|
||||
let args = parse(" echo abc xyz %1 %2 ", &["", "111", "222"]);
|
||||
assert_eq!(args, ["echo", "abc", "xyz", "111", "222"]);
|
||||
let s = parse(" echo abc xyz %1 %2 ", &["", "111", "222"]);
|
||||
assert_eq!(s, " echo abc xyz 111 222 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_quote() {
|
||||
let args = parse("echo 'abc xyz' '%1' %2", &["000", "111", "222"]);
|
||||
assert_eq!(args, ["echo", "abc xyz", "111", "222"]);
|
||||
let s = parse("echo 'abc xyz' '%1' %2", &["000", "111", "222"]);
|
||||
assert_eq!(s, "echo 'abc xyz' '111' 222");
|
||||
|
||||
let args = parse(r#"echo 'abc ""xyz' '%1' %2"#, &["", "111", "222"]);
|
||||
assert_eq!(args, ["echo", r#"abc ""xyz"#, "111", "222"]);
|
||||
let s = parse(r#"echo 'abc ""xyz' '%1' %2"#, &["", "111", "222"]);
|
||||
assert_eq!(s, r#"echo 'abc ""xyz' '111' 222"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_double_quote() {
|
||||
let args = parse("echo \"abc ' 'xyz\" \"%1\" %2 %3", &["", "111", "222"]);
|
||||
assert_eq!(args, ["echo", "abc ' 'xyz", "111", "222", ""]);
|
||||
let s = parse(r#"echo "abc ' 'xyz" "%1" %2 %3"#, &["", "111", "222"]);
|
||||
assert_eq!(s, r#"echo "abc ' 'xyz" 111 222 "#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escaped() {
|
||||
let args = parse("echo \"a\tbc ' 'x\nyz\" \"\\%1\" %2 %3", &["", "111", "22 2"]);
|
||||
assert_eq!(args, ["echo", "a\tbc ' 'x\nyz", "%1", "22 2", ""]);
|
||||
let s = parse(r#"echo "a bc ' 'x\nyz" "\%1" "\"%2"" %3"#, &["", "111", "22 2"]);
|
||||
assert_eq!(s, r#"echo "a bc ' 'x\nyz" "\111" "\"22 2"" "#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent_star() {
|
||||
let args = parse("echo %* xyz", &[]);
|
||||
assert_eq!(args, ["echo", "xyz"]);
|
||||
let s = parse("echo %* xyz", &[]);
|
||||
assert_eq!(s, "echo xyz");
|
||||
|
||||
let args = parse("echo %* xyz", &["000", "111", "222"]);
|
||||
assert_eq!(args, ["echo", "111", "222", "xyz"]);
|
||||
let s = parse("echo %* xyz", &["000", "111", "222"]);
|
||||
assert_eq!(s, "echo 111 222 xyz");
|
||||
|
||||
let args = parse("echo '%*' xyz", &["000", "111", "222"]);
|
||||
assert_eq!(args, ["echo", "111 222", "xyz"]);
|
||||
let s = parse("echo '%*' xyz", &["000", "111", "22 2"]);
|
||||
assert_eq!(s, r#"echo '111 "22 2"' xyz"#);
|
||||
|
||||
let args = parse("echo -C%* xyz", &[]);
|
||||
assert_eq!(args, ["echo", "-C", "xyz"]);
|
||||
let s = parse("echo -C%* xyz", &[]);
|
||||
assert_eq!(s, "echo -C xyz");
|
||||
|
||||
let args = parse("echo -C%* xyz", &["000", "111", "222"]);
|
||||
assert_eq!(args, ["echo", "-C111 222", "xyz"]);
|
||||
let s = parse("echo -C%* xyz", &["000", " 111", "222"]);
|
||||
assert_eq!(s, r#"echo -C" 111" 222 xyz"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_var() {
|
||||
let args = parse(" %EDITOR% %* xyz", &["000", "111", "222"]);
|
||||
assert_eq!(args, ["%EDITOR%", "111", "222", "xyz"]);
|
||||
let s = parse(r#"%EDITOR% %@ "%@" %* "%*" xyz"#, &["000", "1 11", "222"]);
|
||||
assert_eq!(s, r#"%EDITOR% 1 11 222 "1 11 222" "1 11" 222 "\"1 11\" 222" xyz"#);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use parking_lot::Mutex;
|
|||
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, task::JoinHandle};
|
||||
use yazi_config::{open::Opener, plugin::{Fetcher, Preloader}, TASKS};
|
||||
use yazi_dds::Pump;
|
||||
use yazi_shared::{event::Data, fs::{unique_path, Url}, Throttle};
|
||||
use yazi_shared::{event::Data, fs::{unique_name, Url}, Throttle};
|
||||
|
||||
use super::{Ongoing, TaskProg, TaskStage};
|
||||
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};
|
||||
|
|
@ -97,7 +97,7 @@ impl Scheduler {
|
|||
_ = self.micro.try_send(
|
||||
async move {
|
||||
if !force {
|
||||
to = unique_path(to).await;
|
||||
to = unique_name(to).await;
|
||||
}
|
||||
file
|
||||
.paste(FileOpPaste { id, from, to, meta: None, cut: true, follow: false, retry: 0 })
|
||||
|
|
@ -122,7 +122,7 @@ impl Scheduler {
|
|||
_ = self.micro.try_send(
|
||||
async move {
|
||||
if !force {
|
||||
to = unique_path(to).await;
|
||||
to = unique_name(to).await;
|
||||
}
|
||||
file
|
||||
.paste(FileOpPaste { id, from, to, meta: None, cut: false, follow, retry: 0 })
|
||||
|
|
@ -142,7 +142,7 @@ impl Scheduler {
|
|||
_ = self.micro.try_send(
|
||||
async move {
|
||||
if !force {
|
||||
to = unique_path(to).await;
|
||||
to = unique_name(to).await;
|
||||
}
|
||||
file
|
||||
.link(FileOpLink { id, from, to, meta: None, resolve: false, relative, delete: false })
|
||||
|
|
@ -167,7 +167,7 @@ impl Scheduler {
|
|||
_ = self.micro.try_send(
|
||||
async move {
|
||||
if !force {
|
||||
to = unique_path(to).await;
|
||||
to = unique_name(to).await;
|
||||
}
|
||||
file.hardlink(FileOpHardlink { id, from, to, meta: None, follow }).await.ok();
|
||||
}
|
||||
|
|
|
|||
41
yazi-shared/src/escape/mod.rs
Normal file
41
yazi-shared/src/escape/mod.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! Escape characters that may have special meaning in a shell, including
|
||||
//! spaces. This is a modified version of the [`shell-escape`] crate and [`this
|
||||
//! PR`].
|
||||
//!
|
||||
//! [`shell-escape`]: https://crates.io/crates/shell-escape
|
||||
//! [`this PR`]: https://github.com/sfackler/shell-escape/pull/9
|
||||
|
||||
use std::{borrow::Cow, ffi::OsStr};
|
||||
|
||||
mod unix;
|
||||
mod windows;
|
||||
|
||||
#[inline]
|
||||
pub fn unix(s: &str) -> Cow<str> { unix::from_str(s) }
|
||||
|
||||
#[inline]
|
||||
pub fn windows(s: &str) -> Cow<str> { windows::from_str(s) }
|
||||
|
||||
#[inline]
|
||||
pub fn native(s: &str) -> Cow<str> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unix::from_str(s)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
windows::from_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn os_str(s: &OsStr) -> Cow<OsStr> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unix::from_os_str(s)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
windows::from_os_str(s)
|
||||
}
|
||||
}
|
||||
97
yazi-shared/src/escape/unix.rs
Normal file
97
yazi-shared/src/escape/unix.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
pub fn from_str(s: &str) -> Cow<str> {
|
||||
match from_slice(s.as_bytes()) {
|
||||
Cow::Borrowed(_) => Cow::Borrowed(s),
|
||||
Cow::Owned(v) => String::from_utf8(v).expect("Invalid bytes returned from from_slice()").into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn from_os_str(s: &std::ffi::OsStr) -> Cow<std::ffi::OsStr> {
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
|
||||
match from_slice(s.as_bytes()) {
|
||||
Cow::Borrowed(_) => Cow::Borrowed(s),
|
||||
Cow::Owned(v) => std::ffi::OsString::from_vec(v).into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_slice(s: &[u8]) -> Cow<[u8]> {
|
||||
if !s.is_empty() && s.iter().copied().all(allowed) {
|
||||
return Cow::Borrowed(s);
|
||||
}
|
||||
|
||||
let mut escaped = Vec::with_capacity(s.len() + 2);
|
||||
escaped.push(b'\'');
|
||||
|
||||
for &b in s {
|
||||
match b {
|
||||
b'\'' | b'!' => {
|
||||
escaped.reserve(4);
|
||||
escaped.push(b'\'');
|
||||
escaped.push(b'\\');
|
||||
escaped.push(b);
|
||||
escaped.push(b'\'');
|
||||
}
|
||||
_ => escaped.push(b),
|
||||
}
|
||||
}
|
||||
|
||||
escaped.push(b'\'');
|
||||
escaped.into()
|
||||
}
|
||||
|
||||
fn allowed(b: u8) -> bool {
|
||||
matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'=' | b'/' | b',' | b'.' | b'+')
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
assert_eq!(from_str(""), r#"''"#);
|
||||
assert_eq!(from_str(" "), r#"' '"#);
|
||||
assert_eq!(from_str("*"), r#"'*'"#);
|
||||
|
||||
assert_eq!(from_str("--aaa=bbb-ccc"), "--aaa=bbb-ccc");
|
||||
assert_eq!(from_str(r#"--features="default""#), r#"'--features="default"'"#);
|
||||
assert_eq!(from_str("linker=gcc -L/foo -Wl,bar"), r#"'linker=gcc -L/foo -Wl,bar'"#);
|
||||
|
||||
assert_eq!(
|
||||
from_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"),
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
|
||||
);
|
||||
assert_eq!(from_str(r#"'!\$`\\\n "#), r#"''\'''\!'\$`\\\n '"#);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_from_os_str() {
|
||||
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
|
||||
|
||||
fn from_str(input: &str, expected: &str) { from_bytes(input.as_bytes(), expected.as_bytes()) }
|
||||
|
||||
fn from_bytes(input: &[u8], expected: &[u8]) {
|
||||
assert_eq!(from_os_str(OsStr::from_bytes(input)), OsStr::from_bytes(expected));
|
||||
}
|
||||
|
||||
from_str("", r#"''"#);
|
||||
from_str(" ", r#"' '"#);
|
||||
from_str("*", r#"'*'"#);
|
||||
|
||||
from_str("--aaa=bbb-ccc", "--aaa=bbb-ccc");
|
||||
from_str(r#"--features="default""#, r#"'--features="default"'"#);
|
||||
from_str("linker=gcc -L/foo -Wl,bar", r#"'linker=gcc -L/foo -Wl,bar'"#);
|
||||
|
||||
from_str(
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
|
||||
);
|
||||
from_str(r#"'!\$`\\\n "#, r#"''\'''\!'\$`\\\n '"#);
|
||||
|
||||
from_bytes(&[0x66, 0x6f, 0x80, 0x6f], &[b'\'', 0x66, 0x6f, 0x80, 0x6f, b'\'']);
|
||||
}
|
||||
}
|
||||
149
yazi-shared/src/escape/windows.rs
Normal file
149
yazi-shared/src/escape/windows.rs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
use std::{borrow::Cow, iter::repeat};
|
||||
|
||||
pub fn from_str(s: &str) -> Cow<str> {
|
||||
let bytes = s.as_bytes();
|
||||
if !bytes.is_empty() && !bytes.iter().any(|&c| matches!(c, b' ' | b'"' | b'\n' | b'\t')) {
|
||||
return Cow::Borrowed(s);
|
||||
}
|
||||
|
||||
let mut escaped = String::with_capacity(bytes.len() + 2);
|
||||
escaped.push('"');
|
||||
|
||||
let mut chars = bytes.iter().copied().peekable();
|
||||
loop {
|
||||
let mut slashes = 0;
|
||||
while chars.next_if_eq(&b'\\').is_some() {
|
||||
slashes += 1;
|
||||
}
|
||||
match chars.next() {
|
||||
Some(b'"') => {
|
||||
escaped.reserve(slashes * 2 + 2);
|
||||
escaped.extend(repeat('\\').take(slashes * 2 + 1));
|
||||
escaped.push('"');
|
||||
}
|
||||
Some(c) => {
|
||||
escaped.reserve(slashes + 1);
|
||||
escaped.extend(repeat('\\').take(slashes));
|
||||
escaped.push(c as _);
|
||||
}
|
||||
None => {
|
||||
escaped.reserve(slashes * 2);
|
||||
escaped.extend(repeat('\\').take(slashes * 2));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
escaped.push('"');
|
||||
escaped.into()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn from_os_str(s: &std::ffi::OsStr) -> Cow<std::ffi::OsStr> {
|
||||
use std::os::windows::ffi::{OsStrExt, OsStringExt};
|
||||
|
||||
let wide = s.encode_wide();
|
||||
if !s.is_empty() && !wide.clone().into_iter().any(disallowed) {
|
||||
return Cow::Borrowed(s);
|
||||
}
|
||||
|
||||
let mut escaped: Vec<u16> = Vec::with_capacity(s.len() + 2);
|
||||
escaped.push(b'"' as _);
|
||||
|
||||
let mut chars = wide.into_iter().peekable();
|
||||
loop {
|
||||
let mut slashes = 0;
|
||||
while chars.next_if_eq(&(b'\\' as _)).is_some() {
|
||||
slashes += 1;
|
||||
}
|
||||
match chars.next() {
|
||||
Some(c) if c == b'"' as _ => {
|
||||
escaped.reserve(slashes * 2 + 2);
|
||||
escaped.extend(repeat(b'\\' as u16).take(slashes * 2 + 1));
|
||||
escaped.push(b'"' as _);
|
||||
}
|
||||
Some(c) => {
|
||||
escaped.reserve(slashes + 1);
|
||||
escaped.extend(repeat(b'\\' as u16).take(slashes));
|
||||
escaped.push(c);
|
||||
}
|
||||
None => {
|
||||
escaped.reserve(slashes * 2);
|
||||
escaped.extend(repeat(b'\\' as u16).take(slashes * 2));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
escaped.push(b'"' as _);
|
||||
std::ffi::OsString::from_wide(&escaped).into()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn disallowed(b: u16) -> bool {
|
||||
match char::from_u32(b as u32) {
|
||||
Some(c) => matches!(c, ' ' | '"' | '\n' | '\t'),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
assert_eq!(from_str(""), r#""""#);
|
||||
assert_eq!(from_str(r#""""#), r#""\"\"""#);
|
||||
|
||||
assert_eq!(from_str("--aaa=bbb-ccc"), "--aaa=bbb-ccc");
|
||||
assert_eq!(from_str(r#"\path\to\my documents\"#), r#""\path\to\my documents\\""#);
|
||||
|
||||
assert_eq!(from_str(r#"--features="default""#), r#""--features=\"default\"""#);
|
||||
assert_eq!(from_str(r#""--features=\"default\"""#), r#""\"--features=\\\"default\\\"\"""#);
|
||||
assert_eq!(from_str("linker=gcc -L/foo -Wl,bar"), r#""linker=gcc -L/foo -Wl,bar""#);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn test_from_os_str() {
|
||||
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
|
||||
|
||||
fn from_str(input: &str, expected: &str) {
|
||||
let observed = OsString::from(input);
|
||||
let expected = OsString::from(expected);
|
||||
assert_eq!(from_os_str(observed.as_os_str()), expected.as_os_str());
|
||||
}
|
||||
|
||||
fn from_bytes(input: &[u16], expected: &[u16]) {
|
||||
let observed = OsString::from_wide(input);
|
||||
let expected = OsString::from_wide(expected);
|
||||
assert_eq!(from_os_str(observed.as_os_str()), expected.as_os_str());
|
||||
}
|
||||
|
||||
from_str("", r#""""#);
|
||||
from_str(r#""""#, r#""\"\"""#);
|
||||
|
||||
from_str("--aaa=bbb-ccc", "--aaa=bbb-ccc");
|
||||
from_str(r#"\path\to\my documents\"#, r#""\path\to\my documents\\""#);
|
||||
|
||||
from_str(r#"--features="default""#, r#""--features=\"default\"""#);
|
||||
from_str(r#""--features=\"default\"""#, r#""\"--features=\\\"default\\\"\"""#);
|
||||
from_str("linker=gcc -L/foo -Wl,bar", r#""linker=gcc -L/foo -Wl,bar""#);
|
||||
|
||||
from_bytes(&[0x1055, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065], &[
|
||||
0x1055, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065,
|
||||
]);
|
||||
from_bytes(&[0xd801, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065], &[
|
||||
b'"' as u16,
|
||||
0xd801,
|
||||
0x006e,
|
||||
0x0069,
|
||||
0x0063,
|
||||
0x006f,
|
||||
0x0064,
|
||||
0x0065,
|
||||
b'"' as u16,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -55,12 +55,14 @@ pub fn ends_with_slash(p: &Path) -> bool {
|
|||
if let [.., last] = b { *last == MAIN_SEPARATOR as u8 } else { false }
|
||||
}
|
||||
|
||||
pub async fn unique_path(mut p: Url) -> Url {
|
||||
let Some(stem) = p.file_stem().map(|s| s.to_owned()) else {
|
||||
return p;
|
||||
// FIXME: should return a `std::io::Result` to handle errors such as
|
||||
// permission denied
|
||||
pub async fn unique_name(mut u: Url) -> Url {
|
||||
let Some(stem) = u.file_stem().map(|s| s.to_owned()) else {
|
||||
return u;
|
||||
};
|
||||
|
||||
let ext = p
|
||||
let ext = u
|
||||
.extension()
|
||||
.map(|s| {
|
||||
let mut n = OsString::with_capacity(s.len() + 1);
|
||||
|
|
@ -70,20 +72,18 @@ pub async fn unique_path(mut p: Url) -> Url {
|
|||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut i = 0;
|
||||
while maybe_exists(&p).await {
|
||||
i += 1;
|
||||
|
||||
let mut i = 1u64;
|
||||
while maybe_exists(&u).await {
|
||||
let mut name = OsString::with_capacity(stem.len() + ext.len() + 5);
|
||||
name.push(&stem);
|
||||
name.push(format!("_{i}"));
|
||||
if !ext.is_empty() {
|
||||
name.push(&ext);
|
||||
}
|
||||
name.push("_");
|
||||
name.push(i.to_string());
|
||||
name.push(&ext);
|
||||
|
||||
p.set_file_name(name);
|
||||
u.set_file_name(name);
|
||||
i += 1;
|
||||
}
|
||||
p
|
||||
u
|
||||
}
|
||||
|
||||
// Parameters
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ mod condition;
|
|||
mod debounce;
|
||||
mod env;
|
||||
mod errors;
|
||||
pub mod escape;
|
||||
pub mod event;
|
||||
pub mod fs;
|
||||
mod layer;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue