fix: make URLs with invalid UTF-8 serializable (#3023)

This commit is contained in:
三咲雅 misaki masa 2025-07-28 17:21:47 +08:00 committed by GitHub
parent 0e3cd8545c
commit 9535caad5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 293 additions and 113 deletions

33
Cargo.lock generated
View file

@ -1357,9 +1357,9 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litrs"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]]
name = "lock_api"
@ -2155,9 +2155,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.15"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec"
checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792"
dependencies = [
"bitflags 2.9.1",
]
@ -2225,9 +2225,9 @@ checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
[[package]]
name = "rustc-demangle"
version = "0.1.25"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc-hash"
@ -2470,12 +2470,12 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "socket2"
version = "0.5.10"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -2684,9 +2684,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.46.1"
version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
dependencies = [
"backtrace",
"bytes",
@ -2699,7 +2699,7 @@ dependencies = [
"slab",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3295,15 +3295,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"

View file

@ -45,7 +45,7 @@ scopeguard = "1.2.0"
serde = { version = "1.0.219", features = [ "derive" ] }
serde_json = "1.0.141"
syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
tokio = { version = "1.46.1", features = [ "full" ] }
tokio = { version = "1.47.0", features = [ "full" ] }
tokio-stream = "0.1.17"
tokio-util = "0.7.15"
toml = { version = "0.9.2" }

View file

@ -1 +1 @@
{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","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","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","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","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","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash"],"version":"0.2","flagWords":[],"language":"en"}
{"version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","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","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","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","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","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded"],"language":"en"}

View file

@ -1,7 +1,6 @@
use anyhow::{Context, Result};
use yazi_fs::{ok_or_not_found, remove_dir_clean, services::Local};
use yazi_macro::outln;
use yazi_shared::url::Url;
use super::Dependency;
use crate::shared::{maybe_exists, remove_sealed};

View file

@ -2,6 +2,7 @@ use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use super::{Offset, Origin};
use crate::popup::Position;
#[derive(Deserialize, DeserializeOver2)]
pub struct Confirm {
@ -30,4 +31,20 @@ pub struct Confirm {
impl Confirm {
pub const fn border(&self) -> u16 { 2 }
pub const fn trash_position(&self) -> Position {
Position::new(self.trash_origin, self.trash_offset)
}
pub const fn delete_position(&self) -> Position {
Position::new(self.delete_origin, self.delete_offset)
}
pub const fn overwrite_position(&self) -> Position {
Position::new(self.overwrite_origin, self.overwrite_offset)
}
pub const fn quit_position(&self) -> Position {
Position::new(self.quit_origin, self.quit_offset)
}
}

View file

@ -1,7 +1,7 @@
use ratatui::{text::{Line, Text}, widgets::{Paragraph, Wrap}};
use yazi_shared::url::Url;
use yazi_shared::{IntoStringLossy, url::Url};
use super::{Offset, Origin, Position};
use super::{Offset, Position};
use crate::YAZI;
#[derive(Default)]
@ -106,22 +106,22 @@ impl InputCfg {
impl ConfirmCfg {
fn new(
title: String,
(origin, offset): (Origin, Offset),
position: Position,
body: Option<Text<'static>>,
list: Option<Text<'static>>,
) -> Self {
Self {
position: Position::new(origin, offset),
title: Line::raw(title),
body: body.map(|c| Paragraph::new(c).wrap(Wrap { trim: false })).unwrap_or_default(),
list: list.map(|l| Paragraph::new(l).wrap(Wrap { trim: false })).unwrap_or_default(),
position,
title: Line::raw(title),
body: body.map(|b| Paragraph::new(b).wrap(Wrap { trim: false })).unwrap_or_default(),
list: list.map(|l| Paragraph::new(l).wrap(Wrap { trim: false })).unwrap_or_default(),
}
}
pub fn trash(urls: &[yazi_shared::url::Url]) -> Self {
Self::new(
Self::replace_number(&YAZI.confirm.trash_title, urls.len()),
(YAZI.confirm.trash_origin, YAZI.confirm.trash_offset),
YAZI.confirm.trash_position(),
None,
Self::truncate_list(urls.iter(), urls.len(), 100),
)
@ -130,7 +130,7 @@ impl ConfirmCfg {
pub fn delete(urls: &[yazi_shared::url::Url]) -> Self {
Self::new(
Self::replace_number(&YAZI.confirm.delete_title, urls.len()),
(YAZI.confirm.delete_origin, YAZI.confirm.delete_offset),
YAZI.confirm.delete_position(),
None,
Self::truncate_list(urls.iter(), urls.len(), 100),
)
@ -139,16 +139,16 @@ impl ConfirmCfg {
pub fn overwrite(url: &Url) -> Self {
Self::new(
YAZI.confirm.overwrite_title.to_owned(),
(YAZI.confirm.overwrite_origin, YAZI.confirm.overwrite_offset),
YAZI.confirm.overwrite_position(),
Some(Text::raw(&YAZI.confirm.overwrite_body)),
Some(url.to_string().into()),
Some(url.into_string_lossy().into()),
)
}
pub fn quit(len: usize, names: Vec<String>) -> Self {
Self::new(
Self::replace_number(&YAZI.confirm.quit_title, len),
(YAZI.confirm.quit_origin, YAZI.confirm.quit_offset),
YAZI.confirm.quit_position(),
Some(Text::raw(&YAZI.confirm.quit_body)),
Self::truncate_list(names.into_iter(), len, 10),
)
@ -159,7 +159,7 @@ impl ConfirmCfg {
}
fn truncate_list(
it: impl Iterator<Item = impl Into<String>>,
it: impl Iterator<Item = impl IntoStringLossy>,
len: usize,
max: usize,
) -> Option<Text<'static>> {
@ -169,7 +169,7 @@ impl ConfirmCfg {
lines.push(format!("... and {} more", len - max));
break;
}
lines.push(s.into());
lines.push(s.into_string_lossy());
}
Some(Text::from_iter(lines))
}

View file

@ -11,7 +11,7 @@ pub struct Position {
impl Position {
#[inline]
pub fn new(origin: Origin, offset: Offset) -> Self { Self { origin, offset } }
pub const fn new(origin: Origin, offset: Offset) -> Self { Self { origin, offset } }
pub fn rect(&self, WindowSize { columns, rows, .. }: WindowSize) -> Rect {
use Origin::*;

View file

@ -79,7 +79,7 @@ impl Cha {
pub fn from_dummy(_url: &Url, ft: Option<FileType>) -> Self {
let mut me = ft.map(Self::from_half_ft).unwrap_or_default();
#[cfg(unix)]
if yazi_shared::url::Urn::new(_url).is_hidden() {
if _url.urn().is_hidden() {
me.kind |= ChaKind::HIDDEN;
}
me

View file

@ -8,6 +8,7 @@ use crate::{CWD, services};
#[inline]
pub fn clean_url(url: &Url) -> Url { Url::from(clean_path(url)) }
// FIXME: VFS
#[inline]
pub fn clean_path(path: impl AsRef<Path>) -> PathBuf { _clean_path(path.as_ref()) }
@ -30,9 +31,11 @@ fn _clean_path(path: &Path) -> PathBuf {
if out.is_empty() { PathBuf::from(".") } else { out.iter().collect() }
}
// FIXME: VFS
#[inline]
pub fn expand_path(p: impl AsRef<Path>) -> PathBuf { _expand_path(p.as_ref()) }
// FIXME: keep the original scheme of the Url
#[inline]
pub fn expand_url<'a>(url: impl Into<Cow<'a, Url>>) -> Cow<'a, Url> {
let u: Cow<'a, Url> = url.into();

View file

@ -6,94 +6,170 @@ use crate::services::Local;
#[inline]
pub async fn canonicalize(url: impl AsRef<Url>) -> io::Result<Url> {
Local::canonicalize(url.as_ref()).await.map(Into::into)
if let Some(path) = url.as_ref().as_path() {
Local::canonicalize(path).await.map(Into::into)
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn create(url: impl AsRef<Url>) -> io::Result<tokio::fs::File> {
Local::create(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::create(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn create_dir(url: impl AsRef<Url>) -> io::Result<()> {
Local::create_dir(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::create_dir(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn create_dir_all(url: impl AsRef<Url>) -> io::Result<()> {
Local::create_dir_all(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::create_dir_all(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn hard_link(original: impl AsRef<Url>, link: impl AsRef<Url>) -> io::Result<()> {
Local::hard_link(original.as_ref(), link.as_ref()).await
if let (Some(original), Some(link)) = (original.as_ref().as_path(), link.as_ref().as_path()) {
Local::hard_link(original, link).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn metadata(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
Local::metadata(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::metadata(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn open(url: impl AsRef<Url>) -> io::Result<tokio::fs::File> {
Local::open(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::open(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn read_dir(url: impl AsRef<Url>) -> io::Result<tokio::fs::ReadDir> {
Local::read_dir(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::read_dir(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub fn read_dir_sync(url: impl AsRef<Url>) -> io::Result<std::fs::ReadDir> {
Local::read_dir_sync(url.as_ref())
if let Some(path) = url.as_ref().as_path() {
Local::read_dir_sync(path)
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn read_link(url: impl AsRef<Url>) -> io::Result<Url> {
Local::read_link(url.as_ref()).await.map(Into::into)
if let Some(path) = url.as_ref().as_path() {
Local::read_link(path).await.map(Into::into)
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn remove_dir(url: impl AsRef<Url>) -> io::Result<()> {
Local::remove_dir(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::remove_dir(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn remove_dir_all(url: impl AsRef<Url>) -> io::Result<()> {
Local::remove_dir_all(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::remove_dir_all(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn remove_file(url: impl AsRef<Url>) -> io::Result<()> {
Local::remove_file(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::remove_file(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn rename(from: impl AsRef<Url>, to: impl AsRef<Url>) -> io::Result<()> {
Local::rename(from.as_ref(), to.as_ref()).await
if let (Some(from), Some(to)) = (from.as_ref().as_path(), to.as_ref().as_path()) {
Local::rename(from, to).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn symlink_dir(original: impl AsRef<Url>, link: impl AsRef<Url>) -> io::Result<()> {
Local::symlink_dir(original.as_ref(), link.as_ref()).await
if let (Some(original), Some(link)) = (original.as_ref().as_path(), link.as_ref().as_path()) {
Local::symlink_dir(original, link).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn symlink_file(original: impl AsRef<Url>, link: impl AsRef<Url>) -> io::Result<()> {
Local::symlink_file(original.as_ref(), link.as_ref()).await
if let (Some(original), Some(link)) = (original.as_ref().as_path(), link.as_ref().as_path()) {
Local::symlink_file(original, link).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn symlink_metadata(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
Local::symlink_metadata(url.as_ref()).await
if let Some(path) = url.as_ref().as_path() {
Local::symlink_metadata(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
pub fn symlink_metadata_sync(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
Local::symlink_metadata_sync(url.as_ref())
if let Some(path) = url.as_ref().as_path() {
Local::symlink_metadata_sync(path)
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn write(url: impl AsRef<Url>, contents: impl AsRef<[u8]>) -> io::Result<()> {
Local::write(url.as_ref(), contents).await
if let Some(path) = url.as_ref().as_path() {
Local::write(path, contents).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}

View file

@ -1,5 +1,7 @@
use std::borrow::Cow;
use mlua::{ExternalError, IntoLua, Lua, Value};
use yazi_fs::expand_path;
use yazi_fs::expand_url;
use yazi_shared::{event::CmdCow, url::Url};
#[derive(Debug)]
@ -12,9 +14,13 @@ pub struct CdOpt {
impl From<CmdCow> for CdOpt {
fn from(mut c: CmdCow) -> Self {
let mut target = c.take_first_url().unwrap_or_default();
if target.is_regular() && !c.bool("raw") {
target = Url::from(expand_path(target));
if !c.bool("raw")
&& let Cow::Owned(u) = expand_url(&target)
{
target = u;
}
Self { target, interactive: c.bool("interactive"), source: CdSource::Cd }
}
}

View file

@ -1,5 +1,7 @@
use std::borrow::Cow;
use mlua::{ExternalError, IntoLua, Lua, Value};
use yazi_fs::expand_path;
use yazi_fs::expand_url;
use yazi_shared::{event::CmdCow, url::Url};
use crate::mgr::CdSource;
@ -14,8 +16,11 @@ pub struct RevealOpt {
impl From<CmdCow> for RevealOpt {
fn from(mut c: CmdCow) -> Self {
let mut target = c.take_first_url().unwrap_or_default();
if target.is_regular() && !c.bool("raw") {
target = Url::from(expand_path(target));
if !c.bool("raw")
&& let Cow::Owned(u) = expand_url(&target)
{
target = u;
}
Self { target, source: CdSource::Reveal, no_dummy: c.bool("no-dummy") }

View file

@ -1,6 +1,8 @@
use std::borrow::Cow;
use mlua::{ExternalError, IntoLua, Lua, Value};
use yazi_boot::BOOT;
use yazi_fs::expand_path;
use yazi_fs::expand_url;
use yazi_shared::{event::CmdCow, url::Url};
#[derive(Debug)]
@ -16,8 +18,10 @@ impl From<CmdCow> for TabCreateOpt {
let Some(mut wd) = c.take_first_url() else {
return Self { wd: Some(Url::from(&BOOT.cwds[0])) };
};
if wd.is_regular() && !c.bool("raw") {
wd = Url::from(expand_path(wd));
if !c.bool("raw")
&& let Cow::Owned(u) = expand_url(&wd)
{
wd = u;
}
Self { wd: Some(wd) }
}

View file

@ -75,7 +75,7 @@ impl Scheduler {
pub fn file_cut(&self, from: Url, mut to: Url, force: bool) {
let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, format!("Cut {from} to {to}"));
let id = ongoing.add(TaskKind::User, format!("Cut {} to {}", from.display(), to.display()));
if to.starts_with(&from) && to != from {
self.new_and_fail(id, "Cannot cut directory into itself").ok();
@ -108,7 +108,10 @@ impl Scheduler {
}
pub fn file_copy(&self, from: Url, mut to: Url, force: bool, follow: bool) {
let id = self.ongoing.lock().add(TaskKind::User, format!("Copy {from} to {to}"));
let id = self
.ongoing
.lock()
.add(TaskKind::User, format!("Copy {} to {}", from.display(), to.display()));
if to.starts_with(&from) && to != from {
self.new_and_fail(id, "Cannot copy directory into itself").ok();
@ -125,7 +128,10 @@ impl Scheduler {
}
pub fn file_link(&self, from: Url, mut to: Url, relative: bool, force: bool) {
let id = self.ongoing.lock().add(TaskKind::User, format!("Link {from} to {to}"));
let id = self
.ongoing
.lock()
.add(TaskKind::User, format!("Link {} to {}", from.display(), to.display()));
let file = self.file.clone();
self.send_micro(id, LOW, async move {
@ -139,7 +145,10 @@ impl Scheduler {
}
pub fn file_hardlink(&self, from: Url, mut to: Url, force: bool, follow: bool) {
let id = self.ongoing.lock().add(TaskKind::User, format!("Hardlink {from} to {to}"));
let id = self
.ongoing
.lock()
.add(TaskKind::User, format!("Hardlink {} to {}", from.display(), to.display()));
if to.starts_with(&from) && to != from {
self.new_and_fail(id, "Cannot hardlink directory into itself").ok();
@ -157,7 +166,7 @@ impl Scheduler {
pub fn file_delete(&self, target: Url) {
let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, format!("Delete {target}"));
let id = ongoing.add(TaskKind::User, format!("Delete {}", target.display()));
ongoing.hooks.add_async(id, {
let target = target.clone();
@ -186,7 +195,7 @@ impl Scheduler {
pub fn file_trash(&self, target: Url) {
let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, format!("Trash {target}"));
let id = ongoing.add(TaskKind::User, format!("Trash {}", target.display()));
ongoing.hooks.add_async(id, {
let target = target.clone();
@ -251,7 +260,8 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock();
for target in targets {
let id = ongoing.add(TaskKind::Preload, format!("Calculate the size of {target}"));
let id =
ongoing.add(TaskKind::Preload, format!("Calculate the size of {}", target.display()));
let target = target.clone();
let throttle = throttle.clone();

View file

@ -2,7 +2,7 @@
yazi_macro::mod_pub!(errors event shell translit url);
yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os osstr rand ro_cell source sync_cell terminal throttle time utf8);
yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os osstr rand ro_cell source string sync_cell terminal throttle time utf8);
pub fn init() {
LOG_LEVEL.replace(<_>::from(std::env::var("YAZI_LOG").unwrap_or_default()));

37
yazi-shared/src/string.rs Normal file
View file

@ -0,0 +1,37 @@
use std::{borrow::Cow, ffi::{OsStr, OsString}};
use crate::url::Url;
pub trait IntoStringLossy {
fn into_string_lossy(self) -> String;
}
impl IntoStringLossy for String {
fn into_string_lossy(self) -> String { self }
}
impl IntoStringLossy for &OsStr {
fn into_string_lossy(self) -> String { self.to_string_lossy().into_owned() }
}
impl IntoStringLossy for OsString {
fn into_string_lossy(self) -> String {
match self.to_string_lossy() {
Cow::Owned(s) => s,
Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(self.into_encoded_bytes()) },
}
}
}
impl IntoStringLossy for Cow<'_, OsStr> {
fn into_string_lossy(self) -> String {
match self {
Cow::Owned(s) => s.into_string_lossy(),
Cow::Borrowed(s) => s.into_string_lossy(),
}
}
}
impl IntoStringLossy for &Url {
fn into_string_lossy(self) -> String { self.os_str().into_string_lossy() }
}

View file

@ -0,0 +1,20 @@
use crate::url::Url;
pub struct Display<'a> {
inner: &'a Url,
}
impl<'a> Display<'a> {
#[inline]
pub fn new(inner: &'a Url) -> Self { Self { inner } }
}
impl<'a> std::fmt::Display for Display<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Url { loc, scheme } = self.inner;
if scheme.is_virtual() {
scheme.fmt(f)?;
}
loc.display().fmt(f)
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(component loc scheme url urn);
yazi_macro::mod_flat!(component display loc scheme url urn);

View file

@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt::Display};
use anyhow::{Result, bail};
use percent_encoding::{CONTROLS, PercentEncode, percent_decode, percent_encode};
use crate::BytesExt;
use crate::{BytesExt, url::Loc};
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Scheme {
@ -19,24 +19,29 @@ pub enum Scheme {
}
impl Scheme {
pub(super) fn parse(bytes: &[u8]) -> Result<(Self, usize)> {
let Some((protocol, rest)) = bytes.split_by_seq(b"://") else {
return Ok((Self::Regular, 0));
pub(super) fn parse(bytes: &[u8]) -> Result<(Self, usize, bool)> {
let Some((mut protocol, rest)) = bytes.split_by_seq(b"://") else {
return Ok((Self::Regular, 0, false));
};
let tilde = protocol.ends_with(b"~");
if tilde {
protocol = &protocol[..protocol.len() - 1];
}
Ok(match protocol {
b"regular" => (Self::Regular, 10),
b"regular" => (Self::Regular, 10, tilde),
b"search" => {
let (name, skip) = Self::decode_param(rest)?;
(Self::Search(name), 9 + skip)
(Self::Search(name), 9 + skip, tilde)
}
b"archive" => {
let (name, skip) = Self::decode_param(rest)?;
(Self::Archive(name), 10 + skip)
(Self::Archive(name), 10 + skip, tilde)
}
b"sftp" => {
let (name, skip) = Self::decode_param(rest)?;
(Self::Sftp(name), 7 + skip)
(Self::Sftp(name), 7 + skip, tilde)
}
_ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)),
})
@ -77,6 +82,17 @@ impl Scheme {
#[inline]
fn encode_param<'a>(s: &'a str) -> PercentEncode<'a> { percent_encode(s.as_bytes(), CONTROLS) }
pub fn encode_tilded(&self, loc: &Loc) -> String {
let loc = percent_encode(loc.as_os_str().as_encoded_bytes(), CONTROLS);
match self {
Self::Regular => format!("regular~://{loc}"),
Self::Search(kw) => format!("search~://{}/{loc}", Self::encode_param(kw)),
Self::SearchItem => format!("search-item~://{loc}"),
Self::Archive(id) => format!("archive~://{}/{loc}", Self::encode_param(id)),
Self::Sftp(id) => format!("sftp~://{}/{loc}", Self::encode_param(id)),
}
}
}
impl Display for Scheme {
@ -84,7 +100,7 @@ impl Display for Scheme {
match self {
Self::Regular => write!(f, "regular://"),
Self::Search(kw) => write!(f, "search://{}/", Self::encode_param(kw)),
Self::SearchItem => write!(f, "search_item://"),
Self::SearchItem => write!(f, "search-item://"),
Self::Archive(id) => write!(f, "archive://{}/", Self::encode_param(id)),
Self::Sftp(id) => write!(f, "sftp://{}/", Self::encode_param(id)),
}

View file

@ -1,12 +1,10 @@
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Display, Formatter}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use percent_encoding::{AsciiSet, CONTROLS, percent_encode};
use percent_encoding::percent_decode;
use serde::{Deserialize, Serialize};
use super::UrnBuf;
use crate::{IntoOsStr, url::{Components, Loc, Scheme}};
const ENCODE_SET: &AsciiSet = &CONTROLS.add(b'#');
use crate::{IntoOsStr, url::{Components, Display, Loc, Scheme}};
#[derive(Clone, Default, Eq, Ord, PartialOrd)]
pub struct Url {
@ -53,9 +51,15 @@ impl TryFrom<&[u8]> for Url {
type Error = anyhow::Error;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let (scheme, skip) = Scheme::parse(bytes)?;
let (scheme, skip, tilde) = Scheme::parse(bytes)?;
Ok(Url { loc: bytes[skip..].into_os_str()?.into(), scheme })
let rest = &bytes[skip + tilde as usize..];
Ok(if tilde {
Self { loc: Cow::from(percent_decode(rest)).into_os_str()?.into(), scheme }
} else {
Self { loc: rest.into_os_str()?.into(), scheme }
})
}
}
@ -80,25 +84,6 @@ impl AsRef<Path> for Url {
fn as_ref(&self) -> &Path { &self.loc }
}
// FIXME: remove
impl Display for Url {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if matches!(self.scheme, Scheme::Regular | Scheme::SearchItem) {
return write!(f, "{}", self.loc.display());
}
let loc = percent_encode(self.loc.as_os_str().as_encoded_bytes(), ENCODE_SET);
write!(f, "{}{loc}", self.scheme)?;
Ok(())
}
}
// FIXME: remove
impl From<&Url> for String {
fn from(url: &Url) -> Self { url.to_string() }
}
impl<'a> From<&'a Url> for Cow<'a, Url> {
fn from(url: &'a Url) -> Self { Cow::Borrowed(url) }
}
@ -149,6 +134,12 @@ impl Url {
self.scheme.covariant(&other.scheme) && self.loc == other.loc
}
#[inline]
pub fn display(&self) -> Display<'_> { Display::new(self) }
#[inline]
pub fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() }
pub fn parent_url(&self) -> Option<Url> {
let parent = self.loc.parent()?;
let base = self.loc.base();
@ -273,7 +264,12 @@ impl PartialEq for Url {
impl Serialize for Url {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
let Url { scheme, loc } = self;
match (scheme.is_virtual(), loc.to_str()) {
(false, Some(s)) => serializer.serialize_str(s),
(true, Some(s)) => serializer.serialize_str(&format!("{scheme}{s}")),
(_, None) => serializer.serialize_str(&scheme.encode_tilded(loc)),
}
}
}