From 9535caad5b6dd015971b7df9516fb413a3655043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 28 Jul 2025 17:21:47 +0800 Subject: [PATCH] fix: make URLs with invalid UTF-8 serializable (#3023) --- Cargo.lock | 33 ++++----- Cargo.toml | 2 +- cspell.json | 2 +- yazi-cli/src/package/delete.rs | 1 - yazi-config/src/popup/confirm.rs | 17 +++++ yazi-config/src/popup/options.rs | 28 ++++---- yazi-config/src/popup/position.rs | 2 +- yazi-fs/src/cha/cha.rs | 2 +- yazi-fs/src/path.rs | 3 + yazi-fs/src/services/services.rs | 114 +++++++++++++++++++++++++----- yazi-parser/src/mgr/cd.rs | 12 +++- yazi-parser/src/mgr/reveal.rs | 11 ++- yazi-parser/src/mgr/tab_create.rs | 10 ++- yazi-scheduler/src/scheduler.rs | 24 +++++-- yazi-shared/src/lib.rs | 2 +- yazi-shared/src/string.rs | 37 ++++++++++ yazi-shared/src/url/display.rs | 20 ++++++ yazi-shared/src/url/mod.rs | 2 +- yazi-shared/src/url/scheme.rs | 34 ++++++--- yazi-shared/src/url/url.rs | 50 ++++++------- 20 files changed, 293 insertions(+), 113 deletions(-) create mode 100644 yazi-shared/src/string.rs create mode 100644 yazi-shared/src/url/display.rs diff --git a/Cargo.lock b/Cargo.lock index 53ecdc9c..9d6d1b63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 239f1556..958f0899 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/cspell.json b/cspell.json index 3d23639a..6cfa834e 100644 --- a/cspell.json +++ b/cspell.json @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs index e4c220a5..c55d1790 100644 --- a/yazi-cli/src/package/delete.rs +++ b/yazi-cli/src/package/delete.rs @@ -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}; diff --git a/yazi-config/src/popup/confirm.rs b/yazi-config/src/popup/confirm.rs index d892838b..17bda3d5 100644 --- a/yazi-config/src/popup/confirm.rs +++ b/yazi-config/src/popup/confirm.rs @@ -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) + } } diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs index 2900bb90..c5c14571 100644 --- a/yazi-config/src/popup/options.rs +++ b/yazi-config/src/popup/options.rs @@ -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>, list: Option>, ) -> 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) -> 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>, + it: impl Iterator, len: usize, max: usize, ) -> Option> { @@ -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)) } diff --git a/yazi-config/src/popup/position.rs b/yazi-config/src/popup/position.rs index b37a2e0a..ce4d1d3b 100644 --- a/yazi-config/src/popup/position.rs +++ b/yazi-config/src/popup/position.rs @@ -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::*; diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 873dcdcf..49a9d459 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -79,7 +79,7 @@ impl Cha { pub fn from_dummy(_url: &Url, ft: Option) -> 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 diff --git a/yazi-fs/src/path.rs b/yazi-fs/src/path.rs index e97aaa43..72187057 100644 --- a/yazi-fs/src/path.rs +++ b/yazi-fs/src/path.rs @@ -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) -> 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) -> 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> { let u: Cow<'a, Url> = url.into(); diff --git a/yazi-fs/src/services/services.rs b/yazi-fs/src/services/services.rs index c797501b..2b8cba8d 100644 --- a/yazi-fs/src/services/services.rs +++ b/yazi-fs/src/services/services.rs @@ -6,94 +6,170 @@ use crate::services::Local; #[inline] pub async fn canonicalize(url: impl AsRef) -> io::Result { - 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) -> io::Result { - 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) -> 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) -> 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, link: impl AsRef) -> 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) -> io::Result { - 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) -> io::Result { - 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) -> io::Result { - 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) -> io::Result { - 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) -> io::Result { - 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) -> 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) -> 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) -> 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, to: impl AsRef) -> 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, link: impl AsRef) -> 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, link: impl AsRef) -> 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) -> io::Result { - 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) -> io::Result { - 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, 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")) + } } diff --git a/yazi-parser/src/mgr/cd.rs b/yazi-parser/src/mgr/cd.rs index d1ced338..b74a7907 100644 --- a/yazi-parser/src/mgr/cd.rs +++ b/yazi-parser/src/mgr/cd.rs @@ -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 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 } } } diff --git a/yazi-parser/src/mgr/reveal.rs b/yazi-parser/src/mgr/reveal.rs index c5938ff2..70805bd8 100644 --- a/yazi-parser/src/mgr/reveal.rs +++ b/yazi-parser/src/mgr/reveal.rs @@ -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 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") } diff --git a/yazi-parser/src/mgr/tab_create.rs b/yazi-parser/src/mgr/tab_create.rs index 4e1da1f4..b8862fa0 100644 --- a/yazi-parser/src/mgr/tab_create.rs +++ b/yazi-parser/src/mgr/tab_create.rs @@ -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 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) } } diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 2320a74d..a0fbed72 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -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(); diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 797400eb..9f4447af 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -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())); diff --git a/yazi-shared/src/string.rs b/yazi-shared/src/string.rs new file mode 100644 index 00000000..4e484e44 --- /dev/null +++ b/yazi-shared/src/string.rs @@ -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() } +} diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs new file mode 100644 index 00000000..76ce1916 --- /dev/null +++ b/yazi-shared/src/url/display.rs @@ -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) + } +} diff --git a/yazi-shared/src/url/mod.rs b/yazi-shared/src/url/mod.rs index 650ea0b7..b9aa25b3 100644 --- a/yazi-shared/src/url/mod.rs +++ b/yazi-shared/src/url/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(component loc scheme url urn); +yazi_macro::mod_flat!(component display loc scheme url urn); diff --git a/yazi-shared/src/url/scheme.rs b/yazi-shared/src/url/scheme.rs index 655d9d19..3034fa4c 100644 --- a/yazi-shared/src/url/scheme.rs +++ b/yazi-shared/src/url/scheme.rs @@ -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)), } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 885aa2e7..7217d9ac 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -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 { - 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 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 { let parent = self.loc.parent()?; let base = self.loc.base(); @@ -273,7 +264,12 @@ impl PartialEq for Url { impl Serialize for Url { fn serialize(&self, serializer: S) -> Result { - 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)), + } } }