From f9abe886ccfc801348be73f62c863beb87fd0bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Tue, 18 Nov 2025 13:56:20 +0800 Subject: [PATCH 01/53] feat: multi-path architecture (#3334) --- Cargo.lock | 1 + Cargo.toml | 2 + cspell.json | 2 +- yazi-actor/src/cmp/show.rs | 18 +- yazi-actor/src/cmp/trigger.rs | 44 +-- yazi-actor/src/lives/file.rs | 7 +- yazi-actor/src/lives/tab.rs | 4 +- yazi-actor/src/mgr/bulk_rename.rs | 29 +- yazi-actor/src/mgr/copy.rs | 16 +- yazi-actor/src/mgr/create.rs | 5 +- yazi-actor/src/mgr/download.rs | 4 +- yazi-actor/src/mgr/enter.rs | 2 +- yazi-actor/src/mgr/follow.rs | 9 +- yazi-actor/src/mgr/hover.rs | 2 +- yazi-actor/src/mgr/leave.rs | 2 +- yazi-actor/src/mgr/refresh.rs | 4 +- yazi-actor/src/mgr/rename.rs | 7 +- yazi-actor/src/mgr/search.rs | 4 +- yazi-actor/src/mgr/tab_create.rs | 7 +- yazi-actor/src/mgr/update_files.rs | 4 +- yazi-adapter/src/image.rs | 2 +- yazi-adapter/src/lib.rs | 2 +- yazi-binding/src/lib.rs | 4 +- yazi-binding/src/macros.rs | 5 +- yazi-binding/src/{urn.rs => path.rs} | 11 +- yazi-binding/src/scheme.rs | 6 +- yazi-binding/src/url.rs | 62 ++-- yazi-boot/src/boot.rs | 21 +- yazi-cli/src/package/delete.rs | 10 +- yazi-cli/src/package/dependency.rs | 8 +- yazi-cli/src/package/deploy.rs | 14 +- yazi-cli/src/package/hash.rs | 12 +- yazi-cli/src/package/package.rs | 4 +- yazi-cli/src/shared/shared.rs | 9 +- yazi-config/src/mgr/mgr.rs | 2 +- yazi-config/src/pattern.rs | 39 ++- yazi-config/src/preview/preview.rs | 2 +- yazi-config/src/theme/icon.rs | 6 +- yazi-config/src/theme/theme.rs | 2 +- yazi-config/src/vfs/provider.rs | 4 +- yazi-core/src/lib.rs | 7 +- yazi-core/src/tab/selected.rs | 74 ++--- yazi-core/src/tasks/file.rs | 28 +- yazi-dds/src/ember/bulk.rs | 16 +- yazi-dds/src/pubsub.rs | 4 +- yazi-dds/src/state.rs | 6 +- yazi-dds/src/stream.rs | 2 +- yazi-fm/src/app/commands/bootstrap.rs | 6 +- yazi-fm/src/app/commands/quit.rs | 4 +- yazi-fm/src/cmp/cmp.rs | 3 +- yazi-fm/src/dispatcher.rs | 7 +- yazi-fm/src/main.rs | 2 +- yazi-fs/src/cha/cha.rs | 9 +- yazi-fs/src/cha/kind.rs | 12 +- yazi-fs/src/cwd.rs | 7 +- yazi-fs/src/file.rs | 32 +- yazi-fs/src/files.rs | 3 +- yazi-fs/src/filter.rs | 12 +- yazi-fs/src/lib.rs | 2 +- yazi-fs/src/op.rs | 22 +- yazi-fs/src/path/clean.rs | 30 +- yazi-fs/src/path/expand.rs | 84 +++-- yazi-fs/src/path/path.rs | 22 -- yazi-fs/src/path/percent.rs | 16 +- yazi-fs/src/path/relative.rs | 27 +- yazi-fs/src/provider/local/dir_entry.rs | 49 ++- yazi-fs/src/provider/local/gate.rs | 23 +- yazi-fs/src/provider/local/local.rs | 246 ++++++-------- yazi-fs/src/provider/local/read_dir.rs | 16 +- yazi-fs/src/provider/traits.rs | 210 +++++------- yazi-fs/src/scheme.rs | 12 +- yazi-fs/src/sorter.rs | 6 +- yazi-fs/src/url.rs | 40 ++- yazi-parser/src/cmp/show.rs | 10 +- yazi-parser/src/mgr/copy.rs | 18 +- yazi-plugin/src/external/fd.rs | 7 +- yazi-plugin/src/external/highlighter.rs | 2 +- yazi-plugin/src/external/rg.rs | 7 +- yazi-plugin/src/external/rga.rs | 7 +- yazi-plugin/src/fs/fs.rs | 2 +- yazi-plugin/src/lib.rs | 2 +- yazi-plugin/src/loader/loader.rs | 7 +- yazi-plugin/src/utils/image.rs | 2 +- yazi-plugin/src/utils/text.rs | 2 +- yazi-scheduler/src/file/file.rs | 33 +- yazi-scheduler/src/lib.rs | 2 +- yazi-scheduler/src/scheduler.rs | 31 +- yazi-shared/Cargo.toml | 1 + yazi-shared/src/bytes.rs | 53 ++- yazi-shared/src/chars.rs | 22 +- yazi-shared/src/lib.rs | 4 +- yazi-shared/src/loc/buf.rs | 65 ++-- yazi-shared/src/loc/loc.rs | 66 ++-- yazi-shared/src/osstr.rs | 35 -- yazi-shared/src/path/buf.rs | 47 +-- yazi-shared/src/path/conversion.rs | 105 ++++++ yazi-shared/src/path/cow.rs | 56 ++++ yazi-shared/src/path/dyn.rs | 403 +++++++++++++++++------ yazi-shared/src/path/error.rs | 82 +++++ yazi-shared/src/path/inner.rs | 15 - yazi-shared/src/path/kind.rs | 16 + yazi-shared/src/path/mod.rs | 2 +- yazi-shared/src/path/path.rs | 153 +++++++-- yazi-shared/src/path/traits.rs | 154 --------- yazi-shared/src/path/unsafe.rs | 23 ++ yazi-shared/src/path/view.rs | 39 +++ yazi-shared/src/pool/cow.rs | 96 ++++++ yazi-shared/src/pool/mod.rs | 2 +- yazi-shared/src/predictor.rs | 29 ++ yazi-shared/src/scheme/cow.rs | 167 ++++++---- yazi-shared/src/scheme/encode.rs | 63 ++++ yazi-shared/src/scheme/kind.rs | 92 ++++++ yazi-shared/src/scheme/mod.rs | 2 +- yazi-shared/src/scheme/ref.rs | 113 ++++--- yazi-shared/src/scheme/scheme.rs | 26 +- yazi-shared/src/scheme/traits.rs | 14 +- yazi-shared/src/strand/conversion.rs | 159 +++++++++ yazi-shared/src/strand/cow.rs | 75 +++++ yazi-shared/src/strand/dyn.rs | 177 ++++++++++ yazi-shared/src/strand/error.rs | 24 ++ yazi-shared/src/strand/kind.rs | 5 + yazi-shared/src/strand/mod.rs | 1 + yazi-shared/src/strand/strand.rs | 168 ++++++++++ yazi-shared/src/strand/view.rs | 51 +++ yazi-shared/src/url/buf.rs | 135 +++++--- yazi-shared/src/url/component.rs | 68 ++-- yazi-shared/src/url/cov.rs | 6 +- yazi-shared/src/url/cow.rs | 233 +++++++++---- yazi-shared/src/url/display.rs | 6 +- yazi-shared/src/url/encode.rs | 98 +----- yazi-shared/src/url/traits.rs | 67 +++- yazi-shared/src/url/url.rs | 390 +++++++++++++++------- yazi-shared/src/wtf8.rs | 72 ++++ yazi-term/src/lib.rs | 2 - yazi-vfs/src/fns.rs | 33 +- yazi-vfs/src/lib.rs | 2 +- yazi-vfs/src/provider/dir_entry.rs | 53 ++- yazi-vfs/src/provider/gate.rs | 111 +++---- yazi-vfs/src/provider/provider.rs | 118 +++---- yazi-vfs/src/provider/providers.rs | 309 +++++++---------- yazi-vfs/src/provider/read_dir.rs | 17 +- yazi-vfs/src/provider/sftp/conn.rs | 176 ++++++++++ yazi-vfs/src/provider/sftp/gate.rs | 109 +++--- yazi-vfs/src/provider/sftp/mod.rs | 6 +- yazi-vfs/src/provider/sftp/read_dir.rs | 33 +- yazi-vfs/src/provider/sftp/sftp.rs | 369 ++++++--------------- yazi-watcher/src/backend.rs | 4 +- yazi-watcher/src/local/linked.rs | 4 +- yazi-watcher/src/local/local.rs | 2 +- yazi-watcher/src/reporter.rs | 21 +- yazi-watcher/src/watched.rs | 23 +- yazi-widgets/src/clipboard.rs | 38 +-- yazi-widgets/src/input/commands/paste.rs | 2 +- 153 files changed, 4264 insertions(+), 2417 deletions(-) rename yazi-binding/src/{urn.rs => path.rs} (56%) create mode 100644 yazi-shared/src/path/conversion.rs create mode 100644 yazi-shared/src/path/cow.rs create mode 100644 yazi-shared/src/path/error.rs delete mode 100644 yazi-shared/src/path/inner.rs create mode 100644 yazi-shared/src/path/kind.rs delete mode 100644 yazi-shared/src/path/traits.rs create mode 100644 yazi-shared/src/path/unsafe.rs create mode 100644 yazi-shared/src/path/view.rs create mode 100644 yazi-shared/src/pool/cow.rs create mode 100644 yazi-shared/src/predictor.rs create mode 100644 yazi-shared/src/scheme/encode.rs create mode 100644 yazi-shared/src/scheme/kind.rs create mode 100644 yazi-shared/src/strand/conversion.rs create mode 100644 yazi-shared/src/strand/cow.rs create mode 100644 yazi-shared/src/strand/dyn.rs create mode 100644 yazi-shared/src/strand/error.rs create mode 100644 yazi-shared/src/strand/kind.rs create mode 100644 yazi-shared/src/strand/mod.rs create mode 100644 yazi-shared/src/strand/strand.rs create mode 100644 yazi-shared/src/strand/view.rs create mode 100644 yazi-shared/src/wtf8.rs create mode 100644 yazi-vfs/src/provider/sftp/conn.rs diff --git a/Cargo.lock b/Cargo.lock index ecd39549..d8013395 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5072,6 +5072,7 @@ dependencies = [ "parking_lot", "percent-encoding", "serde", + "thiserror 2.0.17", "tokio", "uzers", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index 55fb0ce3..d9e4b1e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.145" syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } +thiserror = "2.0.17" tokio = { version = "1.48.0", features = [ "full" ] } tokio-stream = "0.1.17" tokio-util = "0.7.17" @@ -61,4 +62,5 @@ uzers = "0.12.1" format_push_string = "warn" implicit_clone = "warn" module_inception = "allow" +unit_arg = "allow" use_self = "warn" diff --git a/cspell.json b/cspell.json index c55d3a1a..5631c351 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"language":"en","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","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied"],"version":"0.2"} \ No newline at end of file +{"version":"0.2","flagWords":[],"language":"en","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","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool"]} \ No newline at end of file diff --git a/yazi-actor/src/cmp/show.rs b/yazi-actor/src/cmp/show.rs index 99645d87..3e151c1c 100644 --- a/yazi-actor/src/cmp/show.rs +++ b/yazi-actor/src/cmp/show.rs @@ -1,9 +1,9 @@ -use std::{ffi::OsStr, mem, ops::ControlFlow}; +use std::{mem, ops::ControlFlow}; use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt}; -use yazi_shared::{data::Data, osstr_contains, osstr_starts_with}; +use yazi_shared::{data::Data, path::{AsPathDyn, PathDyn, PathLike}, strand::{AsStrand, StrandLike}}; use crate::{Actor, Ctx}; @@ -30,7 +30,7 @@ impl Actor for Show { }; cmp.ticket = opt.ticket; - cmp.cands = Self::match_candidates(opt.word.as_os_str(), cache); + cmp.cands = Self::match_candidates(opt.word.as_path_dyn(), cache); if cmp.cands.is_empty() { succ!(render!(mem::replace(&mut cmp.visible, false))); } @@ -43,16 +43,20 @@ impl Actor for Show { } impl Show { - fn match_candidates(word: &OsStr, cache: &[CmpItem]) -> Vec { - let smart = !word.as_encoded_bytes().iter().any(|c| c.is_ascii_uppercase()); + fn match_candidates(word: PathDyn, cache: &[CmpItem]) -> Vec { + let smart = !word.encoded_bytes().iter().any(|&b| b.is_ascii_uppercase()); let flow = cache.iter().try_fold((Vec::new(), Vec::new()), |(mut exact, mut fuzzy), item| { - if osstr_starts_with(&item.name, word, smart) { + let name = item.name.as_strand(); + let starts_with = + if smart { name.eq_ignore_ascii_case(word) } else { name.starts_with(word) }; + + if starts_with { exact.push(item); if exact.len() >= LIMIT { return ControlFlow::Break((exact, fuzzy)); } - } else if fuzzy.len() < LIMIT - exact.len() && osstr_contains(&item.name, word) { + } else if fuzzy.len() < LIMIT - exact.len() && name.contains(word) { // Here we don't break the control flow, since we want more exact matching. fuzzy.push(item) } diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index a5ac077d..3cc13427 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -1,11 +1,11 @@ -use std::{ffi::OsString, mem, path::{MAIN_SEPARATOR_STR, PathBuf}}; +use std::{mem, path::MAIN_SEPARATOR_STR}; use anyhow::Result; use yazi_fs::{CWD, path::expand_url, provider::{DirReader, FileHolder}}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_proxy::CmpProxy; -use yazi_shared::{OsStrSplit, data::Data, natsort, scheme::SchemeLike, url::{UrlBuf, UrlCow}}; +use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::StrandBufLike, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::provider; use crate::{Actor, Ctx}; @@ -32,7 +32,7 @@ impl Actor for Trigger { if cmp.caches.contains_key(&parent) { let ticket = cmp.ticket; - return act!(cmp:show, cx, ShowOpt { cache_name: parent, word, ticket, ..Default::default() }); + return act!(cmp:show, cx, ShowOpt { cache: vec![], cache_name: parent, word, ticket }); } let ticket = cmp.ticket; @@ -42,8 +42,8 @@ impl Actor for Trigger { // "/" is both a directory separator and the root directory per se // As there's no parent directory for the FS root, it is a special case - if parent.loc.as_os_str() == "/" { - cache.push(CmpItem { name: OsString::new(), is_dir: true }); + if parent.loc() == "/" { + cache.push(CmpItem { name: Default::default(), is_dir: true }); } while let Ok(Some(ent)) = dir.next().await { @@ -53,9 +53,8 @@ impl Actor for Trigger { } if !cache.is_empty() { - cache.sort_unstable_by(|a, b| { - natsort(a.name.as_encoded_bytes(), b.name.as_encoded_bytes(), false) - }); + cache + .sort_unstable_by(|a, b| natsort(a.name.encoded_bytes(), b.name.encoded_bytes(), false)); CmpProxy::show(ShowOpt { cache, cache_name: parent, word, ticket }); } @@ -67,23 +66,28 @@ impl Actor for Trigger { } impl Trigger { - fn split_url(s: &str) -> Option<(UrlBuf, PathBuf)> { - let (scheme, path, ..) = UrlCow::parse(s.as_bytes()).ok()?; + fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> { + let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?; - if scheme.is_local() && path.as_os_str() == "~" { + if scheme.is_local() && path == "~" { return None; // We don't autocomplete a `~`, but `~/` } - #[cfg(windows)] - const SEP: &[char] = &['/', '\\']; - #[cfg(not(windows))] - const SEP: char = std::path::MAIN_SEPARATOR; + let sep = if cfg!(windows) { + AnyAsciiChar::new(&[b'/', b'\\']).unwrap() + } else { + AnyAsciiChar::new(&[b'/']).unwrap() + }; - Some(match path.as_os_str().rsplit_once(SEP) { + Some(match path.as_path().rsplit_pred(sep) { Some((p, c)) if p.is_empty() => { - (UrlBuf { loc: MAIN_SEPARATOR_STR.into(), scheme: scheme.into() }, c.into()) + let root = PathBufDyn::with(scheme.kind(), MAIN_SEPARATOR_STR).expect("valid root"); + (UrlCow::try_from((scheme, root)).ok()?.into_owned(), c.into()) + } + Some((p, c)) => { + let parent = PathBufDyn::with(scheme.kind(), p.as_dyn()).expect("valid parent"); + (expand_url(UrlCow::try_from((scheme, parent)).ok()?), c.into()) } - Some((p, c)) => (expand_url(UrlBuf { loc: p.into(), scheme: scheme.into() }), c.into()), None => (CWD.load().as_ref().clone(), path.into()), }) } @@ -91,13 +95,13 @@ impl Trigger { #[cfg(test)] mod tests { - use yazi_shared::url::UrlLike; + use yazi_shared::{path::PathBufLike, url::UrlLike}; use super::*; fn compare(s: &str, parent: &str, child: &str) { let (mut p, c) = Trigger::split_url(s).unwrap(); - if let Some(u) = p.strip_prefix(yazi_fs::CWD.load().as_ref()) { + if let Ok(u) = p.try_strip_prefix(yazi_fs::CWD.load().as_ref()) { p = UrlBuf::from(u); } assert_eq!((p, c.to_str().unwrap()), (parent.parse().unwrap(), child)); diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index aeca4b56..fc6aa3b7 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -4,7 +4,7 @@ use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods, Valu use yazi_binding::Style; use yazi_config::THEME; use yazi_plugin::bindings::Range; -use yazi_shared::url::UrlLike; +use yazi_shared::{path::PathLike, url::UrlLike}; use super::Lives; use crate::lives::PtrCell; @@ -88,11 +88,8 @@ impl UserData for File { if !me.url.has_trail() { return Ok(None); } - let Some(path) = me.url.as_path() else { - return Ok(None); - }; - let mut comp = path.strip_prefix(me.url.loc.trail()).unwrap_or(path).components(); + let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components(); comp.next_back(); Some(lua.create_string(comp.as_path().as_os_str().as_encoded_bytes())).transpose() }); diff --git a/yazi-actor/src/lives/tab.rs b/yazi-actor/src/lives/tab.rs index 6c97fbb3..311af5b9 100644 --- a/yazi-actor/src/lives/tab.rs +++ b/yazi-actor/src/lives/tab.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods, Value}; use yazi_binding::{Id, UrlRef, cached_field}; -use yazi_shared::url::UrlLike; +use yazi_shared::{path::PathLike, strand::StrandLike, url::UrlLike}; use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected}; @@ -47,7 +47,7 @@ impl UserData for Tab { fields.add_field_method_get("id", |_, me| Ok(Id(me.id))); cached_field!(fields, name, |lua, me| { let url = &me.current.url; - lua.create_string(url.name().unwrap_or(url.loc.as_os_str()).as_encoded_bytes()) + lua.create_string(url.name().map_or_else(|| url.loc().encoded_bytes(), |n| n.encoded_bytes())) }); cached_field!(fields, mode, |_, me| Mode::make(&me.mode)); diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index bf178b5e..2742c867 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -11,7 +11,7 @@ use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider use yazi_macro::{err, succ}; use yazi_parser::VoidOpt; use yazi_proxy::{AppProxy, HIDER, TasksProxy}; -use yazi_shared::{OsStrJoin, data::Data, terminal_clear, url::{AsUrl, Component, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{OsStrJoin, data::Data, path::PathDyn, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_term::tty::TTY; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; @@ -50,7 +50,12 @@ impl Actor for BulkRename { .write_all(old.join(OsStr::new("\n")).as_encoded_bytes()) .await?; - defer! { tokio::spawn(Local.remove_file(tmp.clone())); } + defer! { + let tmp = tmp.clone(); + tokio::spawn(async move { + Local::regular(&tmp).remove_file().await + }); + } TasksProxy::process_exec( cwd.into(), Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run), @@ -64,8 +69,8 @@ impl Actor for BulkRename { defer!(AppProxy::resume()); AppProxy::stop().await; - let new: Vec<_> = Local - .read_to_string(&tmp) + let new: Vec<_> = Local::regular(&tmp) + .read_to_string() .await? .lines() .take(old.len()) @@ -120,10 +125,12 @@ impl BulkRename { let permit = WATCHER.acquire().await.unwrap(); let (mut failed, mut succeeded) = (Vec::new(), HashMap::with_capacity(todo.len())); for (o, n) in todo { - let (old, new): (UrlBuf, UrlBuf) = ( - selected[o.0].components().take(root).chain([Component::Normal(&o)]).collect(), - selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(), - ); + let (Ok(old), Ok(new)) = + (Self::replace_url(&selected[o.0], root, &o), Self::replace_url(&selected[n.0], root, &n)) + else { + failed.push((o, n, anyhow!("Invalid new or old file name"))); + continue; + }; if maybe_exists(&new).await && !provider::must_identical(&old, &new).await { failed.push((o, n, anyhow!("Destination already exists"))); @@ -137,7 +144,7 @@ impl BulkRename { } if !succeeded.is_empty() { - let it = succeeded.iter().map(|(o, n)| (o, &n.url)); + let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url())); err!(Pubsub::pub_after_bulk(it)); FilesOp::rename(succeeded); } @@ -153,6 +160,10 @@ impl BulkRename { YAZI.opener.block(YAZI.open.all(Path::new("bulk-rename.txt"), "text/plain")) } + fn replace_url(url: &UrlBuf, take: usize, rep: &OsStr) -> Result { + Ok(url.try_replace(take, PathDyn::with(url.kind(), rep)?)?.into_owned()) + } + async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { let mut stdout = TTY.lockout(); terminal_clear(&mut *stdout)?; diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index 152a5565..88402c93 100644 --- a/yazi-actor/src/mgr/copy.rs +++ b/yazi-actor/src/mgr/copy.rs @@ -1,5 +1,3 @@ -use std::ffi::OsString; - use anyhow::{Result, bail}; use yazi_macro::{act, succ}; use yazi_parser::mgr::CopyOpt; @@ -18,7 +16,7 @@ impl Actor for Copy { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; - let mut s = OsString::new(); + let mut s = Vec::::new(); let mut it = if opt.hovered { Box::new(cx.hovered().map(|h| &h.url).into_iter()) } else { @@ -30,29 +28,29 @@ impl Actor for Copy { match opt.r#type.as_ref() { // TODO: rename to "url" "path" => { - s.push(opt.separator.transform(&u.os_str())); + s.extend_from_slice(&opt.separator.transform(&u.os_str())); } "dirname" => { if let Some(p) = u.parent() { - s.push(opt.separator.transform(&p.os_str())); + s.extend_from_slice(&opt.separator.transform(&p.os_str())); } } "filename" => { - s.push(opt.separator.transform(u.name().unwrap_or_default())); + s.extend_from_slice(&opt.separator.transform(&u.name().unwrap_or_default())); } "name_without_ext" => { - s.push(opt.separator.transform(u.stem().unwrap_or_default())); + s.extend_from_slice(&opt.separator.transform(&u.stem().unwrap_or_default())); } _ => bail!("Unknown copy type: {}", opt.r#type), }; if it.peek().is_some() { - s.push("\n"); + s.push(b'\n'); } } // Copy the CWD path regardless even if the directory is empty if s.is_empty() && opt.r#type == "dirname" { - s.push(opt.separator.transform(&cx.cwd().os_str())); + s.extend_from_slice(&opt.separator.transform(&cx.cwd().os_str())); } futures::executor::block_on(CLIPBOARD.set(s)); diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 0b73d6c9..733617f1 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -27,7 +27,10 @@ impl Actor for Create { return; } - let new = cwd.join(&name); + let Ok(new) = cwd.try_join(&name) else { + return; + }; + if !opt.force && maybe_exists(&new).await && !ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await diff --git a/yazi-actor/src/mgr/download.rs b/yazi-actor/src/mgr/download.rs index a0c1090b..cbeb0086 100644 --- a/yazi-actor/src/mgr/download.rs +++ b/yazi-actor/src/mgr/download.rs @@ -8,7 +8,7 @@ use yazi_fs::{File, FsScheme, provider::{Provider, local::Local}}; use yazi_macro::succ; use yazi_parser::mgr::{DownloadOpt, OpenOpt}; use yazi_proxy::MgrProxy; -use yazi_shared::{data::Data, url::UrlCow}; +use yazi_shared::{data::Data, url::{UrlCow, UrlLike}}; use yazi_vfs::VfsFile; use crate::{Actor, Ctx}; @@ -78,7 +78,7 @@ impl Download { let roots: HashSet<_> = urls.iter().filter_map(|u| u.scheme().cache()).collect(); for mut root in roots { root.push("%lock"); - Local.create_dir_all(root).await.ok(); + Local::regular(&root).create_dir_all().await.ok(); } } } diff --git a/yazi-actor/src/mgr/enter.rs b/yazi-actor/src/mgr/enter.rs index efece673..4ec6ef85 100644 --- a/yazi-actor/src/mgr/enter.rs +++ b/yazi-actor/src/mgr/enter.rs @@ -15,7 +15,7 @@ impl Actor for Enter { fn act(cx: &mut Ctx, _: Self::Options) -> Result { let Some(h) = cx.hovered().filter(|h| h.is_dir()) else { succ!() }; - let url = if h.url.is_search() { h.url.to_regular() } else { h.url.clone() }; + let url = if h.url.is_search() { h.url.to_regular()? } else { h.url.clone() }; act!(mgr:cd, cx, (url, CdSource::Enter)) } diff --git a/yazi-actor/src/mgr/follow.rs b/yazi-actor/src/mgr/follow.rs index 00e5f4fc..c79a3bf9 100644 --- a/yazi-actor/src/mgr/follow.rs +++ b/yazi-actor/src/mgr/follow.rs @@ -16,11 +16,8 @@ impl Actor for Follow { fn act(cx: &mut Ctx, _: Self::Options) -> Result { let Some(file) = cx.hovered() else { succ!() }; let Some(link_to) = &file.link_to else { succ!() }; - - if let Some(p) = file.url.parent() { - act!(mgr:reveal, cx, clean_url(p.join(link_to))) - } else { - succ!() - } + let Some(parent) = file.url.parent() else { succ!() }; + let Ok(joined) = parent.try_join(link_to) else { succ!() }; + act!(mgr:reveal, cx, clean_url(joined)) } } diff --git a/yazi-actor/src/mgr/hover.rs b/yazi-actor/src/mgr/hover.rs index 75e8afdb..51c3b379 100644 --- a/yazi-actor/src/mgr/hover.rs +++ b/yazi-actor/src/mgr/hover.rs @@ -18,7 +18,7 @@ impl Actor for Hover { // Parent should always track CWD if let Some(p) = &mut tab.parent { - render!(p.repos(tab.current.url.strip_prefix(&p.url))); + render!(p.repos(tab.current.url.try_strip_prefix(&p.url).ok())); } // Repos CWD diff --git a/yazi-actor/src/mgr/leave.rs b/yazi-actor/src/mgr/leave.rs index 8de02c97..c83d2f23 100644 --- a/yazi-actor/src/mgr/leave.rs +++ b/yazi-actor/src/mgr/leave.rs @@ -21,7 +21,7 @@ impl Actor for Leave { let Some(mut url) = url else { succ!() }; if url.is_search() { - url = url.into_regular(); + url = url.as_regular()?; } act!(mgr:cd, cx, (url, CdSource::Leave)) diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index ed9b231c..b1580cda 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -6,7 +6,7 @@ use yazi_fs::{CWD, Files, FilesOp, cha::Cha}; use yazi_macro::{act, succ}; use yazi_parser::VoidOpt; use yazi_proxy::MgrProxy; -use yazi_shared::{data::Data, scheme::SchemeLike, url::UrlBuf}; +use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; use yazi_term::tty::TTY; use yazi_vfs::{VfsFiles, VfsFilesOp}; @@ -41,7 +41,7 @@ impl Actor for Refresh { impl Refresh { fn cwd_changed() { - if CWD.load().scheme.is_virtual() { + if CWD.load().kind().is_virtual() { MgrProxy::watch(); } } diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 068f7e47..ef60c7a2 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -5,7 +5,7 @@ use yazi_fs::{File, FilesOp}; use yazi_macro::{act, err, ok_or_not_found, succ}; use yazi_parser::mgr::RenameOpt; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; -use yazi_shared::{Id, data::Data, path::PathLike, url::{UrlBuf, UrlLike}}; +use yazi_shared::{Id, data::Data, path::PathLike, strand::StrandLike, url::{UrlBuf, UrlLike}}; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; @@ -49,7 +49,10 @@ impl Actor for Rename { return; } - let new = old.parent().unwrap().join(name); + let Some(Ok(new)) = old.parent().map(|u| u.try_join(name)) else { + return; + }; + if opt.force || !maybe_exists(&new).await || provider::must_identical(&old, &new).await { Self::r#do(tab, old, new).await.ok(); } else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await { diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index 224531d6..44b7571b 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -52,7 +52,7 @@ impl Actor for SearchDo { handle.abort(); } - let cwd = tab.cwd().to_search(opt.subject.as_ref()); + let cwd = tab.cwd().to_search(opt.subject.as_ref())?; let hidden = tab.pref.show_hidden; tab.search = Some(tokio::spawn(async move { @@ -111,7 +111,7 @@ impl Actor for SearchStop { succ!(); } - let rep = tab.history.remove_or(tab.cwd().to_regular()); + let rep = tab.history.remove_or(tab.cwd().to_regular()?); drop(mem::replace(&mut tab.current, rep)); act!(mgr:hidden, cx)?; diff --git a/yazi-actor/src/mgr/tab_create.rs b/yazi-actor/src/mgr/tab_create.rs index 9ddbcea9..2461f6e2 100644 --- a/yazi-actor/src/mgr/tab_create.rs +++ b/yazi-actor/src/mgr/tab_create.rs @@ -29,10 +29,13 @@ impl Actor for TabCreate { (true, wd.into_owned()) } else if let Some(h) = cx.hovered() { tab.pref = cx.tab().pref.clone(); - (false, h.url.to_regular()) + (false, h.url.clone()) + } else if cx.cwd().is_search() { + tab.pref = cx.tab().pref.clone(); + (true, cx.cwd().to_regular()?) } else { tab.pref = cx.tab().pref.clone(); - (true, cx.cwd().to_regular()) + (true, cx.cwd().clone()) }; let tabs = &mut cx.mgr.tabs; diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index b785e38a..e2c1479e 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -17,7 +17,9 @@ impl Actor for UpdateFiles { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let revision = cx.current().files.revision; - let linked: Vec<_> = LINKED.read().from_dir(opt.op.cwd()).map(|u| opt.op.chdir(u)).collect(); + let linked: Vec<_> = + LINKED.read().from_dir(opt.op.cwd()).filter_map(|u| opt.op.chdir(u).ok()).collect(); + for op in [opt.op].into_iter().chain(linked) { cx.mgr.yanked.apply_op(&op); Self::update_tab(cx, op).ok(); diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index 7a058f6f..0eabe67e 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -39,7 +39,7 @@ impl Image { }) .await??; - Ok(Local.write(cache, buf).await?) + Ok(Local::regular(&cache).write(buf).await?) } pub(super) async fn downscale(path: PathBuf, rect: Rect) -> Result { diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index 3decaa14..cd0d3303 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::option_map_unit_fn, clippy::unit_arg)] +#![allow(clippy::option_map_unit_fn)] yazi_macro::mod_pub!(drivers); diff --git a/yazi-binding/src/lib.rs b/yazi-binding/src/lib.rs index 2d1ffdc5..aa051c39 100644 --- a/yazi-binding/src/lib.rs +++ b/yazi-binding/src/lib.rs @@ -1,7 +1,5 @@ -#![allow(clippy::unit_arg)] - mod macros; yazi_macro::mod_pub!(elements); -yazi_macro::mod_flat!(cha color composer error file icon id iter permit runtime scheme stage style url urn utils); +yazi_macro::mod_flat!(cha color composer error file icon id iter path permit runtime scheme stage style url utils); diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index 6bc50e69..7e5c69b4 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -151,10 +151,11 @@ macro_rules! impl_file_fields { ($fields:ident) => { $crate::cached_field!($fields, cha, |_, me| Ok($crate::Cha(me.cha))); $crate::cached_field!($fields, url, |_, me| Ok($crate::Url::new(me.url_owned()))); - $crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to.clone().map($crate::Url::new))); + $crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to_url().map($crate::Url::new))); $crate::cached_field!($fields, name, |lua, me| { - me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + use yazi_shared::strand::StrandLike; + me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); $crate::cached_field!($fields, cache, |_, me| { use yazi_fs::FsUrl; diff --git a/yazi-binding/src/urn.rs b/yazi-binding/src/path.rs similarity index 56% rename from yazi-binding/src/urn.rs rename to yazi-binding/src/path.rs index 6463ca87..edcce546 100644 --- a/yazi-binding/src/urn.rs +++ b/yazi-binding/src/path.rs @@ -1,7 +1,7 @@ use std::ops::Deref; -use mlua::{ExternalError, FromLua, Lua, UserData, Value}; -use yazi_shared::path::PathBufDyn; +use mlua::{ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataMethods, Value}; +use yazi_shared::path::{PathBufDyn, PathBufLike}; pub struct Path(pub PathBufDyn); @@ -24,4 +24,9 @@ impl FromLua for Path { } } -impl UserData for Path {} +impl UserData for Path { + fn add_methods>(methods: &mut M) { + methods + .add_meta_method(MetaMethod::ToString, |lua, me, ()| lua.create_string(me.encoded_bytes())); + } +} diff --git a/yazi-binding/src/scheme.rs b/yazi-binding/src/scheme.rs index defec5fc..8ceecc57 100644 --- a/yazi-binding/src/scheme.rs +++ b/yazi-binding/src/scheme.rs @@ -20,14 +20,14 @@ impl Deref for Scheme { } impl Scheme { - pub fn new(scheme: &yazi_shared::scheme::Scheme) -> Self { - Self { inner: scheme.clone(), v_kind: None, v_cache: None } + pub fn new(scheme: impl Into) -> Self { + Self { inner: scheme.into(), v_kind: None, v_cache: None } } } impl UserData for Scheme { fn add_fields>(fields: &mut F) { - cached_field!(fields, kind, |_, me| Ok(me.kind())); + cached_field!(fields, kind, |_, me| Ok(me.kind().as_str())); cached_field!(fields, cache, |_, me| Ok(me.cache().map(Url::new))); fields.add_field_method_get("is_virtual", |_, me| Ok(me.is_virtual())); diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 4c75f7ea..c86d34f2 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -1,8 +1,8 @@ use std::ops::Deref; -use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use yazi_fs::{FsHash64, FsHash128}; -use yazi_shared::{IntoOsStr, scheme::SchemeLike, url::{AsUrl, UrlCow, UrlLike}}; +use yazi_shared::{path::{PathLike, StripPrefixError}, strand::{StrandCow, StrandLike}, url::{AsUrl, UrlCow, UrlLike}}; use crate::{Scheme, cached_field, deprecate}; @@ -99,26 +99,28 @@ impl FromLua for Url { impl UserData for Url { fn add_fields>(fields: &mut F) { cached_field!(fields, name, |lua, me| { - me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, stem, |lua, me| { - me.stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + me.stem().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, ext, |lua, me| { - me.ext().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + me.ext().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, parent, |_, me| Ok(me.parent().map(Self::new))); cached_field!(fields, urn, |_, me| Ok(super::Path::new(me.urn()))); - cached_field!(fields, base, |_, me| Ok(me.base().map(Self::new))); + cached_field!(fields, base, |_, me| { + Ok(Some(me.base()).filter(|u| !u.loc().is_empty()).map(Self::new)) + }); - cached_field!(fields, scheme, |_, me| Ok(Scheme::new(&me.scheme))); + cached_field!(fields, scheme, |_, me| Ok(Scheme::new(me.scheme()))); cached_field!(fields, domain, |lua, me| { - me.scheme.domain().map(|s| lua.create_string(s)).transpose() + me.scheme().domain().map(|s| lua.create_string(s)).transpose() }); fields.add_field_method_get("frag", |lua, me| { deprecate!(lua, "`frag` property of Url is deprecated and renamed to `domain`, please use the new name instead, in your {}"); - me.scheme.domain().map(|s| lua.create_string(s)).transpose() + me.scheme().domain().map(|s| lua.create_string(s)).transpose() }); fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); @@ -138,42 +140,60 @@ impl UserData for Url { }); methods.add_method("join", |_, me, other: Value| { Ok(Self::new(match other { - Value::String(s) => me.join(s.as_bytes().into_os_str()?), + Value::String(s) => { + let b = s.as_bytes(); + me.try_join(StrandCow::with(me.kind(), &*b).into_lua_err()?).into_lua_err()? + } Value::UserData(ud) => { let url = ud.borrow::()?; - if !me.scheme.covariant(&url.scheme) { + if !me.scheme().covariant(url.scheme()) { return Err("cannot join Urls with different schemes".into_lua_err()); } - me.join(&url.loc) + me.try_join(url.loc()).into_lua_err()? } _ => Err("must be a string or Url".into_lua_err())?, })) }); methods.add_method("starts_with", |_, me, base: Value| { Ok(match base { - Value::String(s) => me.loc.starts_with(s.as_bytes().into_os_str()?), - Value::UserData(ud) => me.starts_with(&*ud.borrow::()?), + Value::String(s) => { + let b = s.as_bytes(); + me.loc().try_starts_with(&StrandCow::with(me.kind(), &*b).into_lua_err()?).into_lua_err() + } + Value::UserData(ud) => me.try_starts_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Url".into_lua_err())?, }) }); methods.add_method("ends_with", |_, me, child: Value| { Ok(match child { - Value::String(s) => me.loc.ends_with(s.as_bytes().into_os_str()?), - Value::UserData(ud) => me.ends_with(&*ud.borrow::()?), + Value::String(s) => { + let b = s.as_bytes(); + me.loc().try_ends_with(&StrandCow::with(me.kind(), &*b).into_lua_err()?).into_lua_err() + } + Value::UserData(ud) => me.try_ends_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Url".into_lua_err())?, }) }); methods.add_method("strip_prefix", |_, me, base: Value| { - let path = match base { - Value::String(s) => me.loc().strip_prefix(&s.as_bytes().into_os_str()?).map(Into::into), - Value::UserData(ud) => me.strip_prefix(&*ud.borrow::()?), + let strip = match base { + Value::String(s) => { + let b = s.as_bytes(); + me.loc().try_strip_prefix(&StrandCow::with(me.kind(), &*b).into_lua_err()?) + } + Value::UserData(ud) => me.try_strip_prefix(&*ud.borrow::()?), _ => Err("must be a string or Url".into_lua_err())?, }; - Ok(path.map(Self::new)) // TODO: return `Path` instead of `Url` + + Ok(match strip { + Ok(p) => Some(Self::new(p)), // TODO: return `Path` instead of `Url` + Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, + Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, + }) }); methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { - Ok(Self::new(ud.take::()?.inner.into_search(domain.to_str()?))) + let url = ud.take::()?.inner.into_search(domain.to_str()?).into_lua_err()?; + Ok(Self::new(url)) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner)); diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index b08fb59d..2c123084 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -4,13 +4,13 @@ use futures::executor::block_on; use hashbrown::HashSet; use serde::Serialize; use yazi_fs::{CWD, Xdg, path::expand_url}; -use yazi_shared::{path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{strand::StrandBuf, url::{UrlBuf, UrlLike}}; use yazi_vfs::provider; #[derive(Debug, Default, Serialize)] pub struct Boot { pub cwds: Vec, - pub files: Vec, + pub files: Vec, pub local_events: HashSet, pub remote_events: HashSet, @@ -22,25 +22,28 @@ pub struct Boot { } impl Boot { - async fn parse_entries(entries: &[UrlBuf]) -> (Vec, Vec) { + async fn parse_entries(entries: &[UrlBuf]) -> (Vec, Vec) { if entries.is_empty() { - return (vec![CWD.load().as_ref().clone()], vec![PathBufDyn::os_default()]); + return (vec![CWD.load().as_ref().clone()], vec![Default::default()]); } - async fn go(entry: &UrlBuf) -> (UrlBuf, PathBufDyn) { + async fn go(entry: &UrlBuf) -> (UrlBuf, StrandBuf) { let mut entry = expand_url(entry); - if let Ok(u @ UrlCow::Owned { .. }) = provider::absolute(&entry).await { + + if let Ok(u) = provider::absolute(&entry).await + && u.is_owned() + { entry = u.into_owned(); } let Some((parent, child)) = entry.pair() else { - return (entry, PathBufDyn::os_default()); + return (entry, Default::default()); }; if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) { - (parent.into(), child.owned()) + (parent.into(), child.into()) } else { - (entry, PathBufDyn::os_default()) + (entry, Default::default()) } } diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs index 02782bc0..4968e143 100644 --- a/yazi-cli/src/package/delete.rs +++ b/yazi-cli/src/package/delete.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use yazi_fs::{ok_or_not_found, provider::{DirReader, FileHolder, Provider, local::Local}}; +use yazi_fs::{ok_or_not_found, provider::{Provider, local::Local}}; use yazi_macro::outln; use super::Dependency; @@ -23,9 +23,9 @@ impl Dependency { pub(super) async fn delete_assets(&self) -> Result<()> { let assets = self.target().join("assets"); - match Local.read_dir(&assets).await { + match tokio::fs::read_dir(&assets).await { Ok(mut it) => { - while let Some(entry) = it.next().await? { + while let Some(entry) = it.next_entry().await? { remove_sealed(&entry.path()) .await .with_context(|| format!("failed to remove `{}`", entry.path().display()))?; @@ -35,7 +35,7 @@ impl Dependency { Err(e) => Err(e).context(format!("failed to read `{}`", assets.display()))?, }; - Local.remove_dir_clean(assets).await; + Local::regular(&assets).remove_dir_clean().await; Ok(()) } @@ -49,7 +49,7 @@ impl Dependency { .with_context(|| format!("failed to delete `{}`", path.display()))?; } - if ok_or_not_found(Local.remove_dir(&dir).await).is_ok() { + if ok_or_not_found(Local::regular(&dir).remove_dir().await).is_ok() { outln!("Done!")?; } else { outln!( diff --git a/yazi-cli/src/package/dependency.rs b/yazi-cli/src/package/dependency.rs index aee07cca..eb3eb146 100644 --- a/yazi-cli/src/package/dependency.rs +++ b/yazi-cli/src/package/dependency.rs @@ -3,7 +3,7 @@ use std::{io::BufWriter, path::{Path, PathBuf}, str::FromStr}; use anyhow::{Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use twox_hash::XxHash3_128; -use yazi_fs::{Xdg, provider::{DirReader, FileHolder, Provider, local::Local}}; +use yazi_fs::Xdg; use yazi_shared::BytesExt; #[derive(Clone, Default)] @@ -62,11 +62,11 @@ impl Dependency { } pub(super) async fn plugin_files(dir: &Path) -> std::io::Result> { - let mut it = Local.read_dir(dir).await?; + let mut it = tokio::fs::read_dir(dir).await?; let mut files: Vec = ["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect(); - while let Some(entry) = it.next().await? { - if let Ok(name) = entry.name().into_owned().into_string() + while let Some(entry) = it.next_entry().await? { + if let Ok(name) = entry.file_name().into_string() && let Some(stripped) = name.strip_suffix(".lua") && stripped != "main" && stripped.as_bytes().kebab_cased() diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index 84d758bc..2cc10ac5 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; -use yazi_fs::provider::{DirReader, FileHolder, Provider, local::Local}; +use yazi_fs::provider::{Provider, local::Local}; use yazi_macro::outln; use super::Dependency; @@ -20,7 +20,7 @@ impl Dependency { self.hash_check().await?; } - Local.create_dir_all(&to).await?; + Local::regular(&to).create_dir_all().await?; self.delete_assets().await?; let res1 = Self::deploy_assets(from.join("assets"), to.join("assets")).await; @@ -30,7 +30,7 @@ impl Dependency { self.delete_sources().await?; } - Local.remove_dir_clean(to).await; + Local::regular(&to).remove_dir_clean().await; self.hash = self.hash().await?; res2?; res1?; @@ -40,11 +40,11 @@ impl Dependency { } async fn deploy_assets(from: PathBuf, to: PathBuf) -> Result<()> { - match Local.read_dir(&from).await { + match tokio::fs::read_dir(&from).await { Ok(mut it) => { - Local.create_dir_all(&to).await?; - while let Some(entry) = it.next().await? { - let (src, dist) = (entry.path(), to.join(entry.name())); + Local::regular(&to).create_dir_all().await?; + while let Some(entry) = it.next_entry().await? { + let (src, dist) = (entry.path(), to.join(entry.file_name())); copy_and_seal(&src, &dist).await.with_context(|| { format!("failed to copy `{}` to `{}`", src.display(), dist.display()) })?; diff --git a/yazi-cli/src/package/hash.rs b/yazi-cli/src/package/hash.rs index 88430f01..37fc5350 100644 --- a/yazi-cli/src/package/hash.rs +++ b/yazi-cli/src/package/hash.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, bail}; use twox_hash::XxHash3_128; -use yazi_fs::provider::{DirReader, FileHolder, Provider, local::Local}; +use yazi_fs::provider::local::Local; use yazi_macro::ok_or_not_found; use super::Dependency; @@ -15,17 +15,17 @@ impl Dependency { for file in files { h.write(file.as_bytes()); h.write(b"VpvFw9Atb7cWGOdqhZCra634CcJJRlsRl72RbZeV0vpG1\0"); - h.write(&ok_or_not_found!(Local.read(dir.join(file)).await)); + h.write(&ok_or_not_found!(Local::regular(&dir.join(file)).read().await)); } let mut assets = vec![]; - match Local.read_dir(dir.join("assets")).await { + match tokio::fs::read_dir(dir.join("assets")).await { Ok(mut it) => { - while let Some(entry) = it.next().await? { - let Ok(name) = entry.name().into_owned().into_string() else { + while let Some(entry) = it.next_entry().await? { + let Ok(name) = entry.file_name().into_string() else { bail!("asset path is not valid UTF-8: {}", entry.path().display()); }; - assets.push((name, Local.read(entry.path()).await?)); + assets.push((name, Local::regular(&entry.path()).read().await?)); } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} diff --git a/yazi-cli/src/package/package.rs b/yazi-cli/src/package/package.rs index 617635da..dc654bc7 100644 --- a/yazi-cli/src/package/package.rs +++ b/yazi-cli/src/package/package.rs @@ -15,7 +15,7 @@ pub(crate) struct Package { impl Package { pub(crate) async fn load() -> Result { - let s = ok_or_not_found!(Local.read_to_string(Self::toml()).await); + let s = ok_or_not_found!(Local::regular(&Self::toml()).read_to_string().await); Ok(toml::from_str(&s)?) } @@ -132,7 +132,7 @@ impl Package { async fn save(&self) -> Result<()> { let s = toml::to_string_pretty(self)?; - Local.write(Self::toml(), s).await.context("Failed to write package.toml") + Local::regular(&Self::toml()).write(s).await.context("Failed to write package.toml") } fn toml() -> PathBuf { Xdg::config_dir().join("package.toml") } diff --git a/yazi-cli/src/shared/shared.rs b/yazi-cli/src/shared/shared.rs index f2077dd9..387b5ccb 100644 --- a/yazi-cli/src/shared/shared.rs +++ b/yazi-cli/src/shared/shared.rs @@ -6,19 +6,19 @@ use yazi_macro::ok_or_not_found; #[inline] pub async fn must_exists(path: impl AsRef) -> bool { - Local.symlink_metadata(path).await.is_ok() + Local::regular(&path).symlink_metadata().await.is_ok() } #[inline] pub async fn maybe_exists(path: impl AsRef) -> bool { - match Local.symlink_metadata(path).await { + match Local::regular(&path).symlink_metadata().await { Ok(_) => true, Err(e) => e.kind() != std::io::ErrorKind::NotFound, } } pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { - let b = Local.read(from).await?; + let b = Local::regular(from).read().await?; ok_or_not_found!(remove_sealed(to).await); let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?; @@ -31,7 +31,6 @@ pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { Ok(()) } -// TODO: use `yazi_fs` instead of `tokio::fs` pub async fn remove_sealed(p: &Path) -> io::Result<()> { #[cfg(windows)] { @@ -40,5 +39,5 @@ pub async fn remove_sealed(p: &Path) -> io::Result<()> { tokio::fs::set_permissions(p, perm).await?; } - Local.remove_file(p).await + Local::regular(p).remove_file().await } diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index 6be3ebfb..f18066c4 100644 --- a/yazi-config/src/mgr/mgr.rs +++ b/yazi-config/src/mgr/mgr.rs @@ -33,7 +33,7 @@ impl Mgr { } let home = UrlBuf::from(dirs::home_dir().unwrap_or_default()); - let cwd = if let Some(p) = CWD.load().strip_prefix(&home) { + let cwd = if let Ok(p) = CWD.load().try_strip_prefix(&home) { format!("~{}{}", std::path::MAIN_SEPARATOR, p.display()) } else { format!("{}", CWD.load().display()) diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index ab33b88c..72a52278 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -1,9 +1,9 @@ use std::{fmt::Debug, str::FromStr}; use anyhow::{Result, bail}; -use globset::GlobBuilder; +use globset::{Candidate, GlobBuilder}; use serde::Deserialize; -use yazi_shared::{scheme::SchemeRef, url::AsUrl}; +use yazi_shared::{path::PathLike, scheme::SchemeKind, url::AsUrl}; #[derive(Deserialize)] #[serde(try_from = "String")] @@ -28,28 +28,32 @@ impl Debug for Pattern { } impl Pattern { + // FIXME: simplify conditional compilation pub fn match_url(&self, url: impl AsUrl, is_dir: bool) -> bool { let url = url.as_url(); if is_dir != self.is_dir { return false; - } else if !self.scheme.matches(url.scheme) { + } else if !self.scheme.matches(url.kind()) { return false; } else if self.is_star { return true; } #[cfg(unix)] - let path = &url.loc; + { + self.inner.is_match_candidate(&Candidate::from_bytes(url.loc().encoded_bytes())) + } #[cfg(windows)] - let path = if self.sep_lit { - yazi_fs::path::backslash_to_slash(&url.loc) + if self.sep_lit { + use yazi_shared::strand::AsStrandDyn; + self.inner.is_match_candidate(&Candidate::from_bytes( + url.loc().as_strand_dyn().backslash_to_slash().encoded_bytes(), + )) } else { - std::borrow::Cow::Borrowed(url.loc.as_path()) - }; - - self.inner.is_match(path) + self.inner.is_match_candidate(&Candidate::from_bytes(url.loc().encoded_bytes())) + } } pub fn match_mime(&self, mime: impl AsRef) -> bool { @@ -144,18 +148,19 @@ impl PatternScheme { } #[inline] - fn matches(self, scheme: SchemeRef) -> bool { - use SchemeRef as S; - match (self, scheme) { + fn matches(self, kind: SchemeKind) -> bool { + use SchemeKind as K; + + match (self, kind) { (Self::Any, _) => true, (Self::Local, s) => s.is_local(), (Self::Remote, s) => s.is_remote(), (Self::Virtual, s) => s.is_virtual(), - (Self::Regular, S::Regular) => true, - (Self::Search, S::Search(_)) => true, - (Self::Archive, S::Archive(_)) => true, - (Self::Sftp, S::Sftp(_)) => true, + (Self::Regular, K::Regular) => true, + (Self::Search, K::Search) => true, + (Self::Archive, K::Archive) => true, + (Self::Sftp, K::Sftp) => true, _ => false, } diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index 50a02941..85a1e6c4 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -50,7 +50,7 @@ impl Preview { self.cache_dir = if self.cache_dir.as_os_str().is_empty() { Xdg::cache_dir().to_owned() - } else if let Some(p) = expand_url(self.cache_dir).into_path() { + } else if let Some(p) = expand_url(self.cache_dir).into_local() { p } else { bail!("[preview].cache_dir must be a path within local filesystem."); diff --git a/yazi-config/src/theme/icon.rs b/yazi-config/src/theme/icon.rs index 69892c2d..98d2c01e 100644 --- a/yazi-config/src/theme/icon.rs +++ b/yazi-config/src/theme/icon.rs @@ -5,7 +5,7 @@ use hashbrown::HashMap; use serde::{Deserialize, Deserializer}; use yazi_codegen::DeserializeOver2; use yazi_fs::File; -use yazi_shared::{Condition, url::UrlLike}; +use yazi_shared::{Condition, strand::StrandLike, url::UrlLike}; use crate::{Color, Icon as I, Pattern, Style}; @@ -74,7 +74,7 @@ impl Icon { } fn match_by_name(&self, file: &File) -> Option<&I> { - let name = file.name()?.to_str()?; + let name = file.name()?.to_str().ok()?; if file.is_dir() { self.dirs.get(name).or_else(|| self.dirs.get(&name.to_ascii_lowercase())) } else { @@ -87,7 +87,7 @@ impl Icon { } fn match_by_ext(&self, file: &File) -> Option<&I> { - let ext = file.url.ext()?.to_str()?; + let ext = file.url.ext()?.to_str().ok()?; self.exts.get(ext).or_else(|| self.exts.get(&ext.to_ascii_lowercase())) } } diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index b607dbb7..8617662e 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -229,7 +229,7 @@ impl Theme { self.mgr.syntect_theme = self .flavor .syntect_path(light) - .or_else(|| expand_url(UrlBuf::from(&self.mgr.syntect_theme)).into_path()) + .or_else(|| expand_url(UrlBuf::from(&self.mgr.syntect_theme)).into_local()) .ok_or(anyhow!("[mgr].syntect_theme must be a path within local filesystem"))?; Ok(self) diff --git a/yazi-config/src/vfs/provider.rs b/yazi-config/src/vfs/provider.rs index 841b47a6..5ae75cdc 100644 --- a/yazi-config/src/vfs/provider.rs +++ b/yazi-config/src/vfs/provider.rs @@ -45,7 +45,7 @@ impl ProviderSftp { fn reshape(&mut self) -> io::Result<()> { if !self.key_file.as_os_str().is_empty() { self.key_file = expand_url(&self.key_file) - .into_path() + .into_local() .ok_or_else(|| io::Error::other("key_file must be a path within local filesystem"))?; } @@ -56,7 +56,7 @@ impl ProviderSftp { .unwrap_or_default() } else { expand_url(&self.identity_agent) - .into_path() + .into_local() .ok_or_else(|| io::Error::other("identity_agent must be a path within local filesystem"))? }; diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index a9b8257a..4f94bf8b 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -1,9 +1,4 @@ -#![allow( - clippy::if_same_then_else, - clippy::len_without_is_empty, - clippy::option_map_unit_fn, - clippy::unit_arg -)] +#![allow(clippy::if_same_then_else, clippy::len_without_is_empty, clippy::option_map_unit_fn)] yazi_macro::mod_pub!(cmp confirm help input mgr notify pick spot tab tasks which); diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs index 87951a92..f5272908 100644 --- a/yazi-core/src/tab/selected.rs +++ b/yazi-core/src/tab/selected.rs @@ -142,23 +142,16 @@ impl Selected { #[cfg(test)] mod tests { - use yazi_shared::{scheme::SchemeCow, url::UrlCow}; + use std::path::Path; use super::*; - fn url(s: &str) -> Url<'_> { - match UrlCow::try_from(s).unwrap() { - UrlCow::Borrowed { loc, scheme: SchemeCow::Borrowed(scheme) } => Url { loc, scheme }, - _ => unreachable!(), - } - } - #[test] fn test_insert_non_conflicting() { let mut s = Selected::default(); - assert!(s.add(url("/a/b"))); - assert!(s.add(url("/c/d"))); + assert!(s.add(Path::new("/a/b"))); + assert!(s.add(Path::new("/c/d"))); assert_eq!(s.inner.len(), 2); } @@ -166,27 +159,27 @@ mod tests { fn test_insert_conflicting_parent() { let mut s = Selected::default(); - assert!(s.add(url("/a"))); - assert!(!s.add(url("/a/b"))); + assert!(s.add(Path::new("/a"))); + assert!(!s.add(Path::new("/a/b"))); } #[test] fn test_insert_conflicting_child() { let mut s = Selected::default(); - assert!(s.add(url("/a/b/c"))); - assert!(!s.add(url("/a/b"))); - assert!(s.add(url("/a/b/d"))); + assert!(s.add(Path::new("/a/b/c"))); + assert!(!s.add(Path::new("/a/b"))); + assert!(s.add(Path::new("/a/b/d"))); } #[test] fn test_remove() { let mut s = Selected::default(); - assert!(s.add(url("/a/b"))); - assert!(!s.remove(url("/a/c"))); - assert!(s.remove(url("/a/b"))); - assert!(!s.remove(url("/a/b"))); + assert!(s.add(Path::new("/a/b"))); + assert!(!s.remove(Path::new("/a/c"))); + assert!(s.remove(Path::new("/a/b"))); + assert!(!s.remove(Path::new("/a/b"))); assert!(s.inner.is_empty()); assert!(s.parents.is_empty()); } @@ -197,7 +190,11 @@ mod tests { assert_eq!( 3, - s.add_same([url("/parent/child1"), url("/parent/child2"), url("/parent/child3")]) + s.add_same([ + Path::new("/parent/child1"), + Path::new("/parent/child2"), + Path::new("/parent/child3") + ]) ); } @@ -205,16 +202,16 @@ mod tests { fn insert_many_with_existing_parent_fails() { let mut s = Selected::default(); - s.add(url("/parent")); - assert_eq!(0, s.add_same([url("/parent/child1"), url("/parent/child2")])); + s.add(Path::new("/parent")); + assert_eq!(0, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); } #[test] fn insert_many_with_existing_child_fails() { let mut s = Selected::default(); - s.add(url("/parent/child1")); - assert_eq!(2, s.add_same([url("/parent/child1"), url("/parent/child2")])); + s.add(Path::new("/parent/child1")); + assert_eq!(2, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); } #[test] @@ -228,48 +225,51 @@ mod tests { fn insert_many_with_parent_as_child_of_another_url() { let mut s = Selected::default(); - s.add(url("/parent/child")); - assert_eq!(0, s.add_same([url("/parent/child/child1"), url("/parent/child/child2")])); + s.add(Path::new("/parent/child")); + assert_eq!( + 0, + s.add_same([Path::new("/parent/child/child1"), Path::new("/parent/child/child2")]) + ); } #[test] fn insert_many_with_direct_parent_fails() { let mut s = Selected::default(); - s.add(url("/a")); - assert_eq!(0, s.add_same([url("/a/b")])); + s.add(Path::new("/a")); + assert_eq!(0, s.add_same([Path::new("/a/b")])); } #[test] fn insert_many_with_nested_child_fails() { let mut s = Selected::default(); - s.add(url("/a/b")); - assert_eq!(0, s.add_same([url("/a")])); - assert_eq!(1, s.add_same([url("/b"), url("/a")])); + s.add(Path::new("/a/b")); + assert_eq!(0, s.add_same([Path::new("/a")])); + assert_eq!(1, s.add_same([Path::new("/b"), Path::new("/a")])); } #[test] fn insert_many_sibling_directories_success() { let mut s = Selected::default(); - assert_eq!(2, s.add_same([url("/a/b"), url("/a/c")])); + assert_eq!(2, s.add_same([Path::new("/a/b"), Path::new("/a/c")])); } #[test] fn insert_many_with_grandchild_fails() { let mut s = Selected::default(); - s.add(url("/a/b")); - assert_eq!(0, s.add_same([url("/a/b/c")])); + s.add(Path::new("/a/b")); + assert_eq!(0, s.add_same([Path::new("/a/b/c")])); } #[test] fn test_insert_many_with_remove() { let mut s = Selected::default(); - let child1 = url("/parent/child1"); - let child2 = url("/parent/child2"); - let child3 = url("/parent/child3"); + let child1 = Path::new("/parent/child1"); + let child2 = Path::new("/parent/child2"); + let child3 = Path::new("/parent/child3"); assert_eq!(3, s.add_same([child1, child2, child3])); assert!(s.remove(child1)); diff --git a/yazi-core/src/tasks/file.rs b/yazi-core/src/tasks/file.rs index b4033d36..1935a014 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -8,9 +8,12 @@ use crate::mgr::Yanked; impl Tasks { pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) { for u in src.iter() { - let to = dest.join(u.name().unwrap()); + let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { + debug!("file_cut: cannot join {u:?} with {dest:?}"); + continue; + }; if force && *u == to { - debug!("file_cut: same file, skipping {:?}", to); + debug!("file_cut: same file, skip {to:?}"); } else { self.scheduler.file_cut(u.0.clone(), to, force); } @@ -19,9 +22,12 @@ impl Tasks { pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) { for u in src.iter() { - let to = dest.join(u.name().unwrap()); + let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { + debug!("file_copy: cannot join {u:?} with {dest:?}"); + continue; + }; if force && *u == to { - debug!("file_copy: same file, skipping {:?}", to); + debug!("file_copy: same file, skip {to:?}"); } else { self.scheduler.file_copy(u.0.clone(), to, force, follow); } @@ -30,9 +36,12 @@ impl Tasks { pub fn file_link(&self, src: &HashSet, dest: &UrlBuf, relative: bool, force: bool) { for u in src { - let to = dest.join(u.name().unwrap()); + let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { + debug!("file_link: cannot join {u:?} with {dest:?}"); + continue; + }; if force && *u == to { - debug!("file_link: same file, skipping {:?}", to); + debug!("file_link: same file, skip {to:?}"); } else { self.scheduler.file_link(u.0.clone(), to, relative, force); } @@ -41,9 +50,12 @@ impl Tasks { pub fn file_hardlink(&self, src: &HashSet, dest: &UrlBuf, force: bool, follow: bool) { for u in src { - let to = dest.join(u.name().unwrap()); + let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { + debug!("file_hardlink: cannot join {u:?} with {dest:?}"); + continue; + }; if force && *u == to { - debug!("file_hardlink: same file, skipping {:?}", to); + debug!("file_hardlink: same file, skip {to:?}"); } else { self.scheduler.file_hardlink(u.0.clone(), to, force, follow); } diff --git a/yazi-dds/src/ember/bulk.rs b/yazi-dds/src/ember/bulk.rs index c4482b55..53343f8e 100644 --- a/yazi-dds/src/ember/bulk.rs +++ b/yazi-dds/src/ember/bulk.rs @@ -1,21 +1,19 @@ -use std::borrow::Cow; - use hashbrown::HashMap; use mlua::{IntoLua, Lua, Value}; use serde::{Deserialize, Serialize}; -use yazi_shared::url::UrlBuf; +use yazi_shared::url::{Url, UrlCow}; use super::Ember; #[derive(Debug, Deserialize, Serialize)] pub struct EmberBulk<'a> { - pub changes: HashMap, Cow<'a, UrlBuf>>, + pub changes: HashMap, UrlCow<'a>>, } impl<'a> EmberBulk<'a> { pub fn borrowed(changes: I) -> Ember<'a> where - I: Iterator, + I: Iterator, Url<'a>)>, { Self { changes: changes.map(|(from, to)| (from.into(), to.into())).collect() }.into() } @@ -24,10 +22,12 @@ impl<'a> EmberBulk<'a> { impl EmberBulk<'static> { pub fn owned<'a, I>(changes: I) -> Ember<'static> where - I: Iterator, + I: Iterator, Url<'a>)>, { - Self { changes: changes.map(|(from, to)| (from.clone().into(), to.clone().into())).collect() } - .into() + Self { + changes: changes.map(|(from, to)| (from.to_owned().into(), to.to_owned().into())).collect(), + } + .into() } } diff --git a/yazi-dds/src/pubsub.rs b/yazi-dds/src/pubsub.rs index 1d3d945e..ab55aac0 100644 --- a/yazi-dds/src/pubsub.rs +++ b/yazi-dds/src/pubsub.rs @@ -4,7 +4,7 @@ use mlua::Function; use parking_lot::RwLock; use yazi_boot::BOOT; use yazi_fs::FolderStage; -use yazi_shared::{Id, RoCell, url::{UrlBuf, UrlBufCov}}; +use yazi_shared::{Id, RoCell, url::{Url, UrlBuf, UrlBufCov}}; use crate::{Client, ID, PEERS, ember::{BodyMoveItem, Ember, EmberBulk, EmberHi}}; @@ -123,7 +123,7 @@ impl Pubsub { pub fn pub_after_bulk<'a, I>(changes: I) -> Result<()> where - I: Iterator + Clone, + I: Iterator, Url<'a>)> + Clone, { if BOOT.local_events.contains("bulk") { EmberBulk::borrowed(changes.clone()).with_receiver(*ID).flush()?; diff --git a/yazi-dds/src/state.rs b/yazi-dds/src/state.rs index 757bc4b4..590181d3 100644 --- a/yazi-dds/src/state.rs +++ b/yazi-dds/src/state.rs @@ -58,7 +58,7 @@ impl State { return Ok(()); } - Local.create_dir_all(&BOOT.state_dir).await?; + Local::regular(&BOOT.state_dir).create_dir_all().await?; let mut buf = BufWriter::new( Gate::default() .write(true) @@ -79,7 +79,7 @@ impl State { } async fn load(&self) -> Result<()> { - let mut file = BufReader::new(Local.open(BOOT.state_dir.join(".dds")).await?); + let mut file = BufReader::new(Local::regular(&BOOT.state_dir.join(".dds")).open().await?); let mut buf = String::new(); let mut inner = HashMap::new(); @@ -103,7 +103,7 @@ impl State { } async fn skip(&self) -> Result { - let cha = Local.symlink_metadata(BOOT.state_dir.join(".dds")).await?; + let cha = Local::regular(&BOOT.state_dir.join(".dds")).symlink_metadata().await?; let modified = cha.mtime_dur()?.as_micros(); Ok(modified >= self.last.load(Ordering::Relaxed) as u128) } diff --git a/yazi-dds/src/stream.rs b/yazi-dds/src/stream.rs index ede51d00..e4b5b6e9 100644 --- a/yazi-dds/src/stream.rs +++ b/yazi-dds/src/stream.rs @@ -40,7 +40,7 @@ impl Stream { let p = Self::socket_file(); - yazi_fs::provider::local::Local.remove_file(&p).await.ok(); + yazi_fs::provider::local::Local::regular(&p).remove_file().await.ok(); tokio::net::UnixListener::bind(p) } diff --git a/yazi-fm/src/app/commands/bootstrap.rs b/yazi-fm/src/app/commands/bootstrap.rs index 574906a5..af83c5b2 100644 --- a/yazi-fm/src/app/commands/bootstrap.rs +++ b/yazi-fm/src/app/commands/bootstrap.rs @@ -3,7 +3,7 @@ use yazi_actor::Ctx; use yazi_boot::BOOT; use yazi_macro::act; use yazi_parser::{VoidOpt, mgr::CdSource}; -use yazi_shared::{data::Data, path::PathBufLike, url::UrlLike}; +use yazi_shared::{data::Data, strand::StrandBufLike, url::UrlLike}; use crate::app::App; @@ -20,8 +20,8 @@ impl App { if file.is_empty() { act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?; - } else { - act!(mgr:reveal, cx, (BOOT.cwds[i].join(file), CdSource::Tab))?; + } else if let Ok(u) = BOOT.cwds[i].try_join(file) { + act!(mgr:reveal, cx, (u, CdSource::Tab))?; } } diff --git a/yazi-fm/src/app/commands/quit.rs b/yazi-fm/src/app/commands/quit.rs index 37efe25d..ecdc1342 100644 --- a/yazi-fm/src/app/commands/quit.rs +++ b/yazi-fm/src/app/commands/quit.rs @@ -26,13 +26,13 @@ impl App { async fn cwd_to_file(&self, no: bool) { if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) { let cwd = self.core.mgr.cwd().os_str(); - Local.write(p, cwd.as_encoded_bytes()).await.ok(); + Local::regular(p).write(cwd.as_encoded_bytes()).await.ok(); } } async fn selected_to_file(&self, selected: Option) { if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) { - Local.write(p, s.as_encoded_bytes()).await.ok(); + Local::regular(p).write(s.as_encoded_bytes()).await.ok(); } } } diff --git a/yazi-fm/src/cmp/cmp.rs b/yazi-fm/src/cmp/cmp.rs index 3fb0f714..f06fb1c0 100644 --- a/yazi-fm/src/cmp/cmp.rs +++ b/yazi-fm/src/cmp/cmp.rs @@ -4,6 +4,7 @@ use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, List, L use yazi_adapter::Dimension; use yazi_config::{THEME, popup::{Offset, Position}}; use yazi_core::Core; +use yazi_shared::strand::{AsStrand, StrandLike}; pub(crate) struct Cmp<'a> { core: &'a Core, @@ -25,7 +26,7 @@ impl Widget for Cmp<'_> { let icon = if x.is_dir { &THEME.cmp.icon_folder } else { &THEME.cmp.icon_file }; let slash = if x.is_dir { MAIN_SEPARATOR_STR } else { "" }; - let mut item = ListItem::new(format!(" {icon} {}{slash}", x.name.display())); + let mut item = ListItem::new(format!(" {icon} {}{slash}", x.name.as_strand().display())); if i == self.core.cmp.rel_cursor() { item = item.style(THEME.cmp.active); } else { diff --git a/yazi-fm/src/dispatcher.rs b/yazi-fm/src/dispatcher.rs index ebc2bd73..e75abc7d 100644 --- a/yazi-fm/src/dispatcher.rs +++ b/yazi-fm/src/dispatcher.rs @@ -2,6 +2,7 @@ use std::sync::atomic::Ordering; use anyhow::Result; use crossterm::event::KeyEvent; +use tracing::warn; use yazi_config::keymap::Key; use yazi_macro::{act, emit, succ}; use yazi_shared::{data::Data, event::{CmdCow, Event, NEED_RENDER}}; @@ -20,7 +21,7 @@ impl<'a> Dispatcher<'a> { #[inline] pub(super) fn dispatch(&mut self, event: Event) -> Result<()> { // FIXME: handle errors - _ = match event { + let result = match event { Event::Call(cmd) => self.dispatch_call(cmd), Event::Seq(cmds) => self.dispatch_seq(cmds), Event::Render => self.dispatch_render(), @@ -30,6 +31,10 @@ impl<'a> Dispatcher<'a> { Event::Paste(str) => self.dispatch_paste(str), Event::Quit(opt) => act!(quit, self.app, opt), }; + + if let Err(err) = result { + warn!("Event dispatch error: {err:?}"); + } Ok(()) } diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index 64ae41e5..bd0032bb 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -1,4 +1,4 @@ -#![allow(clippy::if_same_then_else, clippy::unit_arg)] +#![allow(clippy::if_same_then_else)] #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] #[global_allocator] diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 469e4f54..f64003e7 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -1,8 +1,8 @@ -use std::{ffi::OsStr, fs::Metadata, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}}; +use std::{fs::Metadata, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}}; use anyhow::bail; use yazi_macro::{unix_either, win_either}; -use yazi_shared::url::AsUrl; +use yazi_shared::{strand::AsStrand, url::AsUrl}; use super::ChaKind; use crate::cha::{ChaMode, ChaType}; @@ -48,7 +48,10 @@ impl Default for Cha { impl Cha { #[inline] - pub fn new(name: &OsStr, meta: Metadata) -> Self { + pub fn new(name: T, meta: Metadata) -> Self + where + T: AsStrand, + { Self::from_bare(&meta).attach(ChaKind::hidden(name, &meta)) } diff --git a/yazi-fs/src/cha/kind.rs b/yazi-fs/src/cha/kind.rs index d1d9e367..4e2736f9 100644 --- a/yazi-fs/src/cha/kind.rs +++ b/yazi-fs/src/cha/kind.rs @@ -1,6 +1,7 @@ -use std::{ffi::OsStr, fs::Metadata}; +use std::fs::Metadata; use bitflags::bitflags; +use yazi_shared::strand::AsStrand; bitflags! { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -14,13 +15,16 @@ bitflags! { impl ChaKind { #[inline] - pub(super) fn hidden(_name: &OsStr, _meta: &Metadata) -> Self { + pub(super) fn hidden(_name: T, _meta: &Metadata) -> Self + where + T: AsStrand, + { let mut me = Self::empty(); #[cfg(unix)] { - use std::os::unix::ffi::OsStrExt; - if _name.as_bytes().starts_with(b".") { + use yazi_shared::strand::StrandLike; + if _name.as_strand().starts_with(".") { me |= Self::HIDDEN; } } diff --git a/yazi-fs/src/cwd.rs b/yazi-fs/src/cwd.rs index d1193ffb..c1156dd0 100644 --- a/yazi-fs/src/cwd.rs +++ b/yazi-fs/src/cwd.rs @@ -28,10 +28,7 @@ impl Default for Cwd { } impl Cwd { - pub fn path(&self) -> PathBuf { - let url = self.0.load(); - url.cache().unwrap_or_else(|| url.loc.to_path()) - } + pub fn path(&self) -> PathBuf { self.0.load().as_url().unified_path().into_owned() } pub fn set(&self, url: &UrlBuf, callback: fn()) -> bool { if !url.is_absolute() { @@ -50,7 +47,7 @@ impl Cwd { use std::{io::ErrorKind::{AlreadyExists, NotADirectory, NotFound}, path::Component as C}; let Some(cache) = url.cache() else { - return url.loc.as_path().into(); + return url.unified_path(); }; if !matches!(std::fs::create_dir_all(&cache), Err(e) if e.kind() == NotADirectory || e.kind() == AlreadyExists) diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index 771bd0fc..7e3414c3 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -1,6 +1,7 @@ -use std::{ffi::OsStr, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; +use std::{hash::{Hash, Hasher}, ops::Deref, path::Path}; -use yazi_shared::{path::PathDyn, url::{UrlBuf, UrlLike}}; +use anyhow::Result; +use yazi_shared::{loc::Loc, path::{AsPathDyn, PathBufDyn, PathDyn}, strand::Strand, url::{Url, UrlBuf, UrlLike}}; use crate::cha::{Cha, ChaType}; @@ -8,7 +9,7 @@ use crate::cha::{Cha, ChaType}; pub struct File { pub url: UrlBuf, pub cha: Cha, - pub link_to: Option, + pub link_to: Option, } impl Deref for File { @@ -26,8 +27,8 @@ impl File { } #[inline] - pub fn chdir(&self, wd: &Path) -> Self { - Self { url: self.url.rebase(wd), cha: self.cha, link_to: self.link_to.clone() } + pub fn chdir(&self, wd: &Path) -> Result { + Ok(Self { url: self.url.rebase(wd)?, cha: self.cha, link_to: self.link_to.clone() }) } } @@ -43,10 +44,27 @@ impl File { pub fn urn(&self) -> PathDyn<'_> { self.url.urn() } #[inline] - pub fn name(&self) -> Option<&OsStr> { self.url.name() } + pub fn name(&self) -> Option> { self.url.name() } #[inline] - pub fn stem(&self) -> Option<&OsStr> { self.url.stem() } + pub fn stem(&self) -> Option> { self.url.stem() } + + pub fn link_to_url(&self) -> Option> { + let to = self.link_to.as_ref()?.as_path_dyn(); + let kind = self.url.kind(); + Some(match &self.url { + UrlBuf::Regular(_) => Url::Regular(Loc::bare(to.as_os().ok()?)), + UrlBuf::Search { domain, .. } => { + Url::Search { loc: Loc::saturated(to.as_os().ok()?, kind), domain } + } + UrlBuf::Archive { domain, .. } => { + Url::Archive { loc: Loc::saturated(to.as_os().ok()?, kind), domain } + } + UrlBuf::Sftp { domain, .. } => { + Url::Sftp { loc: Loc::saturated(to.as_os().ok()?, kind), domain } + } + }) + } } impl Hash for File { diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index 557c4891..937e9e9a 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -1,7 +1,7 @@ use std::{mem, ops::{Deref, DerefMut, Not}}; use hashbrown::{HashMap, HashSet}; -use yazi_shared::{Id, path::{PathBufDyn, PathBufLike, PathDyn, PathLike}}; +use yazi_shared::{Id, path::{PathBufDyn, PathDyn, PathLike}}; use super::{FilesSorter, Filter}; use crate::{FILES_TICKET, File, SortBy}; @@ -121,6 +121,7 @@ impl Files { #[cfg(unix)] pub fn update_deleting(&mut self, urns: HashSet) -> Vec { + use yazi_shared::path::PathBufLike; if urns.is_empty() { return vec![]; } diff --git a/yazi-fs/src/filter.rs b/yazi-fs/src/filter.rs index 8bb1394e..8bed557d 100644 --- a/yazi-fs/src/filter.rs +++ b/yazi-fs/src/filter.rs @@ -1,8 +1,8 @@ -use std::{ffi::OsStr, fmt::Display, ops::Range}; +use std::{fmt::Display, ops::Range}; use anyhow::Result; use regex::bytes::{Regex, RegexBuilder}; -use yazi_shared::{event::Cmd, path::{AsPath, PathLike}}; +use yazi_shared::{event::Cmd, strand::{AsStrand, StrandLike}}; pub struct Filter { raw: String, @@ -26,14 +26,14 @@ impl Filter { #[allow(private_bounds)] pub fn matches(&self, name: T) -> bool where - T: AsPath, + T: AsStrand, { - self.regex.is_match(name.as_path().encoded_bytes()) + self.regex.is_match(name.as_strand().encoded_bytes()) } #[inline] - pub fn highlighted(&self, name: impl AsRef) -> Option>> { - self.regex.find(name.as_ref().as_encoded_bytes()).map(|m| vec![m.range()]) + pub fn highlighted(&self, name: impl AsStrand) -> Option>> { + self.regex.find(name.as_strand().encoded_bytes()).map(|m| vec![m.range()]) } } diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index bec80dd3..52fd93ce 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn, clippy::unit_arg)] +#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn)] yazi_macro::mod_pub!(cha error mounts path provider); diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index 0e5d82b2..f6d111bd 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -1,5 +1,6 @@ use std::path::Path; +use anyhow::Result; use hashbrown::{HashMap, HashSet}; use yazi_macro::relay; use yazi_shared::{Id, Ids, path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlLike}}; @@ -98,16 +99,21 @@ impl FilesOp { } } - pub fn chdir(&self, wd: &Path) -> Self { + pub fn chdir(&self, wd: &Path) -> Result { macro_rules! files { - ($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect() }}; + ($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect::>()? }}; } macro_rules! map { - ($map:expr) => {{ $map.iter().map(|(urn, file)| (urn.clone(), file.chdir(wd))).collect() }}; + ($map:expr) => {{ + $map + .iter() + .map(|(urn, file)| file.chdir(wd).map(|file| (urn.clone(), file))) + .collect::>()? + }}; } let w = UrlBuf::from(wd); - match self { + Ok(match self { Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha), Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket), Self::Done(_, cha, ticket) => Self::Done(w, *cha, *ticket), @@ -118,16 +124,18 @@ impl FilesOp { Self::Deleting(_, urns) => Self::Deleting(w, urns.clone()), Self::Updating(_, map) => Self::Updating(w, map!(map)), Self::Upserting(_, map) => Self::Upserting(w, map!(map)), - } + }) } pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec, Vec) { match self { - Self::Deleting(cwd, urns) => (urns.iter().map(|u| cwd.join(u)).collect(), vec![]), + Self::Deleting(cwd, urns) => { + (urns.iter().filter_map(|u| cwd.try_join(u).ok()).collect(), vec![]) + } Self::Updating(cwd, urns) | Self::Upserting(cwd, urns) => urns .iter() .filter(|&(u, f)| u != f.urn()) - .map(|(u, f)| (cwd.join(u), f)) + .filter_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f))) .filter(|(u, _)| contains(u)) .map(|(u, f)| (u, f.url_owned())) .unzip(), diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs index 495a5388..a2ef3f74 100644 --- a/yazi-fs/src/path/clean.rs +++ b/yazi-fs/src/path/clean.rs @@ -1,21 +1,33 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -use yazi_shared::{loc::LocBuf, url::{UrlBuf, UrlCow}}; +use yazi_shared::{loc::LocBuf, path::{PathDyn, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; pub fn clean_url<'a>(url: impl Into>) -> UrlBuf { let cow: UrlCow = url.into(); let (path, uri, urn) = clean_path_impl( - &cow.loc(), - cow.loc().base().components().count(), - cow.loc().trail().components().count(), + cow.loc(), + cow.base().components().count() - 1, + cow.trail().components().count() - 1, ); - let loc = - LocBuf::::with(path, uri, urn).expect("Failed to create Loc from cleaned path"); - UrlBuf { loc, scheme: cow.into_scheme().into() } + match cow.as_url() { + Url::Regular(_) => UrlBuf::Regular(path.into()), + Url::Search { domain, .. } => UrlBuf::Search { + loc: LocBuf::::with(path, uri, urn).expect("create Loc from cleaned path"), + domain: domain.intern(), + }, + Url::Archive { domain, .. } => UrlBuf::Archive { + loc: LocBuf::::with(path, uri, urn).expect("create Loc from cleaned path"), + domain: domain.intern(), + }, + Url::Sftp { domain, .. } => UrlBuf::Sftp { + loc: LocBuf::::with(path, uri, urn).expect("create Loc from cleaned path"), + domain: domain.intern(), + }, + } } -fn clean_path_impl(path: &Path, base: usize, trail: usize) -> (PathBuf, usize, usize) { +fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBuf, usize, usize) { use std::path::Component::*; let mut out = vec![]; diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index 8dfd2fdf..5baba83f 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -1,6 +1,6 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}, path::{Path, PathBuf}}; +use std::{borrow::Cow, path::PathBuf}; -use yazi_shared::{loc::LocBuf, path::PathLike, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{FromWtf8Vec, loc::LocBuf, path::{AsPath, PathBufDyn, PathCow, PathDyn, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use crate::{CWD, path::clean_url}; @@ -10,33 +10,44 @@ pub fn expand_url<'a>(url: impl Into>) -> UrlBuf { } fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> { - let (o_base, o_rest, o_urn) = url.loc.triple(); + let (o_base, o_rest, o_urn) = url.triple(); let n_base = expand_variables(o_base); let n_rest = expand_variables(o_rest); let n_urn = expand_variables(o_urn); - let rest_diff = n_rest.components().count() as isize - o_rest.components().count() as isize; - let urn_diff = n_urn.components().count() as isize - o_urn.components().count() as isize; + let rest_diff = + n_rest.as_path().components().count() as isize - o_rest.components().count() as isize; + let urn_diff = + n_urn.as_path().components().count() as isize - o_urn.components().count() as isize; let uri_count = url.uri().components().count() as isize; let urn_count = url.urn().components().count() as isize; + let mut path = PathBufDyn::with_capacity( + url.kind(), + n_base.as_path().len() + n_rest.as_path().len() + n_urn.as_path().len(), + ); + path.try_extend([n_base, n_rest, n_urn]).expect("extend original parts should not fail"); + let loc = LocBuf::::with( - PathBuf::from_iter([n_base, n_rest, n_urn]), + path.into_os().expect("Failed to convert PathBufDyn to PathBuf"), (uri_count + rest_diff + urn_diff) as usize, (urn_count + urn_diff) as usize, ) .expect("Failed to create Loc from expanded path"); - let url = UrlBuf { loc, scheme: url.scheme.into() }; - match absolute_url(url.as_url()) { - UrlCow::Borrowed { .. } => url.into(), - c @ UrlCow::Owned { .. } => c.into_owned().into(), - } + let expanded = match url { + Url::Regular(_) => UrlBuf::Regular(loc), + Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }, + Url::Archive { domain, .. } => UrlBuf::Archive { loc, domain: domain.intern() }, + Url::Sftp { domain, .. } => UrlBuf::Sftp { loc, domain: domain.intern() }, + }; + + absolute_url(expanded) } -fn expand_variables(p: &Path) -> Cow<'_, Path> { +fn expand_variables<'a>(p: PathDyn<'a>) -> PathCow<'a> { // ${HOME} or $HOME #[cfg(unix)] let re = regex::bytes::Regex::new(r"\$(?:\{([^}]+)\}|([a-zA-Z\d_]+))").unwrap(); @@ -45,7 +56,7 @@ fn expand_variables(p: &Path) -> Cow<'_, Path> { #[cfg(windows)] let re = regex::bytes::Regex::new(r"%([^%]+)%").unwrap(); - let b = p.as_os_str().as_encoded_bytes(); + let b = p.encoded_bytes(); let b = re.replace_all(b, |caps: ®ex::bytes::Captures| { let name = caps.get(2).or_else(|| caps.get(1)).unwrap(); str::from_utf8(name.as_bytes()) @@ -54,51 +65,58 @@ fn expand_variables(p: &Path) -> Cow<'_, Path> { .map_or_else(|| caps.get(0).unwrap().as_bytes().to_owned(), |s| s.into_encoded_bytes()) }); - unsafe { - match b { - Cow::Borrowed(b) => Path::new(OsStr::from_encoded_bytes_unchecked(b)).into(), - Cow::Owned(b) => PathBuf::from(OsString::from_encoded_bytes_unchecked(b)).into(), + match (b, p) { + (Cow::Borrowed(_), _) => p.into(), + (Cow::Owned(b), PathDyn::Os(_)) => { + PathBufDyn::Os(std::path::PathBuf::from_wtf8_vec(b).expect("valid WTF-8 path")).into() } } } -pub fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> { - if url.scheme.is_virtual() { +pub fn absolute_url<'a>(url: impl Into>) -> UrlCow<'a> { absolute_url_impl(url.into()) } + +fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> { + if url.kind().is_virtual() { return url.into(); } - let b = url.loc.as_os_str().as_encoded_bytes(); - if cfg!(windows) && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() { - let loc = LocBuf::::with( + let path = url.loc().as_os().expect("must be a local path"); + let b = path.as_os_str().as_encoded_bytes(); + + let loc = if cfg!(windows) && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() { + LocBuf::::with( format!(r"{}:\", b[0].to_ascii_uppercase() as char).into(), if url.has_base() { 0 } else { 2 }, if url.has_trail() { 0 } else { 2 }, ) - .expect("Failed to create Loc from drive letter"); - UrlBuf { loc, scheme: url.scheme.into() }.into() - } else if let Some(rest) = url.loc.strip_prefix("~/") + .expect("Failed to create Loc from drive letter") + } else if let Ok(rest) = path.strip_prefix("~/") && let Some(home) = dirs::home_dir() && home.is_absolute() { let add = home.components().count() - 1; // Home root ("~") has offset by the absolute root ("/") - let loc = LocBuf::::with( + LocBuf::::with( home.join(rest), url.uri().components().count() + if url.has_base() { 0 } else { add }, url.urn().components().count() + if url.has_trail() { 0 } else { add }, ) - .expect("Failed to create Loc from home directory"); - UrlBuf { loc, scheme: url.scheme.into() }.into() + .expect("Failed to create Loc from home directory") } else if !url.is_absolute() { let cwd = CWD.path(); - let loc = LocBuf::::with( - cwd.join(url.loc), + LocBuf::::with( + cwd.join(path), url.uri().components().count(), url.urn().components().count(), ) - .expect("Failed to create Loc from relative path"); - UrlBuf { loc, scheme: url.scheme.into() }.into() + .expect("Failed to create Loc from relative path") } else { - url.into() + return url; + }; + + match url.as_url() { + Url::Regular(_) => UrlBuf::Regular(loc).into(), + Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }.into(), + Url::Archive { .. } | Url::Sftp { .. } => unreachable!(), } } diff --git a/yazi-fs/src/path/path.rs b/yazi-fs/src/path/path.rs index 1f3803ec..088cf438 100644 --- a/yazi-fs/src/path/path.rs +++ b/yazi-fs/src/path/path.rs @@ -12,28 +12,6 @@ pub fn skip_url(url: &UrlBuf, n: usize) -> Cow<'_, OsStr> { it.os_str() } -#[cfg(windows)] -pub fn backslash_to_slash(p: &std::path::Path) -> Cow<'_, std::path::Path> { - use std::{ffi::OsString, path::PathBuf}; - let bytes = p.as_os_str().as_encoded_bytes(); - - // Fast path to skip if there are no backslashes - let skip_len = bytes.iter().take_while(|&&b| b != b'\\').count(); - if skip_len >= bytes.len() { - return Cow::Borrowed(p); - } - - let (skip, rest) = bytes.split_at(skip_len); - let mut out = Vec::new(); - out.try_reserve_exact(bytes.len()).unwrap_or_else(|_| panic!()); - out.extend(skip); - - for &b in rest { - out.push(if b == b'\\' { b'/' } else { b }); - } - Cow::Owned(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(out) })) -} - #[cfg(test)] mod tests { use yazi_shared::url::{AsUrl, UrlCow}; diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs index a2c97300..98a5e299 100644 --- a/yazi-fs/src/path/percent.rs +++ b/yazi-fs/src/path/percent.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, path::{Path, PathBuf}}; use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; -use yazi_shared::loc::Loc; +use yazi_shared::path::PathDyn; const SET: &AsciiSet = &CONTROLS.add(b'"').add(b'*').add(b':').add(b'<').add(b'>').add(b'?').add(b'\\').add(b'|'); @@ -28,8 +28,16 @@ impl PercentEncoding for Path { } } -impl PercentEncoding for Loc<'_> { - fn percent_encode(&self) -> Cow<'_, Path> { self.as_path().percent_encode() } +impl PercentEncoding for PathDyn<'_> { + fn percent_encode(&self) -> Cow<'_, Path> { + match self { + PathDyn::Os(p) => p.percent_encode(), + } + } - fn percent_decode(&self) -> Cow<'_, [u8]> { self.as_path().percent_decode() } + fn percent_decode(&self) -> Cow<'_, [u8]> { + match self { + PathDyn::Os(p) => p.percent_decode(), + } + } } diff --git a/yazi-fs/src/path/relative.rs b/yazi-fs/src/path/relative.rs index d584e908..674ccb69 100644 --- a/yazi-fs/src/path/relative.rs +++ b/yazi-fs/src/path/relative.rs @@ -1,19 +1,17 @@ -use std::{borrow::Cow, path::{Path, PathBuf}}; +use std::path::PathBuf; use anyhow::{Result, bail}; -use yazi_shared::{loc::LocBuf, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{path::PathBufDyn, url::{UrlCow, UrlLike}}; -pub fn path_relative_to<'a>( - from: impl AsRef, - to: &'a impl AsRef, -) -> Result> { - Ok(match url_relative_to(from.as_ref().into(), to.as_ref().into())? { - UrlCow::Borrowed { loc, .. } => Cow::Borrowed(loc.as_path()), - UrlCow::Owned { loc, .. } => Cow::Owned(loc.into_path()), - }) +pub fn url_relative_to<'a, 'b, U, V>(from: U, to: V) -> Result> +where + U: Into>, + V: Into>, +{ + url_relative_to_(from.into(), to.into()) } -pub(super) fn url_relative_to<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> { +fn url_relative_to_<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> { use yazi_shared::url::Component::*; if from.is_absolute() != to.is_absolute() { @@ -25,7 +23,7 @@ pub(super) fn url_relative_to<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result::zeroed("."), scheme: to.scheme().into() }.into()); + return UrlCow::try_from((to.scheme().zeroed().to_owned(), PathBufDyn::with(to.kind(), ".")?)); } let (mut f_it, mut t_it) = (from.components(), to.components()); @@ -46,6 +44,7 @@ pub(super) fn url_relative_to<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result::zeroed(buf), scheme: to.scheme().into() }.into()) + let buf: PathBuf = dots.chain(rest).collect(); // FIXME: remove PathBuf + let buf = PathBufDyn::from(buf); + UrlCow::try_from((to.scheme().zeroed().to_owned(), buf)) } diff --git a/yazi-fs/src/provider/local/dir_entry.rs b/yazi-fs/src/provider/local/dir_entry.rs index d8985e5b..aa126a85 100644 --- a/yazi-fs/src/provider/local/dir_entry.rs +++ b/yazi-fs/src/provider/local/dir_entry.rs @@ -1,18 +1,47 @@ -use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf}; +use std::{io, sync::Arc}; + +use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; use crate::{cha::{Cha, ChaType}, provider::FileHolder}; -pub struct DirEntry(pub(super) tokio::fs::DirEntry); +pub enum DirEntry { + Regular(tokio::fs::DirEntry), + Others { entry: tokio::fs::DirEntry, dir: Arc }, +} impl FileHolder for DirEntry { - fn path(&self) -> PathBuf { self.0.path() } - - fn name(&self) -> Cow<'_, OsStr> { self.0.file_name().into() } - - async fn metadata(&self) -> io::Result { - let name = self.name(); // TODO: use `file_name_os_str` when stabilized - Ok(Cha::new(&name, self.0.metadata().await?)) + async fn file_type(&self) -> io::Result { + match self { + Self::Regular(entry) | Self::Others { entry, .. } => entry.file_type().await.map(Into::into), + } } - async fn file_type(&self) -> io::Result { self.0.file_type().await.map(Into::into) } + async fn metadata(&self) -> io::Result { + let meta = match self { + Self::Regular(entry) | Self::Others { entry, .. } => entry.metadata().await?, + }; + + Ok(Cha::new(self.name(), meta)) // TODO: use `file_name_os_str` when stabilized + } + + fn name(&self) -> StrandCow<'_> { + match self { + Self::Regular(entry) | Self::Others { entry, .. } => entry.file_name().into(), + } + } + + fn path(&self) -> PathBufDyn { + match self { + Self::Regular(entry) | Self::Others { entry, .. } => entry.path().into(), + } + } + + fn url(&self) -> UrlBuf { + match self { + Self::Regular(entry) => entry.path().into(), + Self::Others { entry, dir } => { + dir.try_join(entry.file_name()).expect("entry name is a valid component of the local URL") + } + } + } } diff --git a/yazi-fs/src/provider/local/gate.rs b/yazi-fs/src/provider/local/gate.rs index dfa21949..2136b2ec 100644 --- a/yazi-fs/src/provider/local/gate.rs +++ b/yazi-fs/src/provider/local/gate.rs @@ -1,6 +1,6 @@ -use std::{io, path::Path}; +use std::io; -use yazi_shared::scheme::SchemeRef; +use yazi_shared::url::AsUrl; use crate::provider::{Attrs, FileBuilder}; @@ -33,19 +33,16 @@ impl FileBuilder for Gate { self } - async fn new(scheme: SchemeRef<'_>) -> io::Result { - if scheme.is_local() { - Ok(Self::default()) - } else { - Err(io::Error::new(io::ErrorKind::InvalidInput, "Not a local filesystem"))? - } - } - - async fn open

(&self, path: P) -> io::Result + async fn open(&self, url: U) -> io::Result where - P: AsRef, + U: AsUrl, { - self.0.open(path).await + let url = url.as_url(); + if let Some(path) = url.as_local() { + self.0.open(path).await + } else { + Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}"))) + } } fn read(&mut self, read: bool) -> &mut Self { diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index 49d297c2..d79e79e6 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -1,206 +1,176 @@ -use std::{io, path::{Path, PathBuf}}; +use std::{io, path::{Path, PathBuf}, sync::Arc}; -use yazi_shared::url::{AsUrl, UrlCow}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn}, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow}}; use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Provider}}; #[derive(Clone, Copy)] -pub struct Local; +pub struct Local<'a> { + url: Url<'a>, + path: &'a Path, +} -impl Provider for Local { +impl<'a> Provider for Local<'a> { type File = tokio::fs::File; type Gate = super::Gate; + type Me<'b> = Local<'b>; type ReadDir = super::ReadDir; + type UrlCow = UrlCow<'a>; - async fn absolute<'a, U>(&self, url: &'a U) -> io::Result> - where - U: AsUrl, - { - let url = url.as_url(); - if url.scheme.is_local() { - Ok(absolute_url(url)) - } else { - Err(io::Error::new(io::ErrorKind::InvalidInput, "Not a local URL")) - } + async fn absolute(&self) -> io::Result { Ok(absolute_url(self.url)) } + + #[inline] + async fn canonicalize(&self) -> io::Result { + tokio::fs::canonicalize(self.path).await.map(Into::into) + } + + async fn casefold(&self) -> io::Result { + super::casefold(self.path).await.map(Into::into) } #[inline] - async fn canonicalize

(&self, path: P) -> io::Result + async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsRef, + P: AsPathDyn, { - tokio::fs::canonicalize(path).await - } - - async fn casefold

(&self, path: P) -> io::Result - where - P: AsRef, - { - super::casefold(path).await - } - - #[inline] - async fn copy(&self, from: P, to: Q, attrs: Attrs) -> io::Result - where - P: AsRef, - Q: AsRef, - { - let from = from.as_ref().to_owned(); - let to = to.as_ref().to_owned(); + let to = to.as_path_dyn().to_os_owned()?; + let from = self.path.to_owned(); Self::copy_impl(from, to, attrs).await } #[inline] - async fn create_dir

(&self, path: P) -> io::Result<()> + async fn create_dir(&self) -> io::Result<()> { tokio::fs::create_dir(self.path).await } + + #[inline] + async fn create_dir_all(&self) -> io::Result<()> { tokio::fs::create_dir_all(self.path).await } + + #[inline] + async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsRef, + P: AsPathDyn, { - tokio::fs::create_dir(path).await + let to = to.as_path_dyn().as_os()?; + + tokio::fs::hard_link(self.path, to).await } #[inline] - async fn create_dir_all

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::create_dir_all(path).await + async fn metadata(&self) -> io::Result { + Ok(Cha::new(self.path.file_name().unwrap_or_default(), tokio::fs::metadata(self.path).await?)) } #[inline] - async fn gate(&self) -> io::Result { Ok(Self::Gate::default()) } - - #[inline] - async fn hard_link(&self, original: P, link: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - tokio::fs::hard_link(original, link).await + async fn new<'b>(url: Url<'b>) -> io::Result> { + match url { + Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_path() }), + Url::Archive { .. } | Url::Sftp { .. } => { + Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}"))) + } + } } #[inline] - async fn metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - Ok(Cha::new(path.file_name().unwrap_or_default(), tokio::fs::metadata(path).await?)) + async fn read_dir(self) -> io::Result { + Ok(match self.url.kind() { + SchemeKind::Regular => Self::ReadDir::Regular(tokio::fs::read_dir(self.path).await?), + SchemeKind::Search => Self::ReadDir::Others { + reader: tokio::fs::read_dir(self.path).await?, + dir: Arc::new(self.url.to_owned()), + }, + SchemeKind::Archive | SchemeKind::Sftp => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Not a local URL: {:?}", self.url), + ))?, + }) } #[inline] - async fn read_dir

(&self, path: P) -> io::Result - where - P: AsRef, - { - tokio::fs::read_dir(path).await.map(super::ReadDir) + async fn read_link(&self) -> io::Result { + Ok(tokio::fs::read_link(self.path).await?.into()) } #[inline] - async fn read_link

(&self, path: P) -> io::Result + async fn remove_dir(&self) -> io::Result<()> { tokio::fs::remove_dir(self.path).await } + + #[inline] + async fn remove_dir_all(&self) -> io::Result<()> { tokio::fs::remove_dir_all(self.path).await } + + #[inline] + async fn remove_file(&self) -> io::Result<()> { tokio::fs::remove_file(self.path).await } + + #[inline] + async fn rename

(&self, to: P) -> io::Result<()> where - P: AsRef, + P: AsPathDyn, { - tokio::fs::read_link(path).await + let to = to.as_path_dyn().as_os()?; + + tokio::fs::rename(self.path, to).await } #[inline] - async fn remove_dir

(&self, path: P) -> io::Result<()> + async fn symlink(&self, original: P, _is_dir: F) -> io::Result<()> where - P: AsRef, - { - tokio::fs::remove_dir(path).await - } - - #[inline] - async fn remove_dir_all

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::remove_dir_all(path).await - } - - #[inline] - async fn remove_file

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::remove_file(path).await - } - - #[inline] - async fn rename(&self, from: P, to: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - tokio::fs::rename(from, to).await - } - - #[inline] - async fn symlink(&self, original: P, link: Q, _is_dir: F) -> io::Result<()> - where - P: AsRef, - Q: AsRef, + P: AsPathDyn, F: AsyncFnOnce() -> io::Result, { #[cfg(unix)] { - tokio::fs::symlink(original, link).await + let original = original.as_path_dyn().as_os()?; + tokio::fs::symlink(original, self.path).await } #[cfg(windows)] if _is_dir().await? { - self.symlink_dir(original, link).await + self.symlink_dir(original).await } else { - self.symlink_file(original, link).await + self.symlink_file(original).await } } #[inline] - async fn symlink_dir(&self, original: P, link: Q) -> io::Result<()> + async fn symlink_dir

(&self, original: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { + let original = original.as_path_dyn().as_os()?; + #[cfg(unix)] { - tokio::fs::symlink(original, link).await + tokio::fs::symlink(original, self.path).await } #[cfg(windows)] { - tokio::fs::symlink_dir(original, link).await + tokio::fs::symlink_dir(original, self.path).await } } #[inline] - async fn symlink_file(&self, original: P, link: Q) -> io::Result<()> + async fn symlink_file

(&self, original: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { + let original = original.as_path_dyn().as_os()?; + #[cfg(unix)] { - tokio::fs::symlink(original, link).await + tokio::fs::symlink(original, self.path).await } #[cfg(windows)] { - tokio::fs::symlink_file(original, link).await + tokio::fs::symlink_file(original, self.path).await } } #[inline] - async fn symlink_metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - Ok(Cha::new(path.file_name().unwrap_or_default(), tokio::fs::symlink_metadata(path).await?)) + async fn symlink_metadata(&self) -> io::Result { + Ok(Cha::new( + self.path.file_name().unwrap_or_default(), + tokio::fs::symlink_metadata(self.path).await?, + )) } - async fn trash

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - let path = path.as_ref().to_owned(); + async fn trash(&self) -> io::Result<()> { + let path = self.path.to_owned(); tokio::task::spawn_blocking(move || { #[cfg(target_os = "android")] { @@ -222,16 +192,18 @@ impl Provider for Local { } #[inline] - async fn write(&self, path: P, contents: C) -> io::Result<()> + fn url(&self) -> Url<'_> { self.url } + + #[inline] + async fn write(&self, contents: C) -> io::Result<()> where - P: AsRef, C: AsRef<[u8]>, { - tokio::fs::write(path, contents).await + tokio::fs::write(self.path, contents).await } } -impl Local { +impl<'a> Local<'a> { async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::Result { #[cfg(any(target_os = "linux", target_os = "android"))] { @@ -273,18 +245,18 @@ impl Local { } #[inline] - pub async fn read

(&self, path: P) -> io::Result> - where - P: AsRef, - { - tokio::fs::read(path).await + pub async fn read(&self) -> io::Result> { tokio::fs::read(self.path).await } + + #[inline] + pub async fn read_to_string(&self) -> io::Result { + tokio::fs::read_to_string(self.path).await } #[inline] - pub async fn read_to_string

(&self, path: P) -> io::Result + pub fn regular

(path: &'a P) -> Self where - P: AsRef, + P: ?Sized + AsRef, { - tokio::fs::read_to_string(path).await + Self { url: Url::regular(path), path: path.as_ref() } } } diff --git a/yazi-fs/src/provider/local/read_dir.rs b/yazi-fs/src/provider/local/read_dir.rs index 88790809..78ee07c9 100644 --- a/yazi-fs/src/provider/local/read_dir.rs +++ b/yazi-fs/src/provider/local/read_dir.rs @@ -1,13 +1,23 @@ -use std::io; +use std::{io, sync::Arc}; + +use yazi_shared::url::UrlBuf; use crate::provider::DirReader; -pub struct ReadDir(pub(super) tokio::fs::ReadDir); +pub enum ReadDir { + Regular(tokio::fs::ReadDir), + Others { reader: tokio::fs::ReadDir, dir: Arc }, +} impl DirReader for ReadDir { type Entry = super::DirEntry; async fn next(&mut self) -> io::Result> { - self.0.next_entry().await.map(|entry| entry.map(super::DirEntry)) + Ok(match self { + Self::Regular(reader) => reader.next_entry().await?.map(Self::Entry::Regular), + Self::Others { reader, dir } => { + reader.next_entry().await?.map(|entry| Self::Entry::Others { entry, dir: dir.clone() }) + } + }) } } diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs index d5f5be44..52ed3ea6 100644 --- a/yazi-fs/src/provider/traits.rs +++ b/yazi-fs/src/provider/traits.rs @@ -1,75 +1,62 @@ -use std::{borrow::Cow, ffi::OsStr, io, path::{Path, PathBuf}}; +use std::io; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use yazi_macro::ok_or_not_found; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, UrlCow}}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn, PathLike}, strand::StrandCow, url::{AsUrl, Url, UrlBuf}}; use crate::{cha::{Cha, ChaType}, provider::Attrs}; -pub trait Provider { +pub trait Provider: Sized { type File: AsyncRead + AsyncWrite + Unpin; type Gate: FileBuilder; - type ReadDir: DirReader; + type ReadDir: DirReader + 'static; + type UrlCow; + type Me<'a>: Provider; - fn absolute<'a, U>(&self, url: &'a U) -> impl Future>> - where - U: AsUrl; + fn absolute(&self) -> impl Future>; - fn canonicalize

(&self, path: P) -> impl Future> - where - P: AsRef; + fn canonicalize(&self) -> impl Future>; - fn casefold

(&self, path: P) -> impl Future> - where - P: AsRef; + fn casefold(&self) -> impl Future>; - fn copy(&self, from: P, to: Q, attrs: Attrs) -> impl Future> + fn copy

(&self, to: P, attrs: Attrs) -> impl Future> where - P: AsRef, - Q: AsRef; + P: AsPathDyn; - fn create

(&self, path: P) -> impl Future> - where - P: AsRef, - { - async move { self.gate().await?.write(true).create(true).truncate(true).open(path).await } + fn create(&self) -> impl Future> { + async move { self.gate().write(true).create(true).truncate(true).open(self.url()).await } } - fn create_dir

(&self, path: P) -> impl Future> - where - P: AsRef; + fn create_dir(&self) -> impl Future>; - fn create_dir_all

(&self, path: P) -> impl Future> - where - P: AsRef, - { + fn create_dir_all(&self) -> impl Future> { async move { - let mut path = path.as_ref(); - if path == Path::new("") { + let mut url = self.url(); + if url.loc().is_empty() { return Ok(()); } let mut stack = Vec::new(); loop { - match self.create_dir(path).await { + match Self::new(url).await?.create_dir().await { Ok(()) => break, Err(e) if e.kind() == io::ErrorKind::NotFound => { - if let Some(parent) = path.parent() { - stack.push(path); - path = parent; + if let Some(parent) = url.parent() { + stack.push(url); + url = parent; } else { return Err(io::Error::other("failed to create whole tree")); } } - Err(_) if self.metadata(path).await.is_ok_and(|m| m.is_dir()) => break, + Err(_) if Self::new(url).await?.metadata().await.is_ok_and(|m| m.is_dir()) => break, Err(e) => return Err(e), } } - while let Some(p) = stack.pop() { - match self.create_dir(p).await { + while let Some(u) = stack.pop() { + match Self::new(u).await?.create_dir().await { Ok(()) => {} - Err(_) if self.metadata(p).await.is_ok_and(|m| m.is_dir()) => {} + Err(_) if Self::new(u).await?.metadata().await.is_ok_and(|m| m.is_dir()) => {} Err(e) => return Err(e), } } @@ -78,87 +65,74 @@ pub trait Provider { } } - fn gate(&self) -> impl Future>; + fn gate(&self) -> Self::Gate { Self::Gate::default() } - fn hard_link(&self, original: P, link: Q) -> impl Future> + fn hard_link

(&self, to: P) -> impl Future> where - P: AsRef, - Q: AsRef; + P: AsPathDyn; - fn metadata

(&self, path: P) -> impl Future> - where - P: AsRef; + fn metadata(&self) -> impl Future>; - fn open

(&self, path: P) -> impl Future> - where - P: AsRef, - { - async move { self.gate().await?.read(true).open(path).await } + fn new<'a>(url: Url<'a>) -> impl Future>>; + + fn open(&self) -> impl Future> { + async move { self.gate().read(true).open(self.url()).await } } - fn read_dir

(&self, path: P) -> impl Future> - where - P: AsRef; + fn read_dir(self) -> impl Future>; - fn read_link

(&self, path: P) -> impl Future> - where - P: AsRef; + fn read_link(&self) -> impl Future>; - fn remove_dir

(&self, path: P) -> impl Future> - where - P: AsRef; + fn remove_dir(&self) -> impl Future>; - fn remove_dir_all

(&self, path: P) -> impl Future> - where - P: AsRef, - { - async fn remove_dir_all_impl

(me: &P, path: &Path) -> io::Result<()> + fn remove_dir_all(&self) -> impl Future> { + async fn remove_dir_all_impl

(url: Url<'_>) -> io::Result<()> where - P: Provider + ?Sized, + P: Provider, { - let mut it = ok_or_not_found!(me.read_dir(path).await, return Ok(())); + let mut it = ok_or_not_found!(P::new(url).await?.read_dir().await, return Ok(())); while let Some(child) = it.next().await? { let ft = ok_or_not_found!(child.file_type().await, continue); let result = if ft.is_dir() { - Box::pin(remove_dir_all_impl(me, &child.path())).await + Box::pin(remove_dir_all_impl::

(child.url().as_url())).await } else { - me.remove_file(&child.path()).await + P::new(child.url().as_url()).await?.remove_file().await }; () = ok_or_not_found!(result); } - Ok(ok_or_not_found!(me.remove_dir(path).await)) + Ok(ok_or_not_found!(P::new(url).await?.remove_dir().await)) } async move { - let path = path.as_ref(); - let cha = ok_or_not_found!(self.symlink_metadata(path).await, return Ok(())); + let cha = ok_or_not_found!(self.symlink_metadata().await, return Ok(())); if cha.is_link() { - self.remove_file(path).await + self.remove_file().await } else { - remove_dir_all_impl(self, path).await + remove_dir_all_impl::(self.url()).await } } } - fn remove_dir_clean

(&self, root: P) -> impl Future - where - P: AsRef, - { - let root = root.as_ref().to_path_buf(); + fn remove_dir_clean(&self) -> impl Future { + let root = self.url().to_owned(); async move { let mut stack = vec![(root, false)]; while let Some((dir, visited)) = stack.pop() { + let Ok(provider) = Self::new(dir.as_url()).await else { + continue; + }; + if visited { - self.remove_dir(&dir).await.ok(); - } else if let Ok(mut it) = self.read_dir(&dir).await { + provider.remove_dir().await.ok(); + } else if let Ok(mut it) = provider.read_dir().await { stack.push((dir, true)); while let Ok(Some(ent)) = it.next().await { if ent.file_type().await.is_ok_and(|t| t.is_dir()) { - stack.push((ent.path(), false)); + stack.push((ent.url(), false)); } } } @@ -166,56 +140,42 @@ pub trait Provider { } } - fn remove_file

(&self, path: P) -> impl Future> - where - P: AsRef; + fn remove_file(&self) -> impl Future>; - fn rename(&self, from: P, to: Q) -> impl Future> + fn rename

(&self, to: P) -> impl Future> where - P: AsRef, - Q: AsRef; + P: AsPathDyn; - fn symlink( - &self, - original: P, - link: Q, - _is_dir: F, - ) -> impl Future> + fn symlink(&self, original: P, _is_dir: F) -> impl Future> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, F: AsyncFnOnce() -> io::Result; - fn symlink_dir(&self, original: P, link: Q) -> impl Future> + fn symlink_dir

(&self, original: P) -> impl Future> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - self.symlink(original, link, async || Ok(true)) + self.symlink(original, async || Ok(true)) } - fn symlink_file(&self, original: P, link: Q) -> impl Future> + fn symlink_file

(&self, original: P) -> impl Future> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - self.symlink(original, link, async || Ok(false)) + self.symlink(original, async || Ok(false)) } - fn symlink_metadata

(&self, path: P) -> impl Future> - where - P: AsRef; + fn symlink_metadata(&self) -> impl Future>; - fn trash

(&self, path: P) -> impl Future> - where - P: AsRef; + fn trash(&self) -> impl Future>; - fn write(&self, path: P, contents: C) -> impl Future> + fn url(&self) -> Url<'_>; + + fn write(&self, contents: C) -> impl Future> where - P: AsRef, C: AsRef<[u8]>, { - async move { self.create(path).await?.write_all(contents.as_ref()).await } + async move { self.create().await?.write_all(contents.as_ref()).await } } } @@ -223,24 +183,30 @@ pub trait Provider { pub trait DirReader { type Entry: FileHolder; + #[must_use] fn next(&mut self) -> impl Future>>; } // --- FileHolder pub trait FileHolder { #[must_use] - fn path(&self) -> PathBuf; + fn file_type(&self) -> impl Future>; #[must_use] - fn name(&self) -> Cow<'_, OsStr>; - fn metadata(&self) -> impl Future>; - fn file_type(&self) -> impl Future>; + #[must_use] + fn name(&self) -> StrandCow<'_>; + + #[must_use] + fn path(&self) -> PathBufDyn; + + #[must_use] + fn url(&self) -> UrlBuf; } -// --- FileOpener -pub trait FileBuilder { +// --- FileBuilder +pub trait FileBuilder: Sized + Default { type File: AsyncRead + AsyncWrite + Unpin; fn append(&mut self, append: bool) -> &mut Self; @@ -251,13 +217,9 @@ pub trait FileBuilder { fn create_new(&mut self, create_new: bool) -> &mut Self; - fn new(scheme: SchemeRef) -> impl Future> + fn open(&self, url: U) -> impl Future> where - Self: Sized; - - fn open

(&self, path: P) -> impl Future> - where - P: AsRef; + U: AsUrl; fn read(&mut self, read: bool) -> &mut Self; diff --git a/yazi-fs/src/scheme.rs b/yazi-fs/src/scheme.rs index f6a6d8d1..4eb65431 100644 --- a/yazi-fs/src/scheme.rs +++ b/yazi-fs/src/scheme.rs @@ -11,12 +11,12 @@ pub trait FsScheme { impl FsScheme for SchemeRef<'_> { fn cache(&self) -> Option { match self { - SchemeRef::Regular | SchemeRef::Search(_) => None, - SchemeRef::Archive(name) => { - Some(Xdg::cache_dir().join(format!("archive-{}", yazi_shared::url::Encode::domain(name)))) - } - SchemeRef::Sftp(name) => { - Some(Xdg::cache_dir().join(format!("sftp-{}", yazi_shared::url::Encode::domain(name)))) + Self::Regular { .. } | Self::Search { .. } => None, + Self::Archive { domain, .. } => Some( + Xdg::cache_dir().join(format!("archive-{}", yazi_shared::scheme::Encode::domain(domain))), + ), + Self::Sftp { domain, .. } => { + Some(Xdg::cache_dir().join(format!("sftp-{}", yazi_shared::scheme::Encode::domain(domain)))) } } } diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index 8d85e020..6c747cbd 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use hashbrown::HashMap; -use yazi_shared::{LcgRng, natsort, path::{PathBufDyn, PathLike}, translit::Transliterator, url::UrlLike}; +use yazi_shared::{LcgRng, natsort, path::{PathBufDyn, PathLike}, strand::StrandLike, translit::Transliterator, url::UrlLike}; use crate::{File, SortBy}; @@ -43,8 +43,8 @@ impl FilesSorter { self.cmp(a.url.ext(), b.url.ext(), self.promote(a, b)) } else { self.cmp_insensitive( - a.url.ext().map_or(&[], |s| s.as_encoded_bytes()), - b.url.ext().map_or(&[], |s| s.as_encoded_bytes()), + a.url.ext().map_or(&[], |s| s.encoded_bytes()), + b.url.ext().map_or(&[], |s| s.encoded_bytes()), self.promote(a, b), ) }; diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index a7c39489..12171d36 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; -use yazi_shared::{loc::Loc, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::{PathDyn, PathLike}, url::{AsUrl, Url, UrlBuf, UrlCow}}; use crate::{FsHash128, FsScheme, path::PercentEncoding}; @@ -24,22 +24,22 @@ pub trait FsUrl<'a> { impl<'a> FsUrl<'a> for Url<'a> { fn cache(&self) -> Option { - fn with_loc(loc: Loc, mut path: PathBuf) -> PathBuf { + fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { let mut it = loc.components(); if it.next() == Some(std::path::Component::RootDir) { - path.push(it.as_path().percent_encode()); + root.push(it.as_path().percent_encode()); } else { - path.push(".%2F"); - path.push(loc.percent_encode()); + root.push(".%2F"); + root.push(loc.percent_encode()); } - path + root } - self.scheme.cache().map(|root| with_loc(self.loc, root)) + self.scheme().cache().map(|root| with_loc(self.loc(), root)) } fn cache_lock(&self) -> Option { - self.scheme.cache().map(|mut root| { + self.scheme().cache().map(|mut root| { root.push("%lock"); root.push(format!("{:x}", self.hash_u128())); root @@ -47,7 +47,12 @@ impl<'a> FsUrl<'a> for Url<'a> { } fn unified_path(self) -> Cow<'a, Path> { - self.cache().map(Cow::Owned).unwrap_or_else(|| Cow::Borrowed(self.loc.as_path())) + match self { + Self::Regular(loc) | Self::Search { loc, .. } => loc.as_path().into(), + Self::Archive { .. } | Self::Sftp { .. } => { + self.cache().expect("non-local URL should have a cache path").into() + } + } } } @@ -57,7 +62,12 @@ impl FsUrl<'_> for UrlBuf { fn cache_lock(&self) -> Option { self.as_url().cache_lock() } fn unified_path(self) -> Cow<'static, Path> { - self.cache().unwrap_or_else(|| self.loc.into_path()).into() + match self { + Self::Regular(loc) | Self::Search { loc, .. } => loc.into_path().into(), + Self::Archive { .. } | Self::Sftp { .. } => { + self.cache().expect("non-local URL should have a cache path").into() + } + } } } @@ -67,10 +77,12 @@ impl<'a> FsUrl<'a> for UrlCow<'a> { fn cache_lock(&self) -> Option { self.as_url().cache_lock() } fn unified_path(self) -> Cow<'a, Path> { - match (self.cache(), self) { - (None, UrlCow::Borrowed { loc, .. }) => loc.as_path().into(), - (None, UrlCow::Owned { loc, .. }) => loc.into_path().into(), - (Some(cache), _) => cache.into(), + match self { + Self::Regular(loc) | Self::Search { loc, .. } => loc.into_path().into(), + Self::RegularRef(loc) | Self::SearchRef { loc, .. } => loc.as_path().into(), + Self::Archive { .. } | Self::ArchiveRef { .. } | Self::Sftp { .. } | Self::SftpRef { .. } => { + self.cache().expect("non-local URL should have a cache path").into() + } } } } diff --git a/yazi-parser/src/cmp/show.rs b/yazi-parser/src/cmp/show.rs index 13508a7d..05478e81 100644 --- a/yazi-parser/src/cmp/show.rs +++ b/yazi-parser/src/cmp/show.rs @@ -1,14 +1,14 @@ -use std::{ffi::OsString, path::{MAIN_SEPARATOR_STR, PathBuf}}; +use std::path::MAIN_SEPARATOR_STR; use anyhow::bail; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_shared::{Id, event::CmdCow, url::UrlBuf}; +use yazi_shared::{Id, event::CmdCow, path::PathBufDyn, strand::{StrandBuf, StrandBufLike}, url::UrlBuf}; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct ShowOpt { pub cache: Vec, pub cache_name: UrlBuf, - pub word: PathBuf, + pub word: PathBufDyn, pub ticket: Id, } @@ -35,7 +35,7 @@ impl IntoLua for ShowOpt { // --- Item #[derive(Debug, Clone)] pub struct CmpItem { - pub name: OsString, + pub name: StrandBuf, pub is_dir: bool, } diff --git a/yazi-parser/src/mgr/copy.rs b/yazi-parser/src/mgr/copy.rs index 6468bd17..1496f154 100644 --- a/yazi-parser/src/mgr/copy.rs +++ b/yazi-parser/src/mgr/copy.rs @@ -1,8 +1,8 @@ -use std::{borrow::Cow, ffi::OsStr, path::Path, str::FromStr}; +use std::{borrow::Cow, str::FromStr}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use serde::Deserialize; -use yazi_shared::{SStr, event::CmdCow}; +use yazi_shared::{SStr, event::CmdCow, strand::{AsStrandDyn, StrandLike}}; #[derive(Debug)] pub struct CopyOpt { @@ -47,14 +47,18 @@ impl FromStr for CopySeparator { } impl CopySeparator { - pub fn transform + ?Sized>(self, p: &T) -> Cow<'_, OsStr> { + pub fn transform(self, s: &T) -> Cow<'_, [u8]> + where + T: ?Sized + AsStrandDyn, + { #[cfg(windows)] if self == Self::Unix { - return match yazi_fs::path::backslash_to_slash(p.as_ref()) { - Cow::Owned(p) => Cow::Owned(p.into_os_string()), - Cow::Borrowed(p) => Cow::Borrowed(p.as_os_str()), + use yazi_shared::strand::{StrandBufLike, StrandCow}; + return match s.as_strand_dyn().backslash_to_slash() { + StrandCow::Borrowed(s) => s.encoded_bytes().into(), + StrandCow::Owned(s) => s.into_encoded_bytes().into(), }; } - Cow::Borrowed(p.as_ref().as_os_str()) + Cow::Borrowed(s.as_strand_dyn().encoded_bytes()) } } diff --git a/yazi-plugin/src/external/fd.rs b/yazi-plugin/src/external/fd.rs index 7cdc38dc..2f9585dc 100644 --- a/yazi-plugin/src/external/fd.rs +++ b/yazi-plugin/src/external/fd.rs @@ -21,7 +21,10 @@ pub fn fd(opt: FdOpt) -> Result> { tokio::spawn(async move { while let Ok(Some(line)) = it.next_line().await { - if let Ok(file) = File::new(opt.cwd.join(line)).await { + let Ok(url) = opt.cwd.try_join(line) else { + continue; + }; + if let Ok(file) = File::new(url).await { tx.send(file).ok(); } } @@ -31,7 +34,7 @@ pub fn fd(opt: FdOpt) -> Result> { } fn spawn(program: &str, opt: &FdOpt) -> std::io::Result { - let Some(path) = opt.cwd.as_path() else { + let Some(path) = opt.cwd.as_local() else { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "fd can only search local filesystem", diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 2d7fc0e2..b352a055 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -44,7 +44,7 @@ impl Highlighter { pub fn abort() { INCR.next(); } pub async fn highlight(&self, skip: usize, size: Size) -> Result, PeekError> { - let mut reader = BufReader::new(Local.open(&self.path).await?); + let mut reader = BufReader::new(Local::regular(&self.path).open().await?); let syntax = Self::find_syntax(&self.path, &mut reader).await; let mut plain = syntax.is_err(); diff --git a/yazi-plugin/src/external/rg.rs b/yazi-plugin/src/external/rg.rs index 99553851..4f3c6452 100644 --- a/yazi-plugin/src/external/rg.rs +++ b/yazi-plugin/src/external/rg.rs @@ -14,7 +14,7 @@ pub struct RgOpt { } pub fn rg(opt: RgOpt) -> Result> { - let Some(path) = opt.cwd.as_path() else { + let Some(path) = opt.cwd.as_local() else { bail!("rg can only search local filesystem"); }; @@ -34,7 +34,10 @@ pub fn rg(opt: RgOpt) -> Result> { tokio::spawn(async move { while let Ok(Some(line)) = it.next_line().await { - if let Ok(file) = File::new(opt.cwd.join(line)).await { + let Ok(url) = opt.cwd.try_join(line) else { + continue; + }; + if let Ok(file) = File::new(url).await { tx.send(file).ok(); } } diff --git a/yazi-plugin/src/external/rga.rs b/yazi-plugin/src/external/rga.rs index db9b161a..d038eae8 100644 --- a/yazi-plugin/src/external/rga.rs +++ b/yazi-plugin/src/external/rga.rs @@ -14,7 +14,7 @@ pub struct RgaOpt { } pub fn rga(opt: RgaOpt) -> Result> { - let Some(path) = opt.cwd.as_path() else { + let Some(path) = opt.cwd.as_local() else { bail!("rga can only search local filesystem"); }; @@ -34,7 +34,10 @@ pub fn rga(opt: RgaOpt) -> Result> { tokio::spawn(async move { while let Ok(Some(line)) = it.next_line().await { - if let Ok(file) = File::new(opt.cwd.join(line)).await { + let Ok(url) = opt.cwd.try_join(line) else { + continue; + }; + if let Ok(file) = File::new(url).await { tx.send(file).ok(); } } diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 76b9f06b..4d0ee1e8 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -148,7 +148,7 @@ fn read_dir(lua: &Lua) -> mlua::Result { fn calc_size(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, url: UrlRef| async move { - let it = if let Some(path) = url.as_path() { + let it = if let Some(path) = url.as_local() { yazi_fs::provider::local::SizeCalculator::new(path).await.map(SizeCalculator::Local) } else { yazi_vfs::provider::SizeCalculator::new(&*url).await.map(SizeCalculator::Remote) diff --git a/yazi-plugin/src/lib.rs b/yazi-plugin/src/lib.rs index da074117..9b767077 100644 --- a/yazi-plugin/src/lib.rs +++ b/yazi-plugin/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::if_same_then_else, clippy::unit_arg)] +#![allow(clippy::if_same_then_else)] yazi_macro::mod_pub!(bindings elements external fs isolate loader process pubsub runtime theme utils); diff --git a/yazi-plugin/src/loader/loader.rs b/yazi-plugin/src/loader/loader.rs index af53db93..d5fff9bd 100644 --- a/yazi-plugin/src/loader/loader.rs +++ b/yazi-plugin/src/loader/loader.rs @@ -65,8 +65,11 @@ impl Loader { } let p = BOOT.plugin_dir.join(format!("{plugin}.yazi/{entry}.lua")); - let chunk = - Local.read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into(); + let chunk = Local::regular(&p) + .read() + .await + .with_context(|| format!("Failed to load plugin from {p:?}"))? + .into(); let result = Self::compatible_or_error(id, &chunk); let inspect = f(&chunk); diff --git a/yazi-plugin/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index 6cef3ab6..4f88b014 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -30,7 +30,7 @@ impl Utils { pub(super) fn image_precache(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (src, dist): (UrlRef, UrlRef)| async move { - let Some(dist) = dist.as_path() else { + let Some(dist) = dist.as_local() else { return (Value::Nil, Error::custom("Destination must be a local path")) .into_lua_multi(&lua); }; diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index 0fffee25..296d0d1f 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -91,7 +91,7 @@ impl Utils { CLIPBOARD.set(text).await; Ok(None) } else { - Some(lua.create_string(CLIPBOARD.get().await.as_encoded_bytes())).transpose() + Some(lua.create_string(CLIPBOARD.get().await)).transpose() } }) } diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 14bdca90..d1a17619 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,12 +1,12 @@ -use std::{borrow::Cow, collections::VecDeque, hash::{BuildHasher, Hash, Hasher}}; +use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}}; use anyhow::{Context, Result, anyhow}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::YAZI; -use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; +use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{skip_url, url_relative_to}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; use yazi_macro::ok_or_not_found; -use yazi_shared::{scheme::SchemeLike, timestamp_us, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{path::PathCow, timestamp_us, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::{VfsCha, copy_with_progress, maybe_exists, provider::{self, DirEntry}, unique_name}; use super::{FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; @@ -64,7 +64,7 @@ impl File { let mut dirs = VecDeque::from([task.from.clone()]); while let Some(src) = dirs.pop_front() { - let dest = root.join(skip_url(&src, skip)); + let dest = continue_unless_ok!(root.try_join(skip_url(&src, skip))); continue_unless_ok!(match provider::create_dir(&dest).await { Err(e) if e.kind() != AlreadyExists => Err(e), _ => Ok(()), @@ -80,7 +80,7 @@ impl File { continue; } - let to = dest.join(from.name().unwrap()); + let to = continue_unless_ok!(dest.try_join(from.name().unwrap())); if cha.is_orphan() || (cha.is_link() && !task.follow) { self.ops.out(task.id, FileOutPaste::New(0)); self.queue(task.spawn(from, to, cha).into_link(), NORMAL); @@ -132,20 +132,21 @@ impl File { } pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> { - let src: Cow<_> = if task.resolve { + let src: PathCow = if task.resolve { ok_or_not_found!( provider::read_link(&task.from).await, return Ok(self.ops.out(task.id, FileOutLink::Succ)) ) .into() - } else if task.from.scheme.covariant(&task.to.scheme) { - task.from.loc.as_path().into() + } else if task.from.scheme().covariant(task.to.scheme()) { + task.from.loc().into() } else { Err(anyhow!("Source and target must be on the same filesystem: {task:?}"))? }; + let src = UrlCow::try_from((task.from.scheme(), src))?; let src = if task.relative { - path_relative_to(provider::canonicalize(task.to.parent().unwrap()).await?.loc, &src)? + url_relative_to(provider::canonicalize(task.to.parent().unwrap()).await?, &src)? } else { src }; @@ -196,7 +197,7 @@ impl File { let mut dirs = VecDeque::from([task.from.clone()]); while let Some(src) = dirs.pop_front() { - let dest = root.join(skip_url(&src, skip)); + let dest = continue_unless_ok!(root.try_join(skip_url(&src, skip))); continue_unless_ok!(match provider::create_dir(&dest).await { Err(e) if e.kind() != AlreadyExists => Err(e), _ => Ok(()), @@ -212,7 +213,7 @@ impl File { continue; } - let to = dest.join(from.name().unwrap()); + let to = continue_unless_ok!(dest.try_join(from.name().unwrap())); self.ops.out(task.id, FileOutHardlink::New); self.queue(task.spawn(from, to, cha), NORMAL); } @@ -353,12 +354,12 @@ impl File { while let Some(res) = it.recv().await { match res { Ok(0) => { - Local.remove_dir_all(&cache).await.ok(); + Local::regular(&cache).remove_dir_all().await.ok(); provider::rename(cache_tmp, cache).await.context("Cannot persist downloaded file")?; let lock = task.url.cache_lock().context("Cannot determine cache lock")?; let hash = format!("{:x}", cha.hash_u128()); - Local.write(lock, hash).await.context("Cannot lock cache")?; + Local::regular(&lock).write(hash).await.context("Cannot lock cache")?; break; } @@ -448,7 +449,7 @@ impl File { pub(crate) async fn upload_do(&self, task: FileInUploadDo) -> Result<(), FileOutUploadDo> { let lock = task.url.cache_lock().context("Cannot determine cache lock")?; - let hash = Local.read_to_string(&lock).await.context("Cannot read cache lock")?; + let hash = Local::regular(&lock).read_to_string().await.context("Cannot read cache lock")?; let hash = u128::from_str_radix(&hash, 16).context("Cannot parse hash from lock")?; if hash != task.cha.hash_u128() { Err(anyhow!("Remote file has changed since last download"))?; @@ -474,7 +475,7 @@ impl File { let cha = Self::cha(&task.url, true, None).await.context("Cannot stat uploaded file")?; let hash = format!("{:x}", cha.hash_u128()); - Local.write(lock, hash).await.context("Cannot lock cache")?; + Local::regular(&lock).write(hash).await.context("Cannot lock cache")?; break; } @@ -510,7 +511,7 @@ impl File { url.hash(&mut h); timestamp_us().hash(&mut h); - unique_name(parent.join(format!(".{:x}.%tmp", h.finish())), async { false }).await + unique_name(parent.try_join(format!(".{:x}.%tmp", h.finish()))?, async { false }).await } } diff --git a/yazi-scheduler/src/lib.rs b/yazi-scheduler/src/lib.rs index 57cb1330..6a288622 100644 --- a/yazi-scheduler/src/lib.rs +++ b/yazi-scheduler/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::option_map_unit_fn, clippy::unit_arg)] +#![allow(clippy::option_map_unit_fn)] mod macros; diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 0459b95b..2c72a50b 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -8,7 +8,7 @@ use yazi_config::{YAZI, plugin::{Fetcher, Preloader}}; use yazi_dds::Pump; use yazi_parser::{app::PluginOpt, tasks::ProcessOpenOpt}; use yazi_proxy::TasksProxy; -use yazi_shared::{Id, Throttle, scheme::SchemeLike, url::{UrlBuf, UrlLike}}; +use yazi_shared::{Id, Throttle, url::{UrlBuf, UrlLike}}; use yazi_vfs::{must_be_dir, provider, unique_name}; use super::{Ongoing, TaskOp}; @@ -79,7 +79,13 @@ impl Scheduler { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Cut {} to {}", from.display(), to.display())); - if to.starts_with(&from) && !to.covariant(&from) { + let Ok(prefixed) = to.try_starts_with(&from) else { + return self + .ops + .out(id, FileOutPaste::Fail("Path being cut has a different encoding".to_owned())); + }; + + if prefixed && !to.covariant(&from) { return self.ops.out(id, FileOutPaste::Fail("Cannot cut directory into itself".to_owned())); } @@ -112,7 +118,13 @@ impl Scheduler { to.display() )); - if to.starts_with(&from) && !to.covariant(&from) { + let Ok(prefixed) = to.try_starts_with(&from) else { + return self + .ops + .out(id, FileOutPaste::Fail("Path being copied has a different encoding".to_owned())); + }; + + if prefixed && !to.covariant(&from) { return self.ops.out(id, FileOutPaste::Fail("Cannot copy directory into itself".to_owned())); } @@ -148,7 +160,14 @@ impl Scheduler { to.display() )); - if to.starts_with(&from) && !to.covariant(&from) { + let Ok(prefixed) = to.try_starts_with(&from) else { + return self.ops.out( + id, + FileOutHardlink::Fail("Path being hardlinked has a different encoding".to_owned()), + ); + }; + + if prefixed && !to.covariant(&from) { return self .ops .out(id, FileOutHardlink::Fail("Cannot hardlink directory into itself".to_owned())); @@ -215,7 +234,7 @@ impl Scheduler { ongoing.hooks.add_sync(id, move |canceled| _ = tx.send(canceled)); } - if !url.scheme.is_remote() { + if !url.kind().is_remote() { return self.ops.out(id, FileOutDownload::Fail("Cannot download non-remote file".to_owned())); }; @@ -229,7 +248,7 @@ impl Scheduler { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Upload {}", url.display())); - if !url.scheme.is_remote() { + if !url.kind().is_remote() { return self.ops.out(id, FileOutUpload::Fail("Cannot upload non-remote file".to_owned())); }; diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 9c14365e..c19fe6bf 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -23,6 +23,7 @@ ordered-float = { workspace = true } parking_lot = { workspace = true } percent-encoding = { workspace = true } serde = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true } [target."cfg(unix)".dependencies] diff --git a/yazi-shared/src/bytes.rs b/yazi-shared/src/bytes.rs index 659c1969..0492b947 100644 --- a/yazi-shared/src/bytes.rs +++ b/yazi-shared/src/bytes.rs @@ -1,16 +1,61 @@ +use std::fmt::Display; + +use crate::BytePredictor; + pub trait BytesExt { + fn display(&self) -> impl Display; + fn kebab_cased(&self) -> bool; - fn split_by_seq(&self, sep: &[u8]) -> Option<(&[u8], &[u8])>; + + fn rsplit_pred_once(&self, pred: P) -> Option<(&[u8], &[u8])>; + + fn rsplit_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])>; + + fn split_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])>; } impl BytesExt for [u8] { + fn display(&self) -> impl Display { + struct D<'a>(&'a [u8]); + + impl Display for D<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for chunk in self.0.utf8_chunks() { + chunk.valid().fmt(f)?; + if !chunk.invalid().is_empty() { + char::REPLACEMENT_CHARACTER.fmt(f)?; + } + } + Ok(()) + } + } + + D(self) + } + fn kebab_cased(&self) -> bool { self.iter().all(|&b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) } - fn split_by_seq(&self, sep: &[u8]) -> Option<(&[u8], &[u8])> { + fn rsplit_pred_once(&self, pred: P) -> Option<(&[u8], &[u8])> { + for (i, &byte) in self.iter().enumerate().rev() { + if pred.predicate(byte) { + let (a, b) = self.split_at(i); + return Some((a, &b[1..])); + } + } + None + } + + fn split_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])> { let idx = memchr::memmem::find(self, sep)?; - let (left, right) = self.split_at(idx); - Some((left, &right[sep.len()..])) + let (a, b) = self.split_at(idx); + Some((a, &b[sep.len()..])) + } + + fn rsplit_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])> { + let idx = memchr::memmem::rfind(self, sep)?; + let (a, b) = self.split_at(idx); + Some((a, &b[sep.len()..])) } } diff --git a/yazi-shared/src/chars.rs b/yazi-shared/src/chars.rs index f6e7a943..590067b6 100644 --- a/yazi-shared/src/chars.rs +++ b/yazi-shared/src/chars.rs @@ -1,5 +1,5 @@ use core::str; -use std::{borrow::Cow, ffi::OsStr}; +use std::borrow::Cow; #[derive(Clone, Copy, PartialEq, Eq)] pub enum CharKind { @@ -117,23 +117,3 @@ pub fn replace_to_printable(s: &[String], tab_size: u8) -> String { } unsafe { String::from_utf8_unchecked(buf) } } - -pub fn osstr_contains(s: impl AsRef, needle: impl AsRef) -> bool { - memchr::memmem::find(s.as_ref().as_encoded_bytes(), needle.as_ref().as_encoded_bytes()).is_some() -} - -pub fn osstr_starts_with( - s: impl AsRef, - prefix: impl AsRef, - insensitive: bool, -) -> bool { - let (s, prefix) = (s.as_ref().as_encoded_bytes(), prefix.as_ref().as_encoded_bytes()); - if s.len() < prefix.len() { - return false; - } - if insensitive { - s[..prefix.len()].eq_ignore_ascii_case(prefix) - } else { - s[..prefix.len()] == *prefix - } -} diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 08f23f36..3b1af6dc 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -1,8 +1,8 @@ #![allow(clippy::option_map_unit_fn)] -yazi_macro::mod_pub!(data errors event loc path pool scheme shell translit url); +yazi_macro::mod_pub!(data errors event loc path pool scheme shell strand translit url); -yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os osstr rand ro_cell source string sync_cell terminal tests throttle time utf8); +yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os osstr predictor rand ro_cell source string sync_cell terminal tests throttle time utf8 wtf8); pub fn init() { pool::init(); diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index 5e669c6c..24f5f325 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -2,7 +2,7 @@ use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, use anyhow::Result; -use crate::{loc::Loc, path::{AsInnerView, AsPathDyn, AsPathView, PathBufLike, PathDyn, PathLike}}; +use crate::{loc::Loc, path::{AsPathDyn, AsPathView, PathBufLike, PathBufUnsafeExt, PathDyn, PathLike, PathUnsafeExt, SetNameError}, scheme::SchemeKind, strand::AsStrandView}; #[derive(Clone, Default, Eq)] pub struct LocBuf { @@ -65,7 +65,8 @@ where // --- Hash impl

Hash for LocBuf

where - P: PathBufLike, + P: PathBufLike + PathBufUnsafeExt, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { fn hash(&self, state: &mut H) { self.as_loc().hash(state) } @@ -73,7 +74,8 @@ where impl

Debug for LocBuf

where - P: PathBufLike + Debug, + P: PathBufLike + PathBufUnsafeExt + Debug, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { fn fmt(&self, f: &mut Formatter) -> fmt::Result { @@ -87,7 +89,8 @@ where impl

From

for LocBuf

where - P: PathBufLike, + P: PathBufLike + PathBufUnsafeExt, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { fn from(path: P) -> Self { @@ -106,12 +109,13 @@ impl> From<&T> for LocBuf { impl

LocBuf

where - P: PathBufLike, + P: PathBufLike + PathBufUnsafeExt, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { - pub fn new<'a, T>(path: P, base: T, trail: T) -> Self + pub fn new<'a, S>(path: P, base: S, trail: S) -> Self where - T: for<'b> AsPathView<'a, as PathLike<'b>>::View<'a>>, + S: for<'b> AsStrandView<'a, as PathLike<'b>>::Strand<'a>>, { let loc = Self::from(path); let Loc { inner, uri, urn, _phantom } = Loc::new(&loc.inner, base, trail); @@ -142,9 +146,9 @@ where Self { inner: loc.inner, uri, urn } } - pub fn floated<'a, T>(path: P, base: T) -> Self + pub fn floated<'a, S>(path: P, base: S) -> Self where - T: for<'b> AsPathView<'a, as PathLike<'b>>::View<'a>>, + S: for<'b> AsStrandView<'a, as PathLike<'b>>::Strand<'a>>, { let loc = Self::from(path); let Loc { inner, uri, urn, _phantom } = Loc::floated(&loc.inner, base); @@ -153,6 +157,14 @@ where Self { inner: loc.inner, uri, urn } } + pub fn saturated(path: P, kind: SchemeKind) -> Self { + let loc = Self::from(path); + let Loc { inner, uri, urn, _phantom } = Loc::saturated(&loc.inner, kind); + + debug_assert!(inner.encoded_bytes() == loc.inner.encoded_bytes()); + Self { inner: loc.inner, uri, urn } + } + #[inline] pub fn as_loc<'a>(&'a self) -> Loc<'a, P::Borrowed<'a>> { Loc { @@ -174,16 +186,16 @@ where #[inline] pub fn into_path(self) -> P { self.inner } - pub fn set_name(&mut self, name: T) + pub fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> where - T: for<'a> AsInnerView<'a, P::InnerRef<'a>>, + T: AsStrandView<'a, P::Strand<'a>>, { let old = self.inner.len(); - self.mutate(|path| path.set_file_name(name)); + self.mutate(|path| path.try_set_name(name))?; let new = self.len(); if new == old { - return; + return Ok(()); } if self.uri != 0 { @@ -200,31 +212,34 @@ where self.urn -= old - new; } } + Ok(()) } #[inline] - pub fn rebase<'a, 'b>(&'a self, base: P::Borrowed<'b>) -> Self + pub fn rebase<'a, 'b>(&'a self, base: P::Borrowed<'b>) -> Result where 'a: 'b, for<'c> as PathLike<'c>>::Owned: Into, { - let mut loc: Self = base.join(self.uri()).into(); + let mut loc: Self = base.try_join(self.uri())?.into(); (loc.uri, loc.urn) = (self.uri, self.urn); - loc + Ok(loc) } #[inline] - fn mutate(&mut self, f: F) { + fn mutate T>(&mut self, f: F) -> T { let mut inner = self.inner.take(); - f(&mut inner); + let result = f(&mut inner); self.inner = Self::from(inner).inner; + result } } // FIXME: macro impl

LocBuf

where - P: PathBufLike, + P: PathBufLike + PathBufUnsafeExt, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { #[inline] @@ -246,13 +261,17 @@ where pub fn has_trail(&self) -> bool { self.as_loc().has_trail() } #[inline] - pub fn name(&self) -> Option< as PathLike<'_>>::Inner> { self.as_loc().name() } + pub fn name(&self) -> Option< as PathLike<'_>>::Strand<'_>> { + self.as_loc().name() + } #[inline] - pub fn stem(&self) -> Option< as PathLike<'_>>::Inner> { self.as_loc().stem() } + pub fn stem(&self) -> Option< as PathLike<'_>>::Strand<'_>> { + self.as_loc().stem() + } #[inline] - pub fn ext(&self) -> Option< as PathLike<'_>>::Inner> { self.as_loc().ext() } + pub fn ext(&self) -> Option< as PathLike<'_>>::Strand<'_>> { self.as_loc().ext() } } impl LocBuf { @@ -360,7 +379,7 @@ mod tests { for (input, name, expected) in cases { let mut a: UrlBuf = input.parse()?; let b: UrlBuf = expected.parse()?; - a.set_name(name); + a.try_set_name(name).unwrap(); assert_eq!( (a.name(), format!("{a:?}").replace(r"\", "/")), (b.name(), expected.replace(r"\", "/")) diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 9d3697a7..a24d5801 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -2,7 +2,7 @@ use std::{hash::{Hash, Hasher}, marker::PhantomData, ops::Deref, path::Path}; use anyhow::{Result, bail}; -use crate::{loc::LocBuf, path::{AsPathView, PathBufLike, PathInner, PathLike}}; +use crate::{loc::LocBuf, path::{AsPathDyn, AsPathView, PathBufLike, PathDyn, PathLike, PathUnsafeExt}, scheme::SchemeKind, strand::{AsStrandView, StrandLike}}; #[derive(Clone, Copy, Debug)] pub struct Loc<'p, P = &'p Path> { @@ -28,6 +28,13 @@ where fn deref(&self) -> &Self::Target { &self.inner } } +impl<'p, P> AsPathDyn for Loc<'p, P> +where + P: PathLike<'p> + AsPathDyn, +{ + fn as_path_dyn(&self) -> PathDyn<'_> { self.inner.as_path_dyn() } +} + // FIXME: remove impl AsRef for Loc<'_, &std::path::Path> { fn as_ref(&self) -> &std::path::Path { self.inner } @@ -63,16 +70,16 @@ impl<'p, P> Eq for Loc<'p, P> where P: PathLike<'p> + Eq {} impl<'p, P> Loc<'p, P> where - P: PathLike<'p>, + P: PathLike<'p> + PathUnsafeExt<'p>, { - pub fn new<'a, T, U>(path: T, base: U, trail: U) -> Self + pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self where T: AsPathView<'p, P>, - U: AsPathView<'a, P::View<'a>>, + S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); - loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len(); - loc.urn = loc.inner.strip_prefix(trail).expect("Loc must start with the given trail").len(); + loc.uri = loc.inner.try_strip_prefix(base).expect("Loc must start with the given base").len(); + loc.urn = loc.inner.try_strip_prefix(trail).expect("Loc must start with the given trail").len(); loc } @@ -98,10 +105,10 @@ where bail!("URI exceeds the entire URL"); } if i == urn { - loc.urn = loc.strip_prefix(it.clone()).unwrap().len(); + loc.urn = loc.try_strip_prefix(it.clone())?.len(); } if i == uri { - loc.uri = loc.strip_prefix(it).unwrap().len(); + loc.uri = loc.try_strip_prefix(it)?.len(); break; } } @@ -113,7 +120,7 @@ where T: AsPathView<'p, P>, { let path = path.as_path_view(); - let Some(name) = path.file_name() else { + let Some(name) = path.name() else { let uri = path.len(); return Self { inner: path, uri, urn: 0, _phantom: PhantomData }; }; @@ -140,22 +147,37 @@ where loc } - pub fn floated<'a, T, U>(path: T, base: U) -> Self + pub fn floated<'a, T, S>(path: T, base: S) -> Self where T: AsPathView<'p, P>, - U: AsPathView<'a, P::View<'a>>, + S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); - loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len(); + loc.uri = loc.inner.try_strip_prefix(base).expect("Loc must start with the given base").len(); loc } + pub fn saturated<'a, T>(path: T, kind: SchemeKind) -> Self + where + T: AsPathView<'p, P>, + { + match kind { + SchemeKind::Regular => Self::bare(path), + SchemeKind::Search => Self::zeroed(path), + SchemeKind::Archive => Self::zeroed(path), + SchemeKind::Sftp => Self::bare(path), + } + } + #[inline] pub fn as_loc(self) -> Self { self } #[inline] pub fn as_path(self) -> P { self.inner } + #[inline] + pub fn is_empty(self) -> bool { self.inner.is_empty() } + #[inline] pub fn uri(self) -> P { unsafe { @@ -190,18 +212,6 @@ where #[inline] pub fn has_trail(self) -> bool { self.inner.len() != self.urn } - #[inline] - pub fn name(self) -> Option { self.inner.file_name() } - - #[inline] - pub fn stem(self) -> Option { self.inner.file_stem() } - - #[inline] - pub fn ext(self) -> Option { self.inner.extension() } - - #[inline] - pub fn parent(self) -> Option

{ self.inner.parent() } - #[inline] pub fn triple(self) -> (P, P, P) { let len = self.inner.len(); @@ -218,12 +228,4 @@ where ) } } - - #[inline] - pub fn strip_prefix<'a, T>(self, base: T) -> Option

- where - T: AsPathView<'a, P::View<'a>>, - { - self.inner.strip_prefix(base) - } } diff --git a/yazi-shared/src/osstr.rs b/yazi-shared/src/osstr.rs index ef3dbb8b..7fc560af 100644 --- a/yazi-shared/src/osstr.rs +++ b/yazi-shared/src/osstr.rs @@ -58,38 +58,3 @@ where result } } - -// --- OsStrSplit -pub trait OsStrSplit { - fn rsplit_once(&self, predicate: P) -> Option<(&Self, &Self)>; -} - -impl OsStrSplit for OsStr { - fn rsplit_once(&self, pat: P) -> Option<(&Self, &Self)> { - let bytes = self.as_encoded_bytes(); - for (i, &byte) in bytes.iter().enumerate().rev() { - if !pat.predicate(byte) { - continue; - } - - let (a, b) = bytes.split_at(i); - // SAFETY: These substrings were separated by a UTF-8 string. - return Some(unsafe { - (Self::from_encoded_bytes_unchecked(a), Self::from_encoded_bytes_unchecked(&b[1..])) - }); - } - None - } -} - -pub trait Pattern { - fn predicate(&self, byte: u8) -> bool; -} - -impl Pattern for char { - fn predicate(&self, byte: u8) -> bool { *self == byte as Self } -} - -impl Pattern for &[char] { - fn predicate(&self, byte: u8) -> bool { self.contains(&(byte as char)) } -} diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 7e3ff236..97a135e5 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -1,54 +1,61 @@ use std::{fmt::Debug, hash::Hash}; -use crate::path::{AsInnerView, AsPathView, PathInner, PathLike}; +use crate::{path::{AsPath, AsPathView, PathBufDyn, PathLike, SetNameError, StartsWithError}, strand::{AsStrandView, StrandLike}}; pub trait PathBufLike where - Self: 'static, + Self: 'static + AsPath, { - type Inner: for<'a> AsInnerView<'a, Self::InnerRef<'a>>; - type InnerRef<'a>: PathInner<'a>; + type Strand<'a>: StrandLike<'a>; type Borrowed<'a>: PathLike<'a> + AsPathView<'a, Self::Borrowed<'a>> + Debug + Hash; fn borrow(&self) -> Self::Borrowed<'_>; - fn encoded_bytes(&self) -> &[u8]; + fn encoded_bytes(&self) -> &[u8] { self.borrow().encoded_bytes() } - unsafe fn from_encoded_bytes(bytes: Vec) -> Self; + fn into_dyn(self) -> PathBufDyn; fn into_encoded_bytes(self) -> Vec; - fn is_empty(&self) -> bool { self.encoded_bytes().is_empty() } + fn is_empty(&self) -> bool { self.borrow().is_empty() } - fn len(&self) -> usize { self.encoded_bytes().len() } + fn len(&self) -> usize { self.borrow().len() } - fn set_file_name(&mut self, name: T) + fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.borrow().to_str() } + + fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> where - T: for<'a> AsInnerView<'a, Self::InnerRef<'a>>; + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_starts_with<'a, T>(&self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; fn take(&mut self) -> Self; } impl PathBufLike for std::path::PathBuf { type Borrowed<'a> = &'a std::path::Path; - type Inner = std::ffi::OsString; - type InnerRef<'a> = &'a std::ffi::OsStr; + type Strand<'a> = &'a std::ffi::OsStr; fn borrow(&self) -> Self::Borrowed<'_> { self.as_path() } - fn encoded_bytes(&self) -> &[u8] { self.as_os_str().as_encoded_bytes() } - - unsafe fn from_encoded_bytes(bytes: Vec) -> Self { - Self::from(unsafe { Self::Inner::from_encoded_bytes_unchecked(bytes) }) - } + fn into_dyn(self) -> PathBufDyn { PathBufDyn::Os(self) } fn into_encoded_bytes(self) -> Vec { self.into_os_string().into_encoded_bytes() } - fn set_file_name(&mut self, name: T) + fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> where - T: for<'a> AsInnerView<'a, Self::InnerRef<'a>>, + T: AsStrandView<'a, Self::Strand<'a>>, { - self.set_file_name(name.as_inner_view()); + Ok(self.set_file_name(name.as_strand_view())) + } + + fn try_starts_with<'a, T>(&self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(self.starts_with(base.as_strand_view())) } fn take(&mut self) -> Self { std::mem::take(self) } diff --git a/yazi-shared/src/path/conversion.rs b/yazi-shared/src/path/conversion.rs new file mode 100644 index 00000000..8eee3e53 --- /dev/null +++ b/yazi-shared/src/path/conversion.rs @@ -0,0 +1,105 @@ +use std::{borrow::Cow, ffi::OsStr}; + +use super::{PathBufDyn, PathDyn}; +use crate::path::{PathBufLike, PathCow, PathLike}; + +// --- AsPath +pub trait AsPath { + fn as_path(&self) -> impl PathLike<'_>; +} + +impl AsPath for OsStr { + fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } +} + +impl AsPath for &OsStr { + fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } +} + +impl AsPath for std::path::Path { + fn as_path(&self) -> impl PathLike<'_> { self } +} + +impl AsPath for std::path::PathBuf { + fn as_path(&self) -> impl PathLike<'_> { self.as_path() } +} + +impl AsPath for PathDyn<'_> { + fn as_path(&self) -> impl PathLike<'_> { *self } +} + +impl AsPath for PathBufDyn { + fn as_path(&self) -> impl PathLike<'_> { self.borrow() } +} + +impl AsPath for &PathBufDyn { + fn as_path(&self) -> impl PathLike<'_> { self.borrow() } +} + +impl AsPath for PathCow<'_> { + fn as_path(&self) -> impl PathLike<'_> { + match self { + PathCow::Borrowed(p) => *p, + PathCow::Owned(p) => p.into(), + } + } +} + +// --- AsPathDyn +pub trait AsPathDyn { + fn as_path_dyn(&self) -> PathDyn<'_>; +} + +impl AsPathDyn for OsStr { + fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } +} + +impl AsPathDyn for &OsStr { + fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } +} + +impl AsPathDyn for Cow<'_, OsStr> { + fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } +} + +impl AsPathDyn for std::path::Path { + fn as_path_dyn(&self) -> PathDyn<'_> { self.into() } +} + +impl AsPathDyn for std::path::PathBuf { + fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } +} + +impl AsPathDyn for &std::path::PathBuf { + fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } +} + +impl AsPathDyn for PathDyn<'_> { + fn as_path_dyn(&self) -> PathDyn<'_> { *self } +} + +impl AsPathDyn for PathBufDyn { + fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } +} + +impl AsPathDyn for &PathBufDyn { + fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } +} + +impl AsPathDyn for PathCow<'_> { + fn as_path_dyn(&self) -> PathDyn<'_> { + match self { + Self::Borrowed(p) => p.as_path_dyn(), + Self::Owned(p) => p.as_path_dyn(), + } + } +} + +// --- AsPathRef +pub trait AsPathRef<'a> { + fn as_path_ref(self) -> PathDyn<'a>; +} + +impl<'a> AsPathRef<'a> for PathDyn<'a> { + fn as_path_ref(self) -> PathDyn<'a> { self } +} diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs new file mode 100644 index 00000000..238a11b5 --- /dev/null +++ b/yazi-shared/src/path/cow.rs @@ -0,0 +1,56 @@ +use std::borrow::Cow; + +use anyhow::Result; + +use crate::{IntoOsStr, path::{AsPathDyn, PathBufDyn, PathBufLike, PathDyn, PathLike}}; + +// --- PathCow +pub enum PathCow<'a> { + Borrowed(PathDyn<'a>), + Owned(PathBufDyn), +} + +impl<'a> From> for PathCow<'a> { + fn from(value: PathDyn<'a>) -> Self { Self::Borrowed(value) } +} + +impl From for PathCow<'_> { + fn from(value: PathBufDyn) -> Self { Self::Owned(value) } +} + +impl From> for PathBufDyn { + fn from(value: PathCow<'_>) -> Self { value.into_owned() } +} + +impl PartialEq<&str> for PathCow<'_> { + fn eq(&self, other: &&str) -> bool { + match self { + Self::Borrowed(s) => s.as_path_dyn() == *other, + Self::Owned(s) => s.as_path_dyn() == *other, + } + } +} + +impl<'a> PathCow<'a> { + pub fn from_os_bytes(bytes: impl Into>) -> Result { + Ok(match bytes.into().into_os_str()? { + Cow::Borrowed(s) => PathDyn::os(s).into(), + Cow::Owned(s) => PathBufDyn::os(s).into(), + }) + } + + pub fn into_owned(self) -> PathBufDyn { + match self { + Self::Borrowed(s) => s.to_buf_dyn(), + Self::Owned(s) => s, + } + } + + // FIXME: remove, instead implement PathLike for PathCow + pub fn encoded_bytes(&self) -> &[u8] { + match self { + Self::Borrowed(s) => s.encoded_bytes(), + Self::Owned(s) => s.encoded_bytes(), + } + } +} diff --git a/yazi-shared/src/path/dyn.rs b/yazi-shared/src/path/dyn.rs index 10f32014..777f2ef7 100644 --- a/yazi-shared/src/path/dyn.rs +++ b/yazi-shared/src/path/dyn.rs @@ -1,8 +1,11 @@ +use std::ffi::OsStr; + +use anyhow::Result; use hashbrown::Equivalent; use serde::Serialize; -use super::{AsInnerView, AsPathView}; -use crate::path::{PathBufLike, PathLike}; +use super::{AsPathView, RsplitOnceError, StartsWithError}; +use crate::{FromWtf8, Utf8BytePredictor, path::{AsPathDyn, EndsWithError, JoinError, PathBufDynError, PathBufLike, PathBufUnsafeExt, PathDynError, PathKind, PathLike, SetNameError, StripPrefixError}, scheme::SchemeKind, strand::{AsStrandDyn, AsStrandView, Strand, StrandError}}; #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum PathDyn<'p> { @@ -18,20 +21,46 @@ impl<'a> AsPathView<'a, PathDyn<'a>> for std::path::Components<'a> { } impl<'a> From<&'a std::path::Path> for PathDyn<'a> { - fn from(value: &'a std::path::Path) -> Self { PathDyn::Os(value) } + fn from(value: &'a std::path::Path) -> Self { Self::Os(value) } } impl<'a> From<&'a PathBufDyn> for PathDyn<'a> { fn from(value: &'a PathBufDyn) -> Self { value.borrow() } } +impl PartialEq for PathDyn<'_> { + fn eq(&self, other: &PathBufDyn) -> bool { *self == other.borrow() } +} + +impl PartialEq> for &std::path::Path { + fn eq(&self, other: &PathDyn<'_>) -> bool { matches!(*other, PathDyn::Os(p) if p == *self) } +} + +impl PartialEq<&std::path::Path> for PathDyn<'_> { + fn eq(&self, other: &&std::path::Path) -> bool { matches!(*self, PathDyn::Os(p) if p == *other) } +} + +impl PartialEq<&str> for PathDyn<'_> { + fn eq(&self, other: &&str) -> bool { + match self { + PathDyn::Os(p) => p == other, + } + } +} + +impl Equivalent for PathDyn<'_> { + fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.borrow() } +} + impl<'p> PathLike<'p> for PathDyn<'p> { type Components<'a> = std::path::Components<'a>; type Display<'a> = std::path::Display<'a>; - type Inner = &'p [u8]; type Owned = PathBufDyn; + type Strand<'a> = Strand<'a>; type View<'a> = PathDyn<'a>; + fn as_dyn(self) -> PathDyn<'p> { self } + fn components(self) -> Self::Components<'p> { match self { Self::Os(p) => p.components(), @@ -53,38 +82,36 @@ impl<'p> PathLike<'p> for PathDyn<'p> { } } - fn extension(self) -> Option { + fn ext(self) -> Option> { Some(match self { - Self::Os(p) => p.extension()?.as_encoded_bytes(), + Self::Os(p) => p.extension()?.into(), }) } - fn file_name(self) -> Option { - Some(match self { - Self::Os(p) => p.file_name()?.as_encoded_bytes(), - }) - } - - fn file_stem(self) -> Option { - Some(match self { - Self::Os(p) => p.file_stem()?.as_encoded_bytes(), - }) - } - - // FIXME: remove - unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self { - Self::Os(std::path::Path::new(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(bytes) })) - } - - fn join<'a, T>(self, base: T) -> Self::Owned - where - T: AsPathView<'a, Self::View<'a>>, - { - match (self, base.as_path_view()) { - (Self::Os(p), PathDyn::Os(q)) => Self::Owned::Os(p.join(q)), + fn has_root(self) -> bool { + match self { + Self::Os(p) => p.has_root(), } } + fn is_absolute(self) -> bool { + match self { + Self::Os(p) => p.is_absolute(), + } + } + + fn kind(self) -> PathKind { + match self { + Self::Os(_) => PathKind::Os, + } + } + + fn name(self) -> Option> { + Some(match self { + Self::Os(p) => p.file_name()?.into(), + }) + } + fn owned(self) -> Self::Owned { match self { Self::Os(p) => Self::Owned::Os(p.to_path_buf()), @@ -97,26 +124,129 @@ impl<'p> PathLike<'p> for PathDyn<'p> { }) } - fn strip_prefix<'a, T>(self, base: T) -> Option + fn stem(self) -> Option> { + Some(match self { + Self::Os(p) => p.file_stem()?.into(), + }) + } + + fn try_ends_with<'a, T>(self, child: T) -> Result where - T: AsPathView<'a, Self::View<'a>>, + T: AsStrandView<'a, Self::Strand<'a>>, { - Some(match (self, base.as_path_view()) { - (Self::Os(p), PathDyn::Os(q)) => Self::Os(p.strip_prefix(q).ok()?), + Ok(match (self, child.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => p.ends_with(q), + (Self::Os(p), Strand::Utf8(q)) => p.ends_with(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.ends_with(OsStr::from_wtf8(b).map_err(|_| EndsWithError)?) + } + }) + } + + fn try_join<'a, T>(self, path: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, path.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => Self::Owned::Os(p.join(q)), + (Self::Os(p), Strand::Utf8(q)) => Self::Owned::Os(p.join(q)), + (Self::Os(p), Strand::Bytes(b)) => { + Self::Owned::Os(p.join(OsStr::from_wtf8(b).map_err(|_| JoinError::FromWtf8)?)) + } + }) + } + + fn rsplit_pred<'a, T>(self, pred: T) -> Option<(Self, Self)> + where + T: Utf8BytePredictor, + { + match self { + PathDyn::Os(p) => p.rsplit_pred(pred).map(|(l, r)| (Self::Os(l), Self::Os(r))), + } + } + + fn try_rsplit_seq<'a, T>(self, pat: T) -> Result<(Self, Self), RsplitOnceError> + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + let pat = pat.as_strand_view(); + match self { + PathDyn::Os(p) => { + let (l, r) = p.try_rsplit_seq(pat.as_os().map_err(|_| RsplitOnceError::AsOs)?)?; + Ok((Self::Os(l), Self::Os(r))) + } + } + } + + fn try_starts_with<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, base.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => p.starts_with(q), + (Self::Os(p), Strand::Utf8(q)) => p.starts_with(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) + } + }) + } + + fn try_strip_prefix<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, base.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => Self::Os(p.strip_prefix(q)?), + (Self::Os(p), Strand::Utf8(q)) => Self::Os(p.strip_prefix(q)?), + (Self::Os(p), Strand::Bytes(b)) => { + Self::Os(p.strip_prefix(OsStr::from_wtf8(b).map_err(|_| StripPrefixError::WrongEncoding)?)?) + } }) } } -impl PartialEq for PathDyn<'_> { - fn eq(&self, other: &PathBufDyn) -> bool { *self == other.borrow() } -} +impl<'a> PathDyn<'a> { + #[inline] + pub fn os(path: &'a T) -> Self + where + T: ?Sized + AsRef, + { + Self::Os(path.as_ref()) + } -impl PartialEq> for &std::path::Path { - fn eq(&self, other: &PathDyn<'_>) -> bool { matches!(*other, PathDyn::Os(p) if p == *self) } -} + #[inline] + pub fn as_os(self) -> Result<&'a std::path::Path, PathDynError> { + match self { + Self::Os(p) => Ok(p), + } + } -impl Equivalent for PathDyn<'_> { - fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.borrow() } + #[inline] + pub fn to_os_owned(self) -> Result { + match self { + Self::Os(p) => Ok(p.to_owned()), + } + } + + pub fn with(kind: SchemeKind, strand: &'a T) -> Result + where + T: ?Sized + AsStrandDyn, + { + use SchemeKind as K; + + let s = strand.as_strand_dyn(); + Ok(match kind { + K::Regular | K::Search | K::Archive => Self::Os(std::path::Path::new(s.as_os()?)), + K::Sftp => Self::Os(std::path::Path::new(s.as_os()?)), // FIXME + }) + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: &'a [u8]) -> Self { + match kind.into() { + PathKind::Os => Self::Os(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }.as_ref()), + } + } } // --- PathBufDyn @@ -126,67 +256,20 @@ pub enum PathBufDyn { Os(std::path::PathBuf), } -impl PathBufDyn { - pub const fn os_default() -> Self { Self::Os(std::path::PathBuf::new()) } -} - -impl PathBufLike for PathBufDyn { - type Borrowed<'a> = PathDyn<'a>; - type Inner = Vec; - type InnerRef<'a> = &'a [u8]; - - fn borrow(&self) -> Self::Borrowed<'_> { - match self { - Self::Os(p) => Self::Borrowed::Os(p.as_path()), - } - } - - fn encoded_bytes(&self) -> &[u8] { - match self { - Self::Os(p) => p.as_os_str().as_encoded_bytes(), - } - } - - // FIXME: remove - unsafe fn from_encoded_bytes(bytes: Vec) -> Self { - Self::Os(std::path::PathBuf::from(unsafe { - std::ffi::OsString::from_encoded_bytes_unchecked(bytes) - })) - } - - fn into_encoded_bytes(self) -> Vec { - match self { - Self::Os(p) => p.into_os_string().into_encoded_bytes(), - } - } - - fn set_file_name(&mut self, name: T) - where - T: for<'a> AsInnerView<'a, Self::InnerRef<'a>>, - { - // TODO: introduce a new `PathInnerDyn` - todo!() - } - - fn take(&mut self) -> Self { - match self { - Self::Os(p) => Self::Os(std::mem::take(p)), - } - } -} - -impl From> for PathBufDyn { - fn from(value: PathDyn<'_>) -> Self { - match value { - PathDyn::Os(p) => Self::Os(p.to_path_buf()), - } - } +impl From for PathBufDyn { + fn from(value: std::path::PathBuf) -> Self { Self::Os(value) } } impl From<&PathBufDyn> for PathBufDyn { fn from(value: &PathBufDyn) -> Self { value.clone() } } +impl TryFrom for std::path::PathBuf { + type Error = PathBufDynError; + + fn try_from(value: PathBufDyn) -> Result { value.into_os() } +} + impl PartialEq> for PathBufDyn { fn eq(&self, other: &PathDyn<'_>) -> bool { self.borrow() == *other } } @@ -198,3 +281,131 @@ impl PartialEq> for &PathBufDyn { impl Equivalent> for PathBufDyn { fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.borrow() == *key } } + +impl PathBufLike for PathBufDyn { + type Borrowed<'a> = PathDyn<'a>; + type Strand<'a> = Strand<'a>; + + fn borrow(&self) -> Self::Borrowed<'_> { + match self { + Self::Os(p) => Self::Borrowed::Os(p.as_path()), + } + } + + fn encoded_bytes(&self) -> &[u8] { + match self { + Self::Os(p) => p.as_os_str().as_encoded_bytes(), + } + } + + fn into_dyn(self) -> PathBufDyn { self } + + fn into_encoded_bytes(self) -> Vec { + match self { + Self::Os(p) => p.into_os_string().into_encoded_bytes(), + } + } + + fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, name.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => p.set_file_name(q), + (Self::Os(p), Strand::Utf8(q)) => p.set_file_name(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.set_file_name(OsStr::from_wtf8(b).map_err(|_| SetNameError::FromWtf8)?) + } + }) + } + + fn try_starts_with<'a, T>(&self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, base.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => p.starts_with(q), + (Self::Os(p), Strand::Utf8(q)) => p.starts_with(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) + } + }) + } + + fn take(&mut self) -> Self { + match self { + Self::Os(p) => Self::Os(std::mem::take(p)), + } + } +} + +impl PathBufDyn { + #[inline] + pub fn os(path: impl Into) -> Self { Self::Os(path.into()) } + + #[inline] + pub fn into_os(self) -> Result { + Ok(match self { + PathBufDyn::Os(p) => p, + }) + } + + #[inline] + pub fn new(kind: SchemeKind) -> Self { + use SchemeKind as K; + + match kind { + K::Regular | K::Search | K::Archive => Self::Os(std::path::PathBuf::new()), + K::Sftp => Self::Os(std::path::PathBuf::new()), // FIXME + } + } + + pub fn with(kind: SchemeKind, strand: T) -> Result + where + T: AsStrandDyn, + { + use SchemeKind as K; + + let s = strand.as_strand_dyn(); + Ok(match kind { + K::Regular | K::Search | K::Archive => Self::Os(std::path::PathBuf::from(s.as_os()?)), + K::Sftp => Self::Os(std::path::PathBuf::from(s.as_os()?)), // FIXME + }) + } + + pub fn with_capacity(kind: SchemeKind, capacity: usize) -> Self { + use SchemeKind as K; + match kind { + K::Regular | K::Search | K::Archive => Self::Os(std::path::PathBuf::with_capacity(capacity)), + K::Sftp => Self::Os(std::path::PathBuf::with_capacity(capacity)), // FIXME + } + } + + pub fn try_push(&mut self, path: T) -> Result<(), StrandError> + where + T: AsPathDyn, + { + let path = path.as_path_dyn(); + Ok(match self { + PathBufDyn::Os(p) => p.push(path.as_os()?), + }) + } + + pub fn try_extend(&mut self, paths: T) -> Result<(), StrandError> + where + T: IntoIterator, + T::Item: AsPathDyn, + { + for p in paths { + self.try_push(p)?; + } + Ok(()) + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: Vec) -> Self { + match kind.into() { + PathKind::Os => Self::Os(unsafe { std::path::PathBuf::from_encoded_bytes(bytes) }), + } + } +} diff --git a/yazi-shared/src/path/error.rs b/yazi-shared/src/path/error.rs new file mode 100644 index 00000000..30f6dc14 --- /dev/null +++ b/yazi-shared/src/path/error.rs @@ -0,0 +1,82 @@ +use thiserror::Error; + +use crate::strand::StrandError; + +// --- EndsWithError +#[derive(Debug, Error)] +#[error("calling ends_with on paths with different encodings")] +pub struct EndsWithError; + +// --- JoinError +#[derive(Debug, Error)] +#[error("calling join on paths with different encodings")] +pub enum JoinError { + FromWtf8, + FromPathBufDyn(#[from] PathBufDynError), +} + +impl From for std::io::Error { + fn from(err: JoinError) -> Self { std::io::Error::other(err) } +} + +// --- PathDynError +#[derive(Debug, Error)] +pub enum PathDynError { + #[error("conversion to OsStr failed")] + AsOs, +} + +impl From for std::io::Error { + fn from(err: PathDynError) -> Self { std::io::Error::other(err) } +} + +// --- PathBufDynError +#[derive(Debug, Error)] +pub enum PathBufDynError { + #[error("conversion to OsString failed")] + IntoOs, +} + +// --- SetNameError +#[derive(Debug, Error)] +#[error("calling set_name on paths with different encodings")] +pub enum SetNameError { + FromWtf8, + FromStrandDyn(#[from] StrandError), +} + +impl From for std::io::Error { + fn from(err: SetNameError) -> Self { std::io::Error::other(err) } +} + +// --- RsplitOnce +#[derive(Error, Debug)] +#[error("calling rsplit_once on paths with different encodings")] +pub enum RsplitOnceError { + #[error("conversion to OsStr failed")] + AsOs, + #[error("conversion to UTF-8 str failed")] + AsUtf8, + #[error("the pattern was not found")] + NotFound, +} + +// --- StartsWithError +#[derive(Error, Debug)] +#[error("calling starts_with on paths with different encodings")] +pub struct StartsWithError; + +// --- StripPrefixError +#[derive(Debug, Error)] +pub enum StripPrefixError { + #[error("calling strip_prefix on URLs with different schemes")] + Exotic, + #[error("the base is not a prefix of the path")] + NotPrefix, + #[error("calling strip_prefix on paths with different encodings")] + WrongEncoding, +} + +impl From for StripPrefixError { + fn from(_: std::path::StripPrefixError) -> Self { Self::NotPrefix } +} diff --git a/yazi-shared/src/path/inner.rs b/yazi-shared/src/path/inner.rs deleted file mode 100644 index 4237078a..00000000 --- a/yazi-shared/src/path/inner.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub trait PathInner<'a>: Copy { - fn encoded_bytes(self) -> &'a [u8]; - - fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } - - fn len(self) -> usize { self.encoded_bytes().len() } -} - -impl<'a> PathInner<'a> for &'a std::ffi::OsStr { - fn encoded_bytes(self) -> &'a [u8] { self.as_encoded_bytes() } -} - -impl<'a> PathInner<'a> for &'a [u8] { - fn encoded_bytes(self) -> &'a [u8] { self } -} diff --git a/yazi-shared/src/path/kind.rs b/yazi-shared/src/path/kind.rs new file mode 100644 index 00000000..4c9e1920 --- /dev/null +++ b/yazi-shared/src/path/kind.rs @@ -0,0 +1,16 @@ +use crate::scheme::SchemeKind; + +pub enum PathKind { + Os, +} + +impl From for PathKind { + fn from(value: SchemeKind) -> Self { + match value { + SchemeKind::Regular => Self::Os, + SchemeKind::Search => Self::Os, + SchemeKind::Archive => Self::Os, + SchemeKind::Sftp => Self::Os, // FIXME + } + } +} diff --git a/yazi-shared/src/path/mod.rs b/yazi-shared/src/path/mod.rs index 99d255a7..30fde723 100644 --- a/yazi-shared/src/path/mod.rs +++ b/yazi-shared/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(buf component components inner path r#dyn traits); +yazi_macro::mod_flat!(buf component components conversion cow error kind path r#dyn r#unsafe view); diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index 51e83577..43e02dea 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -1,15 +1,24 @@ -use crate::path::{AsPathView, PathBufLike, PathInner}; +use std::{borrow::Cow, ffi::OsStr}; + +use anyhow::Result; + +use crate::{BytesExt, Utf8BytePredictor, path::{AsPathView, EndsWithError, JoinError, PathBufDyn, PathBufLike, PathDyn, PathKind, RsplitOnceError, StartsWithError, StripPrefixError}, strand::{AsStrandView, StrandLike}}; pub trait PathLike<'p> where - Self: Copy + AsPathView<'p, Self::View<'p>>, + Self: Copy + AsPathView<'p, Self::View<'p>> + AsStrandView<'p, Self::Strand<'p>>, { - type Inner: PathInner<'p>; + type Strand<'a>: StrandLike<'a>; type Owned: PathBufLike + Into; type View<'a>; - type Components<'a>: AsPathView<'a, Self::View<'a>> + Clone + DoubleEndedIterator; + type Components<'a>: Clone + + DoubleEndedIterator + + AsPathView<'a, Self::View<'a>> + + AsStrandView<'a, Self::Strand<'a>>; type Display<'a>: std::fmt::Display; + fn as_dyn(self) -> PathDyn<'p>; + fn components(self) -> Self::Components<'p>; fn default() -> Self; @@ -18,43 +27,71 @@ where fn encoded_bytes(self) -> &'p [u8]; - fn extension(self) -> Option; + fn ext(self) -> Option>; - fn file_name(self) -> Option; + fn has_root(self) -> bool; - fn file_stem(self) -> Option; - - unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self; + fn is_absolute(self) -> bool; fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } #[cfg(unix)] fn is_hidden(self) -> bool { - self.file_name().is_some_and(|n| n.encoded_bytes().first() == Some(&b'.')) + self.name().is_some_and(|n| n.encoded_bytes().first() == Some(&b'.')) } - fn join<'a, T>(self, base: T) -> Self::Owned - where - T: AsPathView<'a, Self::View<'a>>; + fn kind(self) -> PathKind; fn len(self) -> usize { self.encoded_bytes().len() } + fn name(self) -> Option>; + fn owned(self) -> Self::Owned; fn parent(self) -> Option; - fn strip_prefix<'a, T>(self, base: T) -> Option + fn rsplit_pred(self, pred: T) -> Option<(Self, Self)> where - T: AsPathView<'a, Self::View<'a>>; + T: Utf8BytePredictor; + + fn stem(self) -> Option>; + + fn to_str(self) -> Result<&'p str, std::str::Utf8Error> { str::from_utf8(self.encoded_bytes()) } + + fn to_string_lossy(self) -> Cow<'p, str> { String::from_utf8_lossy(self.encoded_bytes()) } + + fn to_buf_dyn(self) -> PathBufDyn { self.as_dyn().owned() } + + fn try_ends_with<'a, T>(self, child: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_join<'a, T>(self, path: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_rsplit_seq<'a, T>(self, pat: T) -> Result<(Self, Self), RsplitOnceError> + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_starts_with<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_strip_prefix<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; } impl<'p> PathLike<'p> for &'p std::path::Path { type Components<'a> = std::path::Components<'a>; type Display<'a> = std::path::Display<'a>; - type Inner = &'p std::ffi::OsStr; type Owned = std::path::PathBuf; + type Strand<'a> = &'a std::ffi::OsStr; type View<'a> = &'a std::path::Path; + fn as_dyn(self) -> PathDyn<'p> { PathDyn::Os(self) } + fn components(self) -> Self::Components<'p> { self.components() } fn default() -> Self { std::path::Path::new("") } @@ -63,31 +100,85 @@ impl<'p> PathLike<'p> for &'p std::path::Path { fn encoded_bytes(self) -> &'p [u8] { self.as_os_str().as_encoded_bytes() } - fn extension(self) -> Option { self.extension() } + fn ext(self) -> Option> { self.extension() } - fn file_name(self) -> Option { self.file_name() } + fn has_root(self) -> bool { self.has_root() } - fn file_stem(self) -> Option { self.file_stem() } + fn is_absolute(self) -> bool { self.is_absolute() } - unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self { - std::path::Path::new(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(bytes) }) - } + fn kind(self) -> PathKind { PathKind::Os } - fn join<'a, T>(self, base: T) -> Self::Owned - where - T: AsPathView<'a, Self::View<'a>>, - { - self.join(base.as_path_view()) - } + fn name(self) -> Option> { self.file_name() } fn owned(self) -> Self::Owned { self.to_path_buf() } fn parent(self) -> Option { self.parent() } - fn strip_prefix<'a, T>(self, base: T) -> Option + fn stem(self) -> Option> { self.file_stem() } + + fn try_ends_with<'a, T>(self, child: T) -> Result where - T: AsPathView<'a, Self::View<'a>>, + T: AsStrandView<'a, Self::Strand<'a>>, { - self.strip_prefix(base.as_path_view()).ok() + Ok(self.ends_with(child.as_strand_view())) + } + + fn try_join<'a, T>(self, path: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(self.join(path.as_strand_view())) + } + + fn rsplit_pred<'a, T>(self, pred: T) -> Option<(Self, Self)> + where + T: Utf8BytePredictor, + { + let b = self.encoded_bytes(); + let (left, right) = b.rsplit_pred_once(pred)?; + + Some(unsafe { + ( + OsStr::from_encoded_bytes_unchecked(left).as_ref(), + OsStr::from_encoded_bytes_unchecked(right).as_ref(), + ) + }) + } + + fn try_rsplit_seq<'a, T>(self, pat: T) -> Result<(Self, Self), RsplitOnceError> + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + let b = self.encoded_bytes(); + let p = pat.as_strand_view().encoded_bytes(); + + let (left, right) = b.rsplit_seq_once(p).ok_or(RsplitOnceError::NotFound)?; + Ok(unsafe { + ( + OsStr::from_encoded_bytes_unchecked(left).as_ref(), + OsStr::from_encoded_bytes_unchecked(right).as_ref(), + ) + }) + } + + fn try_starts_with<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(self.starts_with(base.as_strand_view())) + } + + fn try_strip_prefix<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(self.strip_prefix(base.as_strand_view())?) } } + +impl<'a, P> From

for PathBufDyn +where + P: PathLike<'a>, +{ + fn from(value: P) -> Self { value.to_buf_dyn() } +} diff --git a/yazi-shared/src/path/traits.rs b/yazi-shared/src/path/traits.rs deleted file mode 100644 index f24f1c4d..00000000 --- a/yazi-shared/src/path/traits.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}}; - -use super::{PathBufDyn, PathDyn}; -use crate::path::{PathBufLike, PathLike}; - -pub trait AsPath { - fn as_path(&self) -> impl PathLike<'_>; -} - -impl AsPath for OsStr { - fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } -} - -impl AsPath for &OsStr { - fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } -} - -impl AsPath for std::path::Path { - fn as_path(&self) -> impl PathLike<'_> { self } -} - -impl AsPath for std::path::PathBuf { - fn as_path(&self) -> impl PathLike<'_> { self.as_path() } -} - -impl AsPath for PathDyn<'_> { - fn as_path(&self) -> impl PathLike<'_> { *self } -} - -impl AsPath for PathBufDyn { - fn as_path(&self) -> impl PathLike<'_> { self.borrow() } -} - -impl AsPath for &PathBufDyn { - fn as_path(&self) -> impl PathLike<'_> { self.borrow() } -} - -// --- AsPathDyn -pub trait AsPathDyn { - fn as_path_dyn(&self) -> PathDyn<'_>; -} - -impl AsPathDyn for &str { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for String { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for &String { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for OsStr { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for &OsStr { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for Cow<'_, OsStr> { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for std::path::PathBuf { - fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } -} - -impl AsPathDyn for &std::path::PathBuf { - fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } -} - -impl AsPathDyn for PathDyn<'_> { - fn as_path_dyn(&self) -> PathDyn<'_> { *self } -} - -impl AsPathDyn for PathBufDyn { - fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } -} - -impl AsPathDyn for &PathBufDyn { - fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } -} - -// --- AsPathView -pub trait AsPathView<'a, T> { - fn as_path_view(self) -> T; -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a str { - fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a OsStr { - fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { - fn as_path_view(self) -> &'a std::path::Path { self } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { - fn as_path_view(self) -> &'a std::path::Path { self } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for std::path::Components<'a> { - fn as_path_view(self) -> &'a std::path::Path { self.as_path() } -} - -impl<'a> AsPathView<'a, PathDyn<'a>> for &'a PathBufDyn { - fn as_path_view(self) -> PathDyn<'a> { - match self { - PathBufDyn::Os(p) => PathDyn::Os(p.as_path()), - } - } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a Cow<'_, OsStr> { - fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for crate::loc::Loc<'a, &'a std::path::Path> { - fn as_path_view(self) -> &'a std::path::Path { *self } -} - -// --- AsInnerView -pub trait AsInnerView<'a, T> { - fn as_inner_view(&'a self) -> T; -} - -impl<'a> AsInnerView<'a, &'a OsStr> for OsStr { - fn as_inner_view(&'a self) -> &'a OsStr { self } -} - -impl<'a> AsInnerView<'a, &'a OsStr> for OsString { - fn as_inner_view(&'a self) -> &'a OsStr { self } -} - -impl<'a> AsInnerView<'a, &'a [u8]> for [u8] { - fn as_inner_view(&'a self) -> &'a [u8] { self } -} - -impl<'a> AsInnerView<'a, &'a [u8]> for Vec { - fn as_inner_view(&'a self) -> &'a [u8] { self } -} - -impl<'a, T, U> AsInnerView<'a, U> for &T -where - T: ?Sized + AsInnerView<'a, U>, -{ - fn as_inner_view(&'a self) -> U { (*self).as_inner_view() } -} diff --git a/yazi-shared/src/path/unsafe.rs b/yazi-shared/src/path/unsafe.rs new file mode 100644 index 00000000..4bfcb847 --- /dev/null +++ b/yazi-shared/src/path/unsafe.rs @@ -0,0 +1,23 @@ +use std::ffi::OsString; + +// --- PathUnsafeExt +pub trait PathUnsafeExt<'p> { + unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self; +} + +impl<'p> PathUnsafeExt<'p> for &'p std::path::Path { + unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self { + std::path::Path::new(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(bytes) }) + } +} + +// --- PathBufUnsafeExt +pub trait PathBufUnsafeExt { + unsafe fn from_encoded_bytes(bytes: Vec) -> Self; +} + +impl PathBufUnsafeExt for std::path::PathBuf { + unsafe fn from_encoded_bytes(bytes: Vec) -> Self { + Self::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }) + } +} diff --git a/yazi-shared/src/path/view.rs b/yazi-shared/src/path/view.rs new file mode 100644 index 00000000..a23c8422 --- /dev/null +++ b/yazi-shared/src/path/view.rs @@ -0,0 +1,39 @@ +use std::{borrow::Cow, ffi::OsStr}; + +use crate::path::{AsPathDyn, PathDyn}; + +// --- AsPathView +pub trait AsPathView<'a, T> { + fn as_path_view(self) -> T; +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for &'a OsStr { + fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { + fn as_path_view(self) -> &'a std::path::Path { self } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { + fn as_path_view(self) -> &'a std::path::Path { self } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for std::path::Components<'a> { + fn as_path_view(self) -> &'a std::path::Path { self.as_path() } +} + +impl<'a, T> AsPathView<'a, PathDyn<'a>> for &'a T +where + T: AsPathDyn, +{ + fn as_path_view(self) -> PathDyn<'a> { self.as_path_dyn() } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for &'a Cow<'_, OsStr> { + fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for crate::loc::Loc<'a, &'a std::path::Path> { + fn as_path_view(self) -> &'a std::path::Path { *self } +} diff --git a/yazi-shared/src/pool/cow.rs b/yazi-shared/src/pool/cow.rs new file mode 100644 index 00000000..5f91283f --- /dev/null +++ b/yazi-shared/src/pool/cow.rs @@ -0,0 +1,96 @@ +use std::ops::Deref; + +use crate::pool::{Pool, Symbol}; + +pub enum SymbolCow<'a, T: ?Sized> { + Borrowed(&'a T), + Owned(Symbol), +} + +impl Clone for SymbolCow<'_, T> { + fn clone(&self) -> Self { + match self { + Self::Borrowed(t) => Self::Borrowed(t), + Self::Owned(t) => Self::Owned(t.clone()), + } + } +} + +impl AsRef<[u8]> for SymbolCow<'_, [u8]> { + fn as_ref(&self) -> &[u8] { + match self { + Self::Borrowed(b) => b, + Self::Owned(b) => b.as_ref(), + } + } +} + +impl AsRef for SymbolCow<'_, str> { + fn as_ref(&self) -> &str { + match self { + Self::Borrowed(s) => s, + Self::Owned(s) => s.as_ref(), + } + } +} + +// --- Deref +impl Deref for SymbolCow<'_, [u8]> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { self.as_ref() } +} + +impl Deref for SymbolCow<'_, str> { + type Target = str; + + fn deref(&self) -> &Self::Target { self.as_ref() } +} + +// --- From +impl<'a, T: ?Sized> From<&'a T> for SymbolCow<'a, T> { + fn from(value: &'a T) -> Self { Self::Borrowed(value) } +} + +impl From> for SymbolCow<'_, T> { + fn from(value: Symbol) -> Self { Self::Owned(value) } +} + +impl From> for Symbol<[u8]> { + fn from(value: SymbolCow<'_, [u8]>) -> Self { value.into_owned() } +} + +impl From> for Symbol { + fn from(value: SymbolCow<'_, str>) -> Self { value.into_owned() } +} + +// --- Debug +impl std::fmt::Debug for SymbolCow<'_, [u8]> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SymbolCow<[u8]>({:?})", self.as_ref()) + } +} + +impl std::fmt::Debug for SymbolCow<'_, str> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SymbolCow({:?})", self.as_ref()) + } +} + +impl SymbolCow<'_, [u8]> { + pub fn into_owned(self) -> Symbol<[u8]> { + match self { + Self::Borrowed(t) => Pool::<[u8]>::intern(t), + Self::Owned(t) => t, + } + } +} + +impl SymbolCow<'_, str> { + pub fn into_owned(self) -> Symbol { + match self { + Self::Borrowed(t) => Pool::::intern(t), + Self::Owned(t) => t, + } + } +} diff --git a/yazi-shared/src/pool/mod.rs b/yazi-shared/src/pool/mod.rs index dd9924e8..cf34b1dc 100644 --- a/yazi-shared/src/pool/mod.rs +++ b/yazi-shared/src/pool/mod.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_flat!(pool ptr symbol traits); +yazi_macro::mod_flat!(cow pool ptr symbol traits); static SYMBOLS: crate::RoCell< parking_lot::Mutex>, diff --git a/yazi-shared/src/predictor.rs b/yazi-shared/src/predictor.rs new file mode 100644 index 00000000..4603e2e9 --- /dev/null +++ b/yazi-shared/src/predictor.rs @@ -0,0 +1,29 @@ +// --- BytePredictor +pub trait BytePredictor { + fn predicate(&self, byte: u8) -> bool; +} + +// --- Utf8BytePredictor +pub trait Utf8BytePredictor { + fn predicate(&self, byte: u8) -> bool; +} + +// --- AnyAsciiChar +pub struct AnyAsciiChar<'a>(&'a [u8]); + +impl<'a> AnyAsciiChar<'a> { + pub fn new(chars: &'a [u8]) -> Option { + if chars.iter().all(|&b| b <= 0x7f) { Some(Self(chars)) } else { None } + } +} + +impl Utf8BytePredictor for AnyAsciiChar<'_> { + fn predicate(&self, byte: u8) -> bool { self.0.contains(&byte) } +} + +impl BytePredictor for T +where + T: Utf8BytePredictor, +{ + fn predicate(&self, byte: u8) -> bool { self.predicate(byte) } +} diff --git a/yazi-shared/src/scheme/cow.rs b/yazi-shared/src/scheme/cow.rs index 7f35a927..3e16e4cb 100644 --- a/yazi-shared/src/scheme/cow.rs +++ b/yazi-shared/src/scheme/cow.rs @@ -1,9 +1,9 @@ -use std::{borrow::Cow, ops::Not, path::Path}; +use std::{borrow::Cow, ops::Not}; -use anyhow::{Result, bail, ensure}; +use anyhow::{Result, ensure}; use percent_encoding::percent_decode; -use crate::{BytesExt, pool::InternStr, scheme::{AsScheme, Scheme, SchemeRef}}; +use crate::{path::{AsPath, PathCow, PathLike}, pool::{InternStr, SymbolCow}, scheme::{AsScheme, Scheme, SchemeKind, SchemeRef}, url::Url}; #[derive(Clone, Debug)] pub enum SchemeCow<'a> { @@ -11,10 +11,6 @@ pub enum SchemeCow<'a> { Owned(Scheme), } -impl Default for SchemeCow<'_> { - fn default() -> Self { Self::Borrowed(SchemeRef::Regular) } -} - impl<'a> From> for SchemeCow<'a> { fn from(value: SchemeRef<'a>) -> Self { Self::Borrowed(value) } } @@ -31,71 +27,71 @@ impl From for SchemeCow<'_> { } impl From> for Scheme { - fn from(value: SchemeCow<'_>) -> Self { - match value { - SchemeCow::Borrowed(s) => s.into(), - SchemeCow::Owned(s) => s, - } - } + fn from(value: SchemeCow<'_>) -> Self { value.into_owned() } } impl<'a> SchemeCow<'a> { - pub fn search(domain: impl Into>) -> Self { + pub fn regular(uri: usize, urn: usize) -> Self { SchemeRef::Regular { uri, urn }.into() } + + pub fn search(domain: T, uri: usize, urn: usize) -> Self + where + T: Into>, + { match domain.into() { - Cow::Borrowed(s) => SchemeRef::Search(s).into(), - Cow::Owned(s) => Scheme::Search(s.intern()).into(), + Cow::Borrowed(domain) => SchemeRef::Search { domain, uri, urn }.into(), + Cow::Owned(domain) => Scheme::Search { domain: domain.intern(), uri, urn }.into(), } } - pub fn archive(domain: impl Into>) -> Self { + pub fn archive(domain: T, uri: usize, urn: usize) -> Self + where + T: Into>, + { match domain.into() { - Cow::Borrowed(s) => SchemeRef::Archive(s).into(), - Cow::Owned(s) => Scheme::Archive(s.intern()).into(), + Cow::Borrowed(domain) => SchemeRef::Archive { domain, uri, urn }.into(), + Cow::Owned(domain) => Scheme::Archive { domain: domain.intern(), uri, urn }.into(), } } - pub fn sftp(domain: impl Into>) -> Self { + pub fn sftp(domain: T, uri: usize, urn: usize) -> Self + where + T: Into>, + { match domain.into() { - Cow::Borrowed(s) => SchemeRef::Sftp(s).into(), - Cow::Owned(s) => Scheme::Sftp(s.intern()).into(), + Cow::Borrowed(domain) => SchemeRef::Sftp { domain, uri, urn }.into(), + Cow::Owned(domain) => Scheme::Sftp { domain: domain.intern(), uri, urn }.into(), } } - pub(crate) fn parse( - bytes: &'a [u8], - skip: &mut usize, - ) -> Result<(Self, bool, Option, Option)> { - let Some((mut protocol, rest)) = bytes.split_by_seq(b"://") else { - return Ok((Self::default(), false, None, None)); + pub fn parse(bytes: &'a [u8]) -> Result<(Self, PathCow<'a>)> { + let Some((kind, tilde)) = SchemeKind::parse(bytes)? else { + let path = Self::decode_path(SchemeKind::Regular, false, bytes)?; + let (uri, urn) = Self::normalize_ports(SchemeKind::Regular, None, None, &path)?; + return Ok((Self::regular(uri, urn), path)); }; - // Advance to the beginning of the path - *skip += 3 + protocol.len(); - - // Tilded schemes - let tilde = protocol.ends_with(b"~"); - if tilde { - protocol = &protocol[..protocol.len() - 1]; - } - - let (scheme, uri, urn) = match protocol { - b"regular" => (Self::default(), None, None), - b"search" => { - let (domain, uri, urn) = Self::decode_param(rest, skip)?; - (Self::search(domain), uri, urn) - } - b"archive" => { - let (domain, uri, urn) = Self::decode_param(rest, skip)?; - (Self::archive(domain), uri, urn) - } - b"sftp" => { - let (domain, uri, urn) = Self::decode_param(rest, skip)?; - (Self::sftp(domain), uri, urn) - } - _ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)), + // Decode domain and ports + let mut skip = kind.offset(tilde); + let (domain, uri, urn) = match kind { + SchemeKind::Regular => ("".into(), None, None), + SchemeKind::Search => Self::decode_param(&bytes[skip..], &mut skip)?, + SchemeKind::Archive => Self::decode_param(&bytes[skip..], &mut skip)?, + SchemeKind::Sftp => Self::decode_param(&bytes[skip..], &mut skip)?, }; - Ok((scheme, tilde, uri, urn)) + // Decode path + let path = Self::decode_path(kind, tilde, &bytes[skip..])?; + + // Build scheme + let (uri, urn) = Self::normalize_ports(kind, uri, urn, &path)?; + let scheme = match kind { + SchemeKind::Regular => Self::regular(uri, urn), + SchemeKind::Search => Self::search(domain, uri, urn), + SchemeKind::Archive => Self::archive(domain, uri, urn), + SchemeKind::Sftp => Self::sftp(domain, uri, urn), + }; + + Ok((scheme, path)) } fn decode_param( @@ -132,35 +128,68 @@ impl<'a> SchemeCow<'a> { Ok((b, a)) } - pub(crate) fn normalize_ports( - &self, + fn decode_path(kind: SchemeKind, tilde: bool, bytes: &'a [u8]) -> Result> { + let bytes: Cow<_> = if tilde { percent_decode(bytes).into() } else { bytes.into() }; + Ok(match kind { + SchemeKind::Regular => PathCow::from_os_bytes(bytes)?, + SchemeKind::Search => PathCow::from_os_bytes(bytes)?, + SchemeKind::Archive => PathCow::from_os_bytes(bytes)?, + SchemeKind::Sftp => PathCow::from_os_bytes(bytes)?, + }) + } + + fn normalize_ports( + kind: SchemeKind, uri: Option, urn: Option, - path: &Path, - ) -> Result> { - Ok(match self.as_scheme() { - SchemeRef::Regular => { + path: &PathCow, + ) -> Result<(usize, usize)> { + let path = path.as_path(); + Ok(match kind { + SchemeKind::Regular => { ensure!(uri.is_none() && urn.is_none(), "Regular scheme cannot have ports"); - None + (path.is_empty().not() as usize, path.name().is_some() as usize) } - SchemeRef::Search(_) => { + SchemeKind::Search => { let (uri, urn) = (uri.unwrap_or(0), urn.unwrap_or(0)); ensure!(uri == urn, "Search scheme requires URI and URN to be equal"); - Some((uri, urn)) + (uri, urn) } - SchemeRef::Archive(_) => Some((uri.unwrap_or(0), urn.unwrap_or(0))), - SchemeRef::Sftp(_) => { - let uri = uri.unwrap_or(path.as_os_str().is_empty().not() as usize); - let urn = urn.unwrap_or(path.file_name().is_some() as usize); - Some((uri, urn)) + SchemeKind::Archive => (uri.unwrap_or(0), urn.unwrap_or(0)), + SchemeKind::Sftp => { + let uri = uri.unwrap_or(path.is_empty().not() as usize); + let urn = urn.unwrap_or(path.name().is_some() as usize); + (uri, urn) } }) } + + pub fn retrieve_ports(url: Url) -> (usize, usize) { + match url { + Url::Regular(loc) => (loc.is_empty().not() as usize, loc.file_name().is_some() as usize), + Url::Search { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), + Url::Archive { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), + Url::Sftp { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), + } + } } -impl SchemeCow<'_> { +impl<'a> SchemeCow<'a> { #[inline] - pub fn into_owned(self) -> Scheme { self.into() } + pub fn into_owned(self) -> Scheme { + match self { + Self::Borrowed(s) => s.to_owned(), + Self::Owned(s) => s, + } + } + + #[inline] + pub fn into_domain(self) -> Option> { + Some(match self { + SchemeCow::Borrowed(s) => s.domain()?.into(), + SchemeCow::Owned(s) => s.into_domain()?.into(), + }) + } } #[cfg(test)] diff --git a/yazi-shared/src/scheme/encode.rs b/yazi-shared/src/scheme/encode.rs new file mode 100644 index 00000000..7b97244c --- /dev/null +++ b/yazi-shared/src/scheme/encode.rs @@ -0,0 +1,63 @@ +use std::{fmt::{self, Display}, ops::Not}; + +use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; + +use crate::{path::PathLike, scheme::SchemeKind, url::Url}; + +#[derive(Clone, Copy)] +pub struct Encode<'a>(pub Url<'a>); + +impl<'a> From> for Encode<'a> { + fn from(value: crate::url::Encode<'a>) -> Self { Self(value.0) } +} + +impl<'a> Encode<'a> { + #[inline] + pub fn domain<'s>(s: &'s str) -> PercentEncode<'s> { + const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); + percent_encode(s.as_bytes(), SET) + } + + pub(crate) fn ports(self) -> impl Display { + struct D<'a>(Encode<'a>); + + impl Display for D<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + macro_rules! w { + ($default_uri:expr, $default_urn:expr) => {{ + let (uri, urn) = self.0.0.scheme().ports(); + match (uri != $default_uri, urn != $default_urn) { + (true, true) => write!(f, ":{uri}:{urn}"), + (true, false) => write!(f, ":{uri}"), + (false, true) => write!(f, "::{urn}"), + (false, false) => Ok(()), + } + }}; + } + + match self.0.0.kind() { + SchemeKind::Regular => Ok(()), + SchemeKind::Search | SchemeKind::Archive => w!(0, 0), + SchemeKind::Sftp => { + w!(self.0.0.loc().is_empty().not() as usize, self.0.0.loc().name().is_some() as usize) + } + } + } + } + + D(self) + } +} + +impl Display for Encode<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Url::Regular(_) => write!(f, "regular://"), + Url::Search { domain, .. } => write!(f, "search://{}{}/", Self::domain(domain), self.ports()), + Url::Archive { domain, .. } => { + write!(f, "archive://{}{}/", Self::domain(domain), self.ports()) + } + Url::Sftp { domain, .. } => write!(f, "sftp://{}{}/", Self::domain(domain), self.ports()), + } + } +} diff --git a/yazi-shared/src/scheme/kind.rs b/yazi-shared/src/scheme/kind.rs new file mode 100644 index 00000000..88bccc30 --- /dev/null +++ b/yazi-shared/src/scheme/kind.rs @@ -0,0 +1,92 @@ +use anyhow::{Result, bail}; + +use crate::{BytesExt, scheme::{AsScheme, SchemeRef}}; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum SchemeKind { + Regular, + Search, + Archive, + Sftp, +} + +impl From for SchemeKind +where + T: AsScheme, +{ + fn from(value: T) -> Self { + match value.as_scheme() { + SchemeRef::Regular { .. } => Self::Regular, + SchemeRef::Search { .. } => Self::Search, + SchemeRef::Archive { .. } => Self::Archive, + SchemeRef::Sftp { .. } => Self::Sftp, + } + } +} + +impl TryFrom<&[u8]> for SchemeKind { + type Error = anyhow::Error; + + fn try_from(value: &[u8]) -> Result { + match value { + b"regular" => Ok(Self::Regular), + b"search" => Ok(Self::Search), + b"archive" => Ok(Self::Archive), + b"sftp" => Ok(Self::Sftp), + _ => bail!("invalid scheme kind: {}", String::from_utf8_lossy(value)), + } + } +} + +impl SchemeKind { + #[inline] + pub const fn as_str(self) -> &'static str { + match self { + Self::Regular => "regular", + Self::Search => "search", + Self::Archive => "archive", + Self::Sftp => "sftp", + } + } + + #[inline] + pub fn is_local(self) -> bool { + match self { + Self::Regular | Self::Search => true, + Self::Archive | Self::Sftp => false, + } + } + + #[inline] + pub fn is_remote(self) -> bool { + match self { + Self::Regular | Self::Search | Self::Archive => false, + Self::Sftp => true, + } + } + + #[inline] + pub fn is_virtual(self) -> bool { + match self { + Self::Regular | Self::Search => false, + Self::Archive | Self::Sftp => true, + } + } + + #[inline] + pub(super) const fn offset(self, tilde: bool) -> usize { + 3 + self.as_str().len() + tilde as usize + } + + pub fn parse(bytes: &[u8]) -> Result> { + let Some((kind, _)) = bytes.split_seq_once(b"://") else { + return Ok(None); + }; + + Ok(Some(if let Some(stripped) = kind.strip_suffix(b"~") { + (Self::try_from(stripped)?, true) + } else { + (Self::try_from(kind)?, false) + })) + } +} diff --git a/yazi-shared/src/scheme/mod.rs b/yazi-shared/src/scheme/mod.rs index 60bc0744..f65b6204 100644 --- a/yazi-shared/src/scheme/mod.rs +++ b/yazi-shared/src/scheme/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(cow r#ref scheme traits); +yazi_macro::mod_flat!(cow encode kind r#ref scheme traits); diff --git a/yazi-shared/src/scheme/ref.rs b/yazi-shared/src/scheme/ref.rs index 06cf2961..1468305e 100644 --- a/yazi-shared/src/scheme/ref.rs +++ b/yazi-shared/src/scheme/ref.rs @@ -1,44 +1,84 @@ -use crate::{pool::InternStr, scheme::{AsScheme, Scheme}}; +use std::{hash::Hash, ops::Deref}; -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +use crate::{pool::InternStr, scheme::{AsScheme, Scheme, SchemeKind}}; + +#[derive(Clone, Copy, Debug)] pub enum SchemeRef<'a> { - #[default] - Regular, - - Search(&'a str), - - Archive(&'a str), - - Sftp(&'a str), + Regular { uri: usize, urn: usize }, + Search { domain: &'a str, uri: usize, urn: usize }, + Archive { domain: &'a str, uri: usize, urn: usize }, + Sftp { domain: &'a str, uri: usize, urn: usize }, } -impl From> for Scheme { - fn from(value: SchemeRef) -> Self { - match value { - SchemeRef::Regular => Self::Regular, - SchemeRef::Search(d) => Self::Search(d.intern()), - SchemeRef::Archive(d) => Self::Archive(d.intern()), - SchemeRef::Sftp(d) => Self::Sftp(d.intern()), +impl Deref for SchemeRef<'_> { + type Target = SchemeKind; + + #[inline] + fn deref(&self) -> &Self::Target { + match self { + Self::Regular { .. } => &SchemeKind::Regular, + Self::Search { .. } => &SchemeKind::Search, + Self::Archive { .. } => &SchemeKind::Archive, + Self::Sftp { .. } => &SchemeKind::Sftp, } } } +impl Hash for SchemeRef<'_> { + fn hash(&self, state: &mut H) { + self.kind().hash(state); + self.domain().hash(state); + } +} + +impl PartialEq> for SchemeRef<'_> { + fn eq(&self, other: &SchemeRef) -> bool { + self.kind() == other.kind() && self.domain() == other.domain() + } +} + +impl From> for Scheme { + fn from(value: SchemeRef) -> Self { value.to_owned() } +} + impl<'a> SchemeRef<'a> { #[inline] - pub const fn kind(self) -> &'static str { + pub const fn kind(self) -> SchemeKind { match self { - Self::Regular => "regular", - Self::Search(_) => "search", - Self::Archive(_) => "archive", - Self::Sftp(_) => "sftp", + Self::Regular { .. } => SchemeKind::Regular, + Self::Search { .. } => SchemeKind::Search, + Self::Archive { .. } => SchemeKind::Archive, + Self::Sftp { .. } => SchemeKind::Sftp, } } #[inline] pub const fn domain(self) -> Option<&'a str> { match self { - Self::Regular => None, - Self::Search(s) | Self::Archive(s) | Self::Sftp(s) => Some(s), + Self::Regular { .. } => None, + Self::Search { domain, .. } | Self::Archive { domain, .. } | Self::Sftp { domain, .. } => { + Some(domain) + } + } + } + + #[inline] + pub const fn ports(self) -> (usize, usize) { + match self { + Self::Regular { uri, urn } => (uri, urn), + Self::Search { uri, urn, .. } => (uri, urn), + Self::Archive { uri, urn, .. } => (uri, urn), + Self::Sftp { uri, urn, .. } => (uri, urn), + } + } + + #[inline] + pub const fn zeroed(self) -> Self { + match self { + Self::Regular { .. } => Self::Regular { uri: 0, urn: 0 }, + Self::Search { domain, .. } => Self::Search { domain, uri: 0, urn: 0 }, + Self::Archive { domain, .. } => Self::Archive { domain, uri: 0, urn: 0 }, + Self::Sftp { domain, .. } => Self::Sftp { domain, uri: 0, urn: 0 }, } } @@ -48,27 +88,12 @@ impl<'a> SchemeRef<'a> { if self.is_virtual() || other.is_virtual() { self == other } else { true } } - #[inline] - pub fn is_local(self) -> bool { + pub fn to_owned(self) -> Scheme { match self { - Self::Regular | Self::Search(_) => true, - Self::Archive(_) | Self::Sftp(_) => false, - } - } - - #[inline] - pub fn is_remote(self) -> bool { - match self { - Self::Regular | Self::Search(_) | Self::Archive(_) => false, - Self::Sftp(_) => true, - } - } - - #[inline] - pub fn is_virtual(self) -> bool { - match self { - Self::Regular | Self::Search(_) => false, - Self::Archive(_) | Self::Sftp(_) => true, + Self::Regular { uri, urn } => Scheme::Regular { uri, urn }, + Self::Search { domain, uri, urn } => Scheme::Search { domain: domain.intern(), uri, urn }, + Self::Archive { domain, uri, urn } => Scheme::Archive { domain: domain.intern(), uri, urn }, + Self::Sftp { domain, uri, urn } => Scheme::Sftp { domain: domain.intern(), uri, urn }, } } } diff --git a/yazi-shared/src/scheme/scheme.rs b/yazi-shared/src/scheme/scheme.rs index a7d54fe6..886dbca0 100644 --- a/yazi-shared/src/scheme/scheme.rs +++ b/yazi-shared/src/scheme/scheme.rs @@ -2,16 +2,12 @@ use std::hash::{Hash, Hasher}; use crate::{pool::Symbol, scheme::{AsScheme, SchemeRef}}; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Scheme { - #[default] - Regular, - - Search(Symbol), - - Archive(Symbol), - - Sftp(Symbol), + Regular { uri: usize, urn: usize }, + Search { domain: Symbol, uri: usize, urn: usize }, + Archive { domain: Symbol, uri: usize, urn: usize }, + Sftp { domain: Symbol, uri: usize, urn: usize }, } impl Hash for Scheme { @@ -21,3 +17,15 @@ impl Hash for Scheme { impl PartialEq> for Scheme { fn eq(&self, other: &SchemeRef<'_>) -> bool { self.as_scheme() == *other } } + +impl Scheme { + #[inline] + pub fn into_domain(self) -> Option> { + match self { + Self::Regular { .. } => None, + Self::Search { domain, .. } | Self::Archive { domain, .. } | Self::Sftp { domain, .. } => { + Some(domain) + } + } + } +} diff --git a/yazi-shared/src/scheme/traits.rs b/yazi-shared/src/scheme/traits.rs index e4a2b477..5467fbf0 100644 --- a/yazi-shared/src/scheme/traits.rs +++ b/yazi-shared/src/scheme/traits.rs @@ -1,4 +1,4 @@ -use crate::scheme::{Scheme, SchemeCow, SchemeRef}; +use crate::scheme::{Scheme, SchemeCow, SchemeKind, SchemeRef}; pub trait AsScheme { fn as_scheme(&self) -> SchemeRef<'_>; @@ -12,11 +12,11 @@ impl AsScheme for SchemeRef<'_> { impl AsScheme for Scheme { #[inline] fn as_scheme(&self) -> SchemeRef<'_> { - match self { - Scheme::Regular => SchemeRef::Regular, - Scheme::Search(d) => SchemeRef::Search(d), - Scheme::Archive(d) => SchemeRef::Archive(d), - Scheme::Sftp(d) => SchemeRef::Sftp(d), + match *self { + Scheme::Regular { uri, urn } => SchemeRef::Regular { uri, urn }, + Scheme::Search { ref domain, uri, urn } => SchemeRef::Search { domain, uri, urn }, + Scheme::Archive { ref domain, uri, urn } => SchemeRef::Archive { domain, uri, urn }, + Scheme::Sftp { ref domain, uri, urn } => SchemeRef::Sftp { domain, uri, urn }, } } } @@ -46,7 +46,7 @@ pub trait SchemeLike where Self: AsScheme + Sized, { - fn kind(&self) -> &'static str { self.as_scheme().kind() } + fn kind(&self) -> SchemeKind { *self.as_scheme() } fn domain(&self) -> Option<&str> { self.as_scheme().domain() } diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs new file mode 100644 index 00000000..ad2ce1aa --- /dev/null +++ b/yazi-shared/src/strand/conversion.rs @@ -0,0 +1,159 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}}; + +use crate::{path::{PathBufDyn, PathDyn}, strand::{Strand, StrandBuf, StrandBufLike, StrandCow, StrandLike}}; + +// --- AsStrand +pub trait AsStrand { + fn as_strand(&self) -> impl StrandLike<'_>; +} + +impl AsStrand for [u8] { + fn as_strand(&self) -> impl StrandLike<'_> { self } +} + +impl AsStrand for &[u8] { + fn as_strand(&self) -> impl StrandLike<'_> { *self } +} + +impl AsStrand for Vec { + fn as_strand(&self) -> impl StrandLike<'_> { self.as_slice() } +} + +impl AsStrand for str { + fn as_strand(&self) -> impl StrandLike<'_> { self } +} + +impl AsStrand for &str { + fn as_strand(&self) -> impl StrandLike<'_> { *self } +} + +impl AsStrand for String { + fn as_strand(&self) -> impl StrandLike<'_> { self.as_str() } +} + +impl AsStrand for OsStr { + fn as_strand(&self) -> impl StrandLike<'_> { self } +} + +impl AsStrand for &OsStr { + fn as_strand(&self) -> impl StrandLike<'_> { *self } +} + +impl AsStrand for OsString { + fn as_strand(&self) -> impl StrandLike<'_> { self.as_os_str() } +} + +impl AsStrand for Cow<'_, OsStr> { + fn as_strand(&self) -> impl StrandLike<'_> { AsRef::::as_ref(self) } +} + +impl AsStrand for PathDyn<'_> { + fn as_strand(&self) -> impl StrandLike<'_> { + match self { + Self::Os(p) => p.as_os_str(), + } + } +} + +impl AsStrand for &PathBufDyn { + fn as_strand(&self) -> impl StrandLike<'_> { + match self { + PathBufDyn::Os(p) => p.as_os_str(), + } + } +} + +impl AsStrand for Strand<'_> { + fn as_strand(&self) -> impl StrandLike<'_> { *self } +} + +impl AsStrand for StrandBuf { + fn as_strand(&self) -> impl StrandLike<'_> { self.borrow() } +} + +impl AsStrand for StrandCow<'_> { + fn as_strand(&self) -> impl StrandLike<'_> { + match self { + StrandCow::Borrowed(s) => *s, + StrandCow::Owned(s) => s.into(), + } + } +} + +impl AsStrand for &StrandCow<'_> { + fn as_strand(&self) -> impl StrandLike<'_> { (**self).as_strand() } +} + +// --- AsStrandDyn +pub trait AsStrandDyn { + fn as_strand_dyn(&self) -> Strand<'_>; +} + +impl AsStrandDyn for OsStr { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } +} + +impl AsStrandDyn for &OsStr { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } +} + +impl AsStrandDyn for OsString { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } +} + +impl AsStrandDyn for &std::path::PathBuf { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } +} + +impl AsStrandDyn for &str { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Utf8(self) } +} + +impl AsStrandDyn for String { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Utf8(self) } +} + +impl AsStrandDyn for &String { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Utf8(self) } +} + +impl AsStrandDyn for Cow<'_, OsStr> { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } +} + +impl AsStrandDyn for PathDyn<'_> { + fn as_strand_dyn(&self) -> Strand<'_> { + match self { + Self::Os(p) => Strand::Os(p.as_os_str()), + } + } +} + +impl AsStrandDyn for &PathBufDyn { + fn as_strand_dyn(&self) -> Strand<'_> { + match self { + PathBufDyn::Os(p) => Strand::Os(p.as_os_str()), + } + } +} + +impl AsStrandDyn for Strand<'_> { + fn as_strand_dyn(&self) -> Strand<'_> { *self } +} + +impl AsStrandDyn for StrandBuf { + fn as_strand_dyn(&self) -> Strand<'_> { self.borrow() } +} + +impl AsStrandDyn for &StrandBuf { + fn as_strand_dyn(&self) -> Strand<'_> { (*self).borrow() } +} + +impl AsStrandDyn for StrandCow<'_> { + fn as_strand_dyn(&self) -> Strand<'_> { + match self { + Self::Borrowed(s) => s.as_strand_dyn(), + Self::Owned(s) => s.as_strand_dyn(), + } + } +} diff --git a/yazi-shared/src/strand/cow.rs b/yazi-shared/src/strand/cow.rs new file mode 100644 index 00000000..7f7fb567 --- /dev/null +++ b/yazi-shared/src/strand/cow.rs @@ -0,0 +1,75 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}}; + +use anyhow::Result; + +use crate::{IntoOsStr, scheme::SchemeKind, strand::{Strand, StrandBuf, StrandBufLike, StrandLike}}; + +// --- StrandCow +pub enum StrandCow<'a> { + Borrowed(Strand<'a>), + Owned(StrandBuf), +} + +impl From for StrandCow<'_> { + fn from(value: OsString) -> Self { Self::Owned(StrandBuf::Os(value)) } +} + +impl<'a> From> for StrandCow<'a> { + fn from(value: Cow<'a, OsStr>) -> Self { + match value { + Cow::Borrowed(s) => Self::Borrowed(Strand::Os(s)), + Cow::Owned(s) => Self::Owned(StrandBuf::Os(s)), + } + } +} + +impl<'a> From> for StrandCow<'a> { + fn from(value: Strand<'a>) -> Self { Self::Borrowed(value) } +} + +impl From for StrandCow<'_> { + fn from(value: StrandBuf) -> Self { Self::Owned(value) } +} + +impl PartialEq> for StrandCow<'_> { + fn eq(&self, other: &Strand) -> bool { + match self { + Self::Borrowed(s) => s == other, + Self::Owned(s) => s == other, + } + } +} + +impl<'a> StrandCow<'a> { + pub fn with(kind: SchemeKind, bytes: T) -> Result + where + T: Into>, + { + match kind { + SchemeKind::Regular | SchemeKind::Search | SchemeKind::Archive => Self::from_os_bytes(bytes), + SchemeKind::Sftp => Self::from_os_bytes(bytes), // FIXME + } + } + + pub fn from_os_bytes(bytes: impl Into>) -> Result { + Ok(match bytes.into().into_os_str()? { + Cow::Borrowed(s) => Strand::Os(s).into(), + Cow::Owned(s) => StrandBuf::Os(s).into(), + }) + } + + pub fn into_owned(self) -> StrandBuf { + match self { + Self::Borrowed(s) => s.to_owned(), + Self::Owned(s) => s, + } + } + + // FIXME: remove, instead implement StrandLike for StrandCow + pub fn encoded_bytes(&self) -> &[u8] { + match self { + Self::Borrowed(s) => s.encoded_bytes(), + Self::Owned(s) => s.encoded_bytes(), + } + } +} diff --git a/yazi-shared/src/strand/dyn.rs b/yazi-shared/src/strand/dyn.rs new file mode 100644 index 00000000..db36d266 --- /dev/null +++ b/yazi-shared/src/strand/dyn.rs @@ -0,0 +1,177 @@ +use std::ffi::{OsStr, OsString}; + +use serde::Serialize; + +use crate::{FromWtf8, path::PathDyn, scheme::SchemeKind, strand::{AsStrandDyn, StrandBufLike, StrandError, StrandKind, StrandLike}}; + +// --- Strand +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Strand<'p> { + Os(&'p OsStr), + Utf8(&'p str), + Bytes(&'p [u8]), +} + +impl Default for Strand<'_> { + fn default() -> Self { Self::Utf8("") } +} + +impl<'a> From<&'a OsStr> for Strand<'a> { + fn from(value: &'a OsStr) -> Self { Self::Os(value) } +} + +impl<'a> From<&'a str> for Strand<'a> { + fn from(value: &'a str) -> Self { Self::Utf8(value) } +} + +impl<'a> From<&'a StrandBuf> for Strand<'a> { + fn from(value: &'a StrandBuf) -> Self { + match value { + StrandBuf::Os(s) => Self::Os(s), + StrandBuf::Utf8(s) => Self::Utf8(s), + StrandBuf::Bytes(s) => Self::Bytes(s), + } + } +} + +impl PartialEq<&str> for Strand<'_> { + fn eq(&self, other: &&str) -> bool { + match self { + Self::Os(s) => s == other, + Self::Utf8(s) => s == other, + Self::Bytes(b) => *b == other.as_bytes(), + } + } +} + +impl<'a> Strand<'a> { + pub fn to_owned(self) -> StrandBuf { + match self { + Self::Os(s) => StrandBuf::Os(s.to_owned()), + Self::Utf8(s) => StrandBuf::Utf8(s.to_owned()), + Self::Bytes(b) => StrandBuf::Bytes(b.to_owned()), + } + } +} + +impl<'a> Strand<'a> { + #[inline] + pub fn as_os(self) -> Result<&'a OsStr, StrandError> { + match self { + Self::Os(s) => Ok(s), + Self::Utf8(s) => Ok(OsStr::new(s)), + Self::Bytes(b) => OsStr::from_wtf8(b).map_err(|_| StrandError::AsOs), + } + } + + #[inline] + pub fn as_utf8(self) -> Result<&'a str, StrandError> { + match self { + Self::Os(s) => s.to_str().ok_or(StrandError::AsUtf8), + Self::Utf8(s) => Ok(s), + Self::Bytes(b) => str::from_utf8(b).map_err(|_| StrandError::AsUtf8), + } + } + + #[cfg(windows)] + pub fn backslash_to_slash(self) -> super::StrandCow<'a> { + let bytes = self.encoded_bytes(); + + // Fast path to skip if there are no backslashes + let skip_len = bytes.iter().take_while(|&&b| b != b'\\').count(); + if skip_len >= bytes.len() { + return self.into(); + } + + let (skip, rest) = bytes.split_at(skip_len); + let mut out = Vec::new(); + out.try_reserve_exact(bytes.len()).unwrap_or_else(|_| panic!()); + out.extend(skip); + + for &b in rest { + out.push(if b == b'\\' { b'/' } else { b }); + } + unsafe { StrandBuf::from_encoded_bytes(self.kind(), out) }.into() + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: &'a [u8]) -> Self { + match kind.into() { + StrandKind::Os => Self::Os(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }), + StrandKind::Utf8 => Self::Utf8(unsafe { str::from_utf8_unchecked(bytes) }), + StrandKind::Bytes => Self::Bytes(bytes), + } + } +} + +// --- StrandBuf +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(untagged)] +pub enum StrandBuf { + Os(OsString), + Utf8(String), + Bytes(Vec), +} + +impl Default for StrandBuf { + fn default() -> Self { Self::Utf8(String::new()) } +} + +impl From for StrandBuf { + fn from(value: OsString) -> Self { Self::Os(value) } +} + +impl From for StrandBuf { + fn from(value: String) -> Self { Self::Utf8(value) } +} + +impl From> for StrandBuf { + fn from(value: PathDyn) -> Self { + match value { + PathDyn::Os(p) => Self::Os(p.as_os_str().to_owned()), + } + } +} + +impl PartialEq> for StrandBuf { + fn eq(&self, other: &Strand<'_>) -> bool { self.borrow() == *other } +} + +impl StrandBuf { + pub fn clear(&mut self) { + match self { + Self::Os(buf) => buf.clear(), + Self::Utf8(buf) => buf.clear(), + Self::Bytes(buf) => buf.clear(), + } + } + + pub fn try_push(&mut self, s: T) -> Result<(), StrandError> + where + T: AsStrandDyn, + { + let s = s.as_strand_dyn(); + Ok(match self { + Self::Os(buf) => buf.push(s.as_os()?), + Self::Utf8(buf) => buf.push_str(s.as_utf8()?), + Self::Bytes(buf) => buf.extend(s.encoded_bytes()), + }) + } + + pub fn with_capacity(kind: SchemeKind, capacity: usize) -> Self { + use SchemeKind as K; + match kind { + K::Regular | K::Search | K::Archive => Self::Os(OsString::with_capacity(capacity)), + K::Sftp => Self::Os(OsString::with_capacity(capacity)), // FIXME + } + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: Vec) -> Self { + match kind.into() { + StrandKind::Os => Self::Os(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), + StrandKind::Utf8 => Self::Utf8(unsafe { String::from_utf8_unchecked(bytes) }), + StrandKind::Bytes => Self::Bytes(bytes), + } + } +} diff --git a/yazi-shared/src/strand/error.rs b/yazi-shared/src/strand/error.rs new file mode 100644 index 00000000..d110a229 --- /dev/null +++ b/yazi-shared/src/strand/error.rs @@ -0,0 +1,24 @@ +use thiserror::Error; + +use crate::path::PathDynError; + +// --- StrandDynError +#[derive(Debug, Error)] +pub enum StrandError { + #[error("conversion to OsStr failed")] + AsOs, + #[error("conversion to UTF-8 str failed")] + AsUtf8, +} + +impl From for StrandError { + fn from(err: PathDynError) -> Self { + match err { + PathDynError::AsOs => Self::AsOs, + } + } +} + +impl From for std::io::Error { + fn from(err: StrandError) -> Self { std::io::Error::other(err) } +} diff --git a/yazi-shared/src/strand/kind.rs b/yazi-shared/src/strand/kind.rs new file mode 100644 index 00000000..77c050aa --- /dev/null +++ b/yazi-shared/src/strand/kind.rs @@ -0,0 +1,5 @@ +pub enum StrandKind { + Os, + Utf8, + Bytes, +} diff --git a/yazi-shared/src/strand/mod.rs b/yazi-shared/src/strand/mod.rs new file mode 100644 index 00000000..25180bc2 --- /dev/null +++ b/yazi-shared/src/strand/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(conversion cow error kind r#dyn strand view); diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs new file mode 100644 index 00000000..a3a1dc3e --- /dev/null +++ b/yazi-shared/src/strand/strand.rs @@ -0,0 +1,168 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}, fmt::Display}; + +use crate::{BytesExt, strand::{AsStrand, Strand, StrandBuf, StrandKind}}; + +// --- StrandLike +pub trait StrandLike<'a>: Copy { + type Owned: StrandBufLike; + + fn contains(self, x: impl AsStrand) -> bool { + memchr::memmem::find(self.encoded_bytes(), x.as_strand().encoded_bytes()).is_some() + } + + fn display(self) -> impl Display; + + fn encoded_bytes(self) -> &'a [u8]; + + fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } + + fn kind(self) -> StrandKind; + + fn len(self) -> usize { self.encoded_bytes().len() } + + fn to_str(self) -> Result<&'a str, std::str::Utf8Error> { str::from_utf8(self.encoded_bytes()) } + + fn to_string_lossy(self) -> Cow<'a, str> { String::from_utf8_lossy(self.encoded_bytes()) } + + fn eq_ignore_ascii_case(self, other: impl AsStrand) -> bool { + self.encoded_bytes().eq_ignore_ascii_case(other.as_strand().encoded_bytes()) + } + + fn starts_with(self, needle: impl AsStrand) -> bool { + self.encoded_bytes().starts_with(needle.as_strand().encoded_bytes()) + } +} + +impl<'a> StrandLike<'a> for &'a [u8] { + type Owned = Vec; + + fn display(self) -> impl Display { BytesExt::display(self) } + + fn encoded_bytes(self) -> &'a [u8] { self } + + fn kind(self) -> StrandKind { StrandKind::Bytes } +} + +impl<'a> StrandLike<'a> for &'a str { + type Owned = String; + + fn display(self) -> impl Display { self } + + fn encoded_bytes(self) -> &'a [u8] { self.as_bytes() } + + fn kind(self) -> StrandKind { StrandKind::Utf8 } +} + +impl<'a> StrandLike<'a> for &'a OsStr { + type Owned = OsString; + + fn display(self) -> impl Display { self.display() } + + fn encoded_bytes(self) -> &'a [u8] { self.as_encoded_bytes() } + + fn kind(self) -> StrandKind { StrandKind::Os } +} + +impl<'a> StrandLike<'a> for Strand<'a> { + type Owned = StrandBuf; + + fn display(self) -> impl Display { + struct D<'a>(Strand<'a>); + + impl<'a> Display for D<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Strand::Os(s) => s.display().fmt(f), + Strand::Utf8(s) => s.fmt(f), + Strand::Bytes(b) => BytesExt::display(b).fmt(f), + } + } + } + + D(self) + } + + fn encoded_bytes(self) -> &'a [u8] { + match self { + Self::Os(s) => s.as_encoded_bytes(), + Self::Utf8(s) => s.as_bytes(), + Self::Bytes(b) => b, + } + } + + fn kind(self) -> StrandKind { + match self { + Self::Os(_) => StrandKind::Os, + Self::Utf8(_) => StrandKind::Utf8, + Self::Bytes(_) => StrandKind::Bytes, + } + } +} + +// --- StrandBufLike +pub trait StrandBufLike +where + Self: 'static + AsStrand, +{ + type Borrowed<'a>: StrandLike<'a>; + + fn borrow(&self) -> Self::Borrowed<'_>; + + fn encoded_bytes(&self) -> &[u8] { self.borrow().encoded_bytes() } + + fn into_encoded_bytes(self) -> Vec; + + fn is_empty(&self) -> bool { self.borrow().is_empty() } + + fn kind(&self) -> StrandKind { self.borrow().kind() } + + fn len(&self) -> usize { self.borrow().len() } + + fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.borrow().to_str() } + + fn to_string_lossy(&self) -> Cow<'_, str> { self.borrow().to_string_lossy() } +} + +impl StrandBufLike for Vec { + type Borrowed<'a> = &'a [u8]; + + fn borrow(&self) -> Self::Borrowed<'_> { self.as_slice() } + + fn into_encoded_bytes(self) -> Vec { self } +} + +impl StrandBufLike for String { + type Borrowed<'a> = &'a str; + + fn borrow(&self) -> Self::Borrowed<'_> { self.as_str() } + + fn into_encoded_bytes(self) -> Vec { self.into_bytes() } +} + +impl StrandBufLike for OsString { + type Borrowed<'a> = &'a OsStr; + + fn borrow(&self) -> Self::Borrowed<'_> { self.as_os_str() } + + fn into_encoded_bytes(self) -> Vec { self.into_encoded_bytes() } +} + +impl StrandBufLike for StrandBuf { + type Borrowed<'a> = Strand<'a>; + + fn borrow(&self) -> Self::Borrowed<'_> { + match self { + Self::Os(s) => Strand::Os(s.as_os_str()), + Self::Utf8(s) => Strand::Utf8(s.as_str()), + Self::Bytes(b) => Strand::Bytes(b), + } + } + + fn into_encoded_bytes(self) -> Vec { + match self { + Self::Os(s) => s.into_encoded_bytes(), + Self::Utf8(s) => s.into_bytes(), + Self::Bytes(b) => b, + } + } +} diff --git a/yazi-shared/src/strand/view.rs b/yazi-shared/src/strand/view.rs new file mode 100644 index 00000000..d29d687a --- /dev/null +++ b/yazi-shared/src/strand/view.rs @@ -0,0 +1,51 @@ +use std::ffi::OsStr; + +use crate::{path::PathDyn, strand::{AsStrandDyn, Strand}}; + +// --- AsStrandView +pub trait AsStrandView<'a, T> { + fn as_strand_view(self) -> T; +} + +impl<'a> AsStrandView<'a, &'a OsStr> for &'a str { + fn as_strand_view(self) -> &'a OsStr { OsStr::new(self) } +} + +impl<'a> AsStrandView<'a, &'a OsStr> for &'a OsStr { + fn as_strand_view(self) -> &'a OsStr { self } +} + +impl<'a> AsStrandView<'a, &'a OsStr> for &'a std::path::Path { + fn as_strand_view(self) -> &'a OsStr { self.as_os_str() } +} + +impl<'a> AsStrandView<'a, &'a OsStr> for std::path::Components<'a> { + fn as_strand_view(self) -> &'a OsStr { self.as_path().as_os_str() } +} + +impl<'a> AsStrandView<'a, Strand<'a>> for &'a str { + fn as_strand_view(self) -> Strand<'a> { Strand::Utf8(self) } +} + +impl<'a> AsStrandView<'a, Strand<'a>> for &'a std::path::Path { + fn as_strand_view(self) -> Strand<'a> { Strand::Os(self.as_os_str()) } +} + +impl<'a> AsStrandView<'a, Strand<'a>> for std::path::Components<'a> { + fn as_strand_view(self) -> Strand<'a> { Strand::Os(self.as_path().as_os_str()) } +} + +impl<'a> AsStrandView<'a, Strand<'a>> for PathDyn<'a> { + fn as_strand_view(self) -> Strand<'a> { + match self { + Self::Os(p) => Strand::Os(p.as_os_str()), + } + } +} + +impl<'a, T> AsStrandView<'a, Strand<'a>> for &'a T +where + T: AsStrandDyn, +{ + fn as_strand_view(self) -> Strand<'a> { self.as_strand_dyn() } +} diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 1120a3d4..8f346907 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -1,14 +1,21 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, path::{Path, PathBuf}, str::FromStr}; +use std::{borrow::Cow, fmt::{Debug, Formatter}, path::{Path, PathBuf}, str::FromStr}; use anyhow::Result; use serde::{Deserialize, Serialize}; -use crate::{loc::{Loc, LocBuf}, path::PathDyn, pool::Pool, scheme::{Scheme, SchemeLike}, url::{AsUrl, Encode, EncodeTilded, Url, UrlCow}}; +use crate::{loc::LocBuf, path::{PathBufDyn, PathBufDynError, PathDyn, PathDynError, PathLike, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::SchemeKind, strand::AsStrandDyn, url::{AsUrl, Url, UrlCow, UrlLike}}; -#[derive(Clone, Default, Eq, Hash, PartialEq)] -pub struct UrlBuf { - pub loc: LocBuf, - pub scheme: Scheme, +#[derive(Clone, Eq, Hash, PartialEq)] +pub enum UrlBuf { + Regular(LocBuf), + Search { loc: LocBuf, domain: Symbol }, + Archive { loc: LocBuf, domain: Symbol }, + Sftp { loc: LocBuf, domain: Symbol }, +} + +// FIXME: remove +impl Default for UrlBuf { + fn default() -> Self { Self::Regular(Default::default()) } } impl From<&UrlBuf> for UrlBuf { @@ -16,15 +23,22 @@ impl From<&UrlBuf> for UrlBuf { } impl From> for UrlBuf { - fn from(url: Url<'_>) -> Self { Self { loc: url.loc.into(), scheme: url.scheme.into() } } + fn from(url: Url<'_>) -> Self { + match url { + Url::Regular(loc) => Self::Regular(loc.into()), + Url::Search { loc, domain } => Self::Search { loc: loc.into(), domain: domain.intern() }, + Url::Archive { loc, domain } => Self::Archive { loc: loc.into(), domain: domain.intern() }, + Url::Sftp { loc, domain } => Self::Sftp { loc: loc.into(), domain: domain.intern() }, + } + } } impl From<&Url<'_>> for UrlBuf { - fn from(url: &Url<'_>) -> Self { Self { loc: url.loc.into(), scheme: url.scheme.into() } } + fn from(url: &Url<'_>) -> Self { (*url).into() } } impl From for UrlBuf { - fn from(loc: LocBuf) -> Self { Self { loc, scheme: Scheme::Regular } } + fn from(loc: LocBuf) -> Self { Self::Regular(loc) } } impl From for UrlBuf { @@ -89,72 +103,95 @@ impl PartialEq> for &UrlBuf { impl UrlBuf { #[inline] pub fn new() -> &'static Self { - static U: UrlBuf = UrlBuf { loc: LocBuf::empty(), scheme: Scheme::Regular }; + static U: UrlBuf = UrlBuf::Regular(LocBuf::empty()); &U } #[inline] - pub fn into_path(self) -> Option { - Some(self.loc.into_path()).filter(|_| self.scheme.is_local()) + pub fn into_loc(self) -> PathBufDyn { + match self { + Self::Regular(loc) => loc.into_path().into(), + Self::Search { loc, .. } => loc.into_path().into(), + Self::Archive { loc, .. } => loc.into_path().into(), + Self::Sftp { loc, .. } => loc.into_path().into(), + } } #[inline] - pub fn set_name(&mut self, name: impl AsRef) { self.loc.set_name(name.as_ref()); } + pub fn into_local(self) -> Option { + if self.kind().is_local() { self.into_loc().into_os().ok() } else { None } + } - #[inline] - pub fn rebase(&self, base: &Path) -> Self { - Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() } + pub fn try_set_name(&mut self, name: impl AsStrandDyn) -> Result<(), SetNameError> { + let name = name.as_strand_dyn(); + Ok(match self { + Self::Regular(loc) => loc.try_set_name(name.as_os()?)?, + Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?, + Self::Archive { loc, .. } => loc.try_set_name(name.as_os()?)?, + Self::Sftp { loc, .. } => loc.try_set_name(name.as_os()?)?, + }) + } + + pub fn rebase(&self, base: &Path) -> Result { + Ok(match self { + Self::Regular(loc) => Self::Regular(loc.rebase(base)?), + Self::Search { loc, domain } => { + Self::Search { loc: loc.rebase(base)?, domain: domain.clone() } + } + Self::Archive { loc, domain } => { + Self::Archive { loc: loc.rebase(base)?, domain: domain.clone() } + } + Self::Sftp { loc, domain } => { + Self::Sftp { loc: loc.rebase(base)?, domain: domain.clone() } + } + }) } } impl UrlBuf { - #[inline] - pub fn loc(&self) -> Loc<'_> { self.loc.as_loc() } - // --- Regular #[inline] pub fn is_regular(&self) -> bool { self.as_url().is_regular() } #[inline] - pub fn to_regular(&self) -> Self { self.as_url().into_regular().into() } + pub fn to_regular(&self) -> Result { Ok(self.as_url().as_regular()?.into()) } #[inline] - pub fn into_regular(mut self) -> Self { - self.loc = self.loc.into_path().into(); - self.scheme = Scheme::Regular; - self + pub fn into_regular(self) -> Result { + Ok(Self::Regular(self.into_loc().into_os()?.into())) } // --- Search #[inline] - pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) } + pub fn is_search(&self) -> bool { self.kind() == SchemeKind::Search } #[inline] - pub fn to_search(&self, domain: impl AsRef) -> Self { - Self { - loc: LocBuf::::zeroed(self.loc.to_path()), - scheme: Scheme::Search(Pool::::intern(domain)), - } + pub fn to_search(&self, domain: impl AsRef) -> Result { + Ok(Self::Search { + loc: LocBuf::::zeroed(self.loc().to_os_owned()?), + domain: Pool::::intern(domain), + }) } #[inline] - pub fn into_search(mut self, domain: impl AsRef) -> Self { - self.loc = LocBuf::::zeroed(self.loc.into_path()); - self.scheme = Scheme::Search(Pool::::intern(domain)); - self + pub fn into_search(self, domain: impl AsRef) -> Result { + Ok(Self::Search { + loc: LocBuf::::zeroed(self.into_loc().into_os()?), + domain: Pool::::intern(domain), + }) } // --- Archive #[inline] - pub fn is_archive(&self) -> bool { matches!(self.scheme, Scheme::Archive(_)) } + pub fn is_archive(&self) -> bool { self.kind() == SchemeKind::Archive } // --- Internal #[inline] pub fn is_internal(&self) -> bool { - match self.scheme { - Scheme::Regular | Scheme::Sftp(_) => true, - Scheme::Search(_) => !self.loc.uri().as_os_str().is_empty(), - Scheme::Archive(_) => false, + match self.kind() { + SchemeKind::Regular | SchemeKind::Sftp => true, + SchemeKind::Search => !self.uri().is_empty(), + SchemeKind::Archive => false, } } } @@ -165,12 +202,7 @@ impl Debug for UrlBuf { impl Serialize for UrlBuf { fn serialize(&self, serializer: S) -> Result { - let Self { scheme, loc } = self; - match (scheme.is_virtual(), loc.to_str()) { - (false, Some(s)) => serializer.serialize_str(s), - (true, Some(s)) => serializer.serialize_str(&format!("{}{s}", Encode::from(self))), - (_, None) => serializer.collect_str(&EncodeTilded::from(self)), - } + self.as_url().serialize(serializer) } } @@ -216,9 +248,12 @@ mod tests { for (base, path, expected) in cases { let base: UrlBuf = base.parse()?; #[cfg(unix)] - assert_eq!(format!("{:?}", base.join(path)), expected); + assert_eq!(format!("{:?}", base.try_join(path)?), expected); #[cfg(windows)] - assert_eq!(format!("{:?}", base.join(path)).replace(r"\", "/"), expected.replace(r"\", "/")); + assert_eq!( + format!("{:?}", base.try_join(path)?).replace(r"\", "/"), + expected.replace(r"\", "/") + ); } Ok(()) @@ -266,14 +301,14 @@ mod tests { let u: UrlBuf = "/root".parse()?; assert_eq!(format!("{u:?}"), "/root"); - let u = u.into_search("kw"); + let u = u.into_search("kw")?; assert_eq!(format!("{u:?}"), "search://kw//root"); assert_eq!(format!("{:?}", u.parent().unwrap()), "/"); - let u = u.join("examples"); + let u = u.try_join("examples")?; assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); - let u = u.join("README.md"); + let u = u.try_join("README.md")?; assert_eq!(format!("{u:?}"), format!("search://kw:2:2//root{S}examples{S}README.md")); let u = u.parent().unwrap(); diff --git a/yazi-shared/src/url/component.rs b/yazi-shared/src/url/component.rs index acc8f025..f7670adb 100644 --- a/yazi-shared/src/url/component.rs +++ b/yazi-shared/src/url/component.rs @@ -1,8 +1,10 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, path::{self, PathBuf, PrefixComponent}}; -use crate::{scheme::{Scheme, SchemeRef}, url::{Encode, Url, UrlBuf}}; +use anyhow::Result; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +use crate::{path::{PathBufDyn, PathCow, PathLike}, scheme::SchemeRef, url::{Encode, Url, UrlBuf, UrlCow}}; + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Component<'a> { Scheme(SchemeRef<'a>), Prefix(PrefixComponent<'a>), @@ -24,12 +26,12 @@ impl<'a> From> for Component<'a> { } } -impl<'a> FromIterator> for UrlBuf { +impl<'a> FromIterator> for Result { fn from_iter>>(iter: I) -> Self { - let mut scheme = Scheme::Regular; let mut buf = PathBuf::new(); + let mut scheme = None; iter.into_iter().for_each(|c| match c { - Component::Scheme(s) => scheme = s.into(), + Component::Scheme(s) => scheme = Some(s), Component::Prefix(p) => buf.push(path::Component::Prefix(p)), Component::RootDir => buf.push(path::Component::RootDir), Component::CurDir => buf.push(path::Component::CurDir), @@ -37,7 +39,11 @@ impl<'a> FromIterator> for UrlBuf { Component::Normal(s) => buf.push(path::Component::Normal(s)), }); - Self { loc: buf.into(), scheme } + Ok(if let Some(s) = scheme { + UrlCow::try_from((s, PathCow::Owned(PathBufDyn::Os(buf))))?.into_owned() + } else { + buf.into() + }) } } @@ -66,18 +72,14 @@ pub struct Components<'a> { impl<'a> From> for Components<'a> { fn from(value: Url<'a>) -> Self { - Self { - inner: value.loc.as_path().components(), - url: value, - scheme_yielded: false, - } + Self { inner: value.loc().components(), url: value, scheme_yielded: false } } } impl<'a> Components<'a> { pub fn os_str(&self) -> Cow<'a, OsStr> { let path = self.inner.as_path(); - if self.url.scheme.is_local() || self.scheme_yielded { + if self.url.kind().is_local() || self.scheme_yielded { return path.as_os_str().into(); } @@ -90,7 +92,7 @@ impl<'a> Components<'a> { pub fn covariant(&self, other: &Self) -> bool { match (self.scheme_yielded, other.scheme_yielded) { (false, false) => {} - (true, true) if self.url.scheme.covariant(other.url.scheme) => {} + (true, true) if self.url.scheme().covariant(other.url.scheme()) => {} _ => return false, } self.inner == other.inner @@ -103,7 +105,7 @@ impl<'a> Iterator for Components<'a> { fn next(&mut self) -> Option { if !self.scheme_yielded { self.scheme_yielded = true; - Some(Component::Scheme(self.url.scheme)) + Some(Component::Scheme(self.url.scheme())) } else { self.inner.next().map(Into::into) } @@ -123,7 +125,7 @@ impl<'a> DoubleEndedIterator for Components<'a> { Some(comp.into()) } else if !self.scheme_yielded { self.scheme_yielded = true; - Some(Component::Scheme(self.url.scheme)) + Some(Component::Scheme(self.url.scheme())) } else { None } @@ -134,8 +136,8 @@ impl<'a> FusedIterator for Components<'a> {} impl<'a> PartialEq for Components<'a> { fn eq(&self, other: &Self) -> bool { - Some(self.url.scheme).filter(|_| !self.scheme_yielded) - == Some(other.url.scheme).filter(|_| !other.scheme_yielded) + Some(self.url.scheme()).filter(|_| !self.scheme_yielded) + == Some(other.url.scheme()).filter(|_| !other.scheme_yielded) && self.inner == other.inner } } @@ -145,31 +147,35 @@ impl<'a> PartialEq for Components<'a> { mod tests { use std::path::Path; + use anyhow::Result; + use super::*; - use crate::{pool::InternStr, url::UrlLike}; + use crate::url::UrlLike; #[test] - fn test_collect() { + fn test_collect() -> Result<()> { crate::init_tests(); - let search: UrlBuf = "search://keyword//root/projects/yazi".parse().unwrap(); - assert_eq!(search.loc.uri().as_os_str(), OsStr::new("")); - assert_eq!(search.scheme, Scheme::Search("keyword".intern())); + let search: UrlBuf = "search://keyword//root/projects/yazi".parse()?; + assert_eq!(search.uri(), ""); + assert_eq!(search.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 }); - let item = search.join("main.rs"); - assert_eq!(item.loc.uri().as_os_str(), OsStr::new("main.rs")); - assert_eq!(item.scheme, Scheme::Search("keyword".intern())); + let item = search.try_join("main.rs")?; + assert_eq!(item.uri(), "main.rs"); + assert_eq!(item.scheme(), SchemeRef::Search { domain: "keyword", uri: 1, urn: 1 }); - let u: UrlBuf = item.components().take(4).collect(); - assert_eq!(u.scheme, Scheme::Search("keyword".intern())); - assert_eq!(u.loc.as_path(), Path::new("/root/projects")); + let u: UrlBuf = item.components().take(4).collect::>()?; + assert_eq!(u.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(u.loc(), Path::new("/root/projects")); let u: UrlBuf = item .components() .take(5) .chain([Component::Normal(OsStr::new("target/release/yazi"))]) - .collect(); - assert_eq!(u.scheme, Scheme::Search("keyword".intern())); - assert_eq!(u.loc.as_path(), Path::new("/root/projects/yazi/target/release/yazi")); + .collect::>()?; + assert_eq!(u.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(u.loc(), Path::new("/root/projects/yazi/target/release/yazi")); + + Ok(()) } } diff --git a/yazi-shared/src/url/cov.rs b/yazi-shared/src/url/cov.rs index d0160ede..f9439f85 100644 --- a/yazi-shared/src/url/cov.rs +++ b/yazi-shared/src/url/cov.rs @@ -24,9 +24,9 @@ impl PartialEq for UrlCov<'_> { impl Hash for UrlCov<'_> { fn hash(&self, state: &mut H) { - self.0.loc.hash(state); - if self.0.scheme.is_virtual() { - self.0.scheme.hash(state); + self.0.loc().hash(state); + if self.0.kind().is_virtual() { + self.0.scheme().hash(state); } } } diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index d18e6199..99fbadad 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -1,22 +1,37 @@ -use std::{borrow::Cow, path::{Path, PathBuf}}; +use std::{borrow::Cow, hash::{Hash, Hasher}, path::PathBuf}; -use anyhow::Result; -use percent_encoding::percent_decode; +use anyhow::{Result, anyhow}; +use serde::{Deserialize, Deserializer, Serialize}; -use crate::{IntoOsStr, loc::{Loc, LocBuf}, scheme::{AsScheme, SchemeCow, SchemeRef}, url::{AsUrl, Url, UrlBuf}}; +use crate::{loc::{Loc, LocBuf}, path::{PathBufDyn, PathCow, PathDyn}, pool::SymbolCow, scheme::{AsScheme, Scheme, SchemeCow, SchemeKind, SchemeRef}, url::{AsUrl, Url, UrlBuf}}; #[derive(Clone, Debug)] pub enum UrlCow<'a> { - Borrowed { loc: Loc<'a>, scheme: SchemeCow<'a> }, - Owned { loc: LocBuf, scheme: SchemeCow<'a> }, + Regular(LocBuf), + Search { loc: LocBuf, domain: SymbolCow<'a, str> }, + Archive { loc: LocBuf, domain: SymbolCow<'a, str> }, + Sftp { loc: LocBuf, domain: SymbolCow<'a, str> }, + + RegularRef(Loc<'a>), + SearchRef { loc: Loc<'a>, domain: SymbolCow<'a, str> }, + ArchiveRef { loc: Loc<'a>, domain: SymbolCow<'a, str> }, + SftpRef { loc: Loc<'a>, domain: SymbolCow<'a, str> }, } +// FIXME: remove impl Default for UrlCow<'_> { - fn default() -> Self { Self::Borrowed { loc: Default::default(), scheme: Default::default() } } + fn default() -> Self { Self::RegularRef(Default::default()) } } impl<'a> From> for UrlCow<'a> { - fn from(value: Url<'a>) -> Self { Self::Borrowed { loc: value.loc, scheme: value.scheme.into() } } + fn from(value: Url<'a>) -> Self { + match value { + Url::Regular(loc) => Self::RegularRef(loc), + Url::Search { loc, domain } => Self::SearchRef { loc, domain: domain.into() }, + Url::Archive { loc, domain } => Self::ArchiveRef { loc, domain: domain.into() }, + Url::Sftp { loc, domain } => Self::SftpRef { loc, domain: domain.into() }, + } + } } impl<'a, T> From<&'a T> for UrlCow<'a> @@ -27,7 +42,14 @@ where } impl From for UrlCow<'_> { - fn from(value: UrlBuf) -> Self { Self::Owned { loc: value.loc, scheme: value.scheme.into() } } + fn from(value: UrlBuf) -> Self { + match value { + UrlBuf::Regular(loc) => Self::Regular(loc), + UrlBuf::Search { loc, domain } => Self::Search { loc, domain: domain.into() }, + UrlBuf::Archive { loc, domain } => Self::Archive { loc, domain: domain.into() }, + UrlBuf::Sftp { loc, domain } => Self::Sftp { loc, domain: domain.into() }, + } + } } impl From for UrlCow<'_> { @@ -45,20 +67,7 @@ impl From<&UrlCow<'_>> for UrlBuf { impl<'a> TryFrom<&'a [u8]> for UrlCow<'a> { type Error = anyhow::Error; - fn try_from(value: &'a [u8]) -> Result { - let (scheme, path, port) = Self::parse(value)?; - - Ok(match (path, port) { - (Cow::Borrowed(p), None) => Self::Borrowed { loc: Loc::bare(p), scheme }, - (Cow::Borrowed(p), Some((uri, urn))) => { - Self::Borrowed { loc: Loc::with(p, uri, urn)?, scheme } - } - (Cow::Owned(p), None) => Self::Owned { loc: LocBuf::from(p), scheme }, - (Cow::Owned(p), Some((uri, urn))) => { - Self::Owned { loc: LocBuf::::with(p, uri, urn)?, scheme } - } - }) - } + fn try_from(value: &'a [u8]) -> Result { SchemeCow::parse(value)?.try_into() } } impl TryFrom> for UrlCow<'_> { @@ -94,69 +103,171 @@ impl<'a> TryFrom> for UrlCow<'a> { } } +impl<'a> TryFrom<(SchemeRef<'a>, PathCow<'a>)> for UrlCow<'a> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (SchemeRef<'a>, PathCow<'a>)) -> Result { + (SchemeCow::Borrowed(scheme), path).try_into() + } +} + +impl TryFrom<(Scheme, PathBufDyn)> for UrlCow<'_> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (Scheme, PathBufDyn)) -> Result { + (SchemeCow::Owned(scheme), path).try_into() + } +} + +impl<'a> TryFrom<(SchemeCow<'a>, PathCow<'a>)> for UrlCow<'a> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (SchemeCow<'a>, PathCow<'a>)) -> Result { + match path { + PathCow::Borrowed(path) => (scheme, path).try_into(), + PathCow::Owned(path) => (scheme, path).try_into(), + } + } +} + +impl<'a> TryFrom<(SchemeCow<'a>, PathDyn<'a>)> for UrlCow<'a> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (SchemeCow<'a>, PathDyn<'a>)) -> Result { + let kind = scheme.as_scheme().kind(); + let (uri, urn) = scheme.as_scheme().ports(); + let domain = scheme.into_domain(); + Ok(match kind { + SchemeKind::Regular => Self::RegularRef(Loc::bare(path.as_os()?)), + SchemeKind::Search => Self::SearchRef { + loc: Loc::with(path.as_os()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for search scheme"))?, + }, + SchemeKind::Archive => Self::ArchiveRef { + loc: Loc::with(path.as_os()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?, + }, + SchemeKind::Sftp => Self::SftpRef { + loc: Loc::with(path.as_os()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?, + }, + }) + } +} + +impl<'a> TryFrom<(SchemeCow<'a>, PathBufDyn)> for UrlCow<'a> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (SchemeCow<'a>, PathBufDyn)) -> Result { + let kind = scheme.as_scheme().kind(); + let (uri, urn) = scheme.as_scheme().ports(); + let domain = scheme.into_domain(); + Ok(match kind { + SchemeKind::Regular => Self::Regular(path.into_os()?.into()), + SchemeKind::Search => Self::Search { + loc: LocBuf::::with(path.try_into()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for search scheme"))?, + }, + SchemeKind::Archive => Self::Archive { + loc: LocBuf::::with(path.try_into()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?, + }, + SchemeKind::Sftp => Self::Sftp { + loc: LocBuf::::with(path.try_into()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?, + }, + }) + } +} + // --- Eq +impl PartialEq for UrlCow<'_> { + fn eq(&self, other: &Self) -> bool { self.as_url() == other.as_url() } +} + impl PartialEq for UrlCow<'_> { fn eq(&self, other: &UrlBuf) -> bool { self.as_url() == other.as_url() } } +impl Eq for UrlCow<'_> {} + +// --- Hash +impl Hash for UrlCow<'_> { + fn hash(&self, state: &mut H) { self.as_url().hash(state); } +} + impl<'a> UrlCow<'a> { - #[inline] - pub fn loc(&self) -> Loc<'_> { + pub fn is_owned(&self) -> bool { match self { - UrlCow::Borrowed { loc, .. } => *loc, - UrlCow::Owned { loc, .. } => loc.as_loc(), + Self::Regular(_) | Self::Search { .. } | Self::Archive { .. } | Self::Sftp { .. } => true, + Self::RegularRef(_) + | Self::SearchRef { .. } + | Self::ArchiveRef { .. } + | Self::SftpRef { .. } => false, } } - #[inline] - pub fn scheme(&self) -> SchemeRef<'_> { - match self { - UrlCow::Borrowed { scheme, .. } => scheme.as_scheme(), - UrlCow::Owned { scheme, .. } => scheme.as_scheme(), - } - } - - #[inline] pub fn into_owned(self) -> UrlBuf { match self { - UrlCow::Borrowed { loc, scheme } => UrlBuf { loc: loc.into(), scheme: scheme.into() }, - UrlCow::Owned { loc, scheme } => UrlBuf { loc, scheme: scheme.into() }, + Self::Regular(loc) => UrlBuf::Regular(loc), + Self::Search { loc, domain } => UrlBuf::Search { loc, domain: domain.into() }, + Self::Archive { loc, domain } => UrlBuf::Archive { loc, domain: domain.into() }, + Self::Sftp { loc, domain } => UrlBuf::Sftp { loc, domain: domain.into() }, + + Self::RegularRef(loc) => UrlBuf::Regular(loc.into()), + Self::SearchRef { loc, domain } => { + UrlBuf::Search { loc: loc.into(), domain: domain.into() } + } + Self::ArchiveRef { loc, domain } => { + UrlBuf::Archive { loc: loc.into(), domain: domain.into() } + } + Self::SftpRef { loc, domain } => UrlBuf::Sftp { loc: loc.into(), domain: domain.into() }, } } - #[inline] pub fn into_scheme(self) -> SchemeCow<'a> { + let (uri, urn) = self.as_url().scheme().ports(); match self { - UrlCow::Borrowed { scheme, .. } => scheme, - UrlCow::Owned { scheme, .. } => scheme, + Self::Regular(_) => Scheme::Regular { uri, urn }.into(), + Self::RegularRef(_) => SchemeRef::Regular { uri, urn }.into(), + Self::Search { domain, .. } | Self::SearchRef { domain, .. } => match domain { + SymbolCow::Borrowed(domain) => SchemeRef::Search { domain, uri, urn }.into(), + SymbolCow::Owned(domain) => Scheme::Search { domain, uri, urn }.into(), + }, + Self::Archive { domain, .. } | Self::ArchiveRef { domain, .. } => match domain { + SymbolCow::Borrowed(domain) => SchemeRef::Archive { domain, uri, urn }.into(), + SymbolCow::Owned(domain) => Scheme::Archive { domain, uri, urn }.into(), + }, + Self::Sftp { domain, .. } | Self::SftpRef { domain, .. } => match domain { + SymbolCow::Borrowed(domain) => SchemeRef::Sftp { domain, uri, urn }.into(), + SymbolCow::Owned(domain) => Scheme::Sftp { domain, uri, urn }.into(), + }, } } #[inline] pub fn to_owned(&self) -> UrlBuf { self.as_url().into() } - - pub fn parse(bytes: &[u8]) -> Result<(SchemeCow<'_>, Cow<'_, Path>, Option<(usize, usize)>)> { - let mut skip = 0; - let (scheme, tilde, uri, urn) = SchemeCow::parse(bytes, &mut skip)?; - - let rest = if tilde { - Cow::from(percent_decode(&bytes[skip..])).into_os_str()? - } else { - bytes[skip..].into_os_str()? - }; - - let path: Cow<_> = match rest { - Cow::Borrowed(s) => Path::new(s).into(), - Cow::Owned(s) => PathBuf::from(s).into(), - }; - - let ports = scheme.normalize_ports(uri, urn, &path)?; - - Ok((scheme, path, ports)) - } } impl UrlCow<'_> { #[inline] pub fn is_regular(&self) -> bool { self.as_url().is_regular() } } + +impl Serialize for UrlCow<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.as_url().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for UrlCow<'_> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + UrlBuf::deserialize(deserializer).map(UrlCow::from) + } +} diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs index b9768530..020402a4 100644 --- a/yazi-shared/src/url/display.rs +++ b/yazi-shared/src/url/display.rs @@ -1,4 +1,4 @@ -use crate::url::{Encode, Url}; +use crate::{path::PathLike, scheme::Encode, url::Url}; pub struct Display<'a> { inner: Url<'a>, @@ -10,8 +10,8 @@ impl<'a> From> for Display<'a> { 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() { + let (kind, loc) = (self.inner.kind(), self.inner.loc()); + if kind.is_virtual() { Encode(self.inner).fmt(f)?; } loc.display().fmt(f) diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index 3e578404..a857ea40 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -1,93 +1,29 @@ -use std::{fmt::{self, Display}, ops::Not}; +use std::fmt::{self, Display}; -use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; +use percent_encoding::{CONTROLS, percent_encode}; -use crate::{scheme::SchemeRef, url::{AsUrl, Url, UrlBuf}}; - -#[derive(Clone, Copy)] -pub struct Encode<'a>(pub Url<'a>); - -impl<'a> From<&'a UrlBuf> for Encode<'a> { - fn from(value: &'a UrlBuf) -> Self { Self(value.as_url()) } -} - -impl<'a> Encode<'a> { - #[inline] - pub fn domain<'s>(s: &'s str) -> PercentEncode<'s> { - const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); - percent_encode(s.as_bytes(), SET) - } - - fn ports(self) -> impl Display { - struct D<'a>(Encode<'a>); - - impl Display for D<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - macro_rules! w { - ($default_uri:expr, $default_urn:expr) => {{ - let uri = self.0.0.loc.uri().components().count(); - let urn = self.0.0.loc.urn().components().count(); - match (uri != $default_uri, urn != $default_urn) { - (true, true) => write!(f, ":{uri}:{urn}"), - (true, false) => write!(f, ":{uri}"), - (false, true) => write!(f, "::{urn}"), - (false, false) => Ok(()), - } - }}; - } - - match self.0.0.scheme { - SchemeRef::Regular => Ok(()), - SchemeRef::Search(_) | SchemeRef::Archive(_) => w!(0, 0), - SchemeRef::Sftp(_) => w!( - self.0.0.loc.as_os_str().is_empty().not() as usize, - self.0.0.loc.file_name().is_some() as usize - ), - } - } - } - - D(self) - } -} - -impl Display for Encode<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use SchemeRef as S; - match self.0.scheme { - S::Regular => write!(f, "regular://"), - S::Search(d) => write!(f, "search://{}{}/", Self::domain(d), self.ports()), - S::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), self.ports()), - S::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), self.ports()), - } - } -} +use crate::{path::PathLike, url::Url}; // --- Tilded #[derive(Clone, Copy)] -pub struct EncodeTilded<'a>(pub Url<'a>); +pub struct Encode<'a>(pub Url<'a>); -impl<'a> From<&'a UrlBuf> for EncodeTilded<'a> { - fn from(value: &'a UrlBuf) -> Self { Self(value.as_url()) } -} - -impl<'a> From> for Encode<'a> { - fn from(value: EncodeTilded<'a>) -> Self { Self(value.0) } -} - -impl Display for EncodeTilded<'_> { +impl Display for Encode<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Encode as E; - use SchemeRef as S; + use crate::scheme::Encode as E; - let loc = percent_encode(self.0.loc.as_os_str().as_encoded_bytes(), CONTROLS); - match self.0.scheme { - S::Regular => write!(f, "regular~://{loc}"), - S::Search(d) => write!(f, "search~://{}{}/{loc}", E::domain(d), E::ports((*self).into())), - S::Archive(d) => { - write!(f, "archive~://{}{}/{loc}", E::domain(d), E::ports((*self).into())) + let loc = percent_encode(self.0.loc().encoded_bytes(), CONTROLS); + match self.0 { + Url::Regular(_) => write!(f, "regular~://{loc}"), + Url::Search { domain, .. } => { + write!(f, "search~://{}{}/{loc}", E::domain(domain), E::ports((*self).into())) + } + Url::Archive { domain, .. } => { + write!(f, "archive~://{}{}/{loc}", E::domain(domain), E::ports((*self).into())) + } + Url::Sftp { domain, .. } => { + write!(f, "sftp~://{}{}/{loc}", E::domain(domain), E::ports((*self).into())) } - S::Sftp(d) => write!(f, "sftp~://{}{}/{loc}", E::domain(d), E::ports((*self).into())), } } } diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index 49fc2950..b8b8aacc 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -1,6 +1,8 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; -use crate::{loc::Loc, path::{AsPathDyn, PathDyn}, scheme::{AsScheme, SchemeRef}, url::{Components, Display, Url, UrlBuf, UrlCow}}; +use anyhow::Result; + +use crate::{loc::Loc, path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, scheme::{SchemeKind, SchemeRef}, strand::{AsStrandDyn, Strand}, url::{Components, Display, Url, UrlBuf, UrlCow}}; // --- AsUrl pub trait AsUrl { @@ -9,7 +11,7 @@ pub trait AsUrl { impl AsUrl for Path { #[inline] - fn as_url(&self) -> Url<'_> { Url { loc: Loc::bare(self), scheme: SchemeRef::Regular } } + fn as_url(&self) -> Url<'_> { Url::Regular(Loc::bare(self)) } } impl AsUrl for &Path { @@ -43,7 +45,14 @@ impl AsUrl for Url<'_> { impl AsUrl for UrlBuf { #[inline] - fn as_url(&self) -> Url<'_> { Url { loc: self.loc.as_loc(), scheme: self.scheme.as_scheme() } } + fn as_url(&self) -> Url<'_> { + match self { + Self::Regular(loc) => Url::Regular(loc.as_loc()), + Self::Search { loc, domain } => Url::Search { loc: loc.as_loc(), domain }, + Self::Archive { loc, domain } => Url::Archive { loc: loc.as_loc(), domain }, + Self::Sftp { loc, domain } => Url::Sftp { loc: loc.as_loc(), domain }, + } + } } impl AsUrl for &UrlBuf { @@ -57,11 +66,17 @@ impl AsUrl for &mut UrlBuf { } impl AsUrl for UrlCow<'_> { - #[inline] fn as_url(&self) -> Url<'_> { match self { - UrlCow::Borrowed { loc, scheme } => Url { loc: *loc, scheme: scheme.as_scheme() }, - UrlCow::Owned { loc, scheme } => Url { loc: loc.as_loc(), scheme: scheme.as_scheme() }, + Self::Regular(loc) => Url::Regular(loc.as_loc()), + Self::Search { loc, domain } => Url::Search { loc: loc.as_loc(), domain }, + Self::Archive { loc, domain } => Url::Archive { loc: loc.as_loc(), domain }, + Self::Sftp { loc, domain } => Url::Sftp { loc: loc.as_loc(), domain }, + + Self::RegularRef(loc) => Url::Regular(*loc), + Self::SearchRef { loc, domain } => Url::Search { loc: *loc, domain }, + Self::ArchiveRef { loc, domain } => Url::Archive { loc: *loc, domain }, + Self::SftpRef { loc, domain } => Url::Sftp { loc: *loc, domain }, } } } @@ -90,9 +105,9 @@ pub trait UrlLike where Self: AsUrl + Sized, { - fn as_path(&self) -> Option<&Path> { self.as_url().as_path() } + fn as_local(&self) -> Option<&Path> { self.as_url().as_local() } - fn base(&self) -> Option> { self.as_url().base() } + fn base(&self) -> Url<'_> { self.as_url().base() } fn components(&self) -> Components<'_> { self.as_url().into() } @@ -100,9 +115,9 @@ where fn display(&self) -> Display<'_> { self.as_url().into() } - fn ends_with(&self, child: impl AsUrl) -> bool { self.as_url().ends_with(child) } + fn ext(&self) -> Option> { self.as_url().ext() } - fn ext(&self) -> Option<&OsStr> { self.as_url().ext() } + fn has_base(&self) -> bool { self.as_url().has_base() } fn has_root(&self) -> bool { self.as_url().has_root() } @@ -110,9 +125,11 @@ where fn is_absolute(&self) -> bool { self.as_url().is_absolute() } - fn join(&self, path: impl AsPathDyn) -> UrlBuf { self.as_url().join(path) } + fn kind(&self) -> SchemeKind { self.as_url().kind() } - fn name(&self) -> Option<&OsStr> { self.as_url().name() } + fn loc(&self) -> PathDyn<'_> { self.as_url().loc() } + + fn name(&self) -> Option> { self.as_url().name() } fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() } @@ -120,12 +137,30 @@ where fn parent(&self) -> Option> { self.as_url().parent() } - fn starts_with(&self, base: impl AsUrl) -> bool { self.as_url().starts_with(base) } + fn scheme(&self) -> SchemeRef<'_> { self.as_url().scheme() } - fn stem(&self) -> Option<&OsStr> { self.as_url().stem() } + fn stem(&self) -> Option> { self.as_url().stem() } - fn strip_prefix(&self, base: impl AsUrl) -> Option> { - self.as_url().strip_prefix(base) + fn trail(&self) -> Url<'_> { self.as_url().trail() } + + fn try_ends_with(&self, child: impl AsUrl) -> Result { + self.as_url().try_ends_with(child) + } + + fn try_join(&self, path: impl AsStrandDyn) -> Result { + self.as_url().try_join(path) + } + + fn try_replace<'a>(&self, take: usize, path: impl AsPathRef<'a>) -> Result> { + self.as_url().try_replace(take, path) + } + + fn try_starts_with(&self, base: impl AsUrl) -> Result { + self.as_url().try_starts_with(base) + } + + fn try_strip_prefix(&self, base: impl AsUrl) -> Result, StripPrefixError> { + self.as_url().try_strip_prefix(base) } fn uri(&self) -> PathDyn<'_> { self.as_url().uri() } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 9c3e38a9..71f67e84 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -1,13 +1,18 @@ use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, path::{Path, PathBuf}}; +use anyhow::Result; use hashbrown::Equivalent; +use serde::Serialize; -use crate::{loc::{Loc, LocBuf}, path::{AsPathDyn, PathDyn, PathLike}, scheme::SchemeRef, url::{AsUrl, Components, Encode, UrlBuf}}; +use super::Encode as EncodeUrl; +use crate::{loc::{Loc, LocBuf}, path::{AsPathDyn, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathBufLike, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError}, pool::InternStr, scheme::{Encode as EncodeScheme, SchemeCow, SchemeKind, SchemeRef}, strand::{AsStrandDyn, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; #[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub struct Url<'a> { - pub loc: Loc<'a>, - pub scheme: SchemeRef<'a>, +pub enum Url<'a> { + Regular(Loc<'a>), + Search { loc: Loc<'a>, domain: &'a str }, + Archive { loc: Loc<'a>, domain: &'a str }, + Sftp { loc: Loc<'a>, domain: &'a str }, } // --- Eq @@ -23,170 +28,315 @@ impl Equivalent for Url<'_> { // --- Debug impl Debug for Url<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.scheme == SchemeRef::Regular { - write!(f, "{}", self.loc.display()) + if self.is_regular() { + write!(f, "{}", self.loc().display()) } else { - write!(f, "{}{}", Encode(*self), self.loc.display()) + write!(f, "{}{}", EncodeScheme(*self), self.loc().display()) + } + } +} + +impl Serialize for Url<'_> { + fn serialize(&self, serializer: S) -> Result { + let (kind, loc) = (self.kind(), self.loc()); + match (kind.is_virtual(), loc.to_str()) { + (false, Ok(s)) => serializer.serialize_str(s), + (true, Ok(s)) => serializer.serialize_str(&format!("{}{s}", EncodeScheme(*self))), + (_, Err(_)) => serializer.collect_str(&EncodeUrl(*self)), } } } impl<'a> Url<'a> { #[inline] - pub fn regular + ?Sized>(path: &'a T) -> Self { - Self { loc: Loc::bare(path.as_ref()), scheme: SchemeRef::Regular } + pub fn kind(&self) -> SchemeKind { + match self { + Self::Regular(_) => SchemeKind::Regular, + Self::Search { .. } => SchemeKind::Search, + Self::Archive { .. } => SchemeKind::Archive, + Self::Sftp { .. } => SchemeKind::Sftp, + } } + // FIXME: add to UrlLike trait #[inline] - pub fn is_regular(self) -> bool { self.scheme == SchemeRef::Regular } - - #[inline] - pub fn into_regular(self) -> Self { - Self { loc: Loc::bare(self.loc.as_path()), scheme: SchemeRef::Regular } - } - - #[inline] - pub fn is_search(self) -> bool { matches!(self.scheme, SchemeRef::Search(_)) } - - #[inline] - pub fn is_absolute(self) -> bool { - use SchemeRef as S; - - match self.scheme { - S::Regular | S::Search(_) => self.loc.is_absolute(), - S::Archive(_) | S::Sftp(_) => self.loc.has_root(), + pub fn loc(self) -> PathDyn<'a> { + match self { + Self::Regular(loc) => loc.as_path_dyn(), + Self::Search { loc, .. } => loc.as_path_dyn(), + Self::Archive { loc, .. } => loc.as_path_dyn(), + Self::Sftp { loc, .. } => loc.as_path_dyn(), } } #[inline] - pub fn has_root(self) -> bool { self.loc.has_root() } + pub fn scheme(self) -> SchemeRef<'a> { + let (uri, urn) = SchemeCow::retrieve_ports(self); + match self { + Self::Regular(_) => SchemeRef::Regular { uri, urn }, + Self::Search { domain, .. } => SchemeRef::Search { domain, uri, urn }, + Self::Archive { domain, .. } => SchemeRef::Archive { domain, uri, urn }, + Self::Sftp { domain, .. } => SchemeRef::Sftp { domain, uri, urn }, + } + } + + #[inline] + pub fn regular + ?Sized>(path: &'a T) -> Self { + Self::Regular(Loc::bare(path.as_ref())) + } + + #[inline] + pub fn is_regular(self) -> bool { matches!(self, Self::Regular(_)) } + + #[inline] + pub fn as_regular(self) -> Result { + Ok(Self::Regular(Loc::bare(self.loc().as_os()?))) + } + + #[inline] + pub fn is_search(self) -> bool { matches!(self, Self::Search { .. }) } + + #[inline] + pub fn is_absolute(self) -> bool { self.loc().is_absolute() } + + #[inline] + pub fn has_root(self) -> bool { self.loc().has_root() } #[inline] pub fn to_owned(self) -> UrlBuf { self.into() } - pub fn join(self, path: impl AsPathDyn) -> UrlBuf { - use SchemeRef as S; + pub fn try_join(self, path: impl AsStrandDyn) -> Result { + let joined = self.loc().try_join(&path)?; - let joined = match path.as_path_dyn() { - PathDyn::Os(p) => self.loc.join(p), - }; - - let loc = match self.scheme { - S::Regular => joined.into(), - S::Search(_) => LocBuf::::new(joined, self.loc.base(), self.loc.base()), - S::Archive(_) => LocBuf::::floated(joined, self.loc.base()), - S::Sftp(_) => joined.into(), - }; - - UrlBuf { loc, scheme: self.scheme.into() } + Ok(match self { + Self::Regular(_) => UrlBuf::Regular(joined.into_os()?.into()), + Self::Search { loc, domain } => UrlBuf::Search { + loc: LocBuf::::new(joined.try_into()?, loc.base(), loc.base()), + domain: domain.intern(), + }, + Self::Archive { loc, domain } => UrlBuf::Archive { + loc: LocBuf::::floated(joined.try_into()?, loc.base()), + domain: domain.intern(), + }, + Self::Sftp { domain, .. } => { + UrlBuf::Sftp { loc: joined.into_os()?.into(), domain: domain.intern() } + } + }) } - pub fn strip_prefix(self, base: impl AsUrl) -> Option> { - use SchemeRef as S; + pub fn try_replace<'b>(self, take: usize, to: impl AsPathRef<'b>) -> Result> { + self.try_replace_impl(take, to.as_path_ref()) + } + + fn try_replace_impl<'b>(self, take: usize, rep: PathDyn<'b>) -> Result> { + let b = rep.encoded_bytes(); + if take == 0 { + return UrlCow::try_from(b); + } else if SchemeKind::parse(b)?.is_some() { + return UrlCow::try_from(b); + } + + let mut path = PathBufDyn::Os(self.loc().components().take(take - 1).collect()); // FIXME + path.try_push(rep)?; + + let url = match self { + Self::Regular(_) => UrlBuf::from(path.into_os()?), + + Self::Search { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Search { + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + domain: domain.intern(), + }, + Self::Archive { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Archive { + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + domain: domain.intern(), + }, + Self::Sftp { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Sftp { + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + domain: domain.intern(), + }, + + Self::Search { domain, .. } => UrlBuf::Search { + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + domain: domain.intern(), + }, + Self::Archive { domain, .. } => UrlBuf::Archive { + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + domain: domain.intern(), + }, + Self::Sftp { domain, .. } => UrlBuf::Sftp { + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + domain: domain.intern(), + }, + }; + + Ok(url.into()) + } + + pub fn try_strip_prefix(self, base: impl AsUrl) -> Result, StripPrefixError> { + use StripPrefixError::{Exotic, NotPrefix}; + use Url as U; let base = base.as_url(); - let prefix = self.loc.strip_prefix(base.loc)?.into(); + let prefix = self.loc().try_strip_prefix(base.loc())?.into(); - match (self.scheme, base.scheme) { + match (self, base) { // Same scheme - (S::Regular, S::Regular) => Some(prefix), - (S::Search(_), S::Search(_)) => Some(prefix), - (S::Archive(a), S::Archive(b)) => Some(prefix).filter(|_| a == b), - (S::Sftp(a), S::Sftp(b)) => Some(prefix).filter(|_| a == b), + (U::Regular(_), U::Regular(_)) => Ok(prefix), + (U::Search { .. }, U::Search { .. }) => Ok(prefix), + (U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => { + Some(prefix).filter(|_| a == b).ok_or(Exotic) + } + (U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => { + Some(prefix).filter(|_| a == b).ok_or(Exotic) + } // Both are local files - (S::Regular, S::Search(_)) => Some(prefix), - (S::Search(_), S::Regular) => Some(prefix), + (U::Regular(_), U::Search { .. }) => Ok(prefix), + (U::Search { .. }, U::Regular(_)) => Ok(prefix), // Only the entry of archives is a local file - (S::Regular, S::Archive(_)) => Some(prefix).filter(|_| base.uri().is_empty()), - (S::Search(_), S::Archive(_)) => Some(prefix).filter(|_| base.uri().is_empty()), - (S::Archive(_), S::Regular) => Some(prefix).filter(|_| self.uri().is_empty()), - (S::Archive(_), S::Search(_)) => Some(prefix).filter(|_| self.uri().is_empty()), + (U::Regular(_), U::Archive { .. }) => { + Some(prefix).filter(|_| base.uri().is_empty()).ok_or(NotPrefix) + } + (U::Search { .. }, U::Archive { .. }) => { + Some(prefix).filter(|_| base.uri().is_empty()).ok_or(NotPrefix) + } + (U::Archive { .. }, U::Regular(_)) => { + Some(prefix).filter(|_| self.uri().is_empty()).ok_or(NotPrefix) + } + (U::Archive { .. }, U::Search { .. }) => { + Some(prefix).filter(|_| self.uri().is_empty()).ok_or(NotPrefix) + } // Independent virtual file space - (S::Regular, S::Sftp(_)) => None, - (S::Search(_), S::Sftp(_)) => None, - (S::Archive(_), S::Sftp(_)) => None, - (S::Sftp(_), S::Regular) => None, - (S::Sftp(_), S::Search(_)) => None, - (S::Sftp(_), S::Archive(_)) => None, + (U::Regular(_), U::Sftp { .. }) => Err(Exotic), + (U::Search { .. }, U::Sftp { .. }) => Err(Exotic), + (U::Archive { .. }, U::Sftp { .. }) => Err(Exotic), + (U::Sftp { .. }, U::Regular(_)) => Err(Exotic), + (U::Sftp { .. }, U::Search { .. }) => Err(Exotic), + (U::Sftp { .. }, U::Archive { .. }) => Err(Exotic), } } #[inline] - pub fn uri(self) -> PathDyn<'a> { self.loc.uri().into() } - - #[inline] - pub fn urn(self) -> PathDyn<'a> { self.loc.urn().into() } - - #[inline] - pub fn name(self) -> Option<&'a OsStr> { self.loc.name() } - - #[inline] - pub fn stem(self) -> Option<&'a OsStr> { self.loc.stem() } - - #[inline] - pub fn ext(self) -> Option<&'a OsStr> { self.loc.ext() } - - pub fn base(self) -> Option { - use SchemeRef as S; - - if !self.loc.has_base() { - return None; + pub fn uri(self) -> PathDyn<'a> { + match self { + Self::Regular(loc) => loc.uri().as_path_dyn(), + Self::Search { loc, .. } => loc.uri().as_path_dyn(), + Self::Archive { loc, .. } => loc.uri().as_path_dyn(), + Self::Sftp { loc, .. } => loc.uri().as_path_dyn(), } + } - let loc = Loc::bare(self.loc.base()); - Some(match self.scheme { - S::Regular => Self { loc, scheme: S::Regular }, - S::Search(_) => Self { loc, scheme: self.scheme }, - S::Archive(_) => Self { loc, scheme: self.scheme }, - S::Sftp(_) => Self { loc, scheme: self.scheme }, + #[inline] + pub fn urn(self) -> PathDyn<'a> { + match self { + Self::Regular(loc) => loc.urn().as_path_dyn(), + Self::Search { loc, .. } => loc.urn().as_path_dyn(), + Self::Archive { loc, .. } => loc.urn().as_path_dyn(), + Self::Sftp { loc, .. } => loc.urn().as_path_dyn(), + } + } + + #[inline] + pub fn name(self) -> Option> { + Some(match self { + Self::Regular(loc) => loc.name()?.as_strand_dyn(), + Self::Search { loc, .. } => loc.name()?.as_strand_dyn(), + Self::Archive { loc, .. } => loc.name()?.as_strand_dyn(), + Self::Sftp { loc, .. } => loc.name()?.as_strand_dyn(), }) } + #[inline] + pub fn stem(self) -> Option> { + Some(match self { + Self::Regular(loc) => loc.stem()?.as_strand_dyn(), + Self::Search { loc, .. } => loc.stem()?.as_strand_dyn(), + Self::Archive { loc, .. } => loc.stem()?.as_strand_dyn(), + Self::Sftp { loc, .. } => loc.stem()?.as_strand_dyn(), + }) + } + + #[inline] + pub fn ext(self) -> Option> { + Some(match self { + Self::Regular(loc) => loc.ext()?.as_strand_dyn(), + Self::Search { loc, .. } => loc.ext()?.as_strand_dyn(), + Self::Archive { loc, .. } => loc.ext()?.as_strand_dyn(), + Self::Sftp { loc, .. } => loc.ext()?.as_strand_dyn(), + }) + } + + pub fn base(self) -> Self { + match self { + Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())), + Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.base()), domain }, + Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.base()), domain }, + Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.base()), domain }, + } + } + + #[inline] + pub fn trail(self) -> Self { + match self { + Self::Regular(loc) => Self::Regular(Loc::bare(loc.trail())), + Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.trail()), domain }, + Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.trail()), domain }, + Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.trail()), domain }, + } + } + + pub fn triple(self) -> (PathDyn<'a>, PathDyn<'a>, PathDyn<'a>) { + match self { + Self::Regular(loc) | Self::Search { loc, .. } | Self::Archive { loc, .. } => { + let (base, rest, urn) = loc.triple(); + (base.as_path_dyn(), rest.as_path_dyn(), urn.as_path_dyn()) + } + Self::Sftp { loc, .. } => { + let (base, rest, urn) = loc.triple(); + (base.as_path_dyn(), rest.as_path_dyn(), urn.as_path_dyn()) + } + } + } + pub fn parent(self) -> Option { - use SchemeRef as S; + let uri = self.uri(); - let parent = self.loc.parent()?; - let uri = self.loc.uri(); - - Some(match self.scheme { + Some(match self { // Regular - S::Regular => Self { loc: Loc::bare(parent), scheme: S::Regular }, + Self::Regular(loc) => Self::regular(loc.parent()?), // Search - S::Search(_) if uri.as_os_str().is_empty() => { - Self { loc: Loc::bare(parent), scheme: S::Regular } - } - S::Search(_) => { - Self { loc: Loc::new(parent, self.loc.base(), self.loc.base()), scheme: self.scheme } + Self::Search { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), + Self::Search { loc, domain } => { + Self::Search { loc: Loc::new(loc.parent()?, loc.base(), loc.base()), domain } } // Archive - S::Archive(_) if uri.as_os_str().is_empty() => { - Self { loc: Loc::bare(parent), scheme: S::Regular } + Self::Archive { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), + Self::Archive { loc, domain } if uri.components().nth(1).is_none() => { + Self::Archive { loc: Loc::zeroed(loc.parent()?), domain } } - S::Archive(_) if uri.components().nth(1).is_none() => { - Self { loc: Loc::zeroed(parent), scheme: self.scheme } + Self::Archive { loc, domain } => { + Self::Archive { loc: Loc::floated(loc.parent()?, loc.base()), domain } } - S::Archive(_) => Self { loc: Loc::floated(parent, self.loc.base()), scheme: self.scheme }, // SFTP - S::Sftp(_) => Self { loc: Loc::bare(parent), scheme: self.scheme }, + Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.parent()?), domain }, }) } #[inline] - pub fn starts_with(self, base: impl AsUrl) -> bool { + pub fn try_starts_with(self, base: impl AsUrl) -> Result { let base = base.as_url(); - self.scheme.covariant(base.scheme) && self.loc.starts_with(base.loc) + Ok(self.loc().try_starts_with(base.loc())? && self.scheme().covariant(base.scheme())) } #[inline] - pub fn ends_with(self, child: impl AsUrl) -> bool { + pub fn try_ends_with(self, child: impl AsUrl) -> Result { let child = child.as_url(); - self.scheme.covariant(child.scheme) && self.loc.ends_with(child.loc) + Ok(self.loc().try_ends_with(child.loc())? && self.scheme().covariant(child.scheme())) } #[inline] @@ -198,20 +348,34 @@ impl<'a> Url<'a> { #[inline] pub fn covariant(self, other: impl AsUrl) -> bool { let other = other.as_url(); - self.scheme.covariant(other.scheme) && self.loc == other.loc + self.loc() == other.loc() && self.scheme().covariant(other.scheme()) } #[inline] - pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.loc.urn().into())) } + pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.urn())) } #[inline] - pub fn as_path(self) -> Option<&'a Path> { - Some(self.loc.as_path()).filter(|_| self.scheme.is_local()) + pub fn as_local(self) -> Option<&'a Path> { + self.loc().as_os().ok().filter(|_| self.kind().is_local()) } #[inline] - pub fn has_base(self) -> bool { self.loc.has_base() } + pub fn has_base(self) -> bool { + match self { + Self::Regular(loc) => loc.has_base(), + Self::Search { loc, .. } => loc.has_base(), + Self::Archive { loc, .. } => loc.has_base(), + Self::Sftp { loc, .. } => loc.has_base(), + } + } #[inline] - pub fn has_trail(self) -> bool { self.loc.has_trail() } + pub fn has_trail(self) -> bool { + match self { + Self::Regular(loc) => loc.has_trail(), + Self::Search { loc, .. } => loc.has_trail(), + Self::Archive { loc, .. } => loc.has_trail(), + Self::Sftp { loc, .. } => loc.has_trail(), + } + } } diff --git a/yazi-shared/src/wtf8.rs b/yazi-shared/src/wtf8.rs new file mode 100644 index 00000000..4364f80a --- /dev/null +++ b/yazi-shared/src/wtf8.rs @@ -0,0 +1,72 @@ +use std::ffi::{OsStr, OsString}; + +use anyhow::Result; + +// --- AsWtf8 +pub trait AsWtf8 { + fn as_wtf8(&self) -> &[u8]; +} + +impl AsWtf8 for OsStr { + fn as_wtf8(&self) -> &[u8] { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + self.as_bytes() + } + #[cfg(windows)] + { + self.as_encoded_bytes() + } + } +} + +// --- FromWtf8 +pub trait FromWtf8 { + fn from_wtf8(wtf8: &[u8]) -> Result<&Self>; +} + +impl FromWtf8 for OsStr { + fn from_wtf8(wtf8: &[u8]) -> Result<&Self> { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + Ok(OsStr::from_bytes(wtf8)) + } + #[cfg(windows)] + { + // FIXME: validate WTF-8 + Ok(unsafe { OsStr::from_encoded_bytes_unchecked(wtf8) }) + } + } +} + +impl FromWtf8 for std::path::Path { + fn from_wtf8(wtf8: &[u8]) -> Result<&Self> { Ok(OsStr::from_wtf8(wtf8)?.as_ref()) } +} + +// --- FromWtf8Vec +pub trait FromWtf8Vec { + fn from_wtf8_vec(wtf8: Vec) -> Result + where + Self: Sized; +} + +impl FromWtf8Vec for OsString { + fn from_wtf8_vec(wtf8: Vec) -> Result { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStringExt; + Ok(OsString::from_vec(wtf8)) + } + #[cfg(windows)] + { + // FIXME: validate WTF-8 + Ok(unsafe { OsString::from_encoded_bytes_unchecked(wtf8) }) + } + } +} + +impl FromWtf8Vec for std::path::PathBuf { + fn from_wtf8_vec(wtf8: Vec) -> Result { Ok(OsString::from_wtf8_vec(wtf8)?.into()) } +} diff --git a/yazi-term/src/lib.rs b/yazi-term/src/lib.rs index c743409c..1be38f4f 100644 --- a/yazi-term/src/lib.rs +++ b/yazi-term/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::unit_arg)] - yazi_macro::mod_pub!(tty); yazi_macro::mod_flat!(background cursor r#if); diff --git a/yazi-vfs/src/fns.rs b/yazi-vfs/src/fns.rs index d6b035e4..5bf08c25 100644 --- a/yazi-vfs/src/fns.rs +++ b/yazi-vfs/src/fns.rs @@ -1,9 +1,9 @@ -use std::{ffi::OsString, io}; +use std::io; use tokio::{select, sync::{mpsc, oneshot}}; use yazi_fs::provider::Attrs; use yazi_macro::ok_or_not_found; -use yazi_shared::url::{AsUrl, Url, UrlBuf, UrlLike}; +use yazi_shared::{strand::{StrandBuf, StrandBufLike, StrandLike}, url::{AsUrl, Url, UrlBuf, UrlLike}}; use crate::provider; @@ -36,27 +36,30 @@ async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result { return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem")); }; - let dot_ext = url.ext().map_or_else(OsString::new, |e| { - let mut s = OsString::with_capacity(e.len() + 1); - s.push("."); - s.push(e); - s - }); + let dot_ext = match url.ext() { + Some(e) => { + let mut s = StrandBuf::with_capacity(url.kind(), e.len() + 1); + s.try_push(".")?; + s.try_push(e)?; + s + } + None => StrandBuf::default(), + }; - let mut name = OsString::with_capacity(stem.len() + dot_ext.len() + 5); + let mut name = StrandBuf::with_capacity(url.kind(), stem.len() + dot_ext.len() + 5); for i in 1u64.. { name.clear(); - name.push(&stem); + name.try_push(&stem)?; if append { - name.push(&dot_ext); - name.push(format!("_{i}")); + name.try_push(&dot_ext)?; + name.try_push(format!("_{i}"))?; } else { - name.push(format!("_{i}")); - name.push(&dot_ext); + name.try_push(format!("_{i}"))?; + name.try_push(&dot_ext)?; } - url.set_name(&name); + url.try_set_name(&name)?; ok_or_not_found!(provider::symlink_metadata(&url).await, break); } diff --git a/yazi-vfs/src/lib.rs b/yazi-vfs/src/lib.rs index 28b92170..1b3ab656 100644 --- a/yazi-vfs/src/lib.rs +++ b/yazi-vfs/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::if_same_then_else, clippy::unit_arg)] +#![allow(clippy::if_same_then_else)] yazi_macro::mod_pub!(provider); diff --git a/yazi-vfs/src/provider/dir_entry.rs b/yazi-vfs/src/provider/dir_entry.rs index db1e0173..40b75e64 100644 --- a/yazi-vfs/src/provider/dir_entry.rs +++ b/yazi-vfs/src/provider/dir_entry.rs @@ -1,55 +1,46 @@ -use std::{borrow::Cow, ffi::OsStr, io, sync::Arc}; +use std::io; use yazi_fs::{cha::{Cha, ChaType}, provider::FileHolder}; -use yazi_shared::url::{UrlBuf, UrlLike}; +use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::UrlBuf}; pub enum DirEntry { - Regular(yazi_fs::provider::local::DirEntry), - Search((Arc, yazi_fs::provider::local::DirEntry)), - Sftp((Arc, super::sftp::DirEntry)), + Local(yazi_fs::provider::local::DirEntry), + Sftp(super::sftp::DirEntry), } impl FileHolder for DirEntry { - fn path(&self) -> std::path::PathBuf { + async fn file_type(&self) -> io::Result { match self { - Self::Regular(ent) => ent.path(), - Self::Search((_, ent)) => ent.path(), - Self::Sftp((_, ent)) => ent.path(), - } - } - - fn name(&self) -> Cow<'_, OsStr> { - match self { - Self::Regular(ent) => ent.name(), - Self::Search((_, ent)) => ent.name(), - Self::Sftp((_, ent)) => ent.name(), + Self::Local(entry) => entry.file_type().await, + Self::Sftp(entry) => entry.file_type().await, } } async fn metadata(&self) -> io::Result { match self { - Self::Regular(ent) => ent.metadata().await, - Self::Search((_, ent)) => ent.metadata().await, - Self::Sftp((_, ent)) => ent.metadata().await, + Self::Local(entry) => entry.metadata().await, + Self::Sftp(entry) => entry.metadata().await, } } - async fn file_type(&self) -> io::Result { + fn name(&self) -> StrandCow<'_> { match self { - Self::Regular(ent) => ent.file_type().await, - Self::Search((_, ent)) => ent.file_type().await, - Self::Sftp((_, ent)) => ent.file_type().await, + Self::Local(entry) => entry.name(), + Self::Sftp(entry) => entry.name(), } } -} -impl DirEntry { - #[must_use] - pub fn url(&self) -> UrlBuf { + fn path(&self) -> PathBufDyn { match self { - Self::Regular(ent) => ent.path().into(), - Self::Search((dir, ent)) => dir.join(ent.name()), - Self::Sftp((dir, ent)) => dir.join(ent.name()), + Self::Local(entry) => entry.path(), + Self::Sftp(entry) => entry.path(), + } + } + + fn url(&self) -> UrlBuf { + match self { + Self::Local(entry) => entry.url(), + Self::Sftp(entry) => entry.url(), } } } diff --git a/yazi-vfs/src/provider/gate.rs b/yazi-vfs/src/provider/gate.rs index 8031e902..a62f9465 100644 --- a/yazi-vfs/src/provider/gate.rs +++ b/yazi-vfs/src/provider/gate.rs @@ -1,99 +1,96 @@ -use std::{io, path::Path}; +use std::io; use yazi_fs::provider::{Attrs, FileBuilder}; -use yazi_shared::scheme::SchemeRef; +use yazi_shared::{scheme::SchemeKind, url::AsUrl}; -pub enum Gate { - Local(yazi_fs::provider::local::Gate), - Sftp(super::sftp::Gate), -} - -impl From for Gate { - fn from(value: yazi_fs::provider::local::Gate) -> Self { Self::Local(value) } -} - -impl From for Gate { - fn from(value: super::sftp::Gate) -> Self { Self::Sftp(value) } +#[derive(Clone, Copy, Default)] +pub struct Gate { + pub(super) append: bool, + pub(super) attrs: Attrs, + pub(super) create: bool, + pub(super) create_new: bool, + pub(super) read: bool, + pub(super) truncate: bool, + pub(super) write: bool, } impl FileBuilder for Gate { type File = super::RwFile; fn append(&mut self, append: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.append(append), - Self::Sftp(g) => _ = g.append(append), - }; + self.append = append; self } fn attrs(&mut self, attrs: Attrs) -> &mut Self { - match self { - Self::Local(g) => _ = g.attrs(attrs), - Self::Sftp(g) => _ = g.attrs(attrs), - }; + self.attrs = attrs; self } fn create(&mut self, create: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.create(create), - Self::Sftp(g) => _ = g.create(create), - }; + self.create = create; self } fn create_new(&mut self, create_new: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.create_new(create_new), - Self::Sftp(g) => _ = g.create_new(create_new), - }; + self.create_new = create_new; self } - async fn new(scheme: SchemeRef<'_>) -> io::Result { - Ok(match scheme { - SchemeRef::Regular | SchemeRef::Search(_) => { - yazi_fs::provider::local::Gate::new(scheme).await?.into() + async fn open(&self, url: U) -> io::Result + where + U: AsUrl, + { + let url = url.as_url(); + Ok(match url.kind() { + SchemeKind::Regular | SchemeKind::Search => { + self.build::().open(url).await?.into() } - SchemeRef::Archive(_) => { + SchemeKind::Archive => { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? } - SchemeRef::Sftp(_) => super::sftp::Gate::new(scheme).await?.into(), - }) - } - - async fn open

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(match self { - Gate::Local(g) => g.open(path).await?.into(), - Gate::Sftp(g) => g.open(path).await?.into(), + SchemeKind::Sftp => self.build::().open(url).await?.into(), }) } fn read(&mut self, read: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.read(read), - Self::Sftp(g) => _ = g.read(read), - }; + self.read = read; self } fn truncate(&mut self, truncate: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.truncate(truncate), - Self::Sftp(g) => _ = g.truncate(truncate), - }; + self.truncate = truncate; self } fn write(&mut self, write: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.write(write), - Self::Sftp(g) => _ = g.write(write), - }; + self.write = write; self } } + +impl Gate { + fn build(self) -> T { + let mut gate = T::default(); + if self.append { + gate.append(true); + } + gate.attrs(self.attrs); + if self.create { + gate.create(true); + } + if self.create_new { + gate.create_new(true); + } + if self.read { + gate.read(true); + } + if self.truncate { + gate.truncate(true); + } + if self.write { + gate.write(true); + } + gate + } +} diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/provider/provider.rs index 1ddddc99..99a57ead 100644 --- a/yazi-vfs/src/provider/provider.rs +++ b/yazi-vfs/src/provider/provider.rs @@ -1,8 +1,8 @@ -use std::{io, path::{Path, PathBuf}}; +use std::io; use tokio::io::{AsyncWriteExt, BufReader, BufWriter}; use yazi_fs::{cha::Cha, provider::{Attrs, Provider, local::Local}}; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, UrlBuf, UrlCow}}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn}, url::{AsUrl, UrlBuf, UrlCow}}; use super::{Providers, ReadDir, RwFile}; @@ -10,7 +10,7 @@ pub async fn absolute<'a, U>(url: &'a U) -> io::Result> where U: AsUrl, { - Providers::new(url.as_url()).await?.absolute(url).await + Providers::new(url.as_url()).await?.absolute().await } pub async fn calculate(url: U) -> io::Result @@ -18,7 +18,7 @@ where U: AsUrl, { let url = url.as_url(); - if let Some(path) = url.as_path() { + if let Some(path) = url.as_local() { yazi_fs::provider::local::SizeCalculator::total(path).await } else { super::SizeCalculator::total(url).await @@ -29,32 +29,14 @@ pub async fn canonicalize(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - let canon = Providers::new(url).await?.canonicalize(url.loc).await?; - - Ok(match url.scheme { - SchemeRef::Regular | SchemeRef::Search(_) => canon.into(), - SchemeRef::Archive(_) => { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? - } - SchemeRef::Sftp(_) => UrlBuf { loc: canon.into(), scheme: url.scheme.into() }, - }) + Providers::new(url.as_url()).await?.canonicalize().await } pub async fn casefold(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - let fold = Providers::new(url).await?.casefold(url.loc).await?; - - Ok(match url.scheme { - SchemeRef::Regular | SchemeRef::Search(_) => fold.into(), - SchemeRef::Archive(_) => { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? - } - SchemeRef::Sftp(_) => UrlBuf { loc: fold.into(), scheme: url.scheme.into() }, - }) + Providers::new(url.as_url()).await?.casefold().await } pub async fn copy(from: U, to: V, attrs: Attrs) -> io::Result @@ -64,14 +46,14 @@ where { let (from, to) = (from.as_url(), to.as_url()); - match (from.as_path(), to.as_path()) { - (Some(from), Some(to)) => Local.copy(from, to, attrs).await, - (None, None) if from.scheme.covariant(to.scheme) => { - Providers::new(from).await?.copy(from.loc, to.loc, attrs).await + match (from.kind().is_local(), to.kind().is_local()) { + (true, true) => Local::new(from).await?.copy(to.loc(), attrs).await, + (false, false) if from.scheme().covariant(to.scheme()) => { + Providers::new(from).await?.copy(to.loc(), attrs).await } - (Some(_), None) | (None, Some(_)) | (None, None) => { - let src = Providers::new(from).await?.open(from.loc).await?; - let dist = Providers::new(to).await?.create(to.loc).await?; + (true, false) | (false, true) | (false, false) => { + let src = Providers::new(from).await?.open().await?; + let dist = Providers::new(to).await?.create().await?; let mut reader = BufReader::with_capacity(524288, src); let mut writer = BufWriter::with_capacity(524288, dist); @@ -89,24 +71,21 @@ pub async fn create(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.create(url.loc).await + Providers::new(url.as_url()).await?.create().await } pub async fn create_dir(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.create_dir(url.loc).await + Providers::new(url.as_url()).await?.create_dir().await } pub async fn create_dir_all(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.create_dir_all(url.loc).await + Providers::new(url.as_url()).await?.create_dir_all().await } pub async fn hard_link(original: U, link: V) -> io::Result<()> @@ -115,8 +94,8 @@ where V: AsUrl, { let (original, link) = (original.as_url(), link.as_url()); - if original.scheme.covariant(link.scheme) { - Providers::new(original).await?.hard_link(original.loc, link.loc).await + if original.scheme().covariant(link.scheme()) { + Providers::new(original).await?.hard_link(link.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) } @@ -127,7 +106,7 @@ where U: AsUrl, V: AsUrl, { - if let (Some(a), Some(b)) = (a.as_url().as_path(), b.as_url().as_path()) { + if let (Some(a), Some(b)) = (a.as_url().as_local(), b.as_url().as_local()) { yazi_fs::provider::local::identical(a, b).await } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) @@ -138,8 +117,7 @@ pub async fn metadata(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.metadata(url.loc).await + Providers::new(url.as_url()).await?.metadata().await } pub async fn must_identical(a: U, b: V) -> bool @@ -154,48 +132,42 @@ pub async fn read_dir(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.read_dir(url.loc).await + Providers::new(url.as_url()).await?.read_dir().await } -pub async fn read_link(url: U) -> io::Result +pub async fn read_link(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.read_link(url.loc).await + Providers::new(url.as_url()).await?.read_link().await } pub async fn remove_dir(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.remove_dir(url.loc).await + Providers::new(url.as_url()).await?.remove_dir().await } pub async fn remove_dir_all(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.remove_dir_all(url.loc).await + Providers::new(url.as_url()).await?.remove_dir_all().await } pub async fn remove_dir_clean(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Ok(Providers::new(url).await?.remove_dir_clean(url.loc).await) + Ok(Providers::new(url.as_url()).await?.remove_dir_clean().await) } pub async fn remove_file(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.remove_file(url.loc).await + Providers::new(url.as_url()).await?.remove_file().await } pub async fn rename(from: U, to: V) -> io::Result<()> @@ -204,52 +176,55 @@ where V: AsUrl, { let (from, to) = (from.as_url(), to.as_url()); - if from.scheme.covariant(to.scheme) { - Providers::new(from).await?.rename(from.loc, to.loc).await + if from.scheme().covariant(to.scheme()) { + Providers::new(from).await?.rename(to.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) } } -pub async fn symlink(original: &Path, link: U, is_dir: F) -> io::Result<()> +pub async fn symlink(original: U, link: V, is_dir: F) -> io::Result<()> where U: AsUrl, + V: AsUrl, F: AsyncFnOnce() -> io::Result, { - let link = link.as_url(); - Providers::new(link).await?.symlink(original, link.loc, is_dir).await + let (original, link) = (original.as_url(), link.as_url()); + if original.scheme().covariant(link.scheme()) { + Providers::new(link).await?.symlink(original.loc(), is_dir).await + } else { + Err(io::Error::from(io::ErrorKind::CrossesDevices)) + } } -pub async fn symlink_dir(original: &Path, link: U) -> io::Result<()> +pub async fn symlink_dir(original: P, link: U) -> io::Result<()> where + P: AsPathDyn, U: AsUrl, { - let link = link.as_url(); - Providers::new(link).await?.symlink_dir(original, link.loc).await + Providers::new(link.as_url()).await?.symlink_dir(original).await } -pub async fn symlink_file(original: &Path, link: U) -> io::Result<()> +pub async fn symlink_file(original: P, link: U) -> io::Result<()> where + P: AsPathDyn, U: AsUrl, { - let link = link.as_url(); - Providers::new(link).await?.symlink_file(original, link.loc).await + Providers::new(link.as_url()).await?.symlink_file(original).await } pub async fn symlink_metadata(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.symlink_metadata(url.loc).await + Providers::new(url.as_url()).await?.symlink_metadata().await } pub async fn trash(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.trash(url.loc).await + Providers::new(url.as_url()).await?.trash().await } pub async fn write(url: U, contents: C) -> io::Result<()> @@ -257,6 +232,5 @@ where U: AsUrl, C: AsRef<[u8]>, { - let url = url.as_url(); - Providers::new(url).await?.write(url.loc, contents).await + Providers::new(url.as_url()).await?.write(contents).await } diff --git a/yazi-vfs/src/provider/providers.rs b/yazi-vfs/src/provider/providers.rs index aac8c07e..e2065160 100644 --- a/yazi-vfs/src/provider/providers.rs +++ b/yazi-vfs/src/provider/providers.rs @@ -1,274 +1,213 @@ -use std::{io, path::{Path, PathBuf}, sync::Arc}; +use std::io; -use yazi_config::vfs::{ProviderSftp, Vfs}; -use yazi_fs::{cha::Cha, provider::{Attrs, Provider, local::Local}}; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, Url, UrlCow}}; +use yazi_fs::{cha::Cha, provider::{Attrs, Provider}}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn}, url::{Url, UrlBuf, UrlCow}}; -pub(super) struct Providers<'a>(Inner<'a>); - -enum Inner<'a> { - Regular, - Search(Url<'a>), - Sftp((super::sftp::Sftp, Url<'a>)), +#[derive(Clone)] +pub(super) enum Providers<'a> { + Local(yazi_fs::provider::local::Local<'a>), + Sftp(super::sftp::Sftp<'a>), } -impl<'a> Providers<'a> { - pub(super) async fn new(url: Url<'a>) -> io::Result { - Ok(match url.scheme { - SchemeRef::Regular => Self(Inner::Regular), - SchemeRef::Search(_) => Self(Inner::Search(url)), - SchemeRef::Archive(_) => { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? - } - SchemeRef::Sftp(name) => { - Self(Inner::Sftp((Vfs::provider::<&ProviderSftp>(name).await?.into(), url))) - } - }) - } -} - -impl Provider for Providers<'_> { +impl<'a> Provider for Providers<'a> { type File = super::RwFile; type Gate = super::Gate; + type Me<'b> = Providers<'b>; type ReadDir = super::ReadDir; + type UrlCow = UrlCow<'a>; - async fn absolute<'a, U>(&self, url: &'a U) -> io::Result> - where - U: AsUrl, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.absolute(url).await, - Inner::Sftp((p, _)) => p.absolute(url).await, + async fn absolute(&self) -> io::Result { + match self { + Self::Local(p) => p.absolute().await, + Self::Sftp(p) => p.absolute().await, } } - async fn canonicalize

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.canonicalize(path).await, - Inner::Sftp((p, _)) => p.canonicalize(path).await, + async fn canonicalize(&self) -> io::Result { + match self { + Self::Local(p) => p.canonicalize().await, + Self::Sftp(p) => p.canonicalize().await, } } - async fn casefold

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.casefold(path).await, - Inner::Sftp((p, _)) => p.casefold(path).await, + async fn casefold(&self) -> io::Result { + match self { + Self::Local(p) => p.casefold().await, + Self::Sftp(p) => p.casefold().await, } } - async fn copy(&self, from: P, to: Q, attrs: Attrs) -> io::Result + async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.copy(from, to, attrs).await, - Inner::Sftp((p, _)) => p.copy(from, to, attrs).await, + match self { + Self::Local(p) => p.copy(to, attrs).await, + Self::Sftp(p) => p.copy(to, attrs).await, } } - async fn create

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(match self.0 { - Inner::Regular | Inner::Search(_) => Local.create(path).await?.into(), - Inner::Sftp((p, _)) => p.create(path).await?.into(), + async fn create(&self) -> io::Result { + Ok(match self { + Self::Local(p) => p.create().await?.into(), + Self::Sftp(p) => p.create().await?.into(), }) } - async fn create_dir

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.create_dir(path).await, - Inner::Sftp((p, _)) => p.create_dir(path).await, + async fn create_dir(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.create_dir().await, + Self::Sftp(p) => p.create_dir().await, } } - async fn create_dir_all

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.create_dir_all(path).await, - Inner::Sftp((p, _)) => p.create_dir_all(path).await, + async fn create_dir_all(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.create_dir_all().await, + Self::Sftp(p) => p.create_dir_all().await, } } - async fn gate(&self) -> io::Result { - Ok(match self.0 { - Inner::Regular | Inner::Search(_) => Local.gate().await?.into(), - Inner::Sftp((p, _)) => p.gate().await?.into(), - }) - } - - async fn hard_link(&self, original: P, link: Q) -> io::Result<()> + async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.hard_link(original, link).await, - Inner::Sftp((p, _)) => p.hard_link(original, link).await, + match self { + Self::Local(p) => p.hard_link(to).await, + Self::Sftp(p) => p.hard_link(to).await, } } - async fn metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.metadata(path).await, - Inner::Sftp((p, _)) => p.metadata(path).await, + async fn metadata(&self) -> io::Result { + match self { + Self::Local(p) => p.metadata().await, + Self::Sftp(p) => p.metadata().await, } } - async fn open

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(match self.0 { - Inner::Regular | Inner::Search(_) => Local.open(path).await?.into(), - Inner::Sftp((p, _)) => p.open(path).await?.into(), - }) - } + async fn new<'b>(url: Url<'b>) -> io::Result> { + use yazi_shared::scheme::SchemeKind as K; - async fn read_dir

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(match self.0 { - Inner::Regular => Self::ReadDir::Regular(Local.read_dir(path).await?), - Inner::Search(dir) => { - Self::ReadDir::Search((Arc::new(dir.to_owned()), Local.read_dir(path).await?)) - } - Inner::Sftp((p, dir)) => { - Self::ReadDir::Sftp((Arc::new(dir.to_owned()), p.read_dir(path).await?)) + Ok(match url.kind() { + K::Regular | K::Search => Self::Me::Local(yazi_fs::provider::local::Local::new(url).await?), + K::Archive => { + Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? } + K::Sftp => Self::Me::Sftp(super::sftp::Sftp::new(url).await?), }) } - async fn read_link

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.read_link(path).await, - Inner::Sftp((p, _)) => p.read_link(path).await, + async fn open(&self) -> io::Result { + Ok(match self { + Self::Local(p) => p.open().await?.into(), + Self::Sftp(p) => p.open().await?.into(), + }) + } + + async fn read_dir(self) -> io::Result { + Ok(match self { + Self::Local(p) => Self::ReadDir::Local(p.read_dir().await?), + Self::Sftp(p) => Self::ReadDir::Sftp(p.read_dir().await?), + }) + } + + async fn read_link(&self) -> io::Result { + match self { + Self::Local(p) => p.read_link().await, + Self::Sftp(p) => p.read_link().await, } } - async fn remove_dir

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.remove_dir(path).await, - Inner::Sftp((p, _)) => p.remove_dir(path).await, + async fn remove_dir(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.remove_dir().await, + Self::Sftp(p) => p.remove_dir().await, } } - async fn remove_dir_all

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.remove_dir_all(path).await, - Inner::Sftp((p, _)) => p.remove_dir_all(path).await, + async fn remove_dir_all(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.remove_dir_all().await, + Self::Sftp(p) => p.remove_dir_all().await, } } - async fn remove_file

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.remove_file(path).await, - Inner::Sftp((p, _)) => p.remove_file(path).await, + async fn remove_file(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.remove_file().await, + Self::Sftp(p) => p.remove_file().await, } } - async fn rename(&self, from: P, to: Q) -> io::Result<()> + async fn rename

(&self, to: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.rename(from, to).await, - Inner::Sftp((p, _)) => p.rename(from, to).await, + match self { + Self::Local(p) => p.rename(to).await, + Self::Sftp(p) => p.rename(to).await, } } - async fn symlink(&self, original: P, link: Q, is_dir: F) -> io::Result<()> + async fn symlink(&self, original: P, is_dir: F) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, F: AsyncFnOnce() -> io::Result, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.symlink(original, link, is_dir).await, - Inner::Sftp((p, _)) => p.symlink(original, link, is_dir).await, + match self { + Self::Local(p) => p.symlink(original, is_dir).await, + Self::Sftp(p) => p.symlink(original, is_dir).await, } } - async fn symlink_dir(&self, original: P, link: Q) -> io::Result<()> + async fn symlink_dir

(&self, original: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.symlink_dir(original, link).await, - Inner::Sftp((p, _)) => p.symlink_dir(original, link).await, + match self { + Self::Local(p) => p.symlink_dir(original).await, + Self::Sftp(p) => p.symlink_dir(original).await, } } - async fn symlink_file(&self, original: P, link: Q) -> io::Result<()> + async fn symlink_file

(&self, original: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.symlink_file(original, link).await, - Inner::Sftp((p, _)) => p.symlink_file(original, link).await, + match self { + Self::Local(p) => p.symlink_file(original).await, + Self::Sftp(p) => p.symlink_file(original).await, } } - async fn symlink_metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.symlink_metadata(path).await, - Inner::Sftp((p, _)) => p.symlink_metadata(path).await, + async fn symlink_metadata(&self) -> io::Result { + match self { + Self::Local(p) => p.symlink_metadata().await, + Self::Sftp(p) => p.symlink_metadata().await, } } - async fn trash

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.trash(path).await, - Inner::Sftp((p, _)) => p.trash(path).await, + async fn trash(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.trash().await, + Self::Sftp(p) => p.trash().await, } } - async fn write(&self, path: P, contents: C) -> io::Result<()> + fn url(&self) -> Url<'_> { + match self { + Self::Local(p) => p.url(), + Self::Sftp(p) => p.url(), + } + } + + async fn write(&self, contents: C) -> io::Result<()> where - P: AsRef, C: AsRef<[u8]>, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.write(path, contents).await, - Inner::Sftp((p, _)) => p.write(path, contents).await, + match self { + Self::Local(p) => p.write(contents).await, + Self::Sftp(p) => p.write(contents).await, } } } diff --git a/yazi-vfs/src/provider/read_dir.rs b/yazi-vfs/src/provider/read_dir.rs index 303144a4..55e63159 100644 --- a/yazi-vfs/src/provider/read_dir.rs +++ b/yazi-vfs/src/provider/read_dir.rs @@ -1,12 +1,10 @@ -use std::{io, sync::Arc}; +use std::io; use yazi_fs::provider::DirReader; -use yazi_shared::url::UrlBuf; pub enum ReadDir { - Regular(yazi_fs::provider::local::ReadDir), - Search((Arc, yazi_fs::provider::local::ReadDir)), - Sftp((Arc, super::sftp::ReadDir)), + Local(yazi_fs::provider::local::ReadDir), + Sftp(super::sftp::ReadDir), } impl DirReader for ReadDir { @@ -14,13 +12,8 @@ impl DirReader for ReadDir { async fn next(&mut self) -> io::Result> { Ok(match self { - Self::Regular(reader) => reader.next().await?.map(Self::Entry::Regular), - Self::Search((dir, reader)) => { - reader.next().await?.map(|ent| Self::Entry::Search((dir.clone(), ent))) - } - Self::Sftp((dir, reader)) => { - reader.next().await?.map(|ent| Self::Entry::Sftp((dir.clone(), ent))) - } + Self::Local(reader) => reader.next().await?.map(Self::Entry::Local), + Self::Sftp(reader) => reader.next().await?.map(Self::Entry::Sftp), }) } } diff --git a/yazi-vfs/src/provider/sftp/conn.rs b/yazi-vfs/src/provider/sftp/conn.rs new file mode 100644 index 00000000..14761226 --- /dev/null +++ b/yazi-vfs/src/provider/sftp/conn.rs @@ -0,0 +1,176 @@ +use std::{io, sync::Arc}; + +use russh::keys::PrivateKeyWithHashAlg; +use yazi_config::vfs::ProviderSftp; +use yazi_fs::provider::local::Local; + +#[derive(Clone, Copy)] +pub(super) struct Conn { + pub(super) name: &'static str, + pub(super) config: &'static ProviderSftp, +} + +impl russh::client::Handler for Conn { + type Error = russh::Error; + + async fn check_server_key( + &mut self, + _server_public_key: &russh::keys::PublicKey, + ) -> Result { + Ok(true) + } +} + +impl deadpool::managed::Manager for Conn { + type Error = io::Error; + type Type = yazi_sftp::Operator; + + async fn create(&self) -> Result { + let channel = self.connect().await.map_err(|e| { + io::Error::other(format!("Failed to connect to SFTP server `{}`: {e}", self.name)) + })?; + + let mut op = yazi_sftp::Operator::make(channel.into_stream()); + op.init().await?; + Ok(op) + } + + async fn recycle( + &self, + obj: &mut Self::Type, + _metrics: &deadpool::managed::Metrics, + ) -> deadpool::managed::RecycleResult { + if obj.is_closed() { + Err(deadpool::managed::RecycleError::Message("Channel closed".into())) + } else { + Ok(()) + } + } +} + +impl Conn { + pub(super) async fn roll(self) -> io::Result> { + use deadpool::managed::PoolError; + + let pool = *super::CONN.lock().entry(self.config).or_insert_with(|| { + Box::leak(Box::new(deadpool::managed::Pool::builder(self).max_size(5).build().unwrap())) + }); + + pool.get().await.map_err(|e| match e { + PoolError::Timeout(_) => io::Error::new(io::ErrorKind::TimedOut, e.to_string()), + PoolError::Backend(e) => e, + PoolError::Closed | PoolError::NoRuntimeSpecified | PoolError::PostCreateHook(_) => { + io::Error::other(e.to_string()) + } + }) + } + + async fn connect(self) -> Result, russh::Error> { + let pref = Arc::new(russh::client::Config { + inactivity_timeout: Some(std::time::Duration::from_secs(30)), + keepalive_interval: Some(std::time::Duration::from_secs(10)), + nodelay: true, + ..Default::default() + }); + + let session = if self.config.password.is_some() { + self.connect_by_password(pref).await + } else if !self.config.key_file.as_os_str().is_empty() { + self.connect_by_key(pref).await + } else { + self.connect_by_agent(pref).await + }?; + + let channel = session.channel_open_session().await?; + channel.request_subsystem(true, "sftp").await?; + Ok(channel) + } + + async fn connect_by_password( + self, + pref: Arc, + ) -> Result, russh::Error> { + let Some(password) = &self.config.password else { + return Err(russh::Error::InvalidConfig("Password not provided".to_owned())); + }; + + let mut session = + russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; + + if session.authenticate_password(&self.config.user, password).await?.success() { + Ok(session) + } else { + Err(russh::Error::InvalidConfig("Password authentication failed".to_owned())) + } + } + + async fn connect_by_key( + self, + pref: Arc, + ) -> Result, russh::Error> { + let key_file = &self.config.key_file; + if key_file.as_os_str().is_empty() { + return Err(russh::Error::InvalidConfig("Key file not provided".to_owned())); + }; + + let key = Local::regular(key_file) + .read_to_string() + .await + .map_err(|e| russh::Error::InvalidConfig(format!("Failed to read key file: {e}")))?; + + let key = russh::keys::decode_secret_key(&key, self.config.key_passphrase.as_deref())?; + + let mut session = + russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; + + let result = session + .authenticate_publickey( + &self.config.user, + PrivateKeyWithHashAlg::new( + Arc::new(key), + session.best_supported_rsa_hash().await?.flatten(), + ), + ) + .await?; + + if result.success() { + Ok(session) + } else { + Err(russh::Error::InvalidConfig("Public key authentication failed".to_owned())) + } + } + + async fn connect_by_agent( + self, + pref: Arc, + ) -> Result, russh::Error> { + let identity_agent = &self.config.identity_agent; + if identity_agent.as_os_str().is_empty() { + return Err(russh::Error::InvalidConfig("Identity agent not provided".to_owned())); + }; + + #[cfg(unix)] + let mut agent = russh::keys::agent::client::AgentClient::connect_uds(identity_agent).await?; + #[cfg(windows)] + let mut agent = + russh::keys::agent::client::AgentClient::connect_named_pipe(identity_agent).await?; + + let keys = agent.request_identities().await?; + if keys.is_empty() { + return Err(russh::Error::InvalidConfig("No keys found in SSH agent".to_owned())); + } + + let mut session = + russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; + + for key in keys { + match session.authenticate_publickey_with(&self.config.user, key, None, &mut agent).await { + Ok(result) if result.success() => return Ok(session), + Ok(_) => {} + Err(e) => tracing::error!("Identity agent authentication error: {e}"), + } + } + + Err(russh::Error::InvalidConfig("Public key authentication via agent failed".to_owned())) + } +} diff --git a/yazi-vfs/src/provider/sftp/gate.rs b/yazi-vfs/src/provider/sftp/gate.rs index 0f10ecb5..795c41b4 100644 --- a/yazi-vfs/src/provider/sftp/gate.rs +++ b/yazi-vfs/src/provider/sftp/gate.rs @@ -1,105 +1,90 @@ -use std::{io, path::Path}; +use std::io; use yazi_config::vfs::{ProviderSftp, Vfs}; use yazi_fs::provider::{Attrs, FileBuilder}; use yazi_sftp::fs::Flags; -use yazi_shared::scheme::SchemeRef; +use yazi_shared::url::{AsUrl, Url}; -pub struct Gate { - sftp: super::Sftp, +use crate::provider::sftp::Conn; - append: bool, - attrs: Attrs, - create: bool, - create_new: bool, - read: bool, - truncate: bool, - write: bool, +#[derive(Clone, Copy, Default)] +pub struct Gate(crate::provider::Gate); + +impl From for Flags { + fn from(Gate(g): Gate) -> Self { + let mut flags = Flags::empty(); + if g.append { + flags |= Flags::APPEND; + } + if g.create { + flags |= Flags::CREATE; + } + if g.create_new { + flags |= Flags::CREATE | Flags::EXCLUDE; + } + if g.read { + flags |= Flags::READ; + } + if g.truncate { + flags |= Flags::TRUNCATE; + } + if g.write { + flags |= Flags::WRITE; + } + flags + } } impl FileBuilder for Gate { type File = yazi_sftp::fs::File; fn append(&mut self, append: bool) -> &mut Self { - self.append = append; + self.0.append(append); self } fn attrs(&mut self, attrs: Attrs) -> &mut Self { - self.attrs = attrs; + self.0.attrs(attrs); self } fn create(&mut self, create: bool) -> &mut Self { - self.create = create; + self.0.create(create); self } fn create_new(&mut self, create_new: bool) -> &mut Self { - self.create_new = create_new; + self.0.create_new(create_new); self } - async fn new(scheme: SchemeRef<'_>) -> io::Result { - let sftp: super::Sftp = match scheme { - SchemeRef::Sftp(name) => Vfs::provider::<&ProviderSftp>(name).await?.into(), - _ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Not an SFTP URL"))?, + async fn open(&self, url: U) -> io::Result + where + U: AsUrl, + { + let url = url.as_url(); + let (path, (name, config)) = match url { + Url::Sftp { loc, domain } => (*loc, Vfs::provider::<&ProviderSftp>(domain).await?), + _ => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not an SFTP URL: {url:?}")))?, }; - Ok(Self { - sftp, - - append: false, - attrs: Attrs::default(), - create: false, - create_new: false, - read: false, - truncate: false, - write: false, - }) - } - - async fn open

(&self, path: P) -> io::Result - where - P: AsRef, - { - let mut flags = Flags::empty(); - if self.append { - flags |= Flags::APPEND; - } - if self.create { - flags |= Flags::CREATE; - } - if self.create_new { - flags |= Flags::CREATE | Flags::EXCLUDE; - } - if self.read { - flags |= Flags::READ; - } - if self.truncate { - flags |= Flags::TRUNCATE; - } - if self.write { - flags |= Flags::WRITE; - } - - let attrs = super::Attrs(self.attrs).into(); - - Ok(self.sftp.op().await?.open(&path, flags, &attrs).await?) + let flags = Flags::from(*self); + let attrs = super::Attrs(self.0.attrs).into(); + Ok(Conn { name, config }.roll().await?.open(path, flags, &attrs).await?) } fn read(&mut self, read: bool) -> &mut Self { - self.read = read; + self.0.read(read); self } fn truncate(&mut self, truncate: bool) -> &mut Self { - self.truncate = truncate; + self.0.truncate(truncate); self } fn write(&mut self, write: bool) -> &mut Self { - self.write = write; + self.0.write(write); self } } diff --git a/yazi-vfs/src/provider/sftp/mod.rs b/yazi-vfs/src/provider/sftp/mod.rs index f8766775..2d9809ae 100644 --- a/yazi-vfs/src/provider/sftp/mod.rs +++ b/yazi-vfs/src/provider/sftp/mod.rs @@ -1,10 +1,10 @@ -yazi_macro::mod_flat!(gate metadata read_dir sftp); +yazi_macro::mod_flat!(conn gate metadata read_dir sftp); -pub(super) static CONN: yazi_shared::RoCell< +static CONN: yazi_shared::RoCell< parking_lot::Mutex< hashbrown::HashMap< &'static yazi_config::vfs::ProviderSftp, - &'static deadpool::managed::Pool, + &'static deadpool::managed::Pool, >, >, > = yazi_shared::RoCell::new(); diff --git a/yazi-vfs/src/provider/sftp/read_dir.rs b/yazi-vfs/src/provider/sftp/read_dir.rs index 8be0a8b4..de55a16d 100644 --- a/yazi-vfs/src/provider/sftp/read_dir.rs +++ b/yazi-vfs/src/provider/sftp/read_dir.rs @@ -1,30 +1,41 @@ -use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf}; +use std::{io, sync::Arc}; use yazi_fs::provider::{DirReader, FileHolder}; +use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; use super::{Cha, ChaMode}; -pub struct ReadDir(pub(super) yazi_sftp::fs::ReadDir); +pub struct ReadDir { + pub(super) dir: Arc, + pub(super) reader: yazi_sftp::fs::ReadDir, +} impl DirReader for ReadDir { type Entry = DirEntry; async fn next(&mut self) -> io::Result> { - Ok(self.0.next().await?.map(DirEntry)) + Ok(self.reader.next().await?.map(|entry| DirEntry { dir: self.dir.clone(), entry })) } } // --- Entry -pub struct DirEntry(yazi_sftp::fs::DirEntry); +pub struct DirEntry { + dir: Arc, + entry: yazi_sftp::fs::DirEntry, +} impl FileHolder for DirEntry { - fn path(&self) -> PathBuf { self.0.path() } - - fn name(&self) -> Cow<'_, OsStr> { self.0.name() } - - async fn metadata(&self) -> io::Result { Ok(Cha::try_from(&self.0)?.0) } - async fn file_type(&self) -> io::Result { - Ok(ChaMode::try_from(self.0.attrs())?.0.into()) + Ok(ChaMode::try_from(self.entry.attrs())?.0.into()) + } + + async fn metadata(&self) -> io::Result { Ok(Cha::try_from(&self.entry)?.0) } + + fn name(&self) -> StrandCow<'_> { self.entry.name().into() } + + fn path(&self) -> PathBufDyn { self.entry.path().into() } + + fn url(&self) -> UrlBuf { + self.dir.try_join(self.entry.name()).expect("entry name is a valid component of the SFTP URL") } } diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 560b4308..b521dfe6 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -1,92 +1,82 @@ -use std::{io, path::{Path, PathBuf}, sync::Arc}; +use std::{io, path::Path, sync::Arc}; -use russh::keys::PrivateKeyWithHashAlg; use tokio::io::{AsyncWriteExt, BufReader, BufWriter}; -use yazi_config::vfs::ProviderSftp; -use yazi_fs::provider::{DirReader, FileBuilder, FileHolder, Provider, local::Local}; +use yazi_config::vfs::{ProviderSftp, Vfs}; +use yazi_fs::provider::{DirReader, FileHolder, Provider}; use yazi_sftp::fs::{Attrs, Flags}; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, UrlBuf, UrlCow}}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn}, pool::InternStr, strand::StrandLike, url::{Url, UrlBuf, UrlCow, UrlLike}}; use super::Cha; +use crate::provider::sftp::Conn; #[derive(Clone, Copy)] -pub struct Sftp { +pub struct Sftp<'a> { + url: Url<'a>, + path: &'a Path, + name: &'static str, config: &'static ProviderSftp, } -impl From<(&'static str, &'static ProviderSftp)> for Sftp { - fn from((name, config): (&'static str, &'static ProviderSftp)) -> Self { Self { name, config } } -} - -impl Provider for Sftp { +impl<'a> Provider for Sftp<'a> { type File = yazi_sftp::fs::File; type Gate = super::Gate; + type Me<'b> = Sftp<'b>; type ReadDir = super::ReadDir; + type UrlCow = UrlCow<'a>; - async fn absolute<'a, U>(&self, url: &'a U) -> io::Result> - where - U: AsUrl, - { - let url = url.as_url(); - Ok(if url.is_absolute() { - url.into() - } else if let SchemeRef::Sftp(_) = url.scheme { - UrlBuf { loc: self.canonicalize(url.loc).await?.into(), scheme: url.scheme.into() }.into() - } else { - Err(io::Error::new(io::ErrorKind::InvalidInput, "Not an SFTP URL"))? + async fn absolute(&self) -> io::Result { + Ok(if self.url.is_absolute() { self.url.into() } else { self.canonicalize().await?.into() }) + } + + async fn canonicalize(&self) -> io::Result { + Ok(UrlBuf::Sftp { + loc: self.op().await?.realpath(self.path).await?.into(), + domain: self.name.intern(), }) } - async fn canonicalize

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(self.op().await?.realpath(&path).await?) - } - - async fn casefold

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - let Some((parent, name)) = path.parent().zip(path.file_name()) else { - return Ok(path.to_owned()); + async fn casefold(&self) -> io::Result { + let Some((parent, name)) = self.url.parent().zip(self.url.name()) else { + return Ok(self.url.to_owned()); }; - if !self.symlink_metadata(path).await?.is_link() { - return match self.canonicalize(path).await?.file_name() { - Some(name) => Ok(parent.join(name)), - None => Err(io::Error::other("Cannot get filename")), - }; + if !self.symlink_metadata().await?.is_link() { + return Ok(match self.canonicalize().await?.name() { + Some(name) => parent.try_join(name)?, + None => Err(io::Error::other("Cannot get filename"))?, + }); } - let mut it = self.read_dir(parent).await?; + let mut it = Self::new(parent).await?.read_dir().await?; let mut similar = None; while let Some(entry) = it.next().await? { let s = entry.name(); - if !s.eq_ignore_ascii_case(name) { + if !name.eq_ignore_ascii_case(&s) { continue; } else if s == name { - return Ok(entry.path()); + return Ok(entry.url()); } else if similar.is_none() { similar = Some(s.into_owned()); } } - similar.map(|n| parent.join(n)).ok_or_else(|| io::Error::from(io::ErrorKind::NotFound)) + similar + .map(|n| parent.try_join(n)) + .transpose()? + .ok_or_else(|| io::Error::from(io::ErrorKind::NotFound)) } - async fn copy(&self, from: P, to: Q, attrs: yazi_fs::provider::Attrs) -> io::Result + async fn copy

(&self, to: P, attrs: yazi_fs::provider::Attrs) -> io::Result where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { + let to = to.as_path_dyn().as_os()?; let attrs = Attrs::from(super::Attrs(attrs)); let op = self.op().await?; - let from = op.open(&from, Flags::READ, &Attrs::default()).await?; - let to = op.open(&to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, &attrs).await?; + let from = op.open(self.path, Flags::READ, &Attrs::default()).await?; + let to = op.open(to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, &attrs).await?; let mut reader = BufReader::with_capacity(524288, from); let mut writer = BufWriter::with_capacity(524288, to); @@ -98,268 +88,95 @@ impl Provider for Sftp { Ok(written) } - async fn create_dir

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - Ok(self.op().await?.mkdir(&path, Attrs::default()).await?) + async fn create_dir(&self) -> io::Result<()> { + Ok(self.op().await?.mkdir(self.path, Attrs::default()).await?) } - async fn gate(&self) -> io::Result { - super::Gate::new(SchemeRef::Sftp(self.name)).await + async fn hard_link

(&self, to: P) -> io::Result<()> + where + P: AsPathDyn, + { + let to = to.as_path_dyn().as_os()?; + + Ok(self.op().await?.hardlink(self.path, to).await?) } - async fn hard_link(&self, original: P, link: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - Ok(self.op().await?.hardlink(&original, &link).await?) + async fn metadata(&self) -> io::Result { + let attrs = self.op().await?.stat(self.path).await?; + Ok(Cha::try_from((self.path.file_name().unwrap_or_default(), &attrs))?.0) } - async fn metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - let attrs = self.op().await?.stat(path).await?; - Ok(Cha::try_from((path.file_name().unwrap_or_default(), &attrs))?.0) + async fn new<'b>(url: Url<'b>) -> io::Result> { + match url { + Url::Regular(_) | Url::Search { .. } | Url::Archive { .. } => { + Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url:?}"))) + } + Url::Sftp { loc, domain } => { + let (name, config) = Vfs::provider::<&ProviderSftp>(domain).await?; + Ok(Self::Me { url, path: loc.as_path(), name, config }) + } + } } - async fn read_dir

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(super::ReadDir(self.op().await?.read_dir(&path).await?)) + async fn read_dir(self) -> io::Result { + Ok(Self::ReadDir { + dir: Arc::new(self.url.to_owned()), + reader: self.op().await?.read_dir(self.path).await?, + }) } - async fn read_link

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(self.op().await?.readlink(&path).await?) + async fn read_link(&self) -> io::Result { + Ok(self.op().await?.readlink(self.path).await?.into()) } - async fn remove_dir

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - Ok(self.op().await?.rmdir(&path).await?) - } + async fn remove_dir(&self) -> io::Result<()> { Ok(self.op().await?.rmdir(self.path).await?) } - async fn remove_file

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - Ok(self.op().await?.remove(&path).await?) - } + async fn remove_file(&self) -> io::Result<()> { Ok(self.op().await?.remove(self.path).await?) } - async fn rename(&self, from: P, to: Q) -> io::Result<()> + async fn rename

(&self, to: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { + let to = to.as_path_dyn().as_os()?; let op = self.op().await?; - match op.rename_posix(&from, &to).await { + + match op.rename_posix(self.path, &to).await { Ok(()) => {} Err(yazi_sftp::Error::Unsupported) => { op.remove(&to).await?; - op.rename(&from, &to).await?; + op.rename(self.path, &to).await?; } Err(e) => Err(e)?, } Ok(()) } - async fn symlink(&self, original: P, link: Q, _is_dir: F) -> io::Result<()> + async fn symlink(&self, original: P, _is_dir: F) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, F: AsyncFnOnce() -> io::Result, { - Ok(self.op().await?.symlink(&original, &link).await?) + let original = original.as_path_dyn().as_os()?; + + Ok(self.op().await?.symlink(&original, self.path).await?) } - async fn symlink_metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - let attrs = self.op().await?.lstat(path).await?; - Ok(Cha::try_from((path.file_name().unwrap_or_default(), &attrs))?.0) + async fn symlink_metadata(&self) -> io::Result { + let attrs = self.op().await?.lstat(self.path).await?; + Ok(Cha::try_from((self.path.file_name().unwrap_or_default(), &attrs))?.0) } - async fn trash

(&self, _path: P) -> io::Result<()> - where - P: AsRef, - { + async fn trash(&self) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::Unsupported, "Trash not supported")) } + + #[inline] + fn url(&self) -> Url<'_> { self.url } } -impl Sftp { - pub(super) async fn op(self) -> io::Result> { - use deadpool::managed::PoolError; - - let pool = *super::CONN.lock().entry(self.config).or_insert_with(|| { - Box::leak(Box::new(deadpool::managed::Pool::builder(self).max_size(5).build().unwrap())) - }); - - pool.get().await.map_err(|e| match e { - PoolError::Timeout(_) => io::Error::new(io::ErrorKind::TimedOut, e.to_string()), - PoolError::Backend(e) => e, - PoolError::Closed | PoolError::NoRuntimeSpecified | PoolError::PostCreateHook(_) => { - io::Error::other(e.to_string()) - } - }) - } -} - -impl russh::client::Handler for Sftp { - type Error = russh::Error; - - async fn check_server_key( - &mut self, - _server_public_key: &russh::keys::PublicKey, - ) -> Result { - Ok(true) - } -} - -impl deadpool::managed::Manager for Sftp { - type Error = io::Error; - type Type = yazi_sftp::Operator; - - async fn create(&self) -> Result { - let channel = self.connect().await.map_err(|e| { - io::Error::other(format!("Failed to connect to SFTP server `{}`: {e}", self.name)) - })?; - - let mut op = yazi_sftp::Operator::make(channel.into_stream()); - op.init().await?; - Ok(op) - } - - async fn recycle( - &self, - obj: &mut Self::Type, - _metrics: &deadpool::managed::Metrics, - ) -> deadpool::managed::RecycleResult { - if obj.is_closed() { - Err(deadpool::managed::RecycleError::Message("Channel closed".into())) - } else { - Ok(()) - } - } -} - -impl Sftp { - async fn connect(self) -> Result, russh::Error> { - let pref = Arc::new(russh::client::Config { - inactivity_timeout: Some(std::time::Duration::from_secs(30)), - keepalive_interval: Some(std::time::Duration::from_secs(10)), - nodelay: true, - ..Default::default() - }); - - let session = if self.config.password.is_some() { - self.connect_by_password(pref).await - } else if !self.config.key_file.as_os_str().is_empty() { - self.connect_by_key(pref).await - } else { - self.connect_by_agent(pref).await - }?; - - let channel = session.channel_open_session().await?; - channel.request_subsystem(true, "sftp").await?; - Ok(channel) - } - - async fn connect_by_password( - self, - pref: Arc, - ) -> Result, russh::Error> { - let Some(password) = &self.config.password else { - return Err(russh::Error::InvalidConfig("Password not provided".to_owned())); - }; - - let mut session = - russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; - - if session.authenticate_password(&self.config.user, password).await?.success() { - Ok(session) - } else { - Err(russh::Error::InvalidConfig("Password authentication failed".to_owned())) - } - } - - async fn connect_by_key( - self, - pref: Arc, - ) -> Result, russh::Error> { - let key_file = &self.config.key_file; - if key_file.as_os_str().is_empty() { - return Err(russh::Error::InvalidConfig("Key file not provided".to_owned())); - }; - - let key = Local - .read_to_string(key_file) - .await - .map_err(|e| russh::Error::InvalidConfig(format!("Failed to read key file: {e}")))?; - - let key = russh::keys::decode_secret_key(&key, self.config.key_passphrase.as_deref())?; - - let mut session = - russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; - - let result = session - .authenticate_publickey( - &self.config.user, - PrivateKeyWithHashAlg::new( - Arc::new(key), - session.best_supported_rsa_hash().await?.flatten(), - ), - ) - .await?; - - if result.success() { - Ok(session) - } else { - Err(russh::Error::InvalidConfig("Public key authentication failed".to_owned())) - } - } - - async fn connect_by_agent( - self, - pref: Arc, - ) -> Result, russh::Error> { - let identity_agent = &self.config.identity_agent; - if identity_agent.as_os_str().is_empty() { - return Err(russh::Error::InvalidConfig("Identity agent not provided".to_owned())); - }; - - #[cfg(unix)] - let mut agent = russh::keys::agent::client::AgentClient::connect_uds(identity_agent).await?; - #[cfg(windows)] - let mut agent = - russh::keys::agent::client::AgentClient::connect_named_pipe(identity_agent).await?; - - let keys = agent.request_identities().await?; - if keys.is_empty() { - return Err(russh::Error::InvalidConfig("No keys found in SSH agent".to_owned())); - } - - let mut session = - russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; - - for key in keys { - match session.authenticate_publickey_with(&self.config.user, key, None, &mut agent).await { - Ok(result) if result.success() => return Ok(session), - Ok(_) => {} - Err(e) => tracing::error!("Identity agent authentication error: {e}"), - } - } - - Err(russh::Error::InvalidConfig("Public key authentication via agent failed".to_owned())) +impl<'a> Sftp<'a> { + #[inline] + pub(super) async fn op(&self) -> io::Result> { + Conn { name: self.name, config: self.config }.roll().await } } diff --git a/yazi-watcher/src/backend.rs b/yazi-watcher/src/backend.rs index 1212de9e..10f6ca4e 100644 --- a/yazi-watcher/src/backend.rs +++ b/yazi-watcher/src/backend.rs @@ -30,7 +30,7 @@ impl Backend { pub(super) fn watch(&mut self, url: impl AsUrl) -> Result<()> { let url = url.as_url(); - if let Some(path) = url.as_path() { + if let Some(path) = url.as_local() { self.local.watch(path)?; } else { self.remote.watch(url)?; @@ -41,7 +41,7 @@ impl Backend { pub(super) fn unwatch(&mut self, url: impl AsUrl) -> Result<()> { let url = url.as_url(); - if let Some(path) = url.as_path() { + if let Some(path) = url.as_local() { self.local.unwatch(path)?; } else { self.remote.unwatch(url)?; diff --git a/yazi-watcher/src/local/linked.rs b/yazi-watcher/src/local/linked.rs index e8c9afb5..44a6c4de 100644 --- a/yazi-watcher/src/local/linked.rs +++ b/yazi-watcher/src/local/linked.rs @@ -26,7 +26,7 @@ impl Linked { U: Into>, { let url: Url = url.into(); - let Some(path) = url.as_path() else { + let Some(path) = url.as_local() else { return Box::new(iter::empty()); }; @@ -38,7 +38,7 @@ impl Linked { } pub fn from_file(&self, url: Url) -> Vec { - let Some(path) = url.as_path() else { return vec![] }; + let Some(path) = url.as_local() else { return vec![] }; if let Some((parent, name)) = path.parent().zip(path.file_name()) { self.from_dir(parent).map(|p| p.join(name)).collect() } else { diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index fd6725e4..67896007 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -74,7 +74,7 @@ impl Local { continue; }; - if let Some(p) = file.url.as_path() + if let Some(p) = file.url.as_local() && !provider::local::must_case_match(p).await { ops.push(FilesOp::Deleting(parent.into(), [urn.owned()].into())); diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index a9a1776c..9fc16c0f 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -1,5 +1,5 @@ use tokio::sync::mpsc; -use yazi_shared::{path::PathLike, scheme::SchemeRef, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{path::PathLike, scheme::SchemeKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use crate::{WATCHED, local::LINKED}; @@ -16,10 +16,10 @@ impl Reporter { I::Item: Into>, { for url in urls.into_iter().map(Into::into) { - match url.as_url().scheme { - SchemeRef::Regular | SchemeRef::Search(_) => self.report_local(url), - SchemeRef::Archive(_) => {} - SchemeRef::Sftp(_) => self.report_remote(url), + match url.as_url().kind() { + SchemeKind::Regular | SchemeKind::Search => self.report_local(url), + SchemeKind::Archive => {} + SchemeKind::Sftp => self.report_remote(url), } } } @@ -37,14 +37,15 @@ impl Reporter { self.local_tx.send(url.to_owned()).ok(); self.local_tx.send(parent.to_owned()).ok(); } - if name.extension() == Some("%tmp".as_ref()) { + if name.ext().is_some_and(|e| e == "%tmp") { continue; } // SFTP caches - if let Some(dir) = watched.find_by_cache(&parent.loc) { - self.remote_tx.send(dir.join(name)).ok(); - self.remote_tx.send(dir.to_owned()).ok(); - } + // todo!(); + // if let Some(dir) = watched.find_by_cache(&parent.loc()) { + // self.remote_tx.send(dir.join(name)).ok(); + // self.remote_tx.send(dir.to_owned()).ok(); + // } } } diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index f8fcd889..bfe322c6 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -3,7 +3,7 @@ use std::path::Path; use hashbrown::HashSet; use percent_encoding::percent_decode_str; use yazi_fs::{Xdg, path::PercentEncoding}; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, UrlBuf, UrlLike}}; +use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; #[derive(Debug, Default)] pub struct Watched(HashSet); @@ -22,7 +22,7 @@ impl Watched { #[inline] pub(crate) fn paths(&self) -> impl Iterator { - self.0.iter().filter_map(|u| u.as_path()) + self.0.iter().filter_map(|u| u.as_local()) } #[inline] @@ -39,16 +39,17 @@ impl Watched { let loc = l2.percent_decode(); self.0.iter().find(|u| { - if u.scheme != SchemeRef::Sftp(&domain) { - return false; - } + todo!() + // if u.scheme != SchemeRef::Sftp(&domain) { + // return false; + // } - let mut it = u.loc.components(); - if it.next() == Some(std::path::Component::RootDir) { - !rel && it.as_path().as_os_str().as_encoded_bytes() == loc.as_ref() - } else { - rel && u.loc.as_os_str().as_encoded_bytes() == loc.as_ref() - } + // let mut it = u.loc.components(); + // if it.next() == Some(std::path::Component::RootDir) { + // !rel && it.as_path().as_os_str().as_encoded_bytes() == loc.as_ref() + // } else { + // rel && u.loc.as_os_str().as_encoded_bytes() == loc.as_ref() + // } }) } } diff --git a/yazi-widgets/src/clipboard.rs b/yazi-widgets/src/clipboard.rs index 3554d052..8b0d20ec 100644 --- a/yazi-widgets/src/clipboard.rs +++ b/yazi-widgets/src/clipboard.rs @@ -1,5 +1,3 @@ -use std::ffi::OsString; - use parking_lot::Mutex; use yazi_shared::RoCell; @@ -7,14 +5,12 @@ pub static CLIPBOARD: RoCell = RoCell::new(); #[derive(Default)] pub struct Clipboard { - content: Mutex, + content: Mutex>, } impl Clipboard { #[cfg(unix)] - pub async fn get(&self) -> OsString { - use std::os::unix::prelude::OsStringExt; - + pub async fn get(&self) -> Vec { use tokio::process::Command; use yazi_shared::in_ssh_connection; @@ -35,26 +31,26 @@ impl Clipboard { continue; }; if output.status.success() { - return OsString::from_vec(output.stdout); + return output.stdout; } } self.content.lock().clone() } #[cfg(windows)] - pub async fn get(&self) -> OsString { - use clipboard_win::{formats, get_clipboard}; + pub async fn get(&self) -> Vec { + use clipboard_win::get_clipboard_string; - let result = tokio::task::spawn_blocking(|| get_clipboard::(formats::Unicode)); + let result = tokio::task::spawn_blocking(get_clipboard_string); if let Ok(Ok(s)) = result.await { - return s.into(); + return s.into_bytes(); } self.content.lock().clone() } #[cfg(unix)] - pub async fn set(&self, s: impl AsRef) { + pub async fn set(&self, s: impl AsRef<[u8]>) { use std::process::Stdio; use crossterm::execute; @@ -84,7 +80,7 @@ impl Clipboard { let Ok(mut child) = cmd else { continue }; let mut stdin = child.stdin.take().unwrap(); - if stdin.write_all(s.as_ref().as_encoded_bytes()).await.is_err() { + if stdin.write_all(s.as_ref()).await.is_err() { continue; } drop(stdin); @@ -96,13 +92,13 @@ impl Clipboard { } #[cfg(windows)] - pub async fn set(&self, s: impl AsRef) { - use clipboard_win::{formats, set_clipboard}; + pub async fn set(&self, s: impl AsRef<[u8]>) { + use clipboard_win::set_clipboard_string; - let s = s.as_ref().to_owned(); - *self.content.lock() = s.clone(); + let b = s.as_ref().to_owned(); + *self.content.lock() = b.clone(); - tokio::task::spawn_blocking(move || set_clipboard(formats::Unicode, s.to_string_lossy())) + tokio::task::spawn_blocking(move || set_clipboard_string(&String::from_utf8_lossy(&b))) .await .ok(); } @@ -110,8 +106,6 @@ impl Clipboard { #[cfg(unix)] mod osc52 { - use std::ffi::OsStr; - use base64::{Engine, engine::general_purpose}; #[derive(Debug)] @@ -120,8 +114,8 @@ mod osc52 { } impl SetClipboard { - pub fn new(content: &OsStr) -> Self { - Self { content: general_purpose::STANDARD.encode(content.as_encoded_bytes()) } + pub fn new(content: &[u8]) -> Self { + Self { content: general_purpose::STANDARD.encode(content) } } } diff --git a/yazi-widgets/src/input/commands/paste.rs b/yazi-widgets/src/input/commands/paste.rs index 5f795ae8..dbda56db 100644 --- a/yazi-widgets/src/input/commands/paste.rs +++ b/yazi-widgets/src/input/commands/paste.rs @@ -18,7 +18,7 @@ impl Input { } act!(insert, self, !opt.before)?; - self.type_str(&s.to_string_lossy())?; + self.type_str(&String::from_utf8_lossy(&s))?; act!(escape, self)?; succ!(render!()); } From 9cd742811a4aa9c87d36bf0f69db07ab0a187eb8 Mon Sep 17 00:00:00 2001 From: walldmtd <85672104+walldmtd@users.noreply.github.com> Date: Tue, 18 Nov 2025 23:26:35 -0600 Subject: [PATCH 02/53] fix: `-flatten` flag should come after the input file in preset ImageMagick previewer (#3339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 三咲雅 misaki masa --- yazi-plugin/preset/plugins/magick.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yazi-plugin/preset/plugins/magick.lua b/yazi-plugin/preset/plugins/magick.lua index df9b6375..ce3e7e49 100644 --- a/yazi-plugin/preset/plugins/magick.lua +++ b/yazi-plugin/preset/plugins/magick.lua @@ -25,11 +25,11 @@ function M:preload(job) return true end - local cmd = M.with_limit() + local cmd = M.with_limit():arg(tostring(job.file.url)) if job.args.flatten then cmd:arg("-flatten") end - cmd:arg { tostring(job.file.url), "-auto-orient", "-strip" } + cmd:arg { "-auto-orient", "-strip" } local size = string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height) if rt.preview.image_filter == "nearest" then From d3ad1c0920719f0779ac0f345d8477f1000cad4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 19 Nov 2025 18:07:04 +0800 Subject: [PATCH 03/53] refactor: refine `PathLike` and `StrandLike` traits (#3340) --- Cargo.toml | 13 +- yazi-actor/src/cmp/show.rs | 9 +- yazi-actor/src/cmp/trigger.rs | 27 +- yazi-actor/src/lives/file.rs | 2 +- yazi-actor/src/lives/tab.rs | 2 +- yazi-actor/src/mgr/cd.rs | 4 +- yazi-actor/src/mgr/create.rs | 6 +- yazi-actor/src/mgr/filter_do.rs | 4 +- yazi-actor/src/mgr/hidden.rs | 4 +- yazi-actor/src/mgr/rename.rs | 11 +- yazi-actor/src/mgr/reveal.rs | 4 +- yazi-actor/src/mgr/sort.rs | 4 +- yazi-actor/src/mgr/update_files.rs | 3 +- yazi-binding/Cargo.toml | 3 + yazi-binding/src/macros.rs | 1 - yazi-binding/src/path.rs | 2 +- yazi-binding/src/runtime.rs | 2 +- yazi-binding/src/url.rs | 2 +- yazi-config/Cargo.toml | 3 + yazi-config/src/mgr/mgr.rs | 2 +- yazi-config/src/pattern.rs | 7 +- yazi-config/src/popup/options.rs | 16 +- yazi-config/src/theme/icon.rs | 2 +- yazi-core/Cargo.toml | 3 + yazi-core/src/tab/finder.rs | 8 +- yazi-core/src/tab/folder.rs | 6 +- yazi-core/src/tab/preference.rs | 2 +- yazi-dds/Cargo.toml | 3 + yazi-ffi/Cargo.toml | 3 + yazi-fm/Cargo.toml | 3 + yazi-fm/src/app/commands/bootstrap.rs | 2 +- yazi-fm/src/cmp/cmp.rs | 4 +- yazi-fs/Cargo.toml | 3 + yazi-fs/src/cha/cha.rs | 2 - yazi-fs/src/cha/kind.rs | 1 - yazi-fs/src/cha/mode.rs | 2 +- yazi-fs/src/error/serde.rs | 16 +- yazi-fs/src/file.rs | 11 +- yazi-fs/src/files.rs | 14 +- yazi-fs/src/filter.rs | 2 +- yazi-fs/src/op.rs | 24 +- yazi-fs/src/path/clean.rs | 2 +- yazi-fs/src/path/expand.rs | 15 +- yazi-fs/src/provider/local/local.rs | 28 +- yazi-fs/src/provider/traits.rs | 14 +- yazi-fs/src/sorter.rs | 2 +- yazi-fs/src/url.rs | 10 +- yazi-macro/Cargo.toml | 3 + yazi-packing/Cargo.toml | 33 +- yazi-parser/Cargo.toml | 3 + yazi-parser/src/cmp/show.rs | 2 +- yazi-parser/src/mgr/copy.rs | 10 +- yazi-plugin/Cargo.toml | 3 + yazi-proxy/Cargo.toml | 3 + yazi-scheduler/Cargo.toml | 3 + yazi-scheduler/src/prework/prework.rs | 4 +- yazi-sftp/Cargo.toml | 3 + yazi-sftp/src/byte_str.rs | 4 +- yazi-shared/Cargo.toml | 3 + yazi-shared/src/data/data.rs | 4 +- yazi-shared/src/loc/able.rs | 142 +++++++++ yazi-shared/src/loc/buf.rs | 121 +++----- yazi-shared/src/loc/loc.rs | 82 ++--- yazi-shared/src/loc/mod.rs | 4 +- yazi-shared/src/path/buf.rs | 178 +++++++---- yazi-shared/src/path/conversion.rs | 80 +---- yazi-shared/src/path/cow.rs | 16 +- yazi-shared/src/path/dyn.rs | 411 ------------------------- yazi-shared/src/path/error.rs | 15 +- yazi-shared/src/path/like.rs | 96 ++++++ yazi-shared/src/path/mod.rs | 2 +- yazi-shared/src/path/path.rs | 348 ++++++++++++--------- yazi-shared/src/path/unsafe.rs | 23 -- yazi-shared/src/path/view.rs | 6 +- yazi-shared/src/scheme/cow.rs | 16 +- yazi-shared/src/scheme/encode.rs | 6 +- yazi-shared/src/scheme/traits.rs | 8 +- yazi-shared/src/strand/buf.rs | 85 +++++ yazi-shared/src/strand/conversion.rs | 150 +++------ yazi-shared/src/strand/cow.rs | 24 +- yazi-shared/src/strand/dyn.rs | 126 ++++---- yazi-shared/src/strand/error.rs | 2 +- yazi-shared/src/strand/like.rs | 47 +++ yazi-shared/src/strand/mod.rs | 2 +- yazi-shared/src/strand/strand.rs | 168 ---------- yazi-shared/src/strand/view.rs | 9 +- yazi-shared/src/url/buf.rs | 38 +-- yazi-shared/src/url/component.rs | 2 +- yazi-shared/src/url/display.rs | 2 +- yazi-shared/src/url/encode.rs | 2 +- yazi-shared/src/url/traits.rs | 6 +- yazi-shared/src/url/url.rs | 61 ++-- yazi-shared/src/wtf8.rs | 8 +- yazi-shim/Cargo.toml | 3 + yazi-term/Cargo.toml | 3 + yazi-term/src/background.rs | 2 +- yazi-vfs/Cargo.toml | 3 + yazi-vfs/src/fns.rs | 4 +- yazi-vfs/src/op.rs | 4 +- yazi-vfs/src/provider/provider.rs | 6 +- yazi-vfs/src/provider/providers.rs | 14 +- yazi-vfs/src/provider/sftp/gate.rs | 14 +- yazi-vfs/src/provider/sftp/metadata.rs | 2 +- yazi-vfs/src/provider/sftp/sftp.rs | 20 +- yazi-watcher/Cargo.toml | 3 + yazi-watcher/src/local/local.rs | 8 +- yazi-watcher/src/remote/remote.rs | 6 +- yazi-watcher/src/reporter.rs | 2 +- yazi-widgets/Cargo.toml | 3 + 109 files changed, 1282 insertions(+), 1492 deletions(-) create mode 100644 yazi-shared/src/loc/able.rs delete mode 100644 yazi-shared/src/path/dyn.rs create mode 100644 yazi-shared/src/path/like.rs delete mode 100644 yazi-shared/src/path/unsafe.rs create mode 100644 yazi-shared/src/strand/buf.rs create mode 100644 yazi-shared/src/strand/like.rs delete mode 100644 yazi-shared/src/strand/strand.rs diff --git a/Cargo.toml b/Cargo.toml index d9e4b1e8..3b04c220 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,8 +59,11 @@ unicode-width = { version = "0.2.0", default-features = false } uzers = "0.12.1" [workspace.lints.clippy] -format_push_string = "warn" -implicit_clone = "warn" -module_inception = "allow" -unit_arg = "allow" -use_self = "warn" +format_push_string = "warn" +if_same_then_else = "allow" +implicit_clone = "warn" +len_without_is_empty = "allow" +missing_safety_doc = "allow" +module_inception = "allow" +unit_arg = "allow" +use_self = "warn" diff --git a/yazi-actor/src/cmp/show.rs b/yazi-actor/src/cmp/show.rs index 3e151c1c..b747baba 100644 --- a/yazi-actor/src/cmp/show.rs +++ b/yazi-actor/src/cmp/show.rs @@ -3,7 +3,7 @@ use std::{mem, ops::ControlFlow}; use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt}; -use yazi_shared::{data::Data, path::{AsPathDyn, PathDyn, PathLike}, strand::{AsStrand, StrandLike}}; +use yazi_shared::{data::Data, path::{AsPath, PathDyn}, strand::StrandLike}; use crate::{Actor, Ctx}; @@ -30,7 +30,7 @@ impl Actor for Show { }; cmp.ticket = opt.ticket; - cmp.cands = Self::match_candidates(opt.word.as_path_dyn(), cache); + cmp.cands = Self::match_candidates(opt.word.as_path(), cache); if cmp.cands.is_empty() { succ!(render!(mem::replace(&mut cmp.visible, false))); } @@ -47,16 +47,15 @@ impl Show { let smart = !word.encoded_bytes().iter().any(|&b| b.is_ascii_uppercase()); let flow = cache.iter().try_fold((Vec::new(), Vec::new()), |(mut exact, mut fuzzy), item| { - let name = item.name.as_strand(); let starts_with = - if smart { name.eq_ignore_ascii_case(word) } else { name.starts_with(word) }; + if smart { item.name.eq_ignore_ascii_case(word) } else { item.name.starts_with(word) }; if starts_with { exact.push(item); if exact.len() >= LIMIT { return ControlFlow::Break((exact, fuzzy)); } - } else if fuzzy.len() < LIMIT - exact.len() && name.contains(word) { + } else if fuzzy.len() < LIMIT - exact.len() && item.name.contains(word) { // Here we don't break the control flow, since we want more exact matching. fuzzy.push(item) } diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index 3cc13427..c092099f 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -5,7 +5,7 @@ use yazi_fs::{CWD, path::expand_url, provider::{DirReader, FileHolder}}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_proxy::CmpProxy; -use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::StrandBufLike, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{PathBufDyn, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::StrandLike, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::provider; use crate::{Actor, Ctx}; @@ -74,20 +74,17 @@ impl Trigger { } let sep = if cfg!(windows) { - AnyAsciiChar::new(&[b'/', b'\\']).unwrap() + AnyAsciiChar::new(b"/\\").unwrap() } else { - AnyAsciiChar::new(&[b'/']).unwrap() + AnyAsciiChar::new(b"/").unwrap() }; - Some(match path.as_path().rsplit_pred(sep) { + Some(match path.rsplit_pred(sep) { Some((p, c)) if p.is_empty() => { - let root = PathBufDyn::with(scheme.kind(), MAIN_SEPARATOR_STR).expect("valid root"); + let root = PathDyn::with(scheme.kind(), MAIN_SEPARATOR_STR).expect("valid root"); (UrlCow::try_from((scheme, root)).ok()?.into_owned(), c.into()) } - Some((p, c)) => { - let parent = PathBufDyn::with(scheme.kind(), p.as_dyn()).expect("valid parent"); - (expand_url(UrlCow::try_from((scheme, parent)).ok()?), c.into()) - } + Some((p, c)) => (expand_url(UrlCow::try_from((scheme, p)).ok()?), c.into()), None => (CWD.load().as_ref().clone(), path.into()), }) } @@ -95,7 +92,7 @@ impl Trigger { #[cfg(test)] mod tests { - use yazi_shared::{path::PathBufLike, url::UrlLike}; + use yazi_shared::url::UrlLike; use super::*; @@ -113,11 +110,19 @@ mod tests { yazi_fs::init(); compare("", "", ""); compare(" ", "", " "); + compare("/", "/", ""); - compare("//", "//", ""); + compare("//", "/", ""); + compare("///", "/", ""); + compare("/foo", "/", "foo"); + compare("//foo", "/", "foo"); + compare("///foo", "/", "foo"); + compare("/foo/", "/foo/", ""); + compare("//foo/", "/foo/", ""); compare("/foo/bar", "/foo/", "bar"); + compare("///foo/bar", "/foo/", "bar"); } #[cfg(windows)] diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index fc6aa3b7..ac978929 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -4,7 +4,7 @@ use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods, Valu use yazi_binding::Style; use yazi_config::THEME; use yazi_plugin::bindings::Range; -use yazi_shared::{path::PathLike, url::UrlLike}; +use yazi_shared::url::UrlLike; use super::Lives; use crate::lives::PtrCell; diff --git a/yazi-actor/src/lives/tab.rs b/yazi-actor/src/lives/tab.rs index 311af5b9..1c4ab5dd 100644 --- a/yazi-actor/src/lives/tab.rs +++ b/yazi-actor/src/lives/tab.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods, Value}; use yazi_binding::{Id, UrlRef, cached_field}; -use yazi_shared::{path::PathLike, strand::StrandLike, url::UrlLike}; +use yazi_shared::url::UrlLike; use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected}; diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 3ed6b1ec..73003575 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -9,7 +9,7 @@ use yazi_fs::{File, FilesOp, path::expand_url}; use yazi_macro::{act, err, render, succ}; use yazi_parser::mgr::CdOpt; use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; -use yazi_shared::{Debounce, data::Data, errors::InputError, path::PathLike, url::{AsUrl, UrlBuf, UrlLike}}; +use yazi_shared::{Debounce, data::Data, errors::InputError, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_vfs::VfsFile; use crate::{Actor, Ctx}; @@ -85,7 +85,7 @@ impl Cd { } if let Some(p) = url.parent() { - FilesOp::Upserting(p.into(), [(url.urn().owned(), file)].into()).emit(); + FilesOp::Upserting(p.into(), [(url.urn().into(), file)].into()).emit(); } MgrProxy::reveal(&url); } diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 733617f1..05e278a9 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -4,7 +4,7 @@ use yazi_fs::{File, FilesOp}; use yazi_macro::{ok_or_not_found, succ}; use yazi_parser::mgr::CreateOpt; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; -use yazi_shared::{data::Data, path::PathLike, url::{UrlBuf, UrlLike}}; +use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; @@ -54,7 +54,7 @@ impl Create { && let Some((parent, urn)) = real.pair() { ok_or_not_found!(provider::remove_file(&new).await); - FilesOp::Deleting(parent.into(), [urn.owned()].into()).emit(); + FilesOp::Deleting(parent.into(), [urn.into()].into()).emit(); provider::create(&new).await?; } else if let Some(parent) = new.parent() { provider::create_dir_all(parent).await.ok(); @@ -68,7 +68,7 @@ impl Create { && let Some((parent, urn)) = real.pair() { let file = File::new(&real).await?; - FilesOp::Upserting(parent.into(), [(urn.owned(), file)].into()).emit(); + FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()).emit(); MgrProxy::reveal(&real); } diff --git a/yazi-actor/src/mgr/filter_do.rs b/yazi-actor/src/mgr/filter_do.rs index 6998907b..37062d6c 100644 --- a/yazi-actor/src/mgr/filter_do.rs +++ b/yazi-actor/src/mgr/filter_do.rs @@ -2,7 +2,7 @@ use anyhow::Result; use yazi_fs::Filter; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::FilterOpt; -use yazi_shared::{data::Data, path::PathLike}; +use yazi_shared::data::Data; use crate::{Actor, Ctx}; @@ -16,7 +16,7 @@ impl Actor for FilterDo { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) }; - let hovered = cx.hovered().map(|f| f.urn().owned()); + let hovered = cx.hovered().map(|f| f.urn().into()); cx.current_mut().files.set_filter(filter); if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { diff --git a/yazi-actor/src/mgr/hidden.rs b/yazi-actor/src/mgr/hidden.rs index 2aba7f2b..1e42ee7d 100644 --- a/yazi-actor/src/mgr/hidden.rs +++ b/yazi-actor/src/mgr/hidden.rs @@ -3,7 +3,7 @@ use yazi_core::tab::Folder; use yazi_fs::FolderStage; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::mgr::HiddenOpt; -use yazi_shared::{data::Data, path::PathLike}; +use yazi_shared::data::Data; use crate::{Actor, Ctx}; @@ -18,7 +18,7 @@ impl Actor for Hidden { let state = opt.state.bool(cx.tab().pref.show_hidden); cx.tab_mut().pref.show_hidden = state; - let hovered = cx.hovered().map(|f| f.urn().owned()); + let hovered = cx.hovered().map(|f| f.urn().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index ef60c7a2..0c857d73 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -5,7 +5,7 @@ use yazi_fs::{File, FilesOp}; use yazi_macro::{act, err, ok_or_not_found, succ}; use yazi_parser::mgr::RenameOpt; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; -use yazi_shared::{Id, data::Data, path::PathLike, strand::StrandLike, url::{UrlBuf, UrlLike}}; +use yazi_shared::{Id, data::Data, url::{UrlBuf, UrlLike}}; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; @@ -73,10 +73,11 @@ impl Rename { provider::rename(&old, &new).await?; if let Ok(u) = overwritten + && u != new && let Some((parent, urn)) = u.pair() { ok_or_not_found!(provider::rename(&u, &new).await); - FilesOp::Deleting(parent.to_owned(), [urn.owned()].into()).emit(); + FilesOp::Deleting(parent.to_owned(), [urn.into()].into()).emit(); } let new = provider::casefold(&new).await?; @@ -84,10 +85,10 @@ impl Rename { let file = File::new(&new).await?; if new_p == old_p { - FilesOp::Upserting(old_p.into(), [(old_n.owned(), file)].into()).emit(); + FilesOp::Upserting(old_p.into(), [(old_n.into(), file)].into()).emit(); } else { - FilesOp::Deleting(old_p.into(), [old_n.owned()].into()).emit(); - FilesOp::Upserting(new_p.into(), [(new_n.owned(), file)].into()).emit(); + FilesOp::Deleting(old_p.into(), [old_n.into()].into()).emit(); + FilesOp::Upserting(new_p.into(), [(new_n.into(), file)].into()).emit(); } MgrProxy::reveal(&new); diff --git a/yazi-actor/src/mgr/reveal.rs b/yazi-actor/src/mgr/reveal.rs index d174d10f..590d0924 100644 --- a/yazi-actor/src/mgr/reveal.rs +++ b/yazi-actor/src/mgr/reveal.rs @@ -2,7 +2,7 @@ use anyhow::Result; use yazi_fs::{File, FilesOp}; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::RevealOpt; -use yazi_shared::{data::Data, path::PathLike, url::UrlLike}; +use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; @@ -31,7 +31,7 @@ impl Actor for Reveal { } // Now, we can safely hover on the target - act!(mgr:hover, cx, Some(child.owned()))?; + act!(mgr:hover, cx, Some(child.into()))?; act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index d29a2efd..d36604d0 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -3,7 +3,7 @@ use yazi_core::tab::Folder; use yazi_fs::{FilesSorter, FolderStage}; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::mgr::SortOpt; -use yazi_shared::{data::Data, path::PathLike}; +use yazi_shared::data::Data; use crate::{Actor, Ctx}; @@ -23,7 +23,7 @@ impl Actor for Sort { pref.sort_translit = opt.translit.unwrap_or(pref.sort_translit); let sorter = FilesSorter::from(&*pref); - let hovered = cx.hovered().map(|f| f.urn().owned()); + let hovered = cx.hovered().map(|f| f.urn().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index e2c1479e..af0cd4a5 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -17,8 +17,7 @@ impl Actor for UpdateFiles { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let revision = cx.current().files.revision; - let linked: Vec<_> = - LINKED.read().from_dir(opt.op.cwd()).filter_map(|u| opt.op.chdir(u).ok()).collect(); + let linked: Vec<_> = LINKED.read().from_dir(opt.op.cwd()).map(|u| opt.op.chdir(u)).collect(); for op in [opt.op].into_iter().chain(linked) { cx.mgr.yanked.apply_op(&op); diff --git a/yazi-binding/Cargo.toml b/yazi-binding/Cargo.toml index 0ed3286d..cd8077cd 100644 --- a/yazi-binding/Cargo.toml +++ b/yazi-binding/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi Lua bindings" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [features] default = [ "vendored-lua" ] vendored-lua = [ "mlua/vendored" ] diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index 7e5c69b4..9e0bbcb3 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -154,7 +154,6 @@ macro_rules! impl_file_fields { $crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to_url().map($crate::Url::new))); $crate::cached_field!($fields, name, |lua, me| { - use yazi_shared::strand::StrandLike; me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); $crate::cached_field!($fields, cache, |_, me| { diff --git a/yazi-binding/src/path.rs b/yazi-binding/src/path.rs index edcce546..03288668 100644 --- a/yazi-binding/src/path.rs +++ b/yazi-binding/src/path.rs @@ -1,7 +1,7 @@ use std::ops::Deref; use mlua::{ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataMethods, Value}; -use yazi_shared::path::{PathBufDyn, PathBufLike}; +use yazi_shared::path::{PathBufDyn, PathLike}; pub struct Path(pub PathBufDyn); diff --git a/yazi-binding/src/runtime.rs b/yazi-binding/src/runtime.rs index 2eec279b..db73b1c2 100644 --- a/yazi-binding/src/runtime.rs +++ b/yazi-binding/src/runtime.rs @@ -49,7 +49,7 @@ impl Runtime { if let Some(v) = self.blocks.get_mut(&cur.id) { v.push(f); } else { - self.blocks.insert(cur.id.to_owned(), vec![f]); + self.blocks.insert(cur.id.clone(), vec![f]); } true } diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index c86d34f2..7577d8b0 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use yazi_fs::{FsHash64, FsHash128}; -use yazi_shared::{path::{PathLike, StripPrefixError}, strand::{StrandCow, StrandLike}, url::{AsUrl, UrlCow, UrlLike}}; +use yazi_shared::{path::StripPrefixError, strand::StrandCow, url::{AsUrl, UrlCow, UrlLike}}; use crate::{Scheme, cached_field, deprecate}; diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index 382716fe..7736f0dc 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi configuration file parser" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-codegen = { path = "../yazi-codegen", version = "25.9.15" } yazi-fs = { path = "../yazi-fs", version = "25.9.15" } diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index f18066c4..3f176ce7 100644 --- a/yazi-config/src/mgr/mgr.rs +++ b/yazi-config/src/mgr/mgr.rs @@ -2,7 +2,7 @@ use anyhow::{Result, bail}; use serde::Deserialize; use yazi_codegen::DeserializeOver2; use yazi_fs::{CWD, SortBy}; -use yazi_shared::{SyncCell, path::PathLike, url::{UrlBuf, UrlLike}}; +use yazi_shared::{SyncCell, url::{UrlBuf, UrlLike}}; use super::{MgrRatio, MouseEvents}; diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 72a52278..acb580ce 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -3,7 +3,7 @@ use std::{fmt::Debug, str::FromStr}; use anyhow::{Result, bail}; use globset::{Candidate, GlobBuilder}; use serde::Deserialize; -use yazi_shared::{path::PathLike, scheme::SchemeKind, url::AsUrl}; +use yazi_shared::{scheme::SchemeKind, url::AsUrl}; #[derive(Deserialize)] #[serde(try_from = "String")] @@ -28,7 +28,6 @@ impl Debug for Pattern { } impl Pattern { - // FIXME: simplify conditional compilation pub fn match_url(&self, url: impl AsUrl, is_dir: bool) -> bool { let url = url.as_url(); @@ -47,9 +46,9 @@ impl Pattern { #[cfg(windows)] if self.sep_lit { - use yazi_shared::strand::AsStrandDyn; + use yazi_shared::strand::{AsStrand, StrandLike}; self.inner.is_match_candidate(&Candidate::from_bytes( - url.loc().as_strand_dyn().backslash_to_slash().encoded_bytes(), + url.loc().as_strand().backslash_to_slash().encoded_bytes(), )) } else { self.inner.is_match_candidate(&Candidate::from_bytes(url.loc().encoded_bytes())) diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs index 66b45b02..85fd42f7 100644 --- a/yazi-config/src/popup/options.rs +++ b/yazi-config/src/popup/options.rs @@ -33,7 +33,7 @@ pub struct ConfirmCfg { impl InputCfg { pub fn cd() -> Self { Self { - title: YAZI.input.cd_title.to_owned(), + title: YAZI.input.cd_title.clone(), position: Position::new(YAZI.input.cd_origin, YAZI.input.cd_offset), completion: true, ..Default::default() @@ -42,7 +42,7 @@ impl InputCfg { pub fn create(dir: bool) -> Self { Self { - title: YAZI.input.create_title[dir as usize].to_owned(), + title: YAZI.input.create_title[dir as usize].clone(), position: Position::new(YAZI.input.create_origin, YAZI.input.create_offset), ..Default::default() } @@ -50,7 +50,7 @@ impl InputCfg { pub fn rename() -> Self { Self { - title: YAZI.input.rename_title.to_owned(), + title: YAZI.input.rename_title.clone(), position: Position::new(YAZI.input.rename_origin, YAZI.input.rename_offset), ..Default::default() } @@ -58,7 +58,7 @@ impl InputCfg { pub fn filter() -> Self { Self { - title: YAZI.input.filter_title.to_owned(), + title: YAZI.input.filter_title.clone(), position: Position::new(YAZI.input.filter_origin, YAZI.input.filter_offset), realtime: true, ..Default::default() @@ -67,7 +67,7 @@ impl InputCfg { pub fn find(prev: bool) -> Self { Self { - title: YAZI.input.find_title[prev as usize].to_owned(), + title: YAZI.input.find_title[prev as usize].clone(), position: Position::new(YAZI.input.find_origin, YAZI.input.find_offset), realtime: true, ..Default::default() @@ -84,7 +84,7 @@ impl InputCfg { pub fn shell(block: bool) -> Self { Self { - title: YAZI.input.shell_title[block as usize].to_owned(), + title: YAZI.input.shell_title[block as usize].clone(), position: Position::new(YAZI.input.shell_origin, YAZI.input.shell_offset), ..Default::default() } @@ -138,7 +138,7 @@ impl ConfirmCfg { pub fn overwrite(url: &UrlBuf) -> Self { Self::new( - YAZI.confirm.overwrite_title.to_owned(), + YAZI.confirm.overwrite_title.clone(), YAZI.confirm.overwrite_position(), Some(Text::raw(&YAZI.confirm.overwrite_body)), Some(url.into_string_lossy().into()), @@ -183,7 +183,7 @@ impl PickCfg { pub fn open(items: Vec) -> Self { let max_height = Self::max_height(items.len()); Self { - title: YAZI.pick.open_title.to_owned(), + title: YAZI.pick.open_title.clone(), items, position: Position::new(YAZI.pick.open_origin, Offset { height: max_height, diff --git a/yazi-config/src/theme/icon.rs b/yazi-config/src/theme/icon.rs index 98d2c01e..d8f0dc38 100644 --- a/yazi-config/src/theme/icon.rs +++ b/yazi-config/src/theme/icon.rs @@ -5,7 +5,7 @@ use hashbrown::HashMap; use serde::{Deserialize, Deserializer}; use yazi_codegen::DeserializeOver2; use yazi_fs::File; -use yazi_shared::{Condition, strand::StrandLike, url::UrlLike}; +use yazi_shared::{Condition, url::UrlLike}; use crate::{Color, Icon as I, Pattern, Style}; diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index 0a0106a8..4a12ce0e 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi core logic" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-adapter = { path = "../yazi-adapter", version = "25.9.15" } yazi-config = { path = "../yazi-config", version = "25.9.15" } diff --git a/yazi-core/src/tab/finder.rs b/yazi-core/src/tab/finder.rs index 4ba100f7..506eb86e 100644 --- a/yazi-core/src/tab/finder.rs +++ b/yazi-core/src/tab/finder.rs @@ -1,7 +1,7 @@ use anyhow::Result; use hashbrown::HashMap; use yazi_fs::{Files, Filter, FilterCase}; -use yazi_shared::{path::{AsPathDyn, PathBufDyn, PathLike}, url::UrlBuf}; +use yazi_shared::{path::{AsPath, PathBufDyn}, url::UrlBuf}; use crate::tab::Folder; @@ -62,7 +62,7 @@ impl Finder { continue; } - self.matched.insert(file.urn().owned(), i); + self.matched.insert(file.urn().into(), i); if self.matched.len() > 99 { break; } @@ -78,9 +78,9 @@ impl Finder { impl Finder { pub fn matched_idx(&self, folder: &Folder, urn: T) -> Option where - T: AsPathDyn, + T: AsPath, { - if self.lock == *folder { self.matched.get(&urn.as_path_dyn()).copied() } else { None } + if self.lock == *folder { self.matched.get(&urn.as_path()).copied() } else { None } } } diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index 51eabad5..41da80b7 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -6,7 +6,7 @@ use yazi_fs::{File, Files, FilesOp, FolderStage, cha::Cha}; use yazi_macro::err; use yazi_parser::Step; use yazi_proxy::MgrProxy; -use yazi_shared::{Id, path::{PathBufDyn, PathBufLike, PathDyn, PathLike}, url::UrlBuf}; +use yazi_shared::{Id, path::{AsPath, PathBufDyn, PathDyn}, url::UrlBuf}; use yazi_widgets::Scrollable; pub struct Folder { @@ -101,7 +101,7 @@ impl Folder { self.scroll(step) }; - self.trace = self.hovered().filter(|_| b).map(|h| h.urn().owned()).or(self.trace.take()); + self.trace = self.hovered().filter(|_| b).map(|h| h.urn().into()).or(self.trace.take()); b |= self.squeeze_offset(); self.sync_page(false); @@ -121,7 +121,7 @@ impl Folder { if let Some(u) = urn { self.hover(u) } else if let Some(u) = &self.trace { - self.hover(u.clone().borrow()) + self.hover(u.clone().as_path()) } else { self.arrow(0) } diff --git a/yazi-core/src/tab/preference.rs b/yazi-core/src/tab/preference.rs index c6ebf35e..275cd40c 100644 --- a/yazi-core/src/tab/preference.rs +++ b/yazi-core/src/tab/preference.rs @@ -26,7 +26,7 @@ impl Default for Preference { sort_translit: YAZI.mgr.sort_translit.get(), // Display - linemode: YAZI.mgr.linemode.to_owned(), + linemode: YAZI.mgr.linemode.clone(), show_hidden: YAZI.mgr.show_hidden.get(), } } diff --git a/yazi-dds/Cargo.toml b/yazi-dds/Cargo.toml index 4bff6675..59ac2629 100644 --- a/yazi-dds/Cargo.toml +++ b/yazi-dds/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi data distribution service" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [features] default = [ "vendored-lua" ] vendored-lua = [ "mlua/vendored" ] diff --git a/yazi-ffi/Cargo.toml b/yazi-ffi/Cargo.toml index 32fe61b3..40ae271e 100644 --- a/yazi-ffi/Cargo.toml +++ b/yazi-ffi/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi foreign function interface" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-macro = { path = "../yazi-macro", version = "25.9.15" } diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 9feaccbb..177a063b 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi file manager" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [profile.release] codegen-units = 1 lto = true diff --git a/yazi-fm/src/app/commands/bootstrap.rs b/yazi-fm/src/app/commands/bootstrap.rs index af83c5b2..bb3e3bbb 100644 --- a/yazi-fm/src/app/commands/bootstrap.rs +++ b/yazi-fm/src/app/commands/bootstrap.rs @@ -3,7 +3,7 @@ use yazi_actor::Ctx; use yazi_boot::BOOT; use yazi_macro::act; use yazi_parser::{VoidOpt, mgr::CdSource}; -use yazi_shared::{data::Data, strand::StrandBufLike, url::UrlLike}; +use yazi_shared::{data::Data, strand::StrandLike, url::UrlLike}; use crate::app::App; diff --git a/yazi-fm/src/cmp/cmp.rs b/yazi-fm/src/cmp/cmp.rs index f06fb1c0..9a7376df 100644 --- a/yazi-fm/src/cmp/cmp.rs +++ b/yazi-fm/src/cmp/cmp.rs @@ -4,7 +4,7 @@ use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, List, L use yazi_adapter::Dimension; use yazi_config::{THEME, popup::{Offset, Position}}; use yazi_core::Core; -use yazi_shared::strand::{AsStrand, StrandLike}; +use yazi_shared::strand::StrandLike; pub(crate) struct Cmp<'a> { core: &'a Core, @@ -26,7 +26,7 @@ impl Widget for Cmp<'_> { let icon = if x.is_dir { &THEME.cmp.icon_folder } else { &THEME.cmp.icon_file }; let slash = if x.is_dir { MAIN_SEPARATOR_STR } else { "" }; - let mut item = ListItem::new(format!(" {icon} {}{slash}", x.name.as_strand().display())); + let mut item = ListItem::new(format!(" {icon} {}{slash}", x.name.display())); if i == self.core.cmp.rel_cursor() { item = item.style(THEME.cmp.active); } else { diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index edd1e4d7..deba1893 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi file system" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-ffi = { path = "../yazi-ffi", version = "25.9.15" } yazi-macro = { path = "../yazi-macro", version = "25.9.15" } diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index f64003e7..f8ebb49d 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -62,8 +62,6 @@ impl Cha { let mut kind = ChaKind::DUMMY; let mode = r#type.map(ChaMode::from_bare).unwrap_or_default(); - #[cfg(unix)] - use yazi_shared::path::PathLike; #[cfg(unix)] if _url.as_url().urn().is_hidden() { kind |= ChaKind::HIDDEN; diff --git a/yazi-fs/src/cha/kind.rs b/yazi-fs/src/cha/kind.rs index 4e2736f9..7a20ce58 100644 --- a/yazi-fs/src/cha/kind.rs +++ b/yazi-fs/src/cha/kind.rs @@ -23,7 +23,6 @@ impl ChaKind { #[cfg(unix)] { - use yazi_shared::strand::StrandLike; if _name.as_strand().starts_with(".") { me |= Self::HIDDEN; } diff --git a/yazi-fs/src/cha/mode.rs b/yazi-fs/src/cha/mode.rs index 0961d1d1..fc498bfb 100644 --- a/yazi-fs/src/cha/mode.rs +++ b/yazi-fs/src/cha/mode.rs @@ -80,7 +80,7 @@ impl From for std::fs::Permissions { fn from(value: ChaMode) -> Self { use std::os::unix::fs::PermissionsExt; - std::fs::Permissions::from_mode(value.bits() as _) + Self::from_mode(value.bits() as _) } } diff --git a/yazi-fs/src/error/serde.rs b/yazi-fs/src/error/serde.rs index 06ee5779..a5b9a289 100644 --- a/yazi-fs/src/error/serde.rs +++ b/yazi-fs/src/error/serde.rs @@ -114,9 +114,9 @@ impl Serialize for Error { } match self { - Error::Kind(kind) => Shadow::Kind { kind: kind_to_str(*kind) }.serialize(serializer), - Error::Raw(code) => Shadow::Raw { code: *code }.serialize(serializer), - Error::Custom { kind, code, message } => { + Self::Kind(kind) => Shadow::Kind { kind: kind_to_str(*kind) }.serialize(serializer), + Self::Raw(code) => Shadow::Raw { code: *code }.serialize(serializer), + Self::Custom { kind, code, message } => { Shadow::Dyn { kind: kind_to_str(*kind), code: *code, message }.serialize(serializer) } } @@ -138,15 +138,15 @@ impl<'de> Deserialize<'de> for Error { let shadow = Shadow::deserialize(deserializer)?; Ok(match shadow { - Shadow::Kind { kind } => Error::Kind(kind_from_str(&kind)), - Shadow::Raw { code } => Error::Raw(code), + Shadow::Kind { kind } => Self::Kind(kind_from_str(&kind)), + Shadow::Raw { code } => Self::Raw(code), Shadow::Dyn { kind, code, message } => { if !message.is_empty() { - Error::Custom { kind: kind_from_str(&kind), code, message: message.into() } + Self::Custom { kind: kind_from_str(&kind), code, message: message.into() } } else if let Some(code) = code { - Error::Raw(code) + Self::Raw(code) } else { - Error::Kind(kind_from_str(&kind)) + Self::Kind(kind_from_str(&kind)) } } }) diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index 7e3414c3..17d46159 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -1,7 +1,6 @@ use std::{hash::{Hash, Hasher}, ops::Deref, path::Path}; -use anyhow::Result; -use yazi_shared::{loc::Loc, path::{AsPathDyn, PathBufDyn, PathDyn}, strand::Strand, url::{Url, UrlBuf, UrlLike}}; +use yazi_shared::{loc::Loc, path::{PathBufDyn, PathDyn, PathLike}, strand::Strand, url::{Url, UrlBuf, UrlLike}}; use crate::cha::{Cha, ChaType}; @@ -27,15 +26,15 @@ impl File { } #[inline] - pub fn chdir(&self, wd: &Path) -> Result { - Ok(Self { url: self.url.rebase(wd)?, cha: self.cha, link_to: self.link_to.clone() }) + pub fn chdir(&self, wd: &Path) -> Self { + Self { url: self.url.rebase(wd), cha: self.cha, link_to: self.link_to.clone() } } } impl File { // --- Url #[inline] - pub fn url_owned(&self) -> UrlBuf { self.url.to_owned() } + pub fn url_owned(&self) -> UrlBuf { self.url.clone() } #[inline] pub fn uri(&self) -> PathDyn<'_> { self.url.uri() } @@ -50,7 +49,7 @@ impl File { pub fn stem(&self) -> Option> { self.url.stem() } pub fn link_to_url(&self) -> Option> { - let to = self.link_to.as_ref()?.as_path_dyn(); + let to = self.link_to.as_ref()?; let kind = self.url.kind(); Some(match &self.url { UrlBuf::Regular(_) => Url::Regular(Loc::bare(to.as_os().ok()?)), diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index 937e9e9a..da21656e 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -1,7 +1,7 @@ use std::{mem, ops::{Deref, DerefMut, Not}}; use hashbrown::{HashMap, HashSet}; -use yazi_shared::{Id, path::{PathBufDyn, PathDyn, PathLike}}; +use yazi_shared::{Id, path::{PathBufDyn, PathDyn}}; use super::{FilesSorter, Filter}; use crate::{FILES_TICKET, File, SortBy}; @@ -97,7 +97,7 @@ impl Files { macro_rules! go { ($dist:expr, $src:expr, $inc:literal) => { - let mut todo: HashMap<_, _> = $src.into_iter().map(|f| (f.urn().owned(), f)).collect(); + let mut todo: HashMap<_, _> = $src.into_iter().map(|f| (f.urn().to_owned(), f)).collect(); for f in &$dist { if todo.remove(&f.urn()).is_some() && todo.is_empty() { break; @@ -121,19 +121,17 @@ impl Files { #[cfg(unix)] pub fn update_deleting(&mut self, urns: HashSet) -> Vec { - use yazi_shared::path::PathBufLike; + use yazi_shared::path::PathLike; if urns.is_empty() { return vec![]; } let (mut hidden, mut items) = if let Some(filter) = &self.filter { - urns - .into_iter() - .partition(|u| (!self.show_hidden && u.borrow().is_hidden()) || !filter.matches(u)) + urns.into_iter().partition(|u| (!self.show_hidden && u.is_hidden()) || !filter.matches(u)) } else if self.show_hidden { (HashSet::new(), urns) } else { - urns.into_iter().partition(|u| u.borrow().is_hidden()) + urns.into_iter().partition(|u| u.is_hidden()) }; let mut deleted = Vec::with_capacity(items.len()); @@ -229,7 +227,7 @@ impl Files { } self.update_deleting( - files.iter().filter(|&(u, f)| u != f.urn()).map(|(_, f)| f.urn().owned()).collect(), + files.iter().filter(|&(u, f)| u != f.urn()).map(|(_, f)| f.urn().into()).collect(), ); let (hidden, items) = self.update_updating(files); diff --git a/yazi-fs/src/filter.rs b/yazi-fs/src/filter.rs index 8bed557d..7baf323c 100644 --- a/yazi-fs/src/filter.rs +++ b/yazi-fs/src/filter.rs @@ -2,7 +2,7 @@ use std::{fmt::Display, ops::Range}; use anyhow::Result; use regex::bytes::{Regex, RegexBuilder}; -use yazi_shared::{event::Cmd, strand::{AsStrand, StrandLike}}; +use yazi_shared::{event::Cmd, strand::AsStrand}; pub struct Filter { raw: String, diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index f6d111bd..3e8fe3bf 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -1,9 +1,8 @@ use std::path::Path; -use anyhow::Result; use hashbrown::{HashMap, HashSet}; use yazi_macro::relay; -use yazi_shared::{Id, Ids, path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlLike}}; +use yazi_shared::{Id, Ids, path::PathBufDyn, url::{UrlBuf, UrlLike}}; use super::File; use crate::{cha::Cha, error::Error}; @@ -58,10 +57,10 @@ impl FilesOp { let Some(o_p) = o.parent() else { continue }; let Some(n_p) = n.url.parent() else { continue }; if o_p == n_p { - parents.entry_ref(&o_p).or_default().1.insert(o.urn().owned(), n); + parents.entry_ref(&o_p).or_default().1.insert(o.urn().into(), n); } else { - parents.entry_ref(&o_p).or_default().0.insert(o.urn().owned()); - parents.entry_ref(&n_p).or_default().1.insert(n.urn().owned(), n); + parents.entry_ref(&o_p).or_default().0.insert(o.urn().into()); + parents.entry_ref(&n_p).or_default().1.insert(n.urn().into(), n); } } for (p, (o, n)) in parents { @@ -99,21 +98,16 @@ impl FilesOp { } } - pub fn chdir(&self, wd: &Path) -> Result { + pub fn chdir(&self, wd: &Path) -> Self { macro_rules! files { - ($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect::>()? }}; + ($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect() }}; } macro_rules! map { - ($map:expr) => {{ - $map - .iter() - .map(|(urn, file)| file.chdir(wd).map(|file| (urn.clone(), file))) - .collect::>()? - }}; + ($map:expr) => {{ $map.iter().map(|(urn, file)| (urn.clone(), file.chdir(wd))).collect() }}; } let w = UrlBuf::from(wd); - Ok(match self { + match self { Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha), Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket), Self::Done(_, cha, ticket) => Self::Done(w, *cha, *ticket), @@ -124,7 +118,7 @@ impl FilesOp { Self::Deleting(_, urns) => Self::Deleting(w, urns.clone()), Self::Updating(_, map) => Self::Updating(w, map!(map)), Self::Upserting(_, map) => Self::Upserting(w, map!(map)), - }) + } } pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec, Vec) { diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs index a2ef3f74..7e5b2e9a 100644 --- a/yazi-fs/src/path/clean.rs +++ b/yazi-fs/src/path/clean.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use yazi_shared::{loc::LocBuf, path::{PathDyn, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{loc::LocBuf, path::PathDyn, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; pub fn clean_url<'a>(url: impl Into>) -> UrlBuf { let cow: UrlCow = url.into(); diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index 5baba83f..562f06db 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, path::PathBuf}; -use yazi_shared::{FromWtf8Vec, loc::LocBuf, path::{AsPath, PathBufDyn, PathCow, PathDyn, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{FromWtf8Vec, loc::LocBuf, path::{PathBufDyn, PathCow, PathDyn, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use crate::{CWD, path::clean_url}; @@ -16,18 +16,13 @@ fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> { let n_rest = expand_variables(o_rest); let n_urn = expand_variables(o_urn); - let rest_diff = - n_rest.as_path().components().count() as isize - o_rest.components().count() as isize; - let urn_diff = - n_urn.as_path().components().count() as isize - o_urn.components().count() as isize; + let rest_diff = n_rest.components().count() as isize - o_rest.components().count() as isize; + let urn_diff = n_urn.components().count() as isize - o_urn.components().count() as isize; let uri_count = url.uri().components().count() as isize; let urn_count = url.urn().components().count() as isize; - let mut path = PathBufDyn::with_capacity( - url.kind(), - n_base.as_path().len() + n_rest.as_path().len() + n_urn.as_path().len(), - ); + let mut path = PathBufDyn::with_capacity(url.kind(), n_base.len() + n_rest.len() + n_urn.len()); path.try_extend([n_base, n_rest, n_urn]).expect("extend original parts should not fail"); let loc = LocBuf::::with( @@ -77,7 +72,7 @@ pub fn absolute_url<'a>(url: impl Into>) -> UrlCow<'a> { absolute_url fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> { if url.kind().is_virtual() { - return url.into(); + return url; } let path = url.loc().as_os().expect("must be a local path"); diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index d79e79e6..abfaaf71 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -1,6 +1,6 @@ use std::{io, path::{Path, PathBuf}, sync::Arc}; -use yazi_shared::{path::{AsPathDyn, PathBufDyn}, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow}}; use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Provider}}; @@ -31,9 +31,9 @@ impl<'a> Provider for Local<'a> { #[inline] async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsPathDyn, + P: AsPath, { - let to = to.as_path_dyn().to_os_owned()?; + let to = to.as_path().to_os_owned()?; let from = self.path.to_owned(); Self::copy_impl(from, to, attrs).await } @@ -47,9 +47,9 @@ impl<'a> Provider for Local<'a> { #[inline] async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { - let to = to.as_path_dyn().as_os()?; + let to = to.as_path().as_os()?; tokio::fs::hard_link(self.path, to).await } @@ -62,7 +62,7 @@ impl<'a> Provider for Local<'a> { #[inline] async fn new<'b>(url: Url<'b>) -> io::Result> { match url { - Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_path() }), + Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_inner() }), Url::Archive { .. } | Url::Sftp { .. } => { Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}"))) } @@ -101,9 +101,9 @@ impl<'a> Provider for Local<'a> { #[inline] async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { - let to = to.as_path_dyn().as_os()?; + let to = to.as_path().as_os()?; tokio::fs::rename(self.path, to).await } @@ -111,12 +111,12 @@ impl<'a> Provider for Local<'a> { #[inline] async fn symlink(&self, original: P, _is_dir: F) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, F: AsyncFnOnce() -> io::Result, { #[cfg(unix)] { - let original = original.as_path_dyn().as_os()?; + let original = original.as_path().as_os()?; tokio::fs::symlink(original, self.path).await } #[cfg(windows)] @@ -130,9 +130,9 @@ impl<'a> Provider for Local<'a> { #[inline] async fn symlink_dir

(&self, original: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { - let original = original.as_path_dyn().as_os()?; + let original = original.as_path().as_os()?; #[cfg(unix)] { @@ -147,9 +147,9 @@ impl<'a> Provider for Local<'a> { #[inline] async fn symlink_file

(&self, original: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { - let original = original.as_path_dyn().as_os()?; + let original = original.as_path().as_os()?; #[cfg(unix)] { diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs index 52ed3ea6..3a92b04d 100644 --- a/yazi-fs/src/provider/traits.rs +++ b/yazi-fs/src/provider/traits.rs @@ -2,7 +2,7 @@ use std::io; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use yazi_macro::ok_or_not_found; -use yazi_shared::{path::{AsPathDyn, PathBufDyn, PathLike}, strand::StrandCow, url::{AsUrl, Url, UrlBuf}}; +use yazi_shared::{path::{AsPath, PathBufDyn}, strand::StrandCow, url::{AsUrl, Url, UrlBuf}}; use crate::{cha::{Cha, ChaType}, provider::Attrs}; @@ -21,7 +21,7 @@ pub trait Provider: Sized { fn copy

(&self, to: P, attrs: Attrs) -> impl Future> where - P: AsPathDyn; + P: AsPath; fn create(&self) -> impl Future> { async move { self.gate().write(true).create(true).truncate(true).open(self.url()).await } @@ -69,7 +69,7 @@ pub trait Provider: Sized { fn hard_link

(&self, to: P) -> impl Future> where - P: AsPathDyn; + P: AsPath; fn metadata(&self) -> impl Future>; @@ -144,23 +144,23 @@ pub trait Provider: Sized { fn rename

(&self, to: P) -> impl Future> where - P: AsPathDyn; + P: AsPath; fn symlink(&self, original: P, _is_dir: F) -> impl Future> where - P: AsPathDyn, + P: AsPath, F: AsyncFnOnce() -> io::Result; fn symlink_dir

(&self, original: P) -> impl Future> where - P: AsPathDyn, + P: AsPath, { self.symlink(original, async || Ok(true)) } fn symlink_file

(&self, original: P) -> impl Future> where - P: AsPathDyn, + P: AsPath, { self.symlink(original, async || Ok(false)) } diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index 6c747cbd..debac466 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use hashbrown::HashMap; -use yazi_shared::{LcgRng, natsort, path::{PathBufDyn, PathLike}, strand::StrandLike, translit::Transliterator, url::UrlLike}; +use yazi_shared::{LcgRng, natsort, path::PathBufDyn, translit::Transliterator, url::UrlLike}; use crate::{File, SortBy}; diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index 12171d36..958fbfd2 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; -use yazi_shared::{path::{PathDyn, PathLike}, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::PathDyn, url::{AsUrl, Url, UrlBuf, UrlCow}}; use crate::{FsHash128, FsScheme, path::PercentEncoding}; @@ -48,7 +48,7 @@ impl<'a> FsUrl<'a> for Url<'a> { fn unified_path(self) -> Cow<'a, Path> { match self { - Self::Regular(loc) | Self::Search { loc, .. } => loc.as_path().into(), + Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(), Self::Archive { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } @@ -63,7 +63,7 @@ impl FsUrl<'_> for UrlBuf { fn unified_path(self) -> Cow<'static, Path> { match self { - Self::Regular(loc) | Self::Search { loc, .. } => loc.into_path().into(), + Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(), Self::Archive { .. } | Self::Sftp { .. } => { self.cache().expect("non-local URL should have a cache path").into() } @@ -78,8 +78,8 @@ impl<'a> FsUrl<'a> for UrlCow<'a> { fn unified_path(self) -> Cow<'a, Path> { match self { - Self::Regular(loc) | Self::Search { loc, .. } => loc.into_path().into(), - Self::RegularRef(loc) | Self::SearchRef { loc, .. } => loc.as_path().into(), + Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(), + Self::RegularRef(loc) | Self::SearchRef { loc, .. } => loc.as_inner().into(), Self::Archive { .. } | Self::ArchiveRef { .. } | Self::Sftp { .. } | Self::SftpRef { .. } => { self.cache().expect("non-local URL should have a cache path").into() } diff --git a/yazi-macro/Cargo.toml b/yazi-macro/Cargo.toml index e267afe7..2b803c84 100644 --- a/yazi-macro/Cargo.toml +++ b/yazi-macro/Cargo.toml @@ -7,3 +7,6 @@ authors = [ "sxyazi " ] description = "Yazi macros" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" + +[lints] +workspace = true diff --git a/yazi-packing/Cargo.toml b/yazi-packing/Cargo.toml index 47f5a87e..38fea9c8 100644 --- a/yazi-packing/Cargo.toml +++ b/yazi-packing/Cargo.toml @@ -8,18 +8,21 @@ description = "Yazi packing" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" - [package.metadata.deb] - name = "yazi" - license-file = [ "../LICENSE", "0" ] - depends = "file, ffmpeg, 7zip, jq, poppler-utils, fd-find|fd, ripgrep, fzf, zoxide, imagemagick, xsel|xclip|wl-clipboard" - recommends = "bash-completion" - extended-description-file = "README.md" - section = "utility" - priority = "optional" - assets = [ - [ "target/release/ya", "usr/bin/", "755" ], - [ "target/release/yazi", "usr/bin/", "755" ], - [ "../README.md", "usr/share/doc/yazi/README", "644" ], - [ "../yazi-cli/completions/ya.bash", "usr/share/bash-completion/completions/ya", "644" ], - [ "../yazi-boot/completions/yazi.bash", "usr/share/bash-completion/completions/yazi", "644" ], - ] +[lints] +workspace = true + +[package.metadata.deb] +name = "yazi" +license-file = [ "../LICENSE", "0" ] +depends = "file, ffmpeg, 7zip, jq, poppler-utils, fd-find|fd, ripgrep, fzf, zoxide, imagemagick, xsel|xclip|wl-clipboard" +recommends = "bash-completion" +extended-description-file = "README.md" +section = "utility" +priority = "optional" +assets = [ + [ "target/release/ya", "usr/bin/", "755" ], + [ "target/release/yazi", "usr/bin/", "755" ], + [ "../README.md", "usr/share/doc/yazi/README", "644" ], + [ "../yazi-cli/completions/ya.bash", "usr/share/bash-completion/completions/ya", "644" ], + [ "../yazi-boot/completions/yazi.bash", "usr/share/bash-completion/completions/yazi", "644" ], +] diff --git a/yazi-parser/Cargo.toml b/yazi-parser/Cargo.toml index 37d7d295..c8e75256 100644 --- a/yazi-parser/Cargo.toml +++ b/yazi-parser/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi command parser" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [features] default = [ "vendored-lua" ] vendored-lua = [ "mlua/vendored" ] diff --git a/yazi-parser/src/cmp/show.rs b/yazi-parser/src/cmp/show.rs index 05478e81..8984c6c9 100644 --- a/yazi-parser/src/cmp/show.rs +++ b/yazi-parser/src/cmp/show.rs @@ -2,7 +2,7 @@ use std::path::MAIN_SEPARATOR_STR; use anyhow::bail; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_shared::{Id, event::CmdCow, path::PathBufDyn, strand::{StrandBuf, StrandBufLike}, url::UrlBuf}; +use yazi_shared::{Id, event::CmdCow, path::PathBufDyn, strand::{StrandBuf, StrandLike}, url::UrlBuf}; #[derive(Debug)] pub struct ShowOpt { diff --git a/yazi-parser/src/mgr/copy.rs b/yazi-parser/src/mgr/copy.rs index 1496f154..55d5d30f 100644 --- a/yazi-parser/src/mgr/copy.rs +++ b/yazi-parser/src/mgr/copy.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, str::FromStr}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use serde::Deserialize; -use yazi_shared::{SStr, event::CmdCow, strand::{AsStrandDyn, StrandLike}}; +use yazi_shared::{SStr, event::CmdCow, strand::AsStrand}; #[derive(Debug)] pub struct CopyOpt { @@ -49,16 +49,16 @@ impl FromStr for CopySeparator { impl CopySeparator { pub fn transform(self, s: &T) -> Cow<'_, [u8]> where - T: ?Sized + AsStrandDyn, + T: ?Sized + AsStrand, { #[cfg(windows)] if self == Self::Unix { - use yazi_shared::strand::{StrandBufLike, StrandCow}; - return match s.as_strand_dyn().backslash_to_slash() { + use yazi_shared::strand::StrandCow; + return match s.as_strand().backslash_to_slash() { StrandCow::Borrowed(s) => s.encoded_bytes().into(), StrandCow::Owned(s) => s.into_encoded_bytes().into(), }; } - Cow::Borrowed(s.as_strand_dyn().encoded_bytes()) + Cow::Borrowed(s.as_strand().encoded_bytes()) } } diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 2af7df35..b573428c 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi plugin system" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [features] default = [ "vendored-lua" ] vendored-lua = [ "mlua/vendored" ] diff --git a/yazi-proxy/Cargo.toml b/yazi-proxy/Cargo.toml index d794362f..a2ec00ac 100644 --- a/yazi-proxy/Cargo.toml +++ b/yazi-proxy/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi event proxy" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-config = { path = "../yazi-config", version = "25.9.15" } yazi-macro = { path = "../yazi-macro", version = "25.9.15" } diff --git a/yazi-scheduler/Cargo.toml b/yazi-scheduler/Cargo.toml index 31228597..fda89e5e 100644 --- a/yazi-scheduler/Cargo.toml +++ b/yazi-scheduler/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi task scheduler" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-config = { path = "../yazi-config", version = "25.9.15" } yazi-dds = { path = "../yazi-dds", version = "25.9.15" } diff --git a/yazi-scheduler/src/prework/prework.rs b/yazi-scheduler/src/prework/prework.rs index c9ec0aa0..e87eabe5 100644 --- a/yazi-scheduler/src/prework/prework.rs +++ b/yazi-scheduler/src/prework/prework.rs @@ -10,7 +10,7 @@ use tracing::error; use yazi_config::Priority; use yazi_fs::{FilesOp, FsHash64}; use yazi_plugin::isolate; -use yazi_shared::{event::CmdCow, path::PathLike, url::{UrlBuf, UrlLike}}; +use yazi_shared::{event::CmdCow, url::{UrlBuf, UrlLike}}; use yazi_vfs::provider; use super::{PreworkInFetch, PreworkInLoad, PreworkInSize}; @@ -106,7 +106,7 @@ impl Prework { let parent = buf[0].0.parent().unwrap(); FilesOp::Size( parent.into(), - HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn().owned(), s))), + HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn().into(), s))), ) .emit(); }); diff --git a/yazi-sftp/Cargo.toml b/yazi-sftp/Cargo.toml index dcd6bb41..20939e46 100644 --- a/yazi-sftp/Cargo.toml +++ b/yazi-sftp/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi SFTP client" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] bitflags = { workspace = true } parking_lot = { workspace = true } diff --git a/yazi-sftp/src/byte_str.rs b/yazi-sftp/src/byte_str.rs index db545ac5..97f2d156 100644 --- a/yazi-sftp/src/byte_str.rs +++ b/yazi-sftp/src/byte_str.rs @@ -17,7 +17,7 @@ impl<'a> From<&'a str> for ByteStr<'a> { fn from(value: &'a str) -> Self { ByteStr(Cow::Borrowed(value.as_bytes())) } } -impl<'a> From<&'a ByteStr<'a>> for ByteStr<'a> { +impl<'a> From<&'a Self> for ByteStr<'a> { fn from(value: &'a ByteStr) -> Self { ByteStr(Cow::Borrowed(&value.0)) } } @@ -67,7 +67,7 @@ impl<'a> ByteStr<'a> { } } - pub fn join(&self, other: impl Into>) -> PathBuf { + pub fn join(&self, other: impl Into) -> PathBuf { let other = other.into(); match self.to_path() { Cow::Borrowed(p) => p.join(other.to_path()), diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index c19fe6bf..5544bcb2 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -9,6 +9,9 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" rust-version = "1.91.0" +[lints] +workspace = true + [dependencies] yazi-macro = { path = "../yazi-macro", version = "25.9.15" } diff --git a/yazi-shared/src/data/data.rs b/yazi-shared/src/data/data.rs index bc82c1e9..261322ed 100644 --- a/yazi-shared/src/data/data.rs +++ b/yazi-shared/src/data/data.rs @@ -15,8 +15,8 @@ pub enum Data { Integer(i64), Number(f64), String(SStr), - List(Vec), - Dict(HashMap), + List(Vec), + Dict(HashMap), Id(Id), #[serde(skip_deserializing)] Url(UrlBuf), diff --git a/yazi-shared/src/loc/able.rs b/yazi-shared/src/loc/able.rs new file mode 100644 index 00000000..4b5b2cd2 --- /dev/null +++ b/yazi-shared/src/loc/able.rs @@ -0,0 +1,142 @@ +use std::{ffi::{OsStr, OsString}, fmt::Debug, hash::Hash}; + +use crate::{path::{AsPath, AsPathView}, strand::AsStrandView}; + +// --- LocAble +pub trait LocAble<'p> +where + Self: Copy + AsStrandView<'p, Self::Strand<'p>>, +{ + type Strand<'a>: StrandAble<'a> + StrandAbleImpl<'a>; + type Owned: LocBufAble + LocBufAbleImpl + Into; + type Components<'a>: Clone + DoubleEndedIterator + AsStrandView<'a, Self::Strand<'a>>; +} + +impl<'p> LocAble<'p> for &'p std::path::Path { + type Components<'a> = std::path::Components<'a>; + type Owned = std::path::PathBuf; + type Strand<'a> = &'a OsStr; +} + +// --- LocBufAble +pub trait LocBufAble +where + Self: 'static + AsPath + Default, +{ + type Strand<'a>: StrandAble<'a>; + type Borrowed<'a>: LocAble<'a> + + LocAbleImpl<'a> + + AsPathView<'a, Self::Borrowed<'a>> + + Debug + + Hash; +} + +impl LocBufAble for std::path::PathBuf { + type Borrowed<'a> = &'a std::path::Path; + type Strand<'a> = &'a OsStr; +} + +// --- StrandAble +pub trait StrandAble<'a>: Copy {} + +impl<'a> StrandAble<'a> for &'a OsStr {} + +// --- LocAbleImpl +pub(super) trait LocAbleImpl<'p>: LocAble<'p> { + fn as_encoded_bytes(self) -> &'p [u8]; + + fn components(self) -> Self::Components<'p>; + + fn empty() -> Self; + + fn file_name(self) -> Option>; + + unsafe fn from_encoded_bytes_unchecked(bytes: &'p [u8]) -> Self; + + fn join<'a, T>(self, path: T) -> Self::Owned + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn len(self) -> usize { self.as_encoded_bytes().len() } + + fn strip_prefix<'a, T>(self, base: T) -> Option + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn to_path_buf(self) -> Self::Owned; +} + +impl<'p> LocAbleImpl<'p> for &'p std::path::Path { + fn as_encoded_bytes(self) -> &'p [u8] { self.as_os_str().as_encoded_bytes() } + + fn components(self) -> Self::Components<'p> { self.components() } + + fn empty() -> Self { std::path::Path::new("") } + + fn file_name(self) -> Option> { self.file_name() } + + unsafe fn from_encoded_bytes_unchecked(bytes: &'p [u8]) -> Self { + std::path::Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }) + } + + fn join<'a, T>(self, path: T) -> Self::Owned + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + self.join(path.as_strand_view()) + } + + fn strip_prefix<'a, T>(self, base: T) -> Option + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + self.strip_prefix(base.as_strand_view()).ok() + } + + fn to_path_buf(self) -> Self::Owned { self.to_path_buf() } +} + +// --- LocBufAbleImpl +pub(super) trait LocBufAbleImpl: LocBufAble { + fn as_encoded_bytes(&self) -> &[u8] { self.borrow().as_encoded_bytes() } + + fn borrow(&self) -> Self::Borrowed<'_>; + + unsafe fn from_encoded_bytes_unchecked(bytes: Vec) -> Self; + + fn into_encoded_bytes(self) -> Vec; + + fn len(&self) -> usize { self.borrow().len() } + + fn set_file_name<'a, T>(&mut self, name: T) + where + T: AsStrandView<'a, Self::Strand<'a>>; +} + +impl LocBufAbleImpl for std::path::PathBuf { + fn borrow(&self) -> Self::Borrowed<'_> { self.as_path() } + + unsafe fn from_encoded_bytes_unchecked(bytes: Vec) -> Self { + Self::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }) + } + + fn into_encoded_bytes(self) -> Vec { self.into_os_string().into_encoded_bytes() } + + fn set_file_name<'a, T>(&mut self, name: T) + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + self.set_file_name(name.as_strand_view()) + } +} + +// --- StrandAbleImpl +pub(super) trait StrandAbleImpl<'a>: StrandAble<'a> { + fn as_encoded_bytes(self) -> &'a [u8]; + + fn len(self) -> usize { self.as_encoded_bytes().len() } +} + +impl<'a> StrandAbleImpl<'a> for &'a OsStr { + fn as_encoded_bytes(self) -> &'a [u8] { self.as_encoded_bytes() } +} diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index 24f5f325..171f9247 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -1,11 +1,11 @@ -use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, marker::PhantomData, ops::Deref, path::PathBuf}; +use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, marker::PhantomData, mem, ops::Deref, path::PathBuf}; use anyhow::Result; -use crate::{loc::Loc, path::{AsPathDyn, AsPathView, PathBufLike, PathBufUnsafeExt, PathDyn, PathLike, PathUnsafeExt, SetNameError}, scheme::SchemeKind, strand::AsStrandView}; +use crate::{loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{AsPath, AsPathView, PathDyn, SetNameError}, scheme::SchemeKind, strand::AsStrandView}; -#[derive(Clone, Default, Eq)] -pub struct LocBuf { +#[derive(Clone, Default, Eq, PartialEq)] +pub struct LocBuf

{ pub(super) inner: P, pub(super) uri: usize, pub(super) urn: usize, @@ -13,7 +13,7 @@ pub struct LocBuf { impl

Deref for LocBuf

where - P: PathBufLike, + P: LocBufAble, { type Target = P; @@ -25,37 +25,30 @@ impl AsRef for LocBuf { fn as_ref(&self) -> &std::path::Path { self.inner.as_ref() } } -impl AsPathDyn for LocBuf +impl AsPath for LocBuf where - T: PathBufLike + AsPathDyn, + T: LocBufAble + AsPath, { - fn as_path_dyn(&self) -> PathDyn<'_> { self.inner.as_path_dyn() } + fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } } -impl AsPathDyn for &LocBuf +impl AsPath for &LocBuf where - T: PathBufLike + AsPathDyn, + T: LocBufAble + AsPath, { - fn as_path_dyn(&self) -> PathDyn<'_> { self.inner.as_path_dyn() } -} - -impl

PartialEq for LocBuf

-where - P: PathBufLike + PartialEq, -{ - fn eq(&self, other: &Self) -> bool { self.inner == other.inner } + fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } } impl

Ord for LocBuf

where - P: PathBufLike + Ord, + P: LocBufAble + Ord, { fn cmp(&self, other: &Self) -> cmp::Ordering { self.inner.cmp(&other.inner) } } impl

PartialOrd for LocBuf

where - P: PathBufLike + PartialOrd, + P: LocBufAble + PartialOrd, { fn partial_cmp(&self, other: &Self) -> Option { self.inner.partial_cmp(&other.inner) @@ -65,8 +58,7 @@ where // --- Hash impl

Hash for LocBuf

where - P: PathBufLike + PathBufUnsafeExt, - for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, + P: LocBufAble + LocBufAbleImpl, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { fn hash(&self, state: &mut H) { self.as_loc().hash(state) } @@ -74,8 +66,7 @@ where impl

Debug for LocBuf

where - P: PathBufLike + PathBufUnsafeExt + Debug, - for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, + P: LocBufAble + LocBufAbleImpl + Debug, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { fn fmt(&self, f: &mut Formatter) -> fmt::Result { @@ -89,8 +80,7 @@ where impl

From

for LocBuf

where - P: PathBufLike + PathBufUnsafeExt, - for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, + P: LocBufAble + LocBufAbleImpl, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { fn from(path: P) -> Self { @@ -99,7 +89,7 @@ where let mut bytes = path.into_encoded_bytes(); bytes.truncate(len); - Self { inner: unsafe { P::from_encoded_bytes(bytes) }, uri, urn } + Self { inner: unsafe { P::from_encoded_bytes_unchecked(bytes) }, uri, urn } } } @@ -109,29 +99,28 @@ impl> From<&T> for LocBuf { impl

LocBuf

where - P: PathBufLike + PathBufUnsafeExt, - for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, + P: LocBufAble + LocBufAbleImpl, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { pub fn new<'a, S>(path: P, base: S, trail: S) -> Self where - S: for<'b> AsStrandView<'a, as PathLike<'b>>::Strand<'a>>, + S: for<'b> AsStrandView<'a, as LocAble<'b>>::Strand<'a>>, { let loc = Self::from(path); let Loc { inner, uri, urn, _phantom } = Loc::new(&loc.inner, base, trail); - debug_assert!(inner.encoded_bytes() == loc.inner.encoded_bytes()); + debug_assert!(inner.as_encoded_bytes() == loc.inner.as_encoded_bytes()); Self { inner: loc.inner, uri, urn } } pub fn with(path: P, uri: usize, urn: usize) -> Result where - for<'a> P::Borrowed<'a>: PathLike<'a>, + for<'a> P::Borrowed<'a>: LocAble<'a>, { let loc = Self::from(path); let Loc { inner, uri, urn, _phantom } = Loc::with(&loc.inner, uri, urn)?; - debug_assert!(inner.encoded_bytes() == loc.inner.encoded_bytes()); + debug_assert!(inner.as_encoded_bytes() == loc.inner.as_encoded_bytes()); Ok(Self { inner: loc.inner, uri, urn }) } @@ -142,18 +131,18 @@ where let loc = Self::from(path.into()); let Loc { inner, uri, urn, _phantom } = Loc::zeroed(&loc.inner); - debug_assert!(inner.encoded_bytes() == loc.inner.encoded_bytes()); + debug_assert!(inner.as_encoded_bytes() == loc.inner.as_encoded_bytes()); Self { inner: loc.inner, uri, urn } } pub fn floated<'a, S>(path: P, base: S) -> Self where - S: for<'b> AsStrandView<'a, as PathLike<'b>>::Strand<'a>>, + S: for<'b> AsStrandView<'a, as LocAble<'b>>::Strand<'a>>, { let loc = Self::from(path); let Loc { inner, uri, urn, _phantom } = Loc::floated(&loc.inner, base); - debug_assert!(inner.encoded_bytes() == loc.inner.encoded_bytes()); + debug_assert!(inner.as_encoded_bytes() == loc.inner.as_encoded_bytes()); Self { inner: loc.inner, uri, urn } } @@ -161,7 +150,7 @@ where let loc = Self::from(path); let Loc { inner, uri, urn, _phantom } = Loc::saturated(&loc.inner, kind); - debug_assert!(inner.encoded_bytes() == loc.inner.encoded_bytes()); + debug_assert!(inner.as_encoded_bytes() == loc.inner.as_encoded_bytes()); Self { inner: loc.inner, uri, urn } } @@ -176,7 +165,7 @@ where } #[inline] - pub fn to_path(&self) -> P + pub fn to_inner(&self) -> P where P: Clone, { @@ -184,14 +173,14 @@ where } #[inline] - pub fn into_path(self) -> P { self.inner } + pub fn into_inner(self) -> P { self.inner } pub fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> where T: AsStrandView<'a, P::Strand<'a>>, { let old = self.inner.len(); - self.mutate(|path| path.try_set_name(name))?; + self.mutate(|path| path.set_file_name(name)); let new = self.len(); if new == old { @@ -216,19 +205,19 @@ where } #[inline] - pub fn rebase<'a, 'b>(&'a self, base: P::Borrowed<'b>) -> Result + pub fn rebase<'a, 'b>(&'a self, base: P::Borrowed<'b>) -> Self where 'a: 'b, - for<'c> as PathLike<'c>>::Owned: Into, + for<'c> as LocAble<'c>>::Owned: Into, { - let mut loc: Self = base.try_join(self.uri())?.into(); + let mut loc: Self = base.join(self.uri()).into(); (loc.uri, loc.urn) = (self.uri, self.urn); - Ok(loc) + loc } #[inline] fn mutate T>(&mut self, f: F) -> T { - let mut inner = self.inner.take(); + let mut inner = mem::take(&mut self.inner); let result = f(&mut inner); self.inner = Self::from(inner).inner; result @@ -238,8 +227,7 @@ where // FIXME: macro impl

LocBuf

where - P: PathBufLike + PathBufUnsafeExt, - for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, + P: LocBufAble + LocBufAbleImpl, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { #[inline] @@ -259,19 +247,6 @@ where #[inline] pub fn has_trail(&self) -> bool { self.as_loc().has_trail() } - - #[inline] - pub fn name(&self) -> Option< as PathLike<'_>>::Strand<'_>> { - self.as_loc().name() - } - - #[inline] - pub fn stem(&self) -> Option< as PathLike<'_>>::Strand<'_>> { - self.as_loc().stem() - } - - #[inline] - pub fn ext(&self) -> Option< as PathLike<'_>>::Strand<'_>> { self.as_loc().ext() } } impl LocBuf { @@ -290,22 +265,22 @@ mod tests { let loc: LocBuf = Path::new("/").into(); assert_eq!(loc.uri().as_os_str(), OsStr::new("/")); assert_eq!(loc.urn().as_os_str(), OsStr::new("")); - assert_eq!(loc.name(), None); + assert_eq!(loc.file_name(), None); assert_eq!(loc.base().as_os_str(), OsStr::new("")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: LocBuf = Path::new("/root").into(); - assert_eq!(loc.uri().as_os_str(), OsStr::new("root")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("/root")); assert_eq!(loc.urn().as_os_str(), OsStr::new("root")); - assert_eq!(loc.name().unwrap(), OsStr::new("root")); - assert_eq!(loc.base().as_os_str(), OsStr::new("/")); + assert_eq!(loc.file_name().unwrap(), OsStr::new("root")); + assert_eq!(loc.base().as_os_str(), OsStr::new("")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: LocBuf = Path::new("/root/code/foo/").into(); - assert_eq!(loc.uri().as_os_str(), OsStr::new("foo")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("/root/code/foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo")); - assert_eq!(loc.name().unwrap(), OsStr::new("foo")); - assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/")); + assert_eq!(loc.file_name().unwrap(), OsStr::new("foo")); + assert_eq!(loc.base().as_os_str(), OsStr::new("")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); } @@ -314,42 +289,42 @@ mod tests { let loc = LocBuf::::with("/".into(), 0, 0)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("")); assert_eq!(loc.urn().as_os_str(), OsStr::new("")); - assert_eq!(loc.name(), None); + assert_eq!(loc.file_name(), None); assert_eq!(loc.base().as_os_str(), OsStr::new("/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc = LocBuf::::with("/root/code/".into(), 1, 1)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("code")); assert_eq!(loc.urn().as_os_str(), OsStr::new("code")); - assert_eq!(loc.name().unwrap(), OsStr::new("code")); + assert_eq!(loc.file_name().unwrap(), OsStr::new("code")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/")); let loc = LocBuf::::with("/root/code/foo//".into(), 2, 1)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo")); - assert_eq!(loc.name().unwrap(), OsStr::new("foo")); + assert_eq!(loc.file_name().unwrap(), OsStr::new("foo")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); let loc = LocBuf::::with("/root/code/foo//".into(), 2, 2)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); - assert_eq!(loc.name().unwrap(), OsStr::new("foo")); + assert_eq!(loc.file_name().unwrap(), OsStr::new("foo")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/")); let loc = LocBuf::::with("/root/code/foo//bar/".into(), 2, 2)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("foo//bar")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo//bar")); - assert_eq!(loc.name().unwrap(), OsStr::new("bar")); + assert_eq!(loc.file_name().unwrap(), OsStr::new("bar")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); let loc = LocBuf::::with("/root/code/foo//bar/".into(), 3, 2)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo//bar")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo//bar")); - assert_eq!(loc.name().unwrap(), OsStr::new("bar")); + assert_eq!(loc.file_name().unwrap(), OsStr::new("bar")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); Ok(()) diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index a24d5801..182284d7 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -2,7 +2,8 @@ use std::{hash::{Hash, Hasher}, marker::PhantomData, ops::Deref, path::Path}; use anyhow::{Result, bail}; -use crate::{loc::LocBuf, path::{AsPathDyn, AsPathView, PathBufLike, PathDyn, PathLike, PathUnsafeExt}, scheme::SchemeKind, strand::{AsStrandView, StrandLike}}; +use super::LocAbleImpl; +use crate::{loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{AsPath, AsPathView, PathDyn}, scheme::SchemeKind, strand::AsStrandView}; #[derive(Clone, Copy, Debug)] pub struct Loc<'p, P = &'p Path> { @@ -14,25 +15,25 @@ pub struct Loc<'p, P = &'p Path> { impl<'p, P> Default for Loc<'p, P> where - P: PathLike<'p>, + P: LocAble<'p> + LocAbleImpl<'p>, { - fn default() -> Self { Self { inner: P::default(), uri: 0, urn: 0, _phantom: PhantomData } } + fn default() -> Self { Self { inner: P::empty(), uri: 0, urn: 0, _phantom: PhantomData } } } impl<'p, P> Deref for Loc<'p, P> where - P: PathLike<'p>, + P: LocAble<'p>, { type Target = P; fn deref(&self) -> &Self::Target { &self.inner } } -impl<'p, P> AsPathDyn for Loc<'p, P> +impl<'p, P> AsPath for Loc<'p, P> where - P: PathLike<'p> + AsPathDyn, + P: LocAble<'p> + AsPath, { - fn as_path_dyn(&self) -> PathDyn<'_> { self.inner.as_path_dyn() } + fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } } // FIXME: remove @@ -43,34 +44,34 @@ impl AsRef for Loc<'_, &std::path::Path> { // --- Hash impl<'p, P> Hash for Loc<'p, P> where - P: PathLike<'p> + Hash, + P: LocAble<'p> + Hash, { fn hash(&self, state: &mut H) { self.inner.hash(state) } } -impl<'p, P> From> for LocBuf<

>::Owned> +impl<'p, P> From> for LocBuf<

>::Owned> where - P: PathLike<'p>, -

>::Owned: PathBufLike, + P: LocAble<'p> + LocAbleImpl<'p>, +

>::Owned: LocBufAble, { fn from(value: Loc<'p, P>) -> Self { - Self { inner: value.inner.owned(), uri: value.uri, urn: value.urn } + Self { inner: value.inner.to_path_buf(), uri: value.uri, urn: value.urn } } } // --- Eq impl<'p, P> PartialEq for Loc<'p, P> where - P: PathLike<'p> + PartialEq, + P: LocAble<'p> + PartialEq, { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } -impl<'p, P> Eq for Loc<'p, P> where P: PathLike<'p> + Eq {} +impl<'p, P> Eq for Loc<'p, P> where P: LocAble<'p> + Eq {} impl<'p, P> Loc<'p, P> where - P: PathLike<'p> + PathUnsafeExt<'p>, + P: LocAble<'p> + LocAbleImpl<'p>, { pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self where @@ -78,8 +79,8 @@ where S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); - loc.uri = loc.inner.try_strip_prefix(base).expect("Loc must start with the given base").len(); - loc.urn = loc.inner.try_strip_prefix(trail).expect("Loc must start with the given trail").len(); + loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len(); + loc.urn = loc.inner.strip_prefix(trail).expect("Loc must start with the given trail").len(); loc } @@ -105,10 +106,10 @@ where bail!("URI exceeds the entire URL"); } if i == urn { - loc.urn = loc.try_strip_prefix(it.clone())?.len(); + loc.urn = loc.strip_prefix(it.clone()).unwrap().len(); } if i == uri { - loc.uri = loc.try_strip_prefix(it)?.len(); + loc.uri = loc.strip_prefix(it).unwrap().len(); break; } } @@ -120,19 +121,20 @@ where T: AsPathView<'p, P>, { let path = path.as_path_view(); - let Some(name) = path.name() else { - let uri = path.len(); + let Some(name) = path.file_name() else { + let uri = path.strip_prefix(P::empty()).unwrap().len(); return Self { inner: path, uri, urn: 0, _phantom: PhantomData }; }; let name_len = name.len(); - let prefix_len = - unsafe { name.encoded_bytes().as_ptr().offset_from_unsigned(path.encoded_bytes().as_ptr()) }; + let prefix_len = unsafe { + name.as_encoded_bytes().as_ptr().offset_from_unsigned(path.as_encoded_bytes().as_ptr()) + }; - let bytes = path.encoded_bytes(); + let bytes = &path.as_encoded_bytes()[..prefix_len + name_len]; Self { - inner: unsafe { P::from_encoded_bytes(&bytes[..prefix_len + name_len]) }, - uri: name_len, + inner: unsafe { P::from_encoded_bytes_unchecked(bytes) }, + uri: bytes.len(), urn: name_len, _phantom: PhantomData, } @@ -153,7 +155,7 @@ where S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); - loc.uri = loc.inner.try_strip_prefix(base).expect("Loc must start with the given base").len(); + loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len(); loc } @@ -173,29 +175,35 @@ where pub fn as_loc(self) -> Self { self } #[inline] - pub fn as_path(self) -> P { self.inner } + pub fn as_inner(self) -> P { self.inner } #[inline] - pub fn is_empty(self) -> bool { self.inner.is_empty() } + pub fn is_empty(self) -> bool { self.inner.len() == 0 } #[inline] pub fn uri(self) -> P { unsafe { - P::from_encoded_bytes(self.inner.encoded_bytes().get_unchecked(self.inner.len() - self.uri..)) + P::from_encoded_bytes_unchecked( + self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.uri..), + ) } } #[inline] pub fn urn(self) -> P { unsafe { - P::from_encoded_bytes(self.inner.encoded_bytes().get_unchecked(self.inner.len() - self.urn..)) + P::from_encoded_bytes_unchecked( + self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.urn..), + ) } } #[inline] pub fn base(self) -> P { unsafe { - P::from_encoded_bytes(self.inner.encoded_bytes().get_unchecked(..self.inner.len() - self.uri)) + P::from_encoded_bytes_unchecked( + self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.uri), + ) } } @@ -205,7 +213,9 @@ where #[inline] pub fn trail(self) -> P { unsafe { - P::from_encoded_bytes(self.inner.encoded_bytes().get_unchecked(..self.inner.len() - self.urn)) + P::from_encoded_bytes_unchecked( + self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.urn), + ) } } @@ -222,9 +232,9 @@ where unsafe { ( - P::from_encoded_bytes(self.inner.encoded_bytes().get_unchecked(base)), - P::from_encoded_bytes(self.inner.encoded_bytes().get_unchecked(rest)), - P::from_encoded_bytes(self.inner.encoded_bytes().get_unchecked(urn)), + P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(base)), + P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(rest)), + P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(urn)), ) } } diff --git a/yazi-shared/src/loc/mod.rs b/yazi-shared/src/loc/mod.rs index d89a0329..5b4556e9 100644 --- a/yazi-shared/src/loc/mod.rs +++ b/yazi-shared/src/loc/mod.rs @@ -1 +1,3 @@ -yazi_macro::mod_flat!(buf loc); +#![allow(private_bounds)] + +yazi_macro::mod_flat!(able buf loc); diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 97a135e5..d9273700 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -1,62 +1,128 @@ -use std::{fmt::Debug, hash::Hash}; +use std::ffi::{OsStr, OsString}; -use crate::{path::{AsPath, AsPathView, PathBufDyn, PathLike, SetNameError, StartsWithError}, strand::{AsStrandView, StrandLike}}; +use hashbrown::Equivalent; +use serde::Serialize; -pub trait PathBufLike -where - Self: 'static + AsPath, -{ - type Strand<'a>: StrandLike<'a>; - type Borrowed<'a>: PathLike<'a> + AsPathView<'a, Self::Borrowed<'a>> + Debug + Hash; +use crate::{FromWtf8, path::{AsPath, PathBufDynError, PathDyn, PathKind, SetNameError}, strand::{AsStrand, Strand, StrandError}}; - fn borrow(&self) -> Self::Borrowed<'_>; - - fn encoded_bytes(&self) -> &[u8] { self.borrow().encoded_bytes() } - - fn into_dyn(self) -> PathBufDyn; - - fn into_encoded_bytes(self) -> Vec; - - fn is_empty(&self) -> bool { self.borrow().is_empty() } - - fn len(&self) -> usize { self.borrow().len() } - - fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.borrow().to_str() } - - fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> - where - T: AsStrandView<'a, Self::Strand<'a>>; - - fn try_starts_with<'a, T>(&self, base: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>; - - fn take(&mut self) -> Self; +// --- PathBufDyn +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(untagged)] +pub enum PathBufDyn { + Os(std::path::PathBuf), } -impl PathBufLike for std::path::PathBuf { - type Borrowed<'a> = &'a std::path::Path; - type Strand<'a> = &'a std::ffi::OsStr; - - fn borrow(&self) -> Self::Borrowed<'_> { self.as_path() } - - fn into_dyn(self) -> PathBufDyn { PathBufDyn::Os(self) } - - fn into_encoded_bytes(self) -> Vec { self.into_os_string().into_encoded_bytes() } - - fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(self.set_file_name(name.as_strand_view())) - } - - fn try_starts_with<'a, T>(&self, base: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(self.starts_with(base.as_strand_view())) - } - - fn take(&mut self) -> Self { std::mem::take(self) } +impl From for PathBufDyn { + fn from(value: std::path::PathBuf) -> Self { Self::Os(value) } +} + +impl From> for PathBufDyn { + fn from(value: PathDyn<'_>) -> Self { value.to_owned() } +} + +impl TryFrom for std::path::PathBuf { + type Error = PathBufDynError; + + fn try_from(value: PathBufDyn) -> Result { value.into_os() } +} + +impl PartialEq> for PathBufDyn { + fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } +} + +impl PartialEq> for &PathBufDyn { + fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } +} + +impl Equivalent> for PathBufDyn { + fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.as_path() == *key } +} + +impl PathBufDyn { + #[inline] + pub unsafe fn from_encoded_bytes(kind: K, bytes: Vec) -> Self + where + K: Into, + { + match kind.into() { + PathKind::Os => Self::Os(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }.into()), + } + } + + pub fn into_encoded_bytes(self) -> Vec { + match self { + Self::Os(p) => p.into_os_string().into_encoded_bytes(), + } + } + + #[inline] + pub fn into_os(self) -> Result { + Ok(match self { + Self::Os(p) => p, + }) + } + + #[inline] + pub fn new(kind: PathKind) -> Self { + match kind { + PathKind::Os => Self::Os(std::path::PathBuf::new()), + } + } + + #[inline] + pub fn os(path: impl Into) -> Self { Self::Os(path.into()) } + + pub fn try_extend(&mut self, paths: T) -> Result<(), StrandError> + where + T: IntoIterator, + T::Item: AsPath, + { + for p in paths { + self.try_push(p)?; + } + Ok(()) + } + + pub fn try_push(&mut self, path: T) -> Result<(), StrandError> + where + T: AsPath, + { + let path = path.as_path(); + Ok(match self { + Self::Os(p) => p.push(path.as_os()?), + }) + } + + pub fn try_set_name(&mut self, name: T) -> Result<(), SetNameError> + where + T: AsStrand, + { + Ok(match (self, name.as_strand()) { + (Self::Os(p), Strand::Os(q)) => p.set_file_name(q), + (Self::Os(p), Strand::Utf8(q)) => p.set_file_name(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.set_file_name(OsStr::from_wtf8(b).map_err(|_| SetNameError::FromWtf8)?) + } + }) + } + + pub fn with(kind: K, strand: T) -> Result + where + K: Into, + T: AsStrand, + { + let s = strand.as_strand(); + Ok(match kind.into() { + PathKind::Os => Self::Os(std::path::PathBuf::from(s.as_os()?)), + }) + } + + pub fn with_capacity(kind: K, capacity: usize) -> Self + where + K: Into, + { + match kind.into() { + PathKind::Os => Self::Os(std::path::PathBuf::with_capacity(capacity)), + } + } } diff --git a/yazi-shared/src/path/conversion.rs b/yazi-shared/src/path/conversion.rs index 8eee3e53..963fef8b 100644 --- a/yazi-shared/src/path/conversion.rs +++ b/yazi-shared/src/path/conversion.rs @@ -1,96 +1,50 @@ -use std::{borrow::Cow, ffi::OsStr}; +use std::ffi::OsStr; use super::{PathBufDyn, PathDyn}; -use crate::path::{PathBufLike, PathCow, PathLike}; +use crate::path::PathCow; // --- AsPath pub trait AsPath { - fn as_path(&self) -> impl PathLike<'_>; + fn as_path(&self) -> PathDyn<'_>; } impl AsPath for OsStr { - fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } + fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self.as_ref()) } } impl AsPath for &OsStr { - fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } + fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self.as_ref()) } } impl AsPath for std::path::Path { - fn as_path(&self) -> impl PathLike<'_> { self } + fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } } impl AsPath for std::path::PathBuf { - fn as_path(&self) -> impl PathLike<'_> { self.as_path() } + fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } } impl AsPath for PathDyn<'_> { - fn as_path(&self) -> impl PathLike<'_> { *self } + fn as_path(&self) -> PathDyn<'_> { *self } } impl AsPath for PathBufDyn { - fn as_path(&self) -> impl PathLike<'_> { self.borrow() } -} - -impl AsPath for &PathBufDyn { - fn as_path(&self) -> impl PathLike<'_> { self.borrow() } -} - -impl AsPath for PathCow<'_> { - fn as_path(&self) -> impl PathLike<'_> { + fn as_path(&self) -> PathDyn<'_> { match self { - PathCow::Borrowed(p) => *p, - PathCow::Owned(p) => p.into(), + Self::Os(p) => PathDyn::Os(p), } } } -// --- AsPathDyn -pub trait AsPathDyn { - fn as_path_dyn(&self) -> PathDyn<'_>; +impl AsPath for &PathBufDyn { + fn as_path(&self) -> PathDyn<'_> { (*self).as_path() } } -impl AsPathDyn for OsStr { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for &OsStr { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for Cow<'_, OsStr> { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for std::path::Path { - fn as_path_dyn(&self) -> PathDyn<'_> { self.into() } -} - -impl AsPathDyn for std::path::PathBuf { - fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } -} - -impl AsPathDyn for &std::path::PathBuf { - fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } -} - -impl AsPathDyn for PathDyn<'_> { - fn as_path_dyn(&self) -> PathDyn<'_> { *self } -} - -impl AsPathDyn for PathBufDyn { - fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } -} - -impl AsPathDyn for &PathBufDyn { - fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } -} - -impl AsPathDyn for PathCow<'_> { - fn as_path_dyn(&self) -> PathDyn<'_> { +impl AsPath for PathCow<'_> { + fn as_path(&self) -> PathDyn<'_> { match self { - Self::Borrowed(p) => p.as_path_dyn(), - Self::Owned(p) => p.as_path_dyn(), + PathCow::Borrowed(p) => *p, + PathCow::Owned(p) => p.as_path(), } } } @@ -101,5 +55,5 @@ pub trait AsPathRef<'a> { } impl<'a> AsPathRef<'a> for PathDyn<'a> { - fn as_path_ref(self) -> PathDyn<'a> { self } + fn as_path_ref(self) -> Self { self } } diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs index 238a11b5..a2523944 100644 --- a/yazi-shared/src/path/cow.rs +++ b/yazi-shared/src/path/cow.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use anyhow::Result; -use crate::{IntoOsStr, path::{AsPathDyn, PathBufDyn, PathBufLike, PathDyn, PathLike}}; +use crate::{IntoOsStr, path::{AsPath, PathBufDyn, PathDyn}}; // --- PathCow pub enum PathCow<'a> { @@ -25,8 +25,8 @@ impl From> for PathBufDyn { impl PartialEq<&str> for PathCow<'_> { fn eq(&self, other: &&str) -> bool { match self { - Self::Borrowed(s) => s.as_path_dyn() == *other, - Self::Owned(s) => s.as_path_dyn() == *other, + Self::Borrowed(s) => s.as_path() == *other, + Self::Owned(s) => s.as_path() == *other, } } } @@ -41,16 +41,8 @@ impl<'a> PathCow<'a> { pub fn into_owned(self) -> PathBufDyn { match self { - Self::Borrowed(s) => s.to_buf_dyn(), + Self::Borrowed(s) => s.to_owned(), Self::Owned(s) => s, } } - - // FIXME: remove, instead implement PathLike for PathCow - pub fn encoded_bytes(&self) -> &[u8] { - match self { - Self::Borrowed(s) => s.encoded_bytes(), - Self::Owned(s) => s.encoded_bytes(), - } - } } diff --git a/yazi-shared/src/path/dyn.rs b/yazi-shared/src/path/dyn.rs deleted file mode 100644 index 777f2ef7..00000000 --- a/yazi-shared/src/path/dyn.rs +++ /dev/null @@ -1,411 +0,0 @@ -use std::ffi::OsStr; - -use anyhow::Result; -use hashbrown::Equivalent; -use serde::Serialize; - -use super::{AsPathView, RsplitOnceError, StartsWithError}; -use crate::{FromWtf8, Utf8BytePredictor, path::{AsPathDyn, EndsWithError, JoinError, PathBufDynError, PathBufLike, PathBufUnsafeExt, PathDynError, PathKind, PathLike, SetNameError, StripPrefixError}, scheme::SchemeKind, strand::{AsStrandDyn, AsStrandView, Strand, StrandError}}; - -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub enum PathDyn<'p> { - Os(&'p std::path::Path), -} - -impl<'a> AsPathView<'a, PathDyn<'a>> for PathDyn<'a> { - fn as_path_view(self) -> PathDyn<'a> { self } -} - -impl<'a> AsPathView<'a, PathDyn<'a>> for std::path::Components<'a> { - fn as_path_view(self) -> PathDyn<'a> { PathDyn::Os(self.as_path()) } -} - -impl<'a> From<&'a std::path::Path> for PathDyn<'a> { - fn from(value: &'a std::path::Path) -> Self { Self::Os(value) } -} - -impl<'a> From<&'a PathBufDyn> for PathDyn<'a> { - fn from(value: &'a PathBufDyn) -> Self { value.borrow() } -} - -impl PartialEq for PathDyn<'_> { - fn eq(&self, other: &PathBufDyn) -> bool { *self == other.borrow() } -} - -impl PartialEq> for &std::path::Path { - fn eq(&self, other: &PathDyn<'_>) -> bool { matches!(*other, PathDyn::Os(p) if p == *self) } -} - -impl PartialEq<&std::path::Path> for PathDyn<'_> { - fn eq(&self, other: &&std::path::Path) -> bool { matches!(*self, PathDyn::Os(p) if p == *other) } -} - -impl PartialEq<&str> for PathDyn<'_> { - fn eq(&self, other: &&str) -> bool { - match self { - PathDyn::Os(p) => p == other, - } - } -} - -impl Equivalent for PathDyn<'_> { - fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.borrow() } -} - -impl<'p> PathLike<'p> for PathDyn<'p> { - type Components<'a> = std::path::Components<'a>; - type Display<'a> = std::path::Display<'a>; - type Owned = PathBufDyn; - type Strand<'a> = Strand<'a>; - type View<'a> = PathDyn<'a>; - - fn as_dyn(self) -> PathDyn<'p> { self } - - fn components(self) -> Self::Components<'p> { - match self { - Self::Os(p) => p.components(), - } - } - - // FIXME: remove - fn default() -> Self { Self::Os(std::path::Path::new("")) } - - fn display(self) -> Self::Display<'p> { - match self { - Self::Os(p) => p.display(), - } - } - - fn encoded_bytes(self) -> &'p [u8] { - match self { - Self::Os(p) => p.as_os_str().as_encoded_bytes(), - } - } - - fn ext(self) -> Option> { - Some(match self { - Self::Os(p) => p.extension()?.into(), - }) - } - - fn has_root(self) -> bool { - match self { - Self::Os(p) => p.has_root(), - } - } - - fn is_absolute(self) -> bool { - match self { - Self::Os(p) => p.is_absolute(), - } - } - - fn kind(self) -> PathKind { - match self { - Self::Os(_) => PathKind::Os, - } - } - - fn name(self) -> Option> { - Some(match self { - Self::Os(p) => p.file_name()?.into(), - }) - } - - fn owned(self) -> Self::Owned { - match self { - Self::Os(p) => Self::Owned::Os(p.to_path_buf()), - } - } - - fn parent(self) -> Option { - Some(match self { - Self::Os(p) => Self::Os(p.parent()?), - }) - } - - fn stem(self) -> Option> { - Some(match self { - Self::Os(p) => p.file_stem()?.into(), - }) - } - - fn try_ends_with<'a, T>(self, child: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(match (self, child.as_strand_view()) { - (Self::Os(p), Strand::Os(q)) => p.ends_with(q), - (Self::Os(p), Strand::Utf8(q)) => p.ends_with(q), - (Self::Os(p), Strand::Bytes(b)) => { - p.ends_with(OsStr::from_wtf8(b).map_err(|_| EndsWithError)?) - } - }) - } - - fn try_join<'a, T>(self, path: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(match (self, path.as_strand_view()) { - (Self::Os(p), Strand::Os(q)) => Self::Owned::Os(p.join(q)), - (Self::Os(p), Strand::Utf8(q)) => Self::Owned::Os(p.join(q)), - (Self::Os(p), Strand::Bytes(b)) => { - Self::Owned::Os(p.join(OsStr::from_wtf8(b).map_err(|_| JoinError::FromWtf8)?)) - } - }) - } - - fn rsplit_pred<'a, T>(self, pred: T) -> Option<(Self, Self)> - where - T: Utf8BytePredictor, - { - match self { - PathDyn::Os(p) => p.rsplit_pred(pred).map(|(l, r)| (Self::Os(l), Self::Os(r))), - } - } - - fn try_rsplit_seq<'a, T>(self, pat: T) -> Result<(Self, Self), RsplitOnceError> - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - let pat = pat.as_strand_view(); - match self { - PathDyn::Os(p) => { - let (l, r) = p.try_rsplit_seq(pat.as_os().map_err(|_| RsplitOnceError::AsOs)?)?; - Ok((Self::Os(l), Self::Os(r))) - } - } - } - - fn try_starts_with<'a, T>(self, base: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(match (self, base.as_strand_view()) { - (Self::Os(p), Strand::Os(q)) => p.starts_with(q), - (Self::Os(p), Strand::Utf8(q)) => p.starts_with(q), - (Self::Os(p), Strand::Bytes(b)) => { - p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) - } - }) - } - - fn try_strip_prefix<'a, T>(self, base: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(match (self, base.as_strand_view()) { - (Self::Os(p), Strand::Os(q)) => Self::Os(p.strip_prefix(q)?), - (Self::Os(p), Strand::Utf8(q)) => Self::Os(p.strip_prefix(q)?), - (Self::Os(p), Strand::Bytes(b)) => { - Self::Os(p.strip_prefix(OsStr::from_wtf8(b).map_err(|_| StripPrefixError::WrongEncoding)?)?) - } - }) - } -} - -impl<'a> PathDyn<'a> { - #[inline] - pub fn os(path: &'a T) -> Self - where - T: ?Sized + AsRef, - { - Self::Os(path.as_ref()) - } - - #[inline] - pub fn as_os(self) -> Result<&'a std::path::Path, PathDynError> { - match self { - Self::Os(p) => Ok(p), - } - } - - #[inline] - pub fn to_os_owned(self) -> Result { - match self { - Self::Os(p) => Ok(p.to_owned()), - } - } - - pub fn with(kind: SchemeKind, strand: &'a T) -> Result - where - T: ?Sized + AsStrandDyn, - { - use SchemeKind as K; - - let s = strand.as_strand_dyn(); - Ok(match kind { - K::Regular | K::Search | K::Archive => Self::Os(std::path::Path::new(s.as_os()?)), - K::Sftp => Self::Os(std::path::Path::new(s.as_os()?)), // FIXME - }) - } - - #[inline] - pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: &'a [u8]) -> Self { - match kind.into() { - PathKind::Os => Self::Os(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }.as_ref()), - } - } -} - -// --- PathBufDyn -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] -#[serde(untagged)] -pub enum PathBufDyn { - Os(std::path::PathBuf), -} - -impl From for PathBufDyn { - fn from(value: std::path::PathBuf) -> Self { Self::Os(value) } -} - -impl From<&PathBufDyn> for PathBufDyn { - fn from(value: &PathBufDyn) -> Self { value.clone() } -} - -impl TryFrom for std::path::PathBuf { - type Error = PathBufDynError; - - fn try_from(value: PathBufDyn) -> Result { value.into_os() } -} - -impl PartialEq> for PathBufDyn { - fn eq(&self, other: &PathDyn<'_>) -> bool { self.borrow() == *other } -} - -impl PartialEq> for &PathBufDyn { - fn eq(&self, other: &PathDyn<'_>) -> bool { self.borrow() == *other } -} - -impl Equivalent> for PathBufDyn { - fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.borrow() == *key } -} - -impl PathBufLike for PathBufDyn { - type Borrowed<'a> = PathDyn<'a>; - type Strand<'a> = Strand<'a>; - - fn borrow(&self) -> Self::Borrowed<'_> { - match self { - Self::Os(p) => Self::Borrowed::Os(p.as_path()), - } - } - - fn encoded_bytes(&self) -> &[u8] { - match self { - Self::Os(p) => p.as_os_str().as_encoded_bytes(), - } - } - - fn into_dyn(self) -> PathBufDyn { self } - - fn into_encoded_bytes(self) -> Vec { - match self { - Self::Os(p) => p.into_os_string().into_encoded_bytes(), - } - } - - fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(match (self, name.as_strand_view()) { - (Self::Os(p), Strand::Os(q)) => p.set_file_name(q), - (Self::Os(p), Strand::Utf8(q)) => p.set_file_name(q), - (Self::Os(p), Strand::Bytes(b)) => { - p.set_file_name(OsStr::from_wtf8(b).map_err(|_| SetNameError::FromWtf8)?) - } - }) - } - - fn try_starts_with<'a, T>(&self, base: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(match (self, base.as_strand_view()) { - (Self::Os(p), Strand::Os(q)) => p.starts_with(q), - (Self::Os(p), Strand::Utf8(q)) => p.starts_with(q), - (Self::Os(p), Strand::Bytes(b)) => { - p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) - } - }) - } - - fn take(&mut self) -> Self { - match self { - Self::Os(p) => Self::Os(std::mem::take(p)), - } - } -} - -impl PathBufDyn { - #[inline] - pub fn os(path: impl Into) -> Self { Self::Os(path.into()) } - - #[inline] - pub fn into_os(self) -> Result { - Ok(match self { - PathBufDyn::Os(p) => p, - }) - } - - #[inline] - pub fn new(kind: SchemeKind) -> Self { - use SchemeKind as K; - - match kind { - K::Regular | K::Search | K::Archive => Self::Os(std::path::PathBuf::new()), - K::Sftp => Self::Os(std::path::PathBuf::new()), // FIXME - } - } - - pub fn with(kind: SchemeKind, strand: T) -> Result - where - T: AsStrandDyn, - { - use SchemeKind as K; - - let s = strand.as_strand_dyn(); - Ok(match kind { - K::Regular | K::Search | K::Archive => Self::Os(std::path::PathBuf::from(s.as_os()?)), - K::Sftp => Self::Os(std::path::PathBuf::from(s.as_os()?)), // FIXME - }) - } - - pub fn with_capacity(kind: SchemeKind, capacity: usize) -> Self { - use SchemeKind as K; - match kind { - K::Regular | K::Search | K::Archive => Self::Os(std::path::PathBuf::with_capacity(capacity)), - K::Sftp => Self::Os(std::path::PathBuf::with_capacity(capacity)), // FIXME - } - } - - pub fn try_push(&mut self, path: T) -> Result<(), StrandError> - where - T: AsPathDyn, - { - let path = path.as_path_dyn(); - Ok(match self { - PathBufDyn::Os(p) => p.push(path.as_os()?), - }) - } - - pub fn try_extend(&mut self, paths: T) -> Result<(), StrandError> - where - T: IntoIterator, - T::Item: AsPathDyn, - { - for p in paths { - self.try_push(p)?; - } - Ok(()) - } - - #[inline] - pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: Vec) -> Self { - match kind.into() { - PathKind::Os => Self::Os(unsafe { std::path::PathBuf::from_encoded_bytes(bytes) }), - } - } -} diff --git a/yazi-shared/src/path/error.rs b/yazi-shared/src/path/error.rs index 30f6dc14..8b28d274 100644 --- a/yazi-shared/src/path/error.rs +++ b/yazi-shared/src/path/error.rs @@ -16,7 +16,7 @@ pub enum JoinError { } impl From for std::io::Error { - fn from(err: JoinError) -> Self { std::io::Error::other(err) } + fn from(err: JoinError) -> Self { Self::other(err) } } // --- PathDynError @@ -27,7 +27,7 @@ pub enum PathDynError { } impl From for std::io::Error { - fn from(err: PathDynError) -> Self { std::io::Error::other(err) } + fn from(err: PathDynError) -> Self { Self::other(err) } } // --- PathBufDynError @@ -46,7 +46,7 @@ pub enum SetNameError { } impl From for std::io::Error { - fn from(err: SetNameError) -> Self { std::io::Error::other(err) } + fn from(err: SetNameError) -> Self { Self::other(err) } } // --- RsplitOnce @@ -61,6 +61,15 @@ pub enum RsplitOnceError { NotFound, } +impl From for RsplitOnceError { + fn from(err: StrandError) -> Self { + match err { + StrandError::AsOs => Self::AsOs, + StrandError::AsUtf8 => Self::AsUtf8, + } + } +} + // --- StartsWithError #[derive(Error, Debug)] #[error("calling starts_with on paths with different encodings")] diff --git a/yazi-shared/src/path/like.rs b/yazi-shared/src/path/like.rs new file mode 100644 index 00000000..99ee4b5e --- /dev/null +++ b/yazi-shared/src/path/like.rs @@ -0,0 +1,96 @@ +use std::borrow::Cow; + +use anyhow::Result; + +use crate::{Utf8BytePredictor, path::{AsPath, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError}, strand::{AsStrand, Strand}}; + +pub trait PathLike: AsPath { + fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.as_path().as_os() } + + fn components(&self) -> std::path::Components<'_> { self.as_path().components() } + + fn display(&self) -> std::path::Display<'_> { self.as_path().display() } + + fn encoded_bytes(&self) -> &[u8] { self.as_path().encoded_bytes() } + + fn ext(&self) -> Option> { self.as_path().ext() } + + fn has_root(&self) -> bool { self.as_path().has_root() } + + fn is_absolute(&self) -> bool { self.as_path().is_absolute() } + + fn is_empty(&self) -> bool { self.as_path().is_empty() } + + #[cfg(unix)] + fn is_hidden(&self) -> bool { self.as_path().is_hidden() } + + fn kind(&self) -> PathKind { self.as_path().kind() } + + fn len(&self) -> usize { self.as_path().len() } + + fn name(&self) -> Option> { self.as_path().name() } + + fn parent(&self) -> Option> { self.as_path().parent() } + + fn rsplit_pred(&self, pred: T) -> Option<(PathDyn<'_>, PathDyn<'_>)> + where + T: Utf8BytePredictor, + { + self.as_path().rsplit_pred(pred) + } + + fn stem(&self) -> Option> { self.as_path().stem() } + + fn to_os_owned(&self) -> Result { self.as_path().to_os_owned() } + + fn to_owned(&self) -> PathBufDyn { self.as_path().to_owned() } + + fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.as_path().to_str() } + + fn to_string_lossy(&self) -> Cow<'_, str> { self.as_path().to_string_lossy() } + + fn try_ends_with(&self, child: T) -> Result + where + T: AsStrand, + { + self.as_path().try_ends_with(child) + } + + fn try_join(&self, path: T) -> Result + where + T: AsStrand, + { + self.as_path().try_join(path) + } + + fn try_rsplit_seq(&self, pat: T) -> Result<(PathDyn<'_>, PathDyn<'_>), RsplitOnceError> + where + T: AsStrand, + { + self.as_path().try_rsplit_seq(pat) + } + + fn try_starts_with(&self, base: T) -> Result + where + T: AsStrand, + { + self.as_path().try_starts_with(base) + } + + fn try_strip_prefix(&self, base: T) -> Result, StripPrefixError> + where + T: AsStrand, + { + self.as_path().try_strip_prefix(base) + } +} + +impl

From<&P> for PathBufDyn +where + P: PathLike, +{ + fn from(value: &P) -> Self { value.to_owned() } +} + +impl PathLike for PathBufDyn {} +impl PathLike for PathCow<'_> {} diff --git a/yazi-shared/src/path/mod.rs b/yazi-shared/src/path/mod.rs index 30fde723..e7691f0c 100644 --- a/yazi-shared/src/path/mod.rs +++ b/yazi-shared/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(buf component components conversion cow error kind path r#dyn r#unsafe view); +yazi_macro::mod_flat!(buf component components conversion cow error kind like path view); diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index 43e02dea..3ac95899 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -1,184 +1,250 @@ use std::{borrow::Cow, ffi::OsStr}; use anyhow::Result; +use hashbrown::Equivalent; -use crate::{BytesExt, Utf8BytePredictor, path::{AsPathView, EndsWithError, JoinError, PathBufDyn, PathBufLike, PathDyn, PathKind, RsplitOnceError, StartsWithError, StripPrefixError}, strand::{AsStrandView, StrandLike}}; +use super::{RsplitOnceError, StartsWithError}; +use crate::{BytesExt, FromWtf8, Utf8BytePredictor, path::{AsPath, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError}, strand::{AsStrand, Strand, StrandError}}; -pub trait PathLike<'p> -where - Self: Copy + AsPathView<'p, Self::View<'p>> + AsStrandView<'p, Self::Strand<'p>>, -{ - type Strand<'a>: StrandLike<'a>; - type Owned: PathBufLike + Into; - type View<'a>; - type Components<'a>: Clone - + DoubleEndedIterator - + AsPathView<'a, Self::View<'a>> - + AsStrandView<'a, Self::Strand<'a>>; - type Display<'a>: std::fmt::Display; +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum PathDyn<'p> { + Os(&'p std::path::Path), +} - fn as_dyn(self) -> PathDyn<'p>; +impl<'a> From<&'a std::path::Path> for PathDyn<'a> { + fn from(value: &'a std::path::Path) -> Self { Self::Os(value) } +} - fn components(self) -> Self::Components<'p>; +impl<'a> From<&'a PathBufDyn> for PathDyn<'a> { + fn from(value: &'a PathBufDyn) -> Self { value.as_path() } +} - fn default() -> Self; +impl PartialEq for PathDyn<'_> { + fn eq(&self, other: &PathBufDyn) -> bool { *self == other.as_path() } +} - fn display(self) -> Self::Display<'p>; +impl PartialEq> for &std::path::Path { + fn eq(&self, other: &PathDyn<'_>) -> bool { matches!(*other, PathDyn::Os(p) if p == *self) } +} - fn encoded_bytes(self) -> &'p [u8]; +impl PartialEq<&std::path::Path> for PathDyn<'_> { + fn eq(&self, other: &&std::path::Path) -> bool { matches!(*self, PathDyn::Os(p) if p == *other) } +} - fn ext(self) -> Option>; +impl PartialEq<&str> for PathDyn<'_> { + fn eq(&self, other: &&str) -> bool { + match self { + PathDyn::Os(p) => p == other, + } + } +} - fn has_root(self) -> bool; +impl Equivalent for PathDyn<'_> { + fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.as_path() } +} - fn is_absolute(self) -> bool; +impl<'p> PathDyn<'p> { + #[inline] + pub fn as_os(self) -> Result<&'p std::path::Path, PathDynError> { + match self { + Self::Os(p) => Ok(p), + } + } - fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } + pub fn components(self) -> std::path::Components<'p> { + match self { + Self::Os(p) => p.components(), + } + } + + pub fn display(self) -> std::path::Display<'p> { + match self { + Self::Os(p) => p.display(), + } + } + + pub fn encoded_bytes(self) -> &'p [u8] { + match self { + Self::Os(p) => p.as_os_str().as_encoded_bytes(), + } + } + + pub fn ext(self) -> Option> { + Some(match self { + Self::Os(p) => p.extension()?.into(), + }) + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: K, bytes: &'p [u8]) -> Self + where + K: Into, + { + match kind.into() { + PathKind::Os => Self::Os(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }.as_ref()), + } + } + + pub fn has_root(self) -> bool { + match self { + Self::Os(p) => p.has_root(), + } + } + + pub fn is_absolute(self) -> bool { + match self { + Self::Os(p) => p.is_absolute(), + } + } + + pub fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } #[cfg(unix)] - fn is_hidden(self) -> bool { + pub fn is_hidden(self) -> bool { self.name().is_some_and(|n| n.encoded_bytes().first() == Some(&b'.')) } - fn kind(self) -> PathKind; - - fn len(self) -> usize { self.encoded_bytes().len() } - - fn name(self) -> Option>; - - fn owned(self) -> Self::Owned; - - fn parent(self) -> Option; - - fn rsplit_pred(self, pred: T) -> Option<(Self, Self)> - where - T: Utf8BytePredictor; - - fn stem(self) -> Option>; - - fn to_str(self) -> Result<&'p str, std::str::Utf8Error> { str::from_utf8(self.encoded_bytes()) } - - fn to_string_lossy(self) -> Cow<'p, str> { String::from_utf8_lossy(self.encoded_bytes()) } - - fn to_buf_dyn(self) -> PathBufDyn { self.as_dyn().owned() } - - fn try_ends_with<'a, T>(self, child: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>; - - fn try_join<'a, T>(self, path: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>; - - fn try_rsplit_seq<'a, T>(self, pat: T) -> Result<(Self, Self), RsplitOnceError> - where - T: AsStrandView<'a, Self::Strand<'a>>; - - fn try_starts_with<'a, T>(self, base: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>; - - fn try_strip_prefix<'a, T>(self, base: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>; -} - -impl<'p> PathLike<'p> for &'p std::path::Path { - type Components<'a> = std::path::Components<'a>; - type Display<'a> = std::path::Display<'a>; - type Owned = std::path::PathBuf; - type Strand<'a> = &'a std::ffi::OsStr; - type View<'a> = &'a std::path::Path; - - fn as_dyn(self) -> PathDyn<'p> { PathDyn::Os(self) } - - fn components(self) -> Self::Components<'p> { self.components() } - - fn default() -> Self { std::path::Path::new("") } - - fn display(self) -> Self::Display<'p> { self.display() } - - fn encoded_bytes(self) -> &'p [u8] { self.as_os_str().as_encoded_bytes() } - - fn ext(self) -> Option> { self.extension() } - - fn has_root(self) -> bool { self.has_root() } - - fn is_absolute(self) -> bool { self.is_absolute() } - - fn kind(self) -> PathKind { PathKind::Os } - - fn name(self) -> Option> { self.file_name() } - - fn owned(self) -> Self::Owned { self.to_path_buf() } - - fn parent(self) -> Option { self.parent() } - - fn stem(self) -> Option> { self.file_stem() } - - fn try_ends_with<'a, T>(self, child: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(self.ends_with(child.as_strand_view())) + pub fn kind(self) -> PathKind { + match self { + Self::Os(_) => PathKind::Os, + } } - fn try_join<'a, T>(self, path: T) -> Result - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - Ok(self.join(path.as_strand_view())) + pub fn len(self) -> usize { self.encoded_bytes().len() } + + pub fn name(self) -> Option> { + Some(match self { + Self::Os(p) => p.file_name()?.into(), + }) } - fn rsplit_pred<'a, T>(self, pred: T) -> Option<(Self, Self)> + #[inline] + pub fn os(path: &'p T) -> Self + where + T: ?Sized + AsRef, + { + Self::Os(path.as_ref()) + } + + pub fn parent(self) -> Option { + Some(match self { + Self::Os(p) => Self::Os(p.parent()?), + }) + } + + pub fn rsplit_pred(self, pred: T) -> Option<(Self, Self)> where T: Utf8BytePredictor, { - let b = self.encoded_bytes(); - let (left, right) = b.rsplit_pred_once(pred)?; - + let (a, b) = self.encoded_bytes().rsplit_pred_once(pred)?; Some(unsafe { - ( - OsStr::from_encoded_bytes_unchecked(left).as_ref(), - OsStr::from_encoded_bytes_unchecked(right).as_ref(), - ) + (Self::from_encoded_bytes(self.kind(), a), Self::from_encoded_bytes(self.kind(), b)) }) } - fn try_rsplit_seq<'a, T>(self, pat: T) -> Result<(Self, Self), RsplitOnceError> - where - T: AsStrandView<'a, Self::Strand<'a>>, - { - let b = self.encoded_bytes(); - let p = pat.as_strand_view().encoded_bytes(); + pub fn stem(self) -> Option> { + Some(match self { + Self::Os(p) => p.file_stem()?.into(), + }) + } + + #[inline] + pub fn to_os_owned(self) -> Result { + match self { + Self::Os(p) => Ok(p.to_owned()), + } + } + + pub fn to_owned(self) -> PathBufDyn { + match self { + Self::Os(p) => PathBufDyn::Os(p.to_path_buf()), + } + } + + pub fn to_str(self) -> Result<&'p str, std::str::Utf8Error> { + str::from_utf8(self.encoded_bytes()) + } + + pub fn to_string_lossy(self) -> Cow<'p, str> { String::from_utf8_lossy(self.encoded_bytes()) } + + pub fn try_ends_with(self, child: T) -> Result + where + T: AsStrand, + { + Ok(match (self, child.as_strand()) { + (Self::Os(p), Strand::Os(q)) => p.ends_with(q), + (Self::Os(p), Strand::Utf8(q)) => p.ends_with(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.ends_with(OsStr::from_wtf8(b).map_err(|_| EndsWithError)?) + } + }) + } + + pub fn try_join(self, path: T) -> Result + where + T: AsStrand, + { + Ok(match (self, path.as_strand()) { + (Self::Os(p), Strand::Os(q)) => PathBufDyn::Os(p.join(q)), + (Self::Os(p), Strand::Utf8(q)) => PathBufDyn::Os(p.join(q)), + (Self::Os(p), Strand::Bytes(b)) => { + PathBufDyn::Os(p.join(OsStr::from_wtf8(b).map_err(|_| JoinError::FromWtf8)?)) + } + }) + } + + pub fn try_rsplit_seq(self, pat: T) -> Result<(Self, Self), RsplitOnceError> + where + T: AsStrand, + { + let pat = pat.as_strand(); + + let (a, b) = match self { + PathDyn::Os(p) => { + p.as_os_str().as_encoded_bytes().rsplit_seq_once(pat.as_os()?.as_encoded_bytes()) + } + } + .ok_or(RsplitOnceError::NotFound)?; - let (left, right) = b.rsplit_seq_once(p).ok_or(RsplitOnceError::NotFound)?; Ok(unsafe { - ( - OsStr::from_encoded_bytes_unchecked(left).as_ref(), - OsStr::from_encoded_bytes_unchecked(right).as_ref(), - ) + (Self::from_encoded_bytes(self.kind(), a), Self::from_encoded_bytes(self.kind(), b)) }) } - fn try_starts_with<'a, T>(self, base: T) -> Result + pub fn try_starts_with(self, base: T) -> Result where - T: AsStrandView<'a, Self::Strand<'a>>, + T: AsStrand, { - Ok(self.starts_with(base.as_strand_view())) + Ok(match (self, base.as_strand()) { + (Self::Os(p), Strand::Os(q)) => p.starts_with(q), + (Self::Os(p), Strand::Utf8(q)) => p.starts_with(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) + } + }) } - fn try_strip_prefix<'a, T>(self, base: T) -> Result + pub fn try_strip_prefix(self, base: T) -> Result where - T: AsStrandView<'a, Self::Strand<'a>>, + T: AsStrand, { - Ok(self.strip_prefix(base.as_strand_view())?) + Ok(match (self, base.as_strand()) { + (Self::Os(p), Strand::Os(q)) => Self::Os(p.strip_prefix(q)?), + (Self::Os(p), Strand::Utf8(q)) => Self::Os(p.strip_prefix(q)?), + (Self::Os(p), Strand::Bytes(b)) => { + Self::Os(p.strip_prefix(OsStr::from_wtf8(b).map_err(|_| StripPrefixError::WrongEncoding)?)?) + } + }) + } + + pub fn with(kind: K, strand: &'p T) -> Result + where + K: Into, + T: ?Sized + AsStrand, + { + let s = strand.as_strand(); + Ok(match kind.into() { + PathKind::Os => Self::Os(std::path::Path::new(s.as_os()?)), + }) } } - -impl<'a, P> From

for PathBufDyn -where - P: PathLike<'a>, -{ - fn from(value: P) -> Self { value.to_buf_dyn() } -} diff --git a/yazi-shared/src/path/unsafe.rs b/yazi-shared/src/path/unsafe.rs deleted file mode 100644 index 4bfcb847..00000000 --- a/yazi-shared/src/path/unsafe.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::ffi::OsString; - -// --- PathUnsafeExt -pub trait PathUnsafeExt<'p> { - unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self; -} - -impl<'p> PathUnsafeExt<'p> for &'p std::path::Path { - unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self { - std::path::Path::new(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(bytes) }) - } -} - -// --- PathBufUnsafeExt -pub trait PathBufUnsafeExt { - unsafe fn from_encoded_bytes(bytes: Vec) -> Self; -} - -impl PathBufUnsafeExt for std::path::PathBuf { - unsafe fn from_encoded_bytes(bytes: Vec) -> Self { - Self::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }) - } -} diff --git a/yazi-shared/src/path/view.rs b/yazi-shared/src/path/view.rs index a23c8422..883977d9 100644 --- a/yazi-shared/src/path/view.rs +++ b/yazi-shared/src/path/view.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, ffi::OsStr}; -use crate::path::{AsPathDyn, PathDyn}; +use crate::path::{AsPath, PathDyn}; // --- AsPathView pub trait AsPathView<'a, T> { @@ -25,9 +25,9 @@ impl<'a> AsPathView<'a, &'a std::path::Path> for std::path::Components<'a> { impl<'a, T> AsPathView<'a, PathDyn<'a>> for &'a T where - T: AsPathDyn, + T: AsPath, { - fn as_path_view(self) -> PathDyn<'a> { self.as_path_dyn() } + fn as_path_view(self) -> PathDyn<'a> { self.as_path() } } impl<'a> AsPathView<'a, &'a std::path::Path> for &'a Cow<'_, OsStr> { diff --git a/yazi-shared/src/scheme/cow.rs b/yazi-shared/src/scheme/cow.rs index 3e16e4cb..641c298d 100644 --- a/yazi-shared/src/scheme/cow.rs +++ b/yazi-shared/src/scheme/cow.rs @@ -1,9 +1,9 @@ -use std::{borrow::Cow, ops::Not}; +use std::borrow::Cow; use anyhow::{Result, ensure}; use percent_encoding::percent_decode; -use crate::{path::{AsPath, PathCow, PathLike}, pool::{InternStr, SymbolCow}, scheme::{AsScheme, Scheme, SchemeKind, SchemeRef}, url::Url}; +use crate::{path::{PathCow, PathLike}, pool::{InternStr, SymbolCow}, scheme::{AsScheme, Scheme, SchemeKind, SchemeRef}, url::Url}; #[derive(Clone, Debug)] pub enum SchemeCow<'a> { @@ -144,11 +144,10 @@ impl<'a> SchemeCow<'a> { urn: Option, path: &PathCow, ) -> Result<(usize, usize)> { - let path = path.as_path(); Ok(match kind { SchemeKind::Regular => { ensure!(uri.is_none() && urn.is_none(), "Regular scheme cannot have ports"); - (path.is_empty().not() as usize, path.name().is_some() as usize) + (path.components().count(), path.name().is_some() as usize) } SchemeKind::Search => { let (uri, urn) = (uri.unwrap_or(0), urn.unwrap_or(0)); @@ -157,7 +156,7 @@ impl<'a> SchemeCow<'a> { } SchemeKind::Archive => (uri.unwrap_or(0), urn.unwrap_or(0)), SchemeKind::Sftp => { - let uri = uri.unwrap_or(path.is_empty().not() as usize); + let uri = uri.unwrap_or_else(|| path.components().count()); let urn = urn.unwrap_or(path.name().is_some() as usize); (uri, urn) } @@ -165,12 +164,7 @@ impl<'a> SchemeCow<'a> { } pub fn retrieve_ports(url: Url) -> (usize, usize) { - match url { - Url::Regular(loc) => (loc.is_empty().not() as usize, loc.file_name().is_some() as usize), - Url::Search { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), - Url::Archive { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), - Url::Sftp { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), - } + (url.uri().components().count(), url.urn().components().count()) } } diff --git a/yazi-shared/src/scheme/encode.rs b/yazi-shared/src/scheme/encode.rs index 7b97244c..f191d2d5 100644 --- a/yazi-shared/src/scheme/encode.rs +++ b/yazi-shared/src/scheme/encode.rs @@ -1,8 +1,8 @@ -use std::{fmt::{self, Display}, ops::Not}; +use std::fmt::{self, Display}; use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; -use crate::{path::PathLike, scheme::SchemeKind, url::Url}; +use crate::{scheme::SchemeKind, url::Url}; #[derive(Clone, Copy)] pub struct Encode<'a>(pub Url<'a>); @@ -39,7 +39,7 @@ impl<'a> Encode<'a> { SchemeKind::Regular => Ok(()), SchemeKind::Search | SchemeKind::Archive => w!(0, 0), SchemeKind::Sftp => { - w!(self.0.0.loc().is_empty().not() as usize, self.0.0.loc().name().is_some() as usize) + w!(self.0.0.loc().components().count(), self.0.0.loc().name().is_some() as usize) } } } diff --git a/yazi-shared/src/scheme/traits.rs b/yazi-shared/src/scheme/traits.rs index 5467fbf0..b8c64c21 100644 --- a/yazi-shared/src/scheme/traits.rs +++ b/yazi-shared/src/scheme/traits.rs @@ -13,10 +13,10 @@ impl AsScheme for Scheme { #[inline] fn as_scheme(&self) -> SchemeRef<'_> { match *self { - Scheme::Regular { uri, urn } => SchemeRef::Regular { uri, urn }, - Scheme::Search { ref domain, uri, urn } => SchemeRef::Search { domain, uri, urn }, - Scheme::Archive { ref domain, uri, urn } => SchemeRef::Archive { domain, uri, urn }, - Scheme::Sftp { ref domain, uri, urn } => SchemeRef::Sftp { domain, uri, urn }, + Self::Regular { uri, urn } => SchemeRef::Regular { uri, urn }, + Self::Search { ref domain, uri, urn } => SchemeRef::Search { domain, uri, urn }, + Self::Archive { ref domain, uri, urn } => SchemeRef::Archive { domain, uri, urn }, + Self::Sftp { ref domain, uri, urn } => SchemeRef::Sftp { domain, uri, urn }, } } } diff --git a/yazi-shared/src/strand/buf.rs b/yazi-shared/src/strand/buf.rs new file mode 100644 index 00000000..f5b53849 --- /dev/null +++ b/yazi-shared/src/strand/buf.rs @@ -0,0 +1,85 @@ +use std::ffi::OsString; + +use serde::Serialize; + +use crate::{path::PathDyn, scheme::SchemeKind, strand::{AsStrand, Strand, StrandError, StrandKind}}; + +// --- StrandBuf +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(untagged)] +pub enum StrandBuf { + Os(OsString), + Utf8(String), + Bytes(Vec), +} + +impl Default for StrandBuf { + fn default() -> Self { Self::Utf8(String::new()) } +} + +impl From for StrandBuf { + fn from(value: OsString) -> Self { Self::Os(value) } +} + +impl From for StrandBuf { + fn from(value: String) -> Self { Self::Utf8(value) } +} + +impl From> for StrandBuf { + fn from(value: PathDyn) -> Self { + match value { + PathDyn::Os(p) => Self::Os(p.as_os_str().to_owned()), + } + } +} + +impl PartialEq> for StrandBuf { + fn eq(&self, other: &Strand<'_>) -> bool { self.as_strand() == *other } +} + +impl StrandBuf { + pub fn clear(&mut self) { + match self { + Self::Os(buf) => buf.clear(), + Self::Utf8(buf) => buf.clear(), + Self::Bytes(buf) => buf.clear(), + } + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: Vec) -> Self { + match kind.into() { + StrandKind::Os => Self::Os(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), + StrandKind::Utf8 => Self::Utf8(unsafe { String::from_utf8_unchecked(bytes) }), + StrandKind::Bytes => Self::Bytes(bytes), + } + } + + pub fn into_encoded_bytes(self) -> Vec { + match self { + Self::Os(s) => s.into_encoded_bytes(), + Self::Utf8(s) => s.into_bytes(), + Self::Bytes(b) => b, + } + } + + pub fn try_push(&mut self, s: T) -> Result<(), StrandError> + where + T: AsStrand, + { + let s = s.as_strand(); + Ok(match self { + Self::Os(buf) => buf.push(s.as_os()?), + Self::Utf8(buf) => buf.push_str(s.as_utf8()?), + Self::Bytes(buf) => buf.extend(s.encoded_bytes()), + }) + } + + pub fn with_capacity(kind: SchemeKind, capacity: usize) -> Self { + use SchemeKind as K; + match kind { + K::Regular | K::Search | K::Archive => Self::Os(OsString::with_capacity(capacity)), + K::Sftp => Self::Os(OsString::with_capacity(capacity)), // FIXME + } + } +} diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs index ad2ce1aa..5f21c101 100644 --- a/yazi-shared/src/strand/conversion.rs +++ b/yazi-shared/src/strand/conversion.rs @@ -1,159 +1,103 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}}; -use crate::{path::{PathBufDyn, PathDyn}, strand::{Strand, StrandBuf, StrandBufLike, StrandCow, StrandLike}}; +use crate::{path::{PathBufDyn, PathDyn}, strand::{Strand, StrandBuf, StrandCow}}; // --- AsStrand pub trait AsStrand { - fn as_strand(&self) -> impl StrandLike<'_>; + fn as_strand(&self) -> Strand<'_>; } impl AsStrand for [u8] { - fn as_strand(&self) -> impl StrandLike<'_> { self } + fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } } impl AsStrand for &[u8] { - fn as_strand(&self) -> impl StrandLike<'_> { *self } + fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } } impl AsStrand for Vec { - fn as_strand(&self) -> impl StrandLike<'_> { self.as_slice() } + fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } } impl AsStrand for str { - fn as_strand(&self) -> impl StrandLike<'_> { self } + fn as_strand(&self) -> Strand<'_> { Strand::Utf8(self) } } impl AsStrand for &str { - fn as_strand(&self) -> impl StrandLike<'_> { *self } + fn as_strand(&self) -> Strand<'_> { Strand::Utf8(self) } } impl AsStrand for String { - fn as_strand(&self) -> impl StrandLike<'_> { self.as_str() } + fn as_strand(&self) -> Strand<'_> { Strand::Utf8(self) } +} + +impl AsStrand for &String { + fn as_strand(&self) -> Strand<'_> { Strand::Utf8(self) } } impl AsStrand for OsStr { - fn as_strand(&self) -> impl StrandLike<'_> { self } + fn as_strand(&self) -> Strand<'_> { Strand::Os(self) } } impl AsStrand for &OsStr { - fn as_strand(&self) -> impl StrandLike<'_> { *self } + fn as_strand(&self) -> Strand<'_> { Strand::Os(self) } } impl AsStrand for OsString { - fn as_strand(&self) -> impl StrandLike<'_> { self.as_os_str() } + fn as_strand(&self) -> Strand<'_> { Strand::Os(self) } +} + +impl AsStrand for &std::path::Path { + fn as_strand(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } } impl AsStrand for Cow<'_, OsStr> { - fn as_strand(&self) -> impl StrandLike<'_> { AsRef::::as_ref(self) } + fn as_strand(&self) -> Strand<'_> { Strand::Os(self) } } impl AsStrand for PathDyn<'_> { - fn as_strand(&self) -> impl StrandLike<'_> { - match self { - Self::Os(p) => p.as_os_str(), - } - } -} - -impl AsStrand for &PathBufDyn { - fn as_strand(&self) -> impl StrandLike<'_> { - match self { - PathBufDyn::Os(p) => p.as_os_str(), - } - } -} - -impl AsStrand for Strand<'_> { - fn as_strand(&self) -> impl StrandLike<'_> { *self } -} - -impl AsStrand for StrandBuf { - fn as_strand(&self) -> impl StrandLike<'_> { self.borrow() } -} - -impl AsStrand for StrandCow<'_> { - fn as_strand(&self) -> impl StrandLike<'_> { - match self { - StrandCow::Borrowed(s) => *s, - StrandCow::Owned(s) => s.into(), - } - } -} - -impl AsStrand for &StrandCow<'_> { - fn as_strand(&self) -> impl StrandLike<'_> { (**self).as_strand() } -} - -// --- AsStrandDyn -pub trait AsStrandDyn { - fn as_strand_dyn(&self) -> Strand<'_>; -} - -impl AsStrandDyn for OsStr { - fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } -} - -impl AsStrandDyn for &OsStr { - fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } -} - -impl AsStrandDyn for OsString { - fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } -} - -impl AsStrandDyn for &std::path::PathBuf { - fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } -} - -impl AsStrandDyn for &str { - fn as_strand_dyn(&self) -> Strand<'_> { Strand::Utf8(self) } -} - -impl AsStrandDyn for String { - fn as_strand_dyn(&self) -> Strand<'_> { Strand::Utf8(self) } -} - -impl AsStrandDyn for &String { - fn as_strand_dyn(&self) -> Strand<'_> { Strand::Utf8(self) } -} - -impl AsStrandDyn for Cow<'_, OsStr> { - fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } -} - -impl AsStrandDyn for PathDyn<'_> { - fn as_strand_dyn(&self) -> Strand<'_> { + fn as_strand(&self) -> Strand<'_> { match self { Self::Os(p) => Strand::Os(p.as_os_str()), } } } -impl AsStrandDyn for &PathBufDyn { - fn as_strand_dyn(&self) -> Strand<'_> { +impl AsStrand for &PathBufDyn { + fn as_strand(&self) -> Strand<'_> { match self { PathBufDyn::Os(p) => Strand::Os(p.as_os_str()), } } } -impl AsStrandDyn for Strand<'_> { - fn as_strand_dyn(&self) -> Strand<'_> { *self } +impl AsStrand for Strand<'_> { + fn as_strand(&self) -> Strand<'_> { *self } } -impl AsStrandDyn for StrandBuf { - fn as_strand_dyn(&self) -> Strand<'_> { self.borrow() } -} - -impl AsStrandDyn for &StrandBuf { - fn as_strand_dyn(&self) -> Strand<'_> { (*self).borrow() } -} - -impl AsStrandDyn for StrandCow<'_> { - fn as_strand_dyn(&self) -> Strand<'_> { +impl AsStrand for StrandBuf { + fn as_strand(&self) -> Strand<'_> { match self { - Self::Borrowed(s) => s.as_strand_dyn(), - Self::Owned(s) => s.as_strand_dyn(), + Self::Os(s) => Strand::Os(s), + Self::Utf8(s) => Strand::Utf8(s), + Self::Bytes(b) => Strand::Bytes(b), } } } + +impl AsStrand for &StrandBuf { + fn as_strand(&self) -> Strand<'_> { (**self).as_strand() } +} + +impl AsStrand for StrandCow<'_> { + fn as_strand(&self) -> Strand<'_> { + match self { + StrandCow::Borrowed(s) => *s, + StrandCow::Owned(s) => s.as_strand(), + } + } +} + +impl AsStrand for &StrandCow<'_> { + fn as_strand(&self) -> Strand<'_> { (**self).as_strand() } +} diff --git a/yazi-shared/src/strand/cow.rs b/yazi-shared/src/strand/cow.rs index 7f7fb567..58b85aa1 100644 --- a/yazi-shared/src/strand/cow.rs +++ b/yazi-shared/src/strand/cow.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}}; use anyhow::Result; -use crate::{IntoOsStr, scheme::SchemeKind, strand::{Strand, StrandBuf, StrandBufLike, StrandLike}}; +use crate::{IntoOsStr, scheme::SchemeKind, strand::{Strand, StrandBuf}}; // --- StrandCow pub enum StrandCow<'a> { @@ -41,16 +41,6 @@ impl PartialEq> for StrandCow<'_> { } impl<'a> StrandCow<'a> { - pub fn with(kind: SchemeKind, bytes: T) -> Result - where - T: Into>, - { - match kind { - SchemeKind::Regular | SchemeKind::Search | SchemeKind::Archive => Self::from_os_bytes(bytes), - SchemeKind::Sftp => Self::from_os_bytes(bytes), // FIXME - } - } - pub fn from_os_bytes(bytes: impl Into>) -> Result { Ok(match bytes.into().into_os_str()? { Cow::Borrowed(s) => Strand::Os(s).into(), @@ -65,11 +55,13 @@ impl<'a> StrandCow<'a> { } } - // FIXME: remove, instead implement StrandLike for StrandCow - pub fn encoded_bytes(&self) -> &[u8] { - match self { - Self::Borrowed(s) => s.encoded_bytes(), - Self::Owned(s) => s.encoded_bytes(), + pub fn with(kind: SchemeKind, bytes: T) -> Result + where + T: Into>, + { + match kind { + SchemeKind::Regular | SchemeKind::Search | SchemeKind::Archive => Self::from_os_bytes(bytes), + SchemeKind::Sftp => Self::from_os_bytes(bytes), // FIXME } } } diff --git a/yazi-shared/src/strand/dyn.rs b/yazi-shared/src/strand/dyn.rs index db36d266..5512cb7a 100644 --- a/yazi-shared/src/strand/dyn.rs +++ b/yazi-shared/src/strand/dyn.rs @@ -1,8 +1,6 @@ -use std::ffi::{OsStr, OsString}; +use std::{borrow::Cow, ffi::OsStr, fmt::Display}; -use serde::Serialize; - -use crate::{FromWtf8, path::PathDyn, scheme::SchemeKind, strand::{AsStrandDyn, StrandBufLike, StrandError, StrandKind, StrandLike}}; +use crate::{BytesExt, FromWtf8, strand::{AsStrand, StrandBuf, StrandError, StrandKind}}; // --- Strand #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -44,16 +42,6 @@ impl PartialEq<&str> for Strand<'_> { } } -impl<'a> Strand<'a> { - pub fn to_owned(self) -> StrandBuf { - match self { - Self::Os(s) => StrandBuf::Os(s.to_owned()), - Self::Utf8(s) => StrandBuf::Utf8(s.to_owned()), - Self::Bytes(b) => StrandBuf::Bytes(b.to_owned()), - } - } -} - impl<'a> Strand<'a> { #[inline] pub fn as_os(self) -> Result<&'a OsStr, StrandError> { @@ -94,6 +82,38 @@ impl<'a> Strand<'a> { unsafe { StrandBuf::from_encoded_bytes(self.kind(), out) }.into() } + pub fn contains(self, x: impl AsStrand) -> bool { + memchr::memmem::find(self.encoded_bytes(), x.as_strand().encoded_bytes()).is_some() + } + + pub fn display(self) -> impl Display { + struct D<'a>(Strand<'a>); + + impl<'a> Display for D<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Strand::Os(s) => s.display().fmt(f), + Strand::Utf8(s) => s.fmt(f), + Strand::Bytes(b) => b.display().fmt(f), + } + } + } + + D(self) + } + + pub fn encoded_bytes(self) -> &'a [u8] { + match self { + Self::Os(s) => s.as_encoded_bytes(), + Self::Utf8(s) => s.as_bytes(), + Self::Bytes(b) => b, + } + } + + pub fn eq_ignore_ascii_case(self, other: impl AsStrand) -> bool { + self.encoded_bytes().eq_ignore_ascii_case(other.as_strand().encoded_bytes()) + } + #[inline] pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: &'a [u8]) -> Self { match kind.into() { @@ -102,76 +122,34 @@ impl<'a> Strand<'a> { StrandKind::Bytes => Self::Bytes(bytes), } } -} -// --- StrandBuf -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] -#[serde(untagged)] -pub enum StrandBuf { - Os(OsString), - Utf8(String), - Bytes(Vec), -} + pub fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } -impl Default for StrandBuf { - fn default() -> Self { Self::Utf8(String::new()) } -} - -impl From for StrandBuf { - fn from(value: OsString) -> Self { Self::Os(value) } -} - -impl From for StrandBuf { - fn from(value: String) -> Self { Self::Utf8(value) } -} - -impl From> for StrandBuf { - fn from(value: PathDyn) -> Self { - match value { - PathDyn::Os(p) => Self::Os(p.as_os_str().to_owned()), - } - } -} - -impl PartialEq> for StrandBuf { - fn eq(&self, other: &Strand<'_>) -> bool { self.borrow() == *other } -} - -impl StrandBuf { - pub fn clear(&mut self) { + pub fn kind(self) -> StrandKind { match self { - Self::Os(buf) => buf.clear(), - Self::Utf8(buf) => buf.clear(), - Self::Bytes(buf) => buf.clear(), + Self::Os(_) => StrandKind::Os, + Self::Utf8(_) => StrandKind::Utf8, + Self::Bytes(_) => StrandKind::Bytes, } } - pub fn try_push(&mut self, s: T) -> Result<(), StrandError> - where - T: AsStrandDyn, - { - let s = s.as_strand_dyn(); - Ok(match self { - Self::Os(buf) => buf.push(s.as_os()?), - Self::Utf8(buf) => buf.push_str(s.as_utf8()?), - Self::Bytes(buf) => buf.extend(s.encoded_bytes()), - }) + pub fn len(self) -> usize { self.encoded_bytes().len() } + + pub fn starts_with(self, needle: impl AsStrand) -> bool { + self.encoded_bytes().starts_with(needle.as_strand().encoded_bytes()) } - pub fn with_capacity(kind: SchemeKind, capacity: usize) -> Self { - use SchemeKind as K; - match kind { - K::Regular | K::Search | K::Archive => Self::Os(OsString::with_capacity(capacity)), - K::Sftp => Self::Os(OsString::with_capacity(capacity)), // FIXME + pub fn to_owned(self) -> StrandBuf { + match self { + Self::Os(s) => StrandBuf::Os(s.to_owned()), + Self::Utf8(s) => StrandBuf::Utf8(s.to_owned()), + Self::Bytes(b) => StrandBuf::Bytes(b.to_owned()), } } - #[inline] - pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: Vec) -> Self { - match kind.into() { - StrandKind::Os => Self::Os(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), - StrandKind::Utf8 => Self::Utf8(unsafe { String::from_utf8_unchecked(bytes) }), - StrandKind::Bytes => Self::Bytes(bytes), - } + pub fn to_str(self) -> Result<&'a str, std::str::Utf8Error> { + str::from_utf8(self.encoded_bytes()) } + + pub fn to_string_lossy(self) -> Cow<'a, str> { String::from_utf8_lossy(self.encoded_bytes()) } } diff --git a/yazi-shared/src/strand/error.rs b/yazi-shared/src/strand/error.rs index d110a229..83e521da 100644 --- a/yazi-shared/src/strand/error.rs +++ b/yazi-shared/src/strand/error.rs @@ -20,5 +20,5 @@ impl From for StrandError { } impl From for std::io::Error { - fn from(err: StrandError) -> Self { std::io::Error::other(err) } + fn from(err: StrandError) -> Self { Self::other(err) } } diff --git a/yazi-shared/src/strand/like.rs b/yazi-shared/src/strand/like.rs new file mode 100644 index 00000000..e55f5d44 --- /dev/null +++ b/yazi-shared/src/strand/like.rs @@ -0,0 +1,47 @@ +use std::{borrow::Cow, ffi::OsStr, fmt::Display}; + +use crate::strand::{AsStrand, StrandBuf, StrandCow, StrandError, StrandKind}; + +// --- StrandLike +pub trait StrandLike: AsStrand { + fn as_os(&self) -> Result<&OsStr, StrandError> { self.as_strand().as_os() } + + fn as_utf8(&self) -> Result<&str, StrandError> { self.as_strand().as_utf8() } + + #[cfg(windows)] + fn backslash_to_slash(&self) -> StrandCow<'_> { self.as_strand().backslash_to_slash() } + + fn contains(&self, x: impl AsStrand) -> bool { self.as_strand().contains(x) } + + fn display(&self) -> impl Display { self.as_strand().display() } + + fn encoded_bytes(&self) -> &[u8] { self.as_strand().encoded_bytes() } + + fn eq_ignore_ascii_case(&self, other: impl AsStrand) -> bool { + self.as_strand().eq_ignore_ascii_case(other) + } + + fn is_empty(&self) -> bool { self.as_strand().is_empty() } + + fn kind(&self) -> StrandKind { self.as_strand().kind() } + + fn len(&self) -> usize { self.as_strand().len() } + + fn starts_with(&self, needle: impl AsStrand) -> bool { self.as_strand().starts_with(needle) } + + fn to_owned(&self) -> StrandBuf { self.as_strand().to_owned() } + + fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.as_strand().to_str() } + + fn to_string_lossy(&self) -> Cow<'_, str> { self.as_strand().to_string_lossy() } +} + +impl From<&S> for StrandBuf +where + S: StrandLike, +{ + fn from(value: &S) -> Self { value.to_owned() } +} + +impl StrandLike for StrandBuf {} +impl StrandLike for StrandCow<'_> {} diff --git a/yazi-shared/src/strand/mod.rs b/yazi-shared/src/strand/mod.rs index 25180bc2..ca1ffd07 100644 --- a/yazi-shared/src/strand/mod.rs +++ b/yazi-shared/src/strand/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(conversion cow error kind r#dyn strand view); +yazi_macro::mod_flat!(buf conversion cow error kind like r#dyn view); diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs deleted file mode 100644 index a3a1dc3e..00000000 --- a/yazi-shared/src/strand/strand.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}, fmt::Display}; - -use crate::{BytesExt, strand::{AsStrand, Strand, StrandBuf, StrandKind}}; - -// --- StrandLike -pub trait StrandLike<'a>: Copy { - type Owned: StrandBufLike; - - fn contains(self, x: impl AsStrand) -> bool { - memchr::memmem::find(self.encoded_bytes(), x.as_strand().encoded_bytes()).is_some() - } - - fn display(self) -> impl Display; - - fn encoded_bytes(self) -> &'a [u8]; - - fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } - - fn kind(self) -> StrandKind; - - fn len(self) -> usize { self.encoded_bytes().len() } - - fn to_str(self) -> Result<&'a str, std::str::Utf8Error> { str::from_utf8(self.encoded_bytes()) } - - fn to_string_lossy(self) -> Cow<'a, str> { String::from_utf8_lossy(self.encoded_bytes()) } - - fn eq_ignore_ascii_case(self, other: impl AsStrand) -> bool { - self.encoded_bytes().eq_ignore_ascii_case(other.as_strand().encoded_bytes()) - } - - fn starts_with(self, needle: impl AsStrand) -> bool { - self.encoded_bytes().starts_with(needle.as_strand().encoded_bytes()) - } -} - -impl<'a> StrandLike<'a> for &'a [u8] { - type Owned = Vec; - - fn display(self) -> impl Display { BytesExt::display(self) } - - fn encoded_bytes(self) -> &'a [u8] { self } - - fn kind(self) -> StrandKind { StrandKind::Bytes } -} - -impl<'a> StrandLike<'a> for &'a str { - type Owned = String; - - fn display(self) -> impl Display { self } - - fn encoded_bytes(self) -> &'a [u8] { self.as_bytes() } - - fn kind(self) -> StrandKind { StrandKind::Utf8 } -} - -impl<'a> StrandLike<'a> for &'a OsStr { - type Owned = OsString; - - fn display(self) -> impl Display { self.display() } - - fn encoded_bytes(self) -> &'a [u8] { self.as_encoded_bytes() } - - fn kind(self) -> StrandKind { StrandKind::Os } -} - -impl<'a> StrandLike<'a> for Strand<'a> { - type Owned = StrandBuf; - - fn display(self) -> impl Display { - struct D<'a>(Strand<'a>); - - impl<'a> Display for D<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.0 { - Strand::Os(s) => s.display().fmt(f), - Strand::Utf8(s) => s.fmt(f), - Strand::Bytes(b) => BytesExt::display(b).fmt(f), - } - } - } - - D(self) - } - - fn encoded_bytes(self) -> &'a [u8] { - match self { - Self::Os(s) => s.as_encoded_bytes(), - Self::Utf8(s) => s.as_bytes(), - Self::Bytes(b) => b, - } - } - - fn kind(self) -> StrandKind { - match self { - Self::Os(_) => StrandKind::Os, - Self::Utf8(_) => StrandKind::Utf8, - Self::Bytes(_) => StrandKind::Bytes, - } - } -} - -// --- StrandBufLike -pub trait StrandBufLike -where - Self: 'static + AsStrand, -{ - type Borrowed<'a>: StrandLike<'a>; - - fn borrow(&self) -> Self::Borrowed<'_>; - - fn encoded_bytes(&self) -> &[u8] { self.borrow().encoded_bytes() } - - fn into_encoded_bytes(self) -> Vec; - - fn is_empty(&self) -> bool { self.borrow().is_empty() } - - fn kind(&self) -> StrandKind { self.borrow().kind() } - - fn len(&self) -> usize { self.borrow().len() } - - fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.borrow().to_str() } - - fn to_string_lossy(&self) -> Cow<'_, str> { self.borrow().to_string_lossy() } -} - -impl StrandBufLike for Vec { - type Borrowed<'a> = &'a [u8]; - - fn borrow(&self) -> Self::Borrowed<'_> { self.as_slice() } - - fn into_encoded_bytes(self) -> Vec { self } -} - -impl StrandBufLike for String { - type Borrowed<'a> = &'a str; - - fn borrow(&self) -> Self::Borrowed<'_> { self.as_str() } - - fn into_encoded_bytes(self) -> Vec { self.into_bytes() } -} - -impl StrandBufLike for OsString { - type Borrowed<'a> = &'a OsStr; - - fn borrow(&self) -> Self::Borrowed<'_> { self.as_os_str() } - - fn into_encoded_bytes(self) -> Vec { self.into_encoded_bytes() } -} - -impl StrandBufLike for StrandBuf { - type Borrowed<'a> = Strand<'a>; - - fn borrow(&self) -> Self::Borrowed<'_> { - match self { - Self::Os(s) => Strand::Os(s.as_os_str()), - Self::Utf8(s) => Strand::Utf8(s.as_str()), - Self::Bytes(b) => Strand::Bytes(b), - } - } - - fn into_encoded_bytes(self) -> Vec { - match self { - Self::Os(s) => s.into_encoded_bytes(), - Self::Utf8(s) => s.into_bytes(), - Self::Bytes(b) => b, - } - } -} diff --git a/yazi-shared/src/strand/view.rs b/yazi-shared/src/strand/view.rs index d29d687a..454f713b 100644 --- a/yazi-shared/src/strand/view.rs +++ b/yazi-shared/src/strand/view.rs @@ -1,6 +1,6 @@ use std::ffi::OsStr; -use crate::{path::PathDyn, strand::{AsStrandDyn, Strand}}; +use crate::{path::PathDyn, strand::Strand}; // --- AsStrandView pub trait AsStrandView<'a, T> { @@ -42,10 +42,3 @@ impl<'a> AsStrandView<'a, Strand<'a>> for PathDyn<'a> { } } } - -impl<'a, T> AsStrandView<'a, Strand<'a>> for &'a T -where - T: AsStrandDyn, -{ - fn as_strand_view(self) -> Strand<'a> { self.as_strand_dyn() } -} diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 8f346907..4206a177 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt::{Debug, Formatter}, path::{Path, PathBuf}, str::From use anyhow::Result; use serde::{Deserialize, Serialize}; -use crate::{loc::LocBuf, path::{PathBufDyn, PathBufDynError, PathDyn, PathDynError, PathLike, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::SchemeKind, strand::AsStrandDyn, url::{AsUrl, Url, UrlCow, UrlLike}}; +use crate::{loc::LocBuf, path::{PathBufDyn, PathBufDynError, PathDyn, PathDynError, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::SchemeKind, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlLike}}; #[derive(Clone, Eq, Hash, PartialEq)] pub enum UrlBuf { @@ -18,8 +18,8 @@ impl Default for UrlBuf { fn default() -> Self { Self::Regular(Default::default()) } } -impl From<&UrlBuf> for UrlBuf { - fn from(url: &UrlBuf) -> Self { url.clone() } +impl From<&Self> for UrlBuf { + fn from(url: &Self) -> Self { url.clone() } } impl From> for UrlBuf { @@ -110,10 +110,10 @@ impl UrlBuf { #[inline] pub fn into_loc(self) -> PathBufDyn { match self { - Self::Regular(loc) => loc.into_path().into(), - Self::Search { loc, .. } => loc.into_path().into(), - Self::Archive { loc, .. } => loc.into_path().into(), - Self::Sftp { loc, .. } => loc.into_path().into(), + Self::Regular(loc) => loc.into_inner().into(), + Self::Search { loc, .. } => loc.into_inner().into(), + Self::Archive { loc, .. } => loc.into_inner().into(), + Self::Sftp { loc, .. } => loc.into_inner().into(), } } @@ -122,8 +122,8 @@ impl UrlBuf { if self.kind().is_local() { self.into_loc().into_os().ok() } else { None } } - pub fn try_set_name(&mut self, name: impl AsStrandDyn) -> Result<(), SetNameError> { - let name = name.as_strand_dyn(); + pub fn try_set_name(&mut self, name: impl AsStrand) -> Result<(), SetNameError> { + let name = name.as_strand(); Ok(match self { Self::Regular(loc) => loc.try_set_name(name.as_os()?)?, Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?, @@ -132,19 +132,17 @@ impl UrlBuf { }) } - pub fn rebase(&self, base: &Path) -> Result { - Ok(match self { - Self::Regular(loc) => Self::Regular(loc.rebase(base)?), + pub fn rebase(&self, base: &Path) -> Self { + match self { + Self::Regular(loc) => Self::Regular(loc.rebase(base)), Self::Search { loc, domain } => { - Self::Search { loc: loc.rebase(base)?, domain: domain.clone() } + Self::Search { loc: loc.rebase(base), domain: domain.clone() } } Self::Archive { loc, domain } => { - Self::Archive { loc: loc.rebase(base)?, domain: domain.clone() } + Self::Archive { loc: loc.rebase(base), domain: domain.clone() } } - Self::Sftp { loc, domain } => { - Self::Sftp { loc: loc.rebase(base)?, domain: domain.clone() } - } - }) + Self::Sftp { loc, domain } => Self::Sftp { loc: loc.rebase(base), domain: domain.clone() }, + } } } @@ -275,8 +273,10 @@ mod tests { ("archive://:1:1//a/b.zip/c", Some("archive:////a/b.zip")), ("archive:////a/b.zip", Some("/a")), // SFTP - ("sftp://remote:1:1//a/b", Some("sftp://remote//a")), + ("sftp://remote:3:1//a/b", Some("sftp://remote//a")), + ("sftp://remote:2:1//a", Some("sftp://remote//")), ("sftp://remote:1:1//a", Some("sftp://remote//")), + ("sftp://remote//a", Some("sftp://remote//")), ("sftp://remote:1//", None), ("sftp://remote//", None), // Relative diff --git a/yazi-shared/src/url/component.rs b/yazi-shared/src/url/component.rs index f7670adb..fdba0425 100644 --- a/yazi-shared/src/url/component.rs +++ b/yazi-shared/src/url/component.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, pa use anyhow::Result; -use crate::{path::{PathBufDyn, PathCow, PathLike}, scheme::SchemeRef, url::{Encode, Url, UrlBuf, UrlCow}}; +use crate::{path::{PathBufDyn, PathCow}, scheme::SchemeRef, url::{Encode, Url, UrlBuf, UrlCow}}; #[derive(Clone, Copy, Debug, PartialEq)] pub enum Component<'a> { diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs index 020402a4..176600b6 100644 --- a/yazi-shared/src/url/display.rs +++ b/yazi-shared/src/url/display.rs @@ -1,4 +1,4 @@ -use crate::{path::PathLike, scheme::Encode, url::Url}; +use crate::{scheme::Encode, url::Url}; pub struct Display<'a> { inner: Url<'a>, diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index a857ea40..fe2a9d3f 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Display}; use percent_encoding::{CONTROLS, percent_encode}; -use crate::{path::PathLike, url::Url}; +use crate::url::Url; // --- Tilded #[derive(Clone, Copy)] diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index b8b8aacc..717cb0b7 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; use anyhow::Result; -use crate::{loc::Loc, path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, scheme::{SchemeKind, SchemeRef}, strand::{AsStrandDyn, Strand}, url::{Components, Display, Url, UrlBuf, UrlCow}}; +use crate::{loc::Loc, path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, scheme::{SchemeKind, SchemeRef}, strand::{AsStrand, Strand}, url::{Components, Display, Url, UrlBuf, UrlCow}}; // --- AsUrl pub trait AsUrl { @@ -103,7 +103,7 @@ where // UrlLike pub trait UrlLike where - Self: AsUrl + Sized, + Self: AsUrl, { fn as_local(&self) -> Option<&Path> { self.as_url().as_local() } @@ -147,7 +147,7 @@ where self.as_url().try_ends_with(child) } - fn try_join(&self, path: impl AsStrandDyn) -> Result { + fn try_join(&self, path: impl AsStrand) -> Result { self.as_url().try_join(path) } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 71f67e84..d0e86fbb 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -5,7 +5,7 @@ use hashbrown::Equivalent; use serde::Serialize; use super::Encode as EncodeUrl; -use crate::{loc::{Loc, LocBuf}, path::{AsPathDyn, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathBufLike, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError}, pool::InternStr, scheme::{Encode as EncodeScheme, SchemeCow, SchemeKind, SchemeRef}, strand::{AsStrandDyn, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; +use crate::{loc::{Loc, LocBuf}, path::{AsPath, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError}, pool::InternStr, scheme::{Encode as EncodeScheme, SchemeCow, SchemeKind, SchemeRef}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub enum Url<'a> { @@ -58,14 +58,13 @@ impl<'a> Url<'a> { } } - // FIXME: add to UrlLike trait #[inline] pub fn loc(self) -> PathDyn<'a> { match self { - Self::Regular(loc) => loc.as_path_dyn(), - Self::Search { loc, .. } => loc.as_path_dyn(), - Self::Archive { loc, .. } => loc.as_path_dyn(), - Self::Sftp { loc, .. } => loc.as_path_dyn(), + Self::Regular(loc) => loc.as_path(), + Self::Search { loc, .. } => loc.as_path(), + Self::Archive { loc, .. } => loc.as_path(), + Self::Sftp { loc, .. } => loc.as_path(), } } @@ -105,8 +104,8 @@ impl<'a> Url<'a> { #[inline] pub fn to_owned(self) -> UrlBuf { self.into() } - pub fn try_join(self, path: impl AsStrandDyn) -> Result { - let joined = self.loc().try_join(&path)?; + pub fn try_join(self, path: impl AsStrand) -> Result { + let joined = self.loc().try_join(path)?; Ok(match self { Self::Regular(_) => UrlBuf::Regular(joined.into_os()?.into()), @@ -177,7 +176,7 @@ impl<'a> Url<'a> { use Url as U; let base = base.as_url(); - let prefix = self.loc().try_strip_prefix(base.loc())?.into(); + let prefix = self.loc().try_strip_prefix(base.loc())?; match (self, base) { // Same scheme @@ -221,50 +220,50 @@ impl<'a> Url<'a> { #[inline] pub fn uri(self) -> PathDyn<'a> { match self { - Self::Regular(loc) => loc.uri().as_path_dyn(), - Self::Search { loc, .. } => loc.uri().as_path_dyn(), - Self::Archive { loc, .. } => loc.uri().as_path_dyn(), - Self::Sftp { loc, .. } => loc.uri().as_path_dyn(), + Self::Regular(loc) => loc.uri().as_path(), + Self::Search { loc, .. } => loc.uri().as_path(), + Self::Archive { loc, .. } => loc.uri().as_path(), + Self::Sftp { loc, .. } => loc.uri().as_path(), } } #[inline] pub fn urn(self) -> PathDyn<'a> { match self { - Self::Regular(loc) => loc.urn().as_path_dyn(), - Self::Search { loc, .. } => loc.urn().as_path_dyn(), - Self::Archive { loc, .. } => loc.urn().as_path_dyn(), - Self::Sftp { loc, .. } => loc.urn().as_path_dyn(), + Self::Regular(loc) => loc.urn().as_path(), + Self::Search { loc, .. } => loc.urn().as_path(), + Self::Archive { loc, .. } => loc.urn().as_path(), + Self::Sftp { loc, .. } => loc.urn().as_path(), } } #[inline] pub fn name(self) -> Option> { Some(match self { - Self::Regular(loc) => loc.name()?.as_strand_dyn(), - Self::Search { loc, .. } => loc.name()?.as_strand_dyn(), - Self::Archive { loc, .. } => loc.name()?.as_strand_dyn(), - Self::Sftp { loc, .. } => loc.name()?.as_strand_dyn(), + Self::Regular(loc) => loc.file_name()?.as_strand(), + Self::Search { loc, .. } => loc.file_name()?.as_strand(), + Self::Archive { loc, .. } => loc.file_name()?.as_strand(), + Self::Sftp { loc, .. } => loc.file_name()?.as_strand(), }) } #[inline] pub fn stem(self) -> Option> { Some(match self { - Self::Regular(loc) => loc.stem()?.as_strand_dyn(), - Self::Search { loc, .. } => loc.stem()?.as_strand_dyn(), - Self::Archive { loc, .. } => loc.stem()?.as_strand_dyn(), - Self::Sftp { loc, .. } => loc.stem()?.as_strand_dyn(), + Self::Regular(loc) => loc.file_stem()?.as_strand(), + Self::Search { loc, .. } => loc.file_stem()?.as_strand(), + Self::Archive { loc, .. } => loc.file_stem()?.as_strand(), + Self::Sftp { loc, .. } => loc.file_stem()?.as_strand(), }) } #[inline] pub fn ext(self) -> Option> { Some(match self { - Self::Regular(loc) => loc.ext()?.as_strand_dyn(), - Self::Search { loc, .. } => loc.ext()?.as_strand_dyn(), - Self::Archive { loc, .. } => loc.ext()?.as_strand_dyn(), - Self::Sftp { loc, .. } => loc.ext()?.as_strand_dyn(), + Self::Regular(loc) => loc.extension()?.as_strand(), + Self::Search { loc, .. } => loc.extension()?.as_strand(), + Self::Archive { loc, .. } => loc.extension()?.as_strand(), + Self::Sftp { loc, .. } => loc.extension()?.as_strand(), }) } @@ -291,11 +290,11 @@ impl<'a> Url<'a> { match self { Self::Regular(loc) | Self::Search { loc, .. } | Self::Archive { loc, .. } => { let (base, rest, urn) = loc.triple(); - (base.as_path_dyn(), rest.as_path_dyn(), urn.as_path_dyn()) + (base.as_path(), rest.as_path(), urn.as_path()) } Self::Sftp { loc, .. } => { let (base, rest, urn) = loc.triple(); - (base.as_path_dyn(), rest.as_path_dyn(), urn.as_path_dyn()) + (base.as_path(), rest.as_path(), urn.as_path()) } } } diff --git a/yazi-shared/src/wtf8.rs b/yazi-shared/src/wtf8.rs index 4364f80a..cf785aba 100644 --- a/yazi-shared/src/wtf8.rs +++ b/yazi-shared/src/wtf8.rs @@ -31,12 +31,12 @@ impl FromWtf8 for OsStr { #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; - Ok(OsStr::from_bytes(wtf8)) + Ok(Self::from_bytes(wtf8)) } #[cfg(windows)] { // FIXME: validate WTF-8 - Ok(unsafe { OsStr::from_encoded_bytes_unchecked(wtf8) }) + Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) } } } @@ -57,12 +57,12 @@ impl FromWtf8Vec for OsString { #[cfg(unix)] { use std::os::unix::ffi::OsStringExt; - Ok(OsString::from_vec(wtf8)) + Ok(Self::from_vec(wtf8)) } #[cfg(windows)] { // FIXME: validate WTF-8 - Ok(unsafe { OsString::from_encoded_bytes_unchecked(wtf8) }) + Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) } } } diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml index 069ae5c2..6933f1ab 100644 --- a/yazi-shim/Cargo.toml +++ b/yazi-shim/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi crate shims" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-macro = { path = "../yazi-macro", version = "25.9.15" } diff --git a/yazi-term/Cargo.toml b/yazi-term/Cargo.toml index a75c8946..d522deb5 100644 --- a/yazi-term/Cargo.toml +++ b/yazi-term/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi terminal extensions" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-macro = { path = "../yazi-macro", version = "25.9.15" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" } diff --git a/yazi-term/src/background.rs b/yazi-term/src/background.rs index de20c482..0ec18df7 100644 --- a/yazi-term/src/background.rs +++ b/yazi-term/src/background.rs @@ -3,7 +3,7 @@ pub struct SetBackground(pub bool, pub String); impl crossterm::Command for SetBackground { fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { if self.1.is_empty() { - return Ok(()); + Ok(()) } else if self.0 { write!(f, "\x1b]11;{}\x1b\\", self.1) } else { diff --git a/yazi-vfs/Cargo.toml b/yazi-vfs/Cargo.toml index 4e755ae8..3bed8acd 100644 --- a/yazi-vfs/Cargo.toml +++ b/yazi-vfs/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi virtual file system" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-config = { path = "../yazi-config", version = "25.9.15" } yazi-fs = { path = "../yazi-fs", version = "25.9.15" } diff --git a/yazi-vfs/src/fns.rs b/yazi-vfs/src/fns.rs index 5bf08c25..25115c72 100644 --- a/yazi-vfs/src/fns.rs +++ b/yazi-vfs/src/fns.rs @@ -3,7 +3,7 @@ use std::io; use tokio::{select, sync::{mpsc, oneshot}}; use yazi_fs::provider::Attrs; use yazi_macro::ok_or_not_found; -use yazi_shared::{strand::{StrandBuf, StrandBufLike, StrandLike}, url::{AsUrl, Url, UrlBuf, UrlLike}}; +use yazi_shared::{strand::{StrandBuf, StrandLike}, url::{AsUrl, Url, UrlBuf, UrlLike}}; use crate::provider; @@ -91,7 +91,7 @@ fn _copy_with_progress(from: Url, to: Url, attrs: Attrs) -> mpsc::Receiver(original: P, link: U) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, U: AsUrl, { Providers::new(link.as_url()).await?.symlink_dir(original).await @@ -207,7 +207,7 @@ where pub async fn symlink_file(original: P, link: U) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, U: AsUrl, { Providers::new(link.as_url()).await?.symlink_file(original).await diff --git a/yazi-vfs/src/provider/providers.rs b/yazi-vfs/src/provider/providers.rs index e2065160..d0ec7f90 100644 --- a/yazi-vfs/src/provider/providers.rs +++ b/yazi-vfs/src/provider/providers.rs @@ -1,7 +1,7 @@ use std::io; use yazi_fs::{cha::Cha, provider::{Attrs, Provider}}; -use yazi_shared::{path::{AsPathDyn, PathBufDyn}, url::{Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::{AsPath, PathBufDyn}, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] pub(super) enum Providers<'a> { @@ -39,7 +39,7 @@ impl<'a> Provider for Providers<'a> { async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsPathDyn, + P: AsPath, { match self { Self::Local(p) => p.copy(to, attrs).await, @@ -70,7 +70,7 @@ impl<'a> Provider for Providers<'a> { async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { match self { Self::Local(p) => p.hard_link(to).await, @@ -141,7 +141,7 @@ impl<'a> Provider for Providers<'a> { async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { match self { Self::Local(p) => p.rename(to).await, @@ -151,7 +151,7 @@ impl<'a> Provider for Providers<'a> { async fn symlink(&self, original: P, is_dir: F) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, F: AsyncFnOnce() -> io::Result, { match self { @@ -162,7 +162,7 @@ impl<'a> Provider for Providers<'a> { async fn symlink_dir

(&self, original: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { match self { Self::Local(p) => p.symlink_dir(original).await, @@ -172,7 +172,7 @@ impl<'a> Provider for Providers<'a> { async fn symlink_file

(&self, original: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { match self { Self::Local(p) => p.symlink_file(original).await, diff --git a/yazi-vfs/src/provider/sftp/gate.rs b/yazi-vfs/src/provider/sftp/gate.rs index 795c41b4..4585b531 100644 --- a/yazi-vfs/src/provider/sftp/gate.rs +++ b/yazi-vfs/src/provider/sftp/gate.rs @@ -12,24 +12,24 @@ pub struct Gate(crate::provider::Gate); impl From for Flags { fn from(Gate(g): Gate) -> Self { - let mut flags = Flags::empty(); + let mut flags = Self::empty(); if g.append { - flags |= Flags::APPEND; + flags |= Self::APPEND; } if g.create { - flags |= Flags::CREATE; + flags |= Self::CREATE; } if g.create_new { - flags |= Flags::CREATE | Flags::EXCLUDE; + flags |= Self::CREATE | Self::EXCLUDE; } if g.read { - flags |= Flags::READ; + flags |= Self::READ; } if g.truncate { - flags |= Flags::TRUNCATE; + flags |= Self::TRUNCATE; } if g.write { - flags |= Flags::WRITE; + flags |= Self::WRITE; } flags } diff --git a/yazi-vfs/src/provider/sftp/metadata.rs b/yazi-vfs/src/provider/sftp/metadata.rs index 5e2282a1..23c2ee9e 100644 --- a/yazi-vfs/src/provider/sftp/metadata.rs +++ b/yazi-vfs/src/provider/sftp/metadata.rs @@ -39,7 +39,7 @@ impl TryFrom<(&OsStr, &yazi_sftp::fs::Attrs)> for Cha { let kind = if name.as_encoded_bytes().starts_with(b".") { ChaKind::HIDDEN } else { ChaKind::empty() }; - Ok(Cha(yazi_fs::cha::Cha { + Ok(Self(yazi_fs::cha::Cha { kind, mode: ChaMode::try_from(attrs)?.0, len: attrs.size.unwrap_or(0), diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index b521dfe6..6bb37475 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -4,7 +4,7 @@ use tokio::io::{AsyncWriteExt, BufReader, BufWriter}; use yazi_config::vfs::{ProviderSftp, Vfs}; use yazi_fs::provider::{DirReader, FileHolder, Provider}; use yazi_sftp::fs::{Attrs, Flags}; -use yazi_shared::{path::{AsPathDyn, PathBufDyn}, pool::InternStr, strand::StrandLike, url::{Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{path::{AsPath, PathBufDyn}, pool::InternStr, url::{Url, UrlBuf, UrlCow, UrlLike}}; use super::Cha; use crate::provider::sftp::Conn; @@ -69,9 +69,9 @@ impl<'a> Provider for Sftp<'a> { async fn copy

(&self, to: P, attrs: yazi_fs::provider::Attrs) -> io::Result where - P: AsPathDyn, + P: AsPath, { - let to = to.as_path_dyn().as_os()?; + let to = to.as_path().as_os()?; let attrs = Attrs::from(super::Attrs(attrs)); let op = self.op().await?; @@ -94,9 +94,9 @@ impl<'a> Provider for Sftp<'a> { async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { - let to = to.as_path_dyn().as_os()?; + let to = to.as_path().as_os()?; Ok(self.op().await?.hardlink(self.path, to).await?) } @@ -113,7 +113,7 @@ impl<'a> Provider for Sftp<'a> { } Url::Sftp { loc, domain } => { let (name, config) = Vfs::provider::<&ProviderSftp>(domain).await?; - Ok(Self::Me { url, path: loc.as_path(), name, config }) + Ok(Self::Me { url, path: loc.as_inner(), name, config }) } } } @@ -135,9 +135,9 @@ impl<'a> Provider for Sftp<'a> { async fn rename

(&self, to: P) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, { - let to = to.as_path_dyn().as_os()?; + let to = to.as_path().as_os()?; let op = self.op().await?; match op.rename_posix(self.path, &to).await { @@ -153,10 +153,10 @@ impl<'a> Provider for Sftp<'a> { async fn symlink(&self, original: P, _is_dir: F) -> io::Result<()> where - P: AsPathDyn, + P: AsPath, F: AsyncFnOnce() -> io::Result, { - let original = original.as_path_dyn().as_os()?; + let original = original.as_path().as_os()?; Ok(self.op().await?.symlink(&original, self.path).await?) } diff --git a/yazi-watcher/Cargo.toml b/yazi-watcher/Cargo.toml index 00a6aa2b..ed9af0ba 100644 --- a/yazi-watcher/Cargo.toml +++ b/yazi-watcher/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi file watcher" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-adapter = { path = "../yazi-adapter", version = "25.9.15" } yazi-dds = { path = "../yazi-dds", version = "25.9.15" } diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index 67896007..2c317c3c 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -7,7 +7,7 @@ use tokio::{pin, sync::mpsc::{self, UnboundedReceiver}}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tracing::error; use yazi_fs::{File, FilesOp, provider}; -use yazi_shared::{path::PathLike, url::{UrlBuf, UrlLike}}; +use yazi_shared::url::{UrlBuf, UrlLike}; use yazi_vfs::VfsFile; use crate::{Reporter, WATCHER}; @@ -70,18 +70,18 @@ impl Local { for u in urls { let Some((parent, urn)) = u.pair() else { continue }; let Ok(file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [urn.owned()].into())); + ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); continue; }; if let Some(p) = file.url.as_local() && !provider::local::must_case_match(p).await { - ops.push(FilesOp::Deleting(parent.into(), [urn.owned()].into())); + ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); continue; } - ops.push(FilesOp::Upserting(parent.into(), [(urn.owned(), file)].into())); + ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); } FilesOp::mutate(ops); diff --git a/yazi-watcher/src/remote/remote.rs b/yazi-watcher/src/remote/remote.rs index 55269834..dde45220 100644 --- a/yazi-watcher/src/remote/remote.rs +++ b/yazi-watcher/src/remote/remote.rs @@ -6,7 +6,7 @@ use tokio::{pin, sync::mpsc::UnboundedReceiver}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_fs::{File, FilesOp}; use yazi_proxy::MgrProxy; -use yazi_shared::{path::PathLike, url::{Url, UrlBuf, UrlLike}}; +use yazi_shared::url::{Url, UrlBuf, UrlLike}; use yazi_vfs::VfsFile; use crate::{Reporter, WATCHER}; @@ -39,14 +39,14 @@ impl Remote { for u in urls { let Some((parent, urn)) = u.pair() else { continue }; let Ok(mut file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [urn.owned()].into())); + ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); continue; }; let is_file = file.is_file(); file.cha.ctime = Some(SystemTime::now()); - ops.push(FilesOp::Upserting(parent.into(), [(urn.owned(), file)].into())); + ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); if is_file { ups.push(u); } diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index 9fc16c0f..5b2f4ac0 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -1,5 +1,5 @@ use tokio::sync::mpsc; -use yazi_shared::{path::PathLike, scheme::SchemeKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{scheme::SchemeKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use crate::{WATCHED, local::LINKED}; diff --git a/yazi-widgets/Cargo.toml b/yazi-widgets/Cargo.toml index 815eb3c0..b7909ba5 100644 --- a/yazi-widgets/Cargo.toml +++ b/yazi-widgets/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi user interface widgets" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-binding = { path = "../yazi-binding", version = "25.9.15" } yazi-config = { path = "../yazi-config", version = "25.9.15" } From a08b345a02c6b4c65239a0522f67e77a0132e88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Thu, 20 Nov 2025 09:00:01 +0800 Subject: [PATCH 04/53] docs: document the applicability of all `yazi-*` crates (#3345) --- Cargo.lock | 371 ++++++++++++++-------------- Cargo.toml | 2 +- yazi-actor/README.md | 5 + yazi-adapter/Cargo.toml | 4 +- yazi-adapter/README.md | 5 + yazi-adapter/src/drivers/sixel.rs | 21 +- yazi-binding/README.md | 5 + yazi-boot/Cargo.toml | 2 +- yazi-boot/README.md | 5 + yazi-build/README.md | 5 + yazi-cli/Cargo.toml | 2 +- yazi-cli/README.md | 5 + yazi-codegen/Cargo.toml | 4 +- yazi-codegen/README.md | 5 + yazi-config/README.md | 5 + yazi-core/README.md | 5 + yazi-dds/README.md | 5 + yazi-ffi/README.md | 5 + yazi-fm/README.md | 5 + yazi-fs/README.md | 5 + yazi-macro/README.md | 5 + yazi-packing/README.md | 5 + yazi-parser/README.md | 5 + yazi-plugin/README.md | 5 + yazi-proxy/README.md | 5 + yazi-scheduler/README.md | 5 + yazi-scheduler/src/process/shell.rs | 2 +- yazi-shared/README.md | 5 + yazi-shim/README.md | 5 + yazi-term/README.md | 5 + yazi-vfs/README.md | 5 + yazi-watcher/README.md | 5 + yazi-widgets/README.md | 5 + 33 files changed, 333 insertions(+), 200 deletions(-) create mode 100644 yazi-actor/README.md create mode 100644 yazi-adapter/README.md create mode 100644 yazi-binding/README.md create mode 100644 yazi-boot/README.md create mode 100644 yazi-build/README.md create mode 100644 yazi-cli/README.md create mode 100644 yazi-codegen/README.md create mode 100644 yazi-config/README.md create mode 100644 yazi-core/README.md create mode 100644 yazi-dds/README.md create mode 100644 yazi-ffi/README.md create mode 100644 yazi-fm/README.md create mode 100644 yazi-fs/README.md create mode 100644 yazi-macro/README.md create mode 100644 yazi-packing/README.md create mode 100644 yazi-parser/README.md create mode 100644 yazi-plugin/README.md create mode 100644 yazi-proxy/README.md create mode 100644 yazi-scheduler/README.md create mode 100644 yazi-shared/README.md create mode 100644 yazi-shim/README.md create mode 100644 yazi-term/README.md create mode 100644 yazi-vfs/README.md create mode 100644 yazi-watcher/README.md create mode 100644 yazi-widgets/README.md diff --git a/Cargo.lock b/Cargo.lock index d8013395..6eaccab4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" +dependencies = [ + "as-slice", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -130,22 +139,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -154,15 +163,6 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - [[package]] name = "arbitrary" version = "1.4.2" @@ -204,6 +204,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "async-priority-channel" version = "0.2.0" @@ -219,6 +228,26 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + [[package]] name = "av1-grain" version = "0.2.5" @@ -328,9 +357,12 @@ dependencies = [ [[package]] name = "bitstream-io" -version = "2.6.0" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] [[package]] name = "bitvec" @@ -393,9 +425,9 @@ dependencies = [ [[package]] name = "built" -version = "0.7.7" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" @@ -414,6 +446,20 @@ name = "bytemuck" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder" @@ -429,9 +475,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cassowary" @@ -459,9 +505,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.44" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -469,16 +515,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -527,9 +563,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -537,9 +573,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -549,9 +585,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.60" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" +checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" dependencies = [ "clap", ] @@ -682,6 +718,15 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -800,9 +845,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -908,9 +953,9 @@ dependencies = [ [[package]] name = "delegate" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6178a82cf56c836a3ba61a7935cdb1c49bfaa6fa4327cd5bf554a503087de26b" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", @@ -1149,9 +1194,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ "serde", "serde_core", @@ -1187,15 +1232,14 @@ dependencies = [ [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", "lebe", "miniz_oxide", - "rayon-core", "smallvec", "zune-inflate", ] @@ -1254,9 +1298,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" @@ -1392,9 +1436,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1438,9 +1482,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +checksum = "f954a9e9159ec994f73a30a12b96a702dde78f5547bcb561174597924f7d4162" dependencies = [ "color_quant", "weezl", @@ -1630,9 +1674,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "image" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", @@ -1775,15 +1819,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -2019,9 +2054,9 @@ dependencies = [ [[package]] name = "luajit-src" -version = "210.6.2+25a61a1" +version = "210.6.4+e17ee83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b3e51e810ed7940b3d057d4cb63ef5faa8261fc8f46fb1905947a22f1c89ec" +checksum = "35a0ceb2a395ffa403a863adcf365e82cc8d8338ac7f5f949b9df5ca3de251e1" dependencies = [ "cc", "which", @@ -2233,9 +2268,9 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ "lazy_static", "libm", @@ -2413,15 +2448,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-float" version = "5.1.0" @@ -2494,8 +2520,9 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" dependencies = [ - "approx", + "bytemuck", "fast-srgb8", + "libm", "palette_derive", ] @@ -2787,14 +2814,17 @@ dependencies = [ [[package]] name = "quantette" -version = "0.3.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3ba8b34ab55d43cea1f7c183ef3c124cb96fe935c6ec5c6d97127ee340c96e" +checksum = "c98fecda8b16396ff9adac67644a523dd1778c42b58606a29df5c31ca925d174" dependencies = [ "bitvec", + "bytemuck", + "libm", "num-traits", - "ordered-float 4.6.0", + "ordered-float 5.1.0", "palette", + "ref-cast", "wide", ] @@ -2806,18 +2836,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -2918,19 +2948,21 @@ dependencies = [ [[package]] name = "rav1e" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" dependencies = [ + "aligned-vec", "arbitrary", "arg_enum_proc_macro", "arrayvec", + "av-scenechange", "av1-grain", "bitstream-io", "built", "cfg-if", "interpolate_name", - "itertools 0.12.1", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -2939,23 +2971,21 @@ dependencies = [ "noop_proc_macro", "num-derive", "num-traits", - "once_cell", "paste", "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.2", + "rand_chacha 0.9.0", "simd_helpers", - "system-deps", - "thiserror 1.0.69", + "thiserror 2.0.17", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.20" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" dependencies = [ "avif-serialize", "imgref", @@ -2965,6 +2995,16 @@ dependencies = [ "rgb", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + [[package]] name = "rayon-core" version = "1.13.0" @@ -2995,6 +3035,26 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.2" @@ -3056,9 +3116,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest", @@ -3224,9 +3284,9 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "safe_arch" -version = "0.7.4" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +checksum = "629516c85c29fe757770fa03f2074cf1eac43d44c02a3de9fc2ef7b0e207dfdd" dependencies = [ "bytemuck", ] @@ -3339,15 +3399,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.0.3" @@ -3534,6 +3585,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3576,9 +3633,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.108" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -3605,31 +3662,12 @@ dependencies = [ "walkdir", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml 0.8.23", - "version-compare", -] - [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "thiserror" version = "1.0.69" @@ -3784,18 +3822,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - [[package]] name = "toml" version = "0.9.8" @@ -3804,22 +3830,13 @@ checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned", + "toml_datetime", "toml_parser", "toml_writer", "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "0.7.3" @@ -3829,19 +3846,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "winnow", -] - [[package]] name = "toml_parser" version = "1.0.4" @@ -4109,12 +4113,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - [[package]] name = "version_check" version = "0.9.5" @@ -4216,9 +4214,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "which" @@ -4233,9 +4231,9 @@ dependencies = [ [[package]] name = "wide" -version = "0.7.33" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +checksum = "13ca908d26e4786149c48efcf6c0ea09ab0e06d1fe3c17dc1b4b0f1ca4a7e788" dependencies = [ "bytemuck", "safe_arch", @@ -4612,9 +4610,6 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] [[package]] name = "winsafe" @@ -4637,6 +4632,12 @@ dependencies = [ "tap", ] +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yazi-actor" version = "25.9.15" @@ -4753,7 +4754,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "toml 0.9.8", + "toml", "twox-hash", "vergen-gitcl", "yazi-boot", @@ -4786,7 +4787,7 @@ dependencies = [ "regex", "serde", "tokio", - "toml 0.9.8", + "toml", "tracing", "yazi-codegen", "yazi-fs", @@ -5189,9 +5190,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zune-core" -version = "0.4.12" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" [[package]] name = "zune-inflate" @@ -5204,9 +5205,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.21" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 3b04c220..09e0246c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ ansi-to-tui = "7.0.0" anyhow = "1.0.100" base64 = "0.22.1" bitflags = { version = "2.10.0", features = [ "serde" ] } -clap = { version = "4.5.51", features = [ "derive" ] } +clap = { version = "4.5.53", features = [ "derive" ] } core-foundation-sys = "0.8.7" crossterm = { version = "0.29.0", features = [ "event-stream" ] } dirs = "6.0.0" diff --git a/yazi-actor/README.md b/yazi-actor/README.md new file mode 100644 index 00000000..d768a010 --- /dev/null +++ b/yazi-actor/README.md @@ -0,0 +1,5 @@ +# yazi-actor + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index 32010648..9a7bd550 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -20,9 +20,9 @@ ansi-to-tui = { workspace = true } anyhow = { workspace = true } base64 = { workspace = true } crossterm = { workspace = true } -image = { version = "0.25.8", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] } +image = { version = "0.25.9", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] } palette = { version = "0.7.6", default-features = false } -quantette = { version = "0.3.0", default-features = false } +quantette = { version = "0.5.1", default-features = false } ratatui = { workspace = true } scopeguard = { workspace = true } tokio = { workspace = true } diff --git a/yazi-adapter/README.md b/yazi-adapter/README.md new file mode 100644 index 00000000..de79a81f --- /dev/null +++ b/yazi-adapter/README.md @@ -0,0 +1,5 @@ +# yazi-adapter + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-adapter/src/drivers/sixel.rs b/yazi-adapter/src/drivers/sixel.rs index d8e4ce8e..8c80d7e8 100644 --- a/yazi-adapter/src/drivers/sixel.rs +++ b/yazi-adapter/src/drivers/sixel.rs @@ -4,13 +4,18 @@ use anyhow::{Result, bail}; use crossterm::{cursor::MoveTo, queue}; use image::{DynamicImage, GenericImageView, RgbImage}; use palette::{Srgb, cast::ComponentsAs}; -use quantette::{ColorSlice, PaletteSize, QuantizeOutput, wu::UIntBinner}; +use quantette::{PaletteSize, color_map::IndexedColorMap, wu::{BinnerU8x3, WuU8x3}}; use ratatui::layout::Rect; use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter}; pub(crate) struct Sixel; +struct QuantizeOutput { + indices: Vec, + palette: Vec, +} + impl Sixel { pub(crate) async fn image_show(path: PathBuf, max: Rect) -> Result { let img = Image::downscale(path, max).await?; @@ -110,12 +115,14 @@ impl Sixel { fn quantify(rgb: &RgbImage, alpha: bool) -> Result>> { let buf = &rgb.as_raw()[..(rgb.pixels().len() * 3)]; - let slice: ColorSlice> = buf.components_as().try_into()?; + let colors: &[Srgb] = buf.components_as(); - Ok(quantette::wu::indexed_palette( - &slice, - PaletteSize::try_from(256u16 - alpha as u16)?, - &UIntBinner::<32>, - )) + let wu = WuU8x3::run_slice(colors, BinnerU8x3::rgb())?; + let color_map = wu.color_map(PaletteSize::try_from(256u16 - alpha as u16)?); + + Ok(QuantizeOutput { + indices: color_map.map_to_indices(colors), + palette: color_map.into_palette().into_vec(), + }) } } diff --git a/yazi-binding/README.md b/yazi-binding/README.md new file mode 100644 index 00000000..bc60416b --- /dev/null +++ b/yazi-binding/README.md @@ -0,0 +1,5 @@ +# yazi-binding + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 6670c893..724e99a3 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -28,7 +28,7 @@ yazi-shared = { path = "../yazi-shared", version = "25.9.15" } # External dependencies clap = { workspace = true } -clap_complete = "4.5.60" +clap_complete = "4.5.61" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.5.10" vergen-gitcl = { version = "1.0.8", features = [ "build", "rustc" ] } diff --git a/yazi-boot/README.md b/yazi-boot/README.md new file mode 100644 index 00000000..ae559d9f --- /dev/null +++ b/yazi-boot/README.md @@ -0,0 +1,5 @@ +# yazi-boot + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-build/README.md b/yazi-build/README.md new file mode 100644 index 00000000..4d40eb8e --- /dev/null +++ b/yazi-build/README.md @@ -0,0 +1,5 @@ +# yazi-build + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 2fdbe660..9ea61a80 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -41,7 +41,7 @@ yazi-shared = { path = "../yazi-shared", version = "25.9.15" } # External build dependencies anyhow = { workspace = true } clap = { workspace = true } -clap_complete = "4.5.60" +clap_complete = "4.5.61" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.5.10" serde_json = { workspace = true } diff --git a/yazi-cli/README.md b/yazi-cli/README.md new file mode 100644 index 00000000..646e0c73 --- /dev/null +++ b/yazi-cli/README.md @@ -0,0 +1,5 @@ +# yazi-cli + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index ff14e7d7..c39c5fce 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -13,5 +13,5 @@ proc-macro = true [dependencies] # External dependencies -syn = { version = "2.0.108", features = [ "full" ] } -quote = "1.0.41" +syn = { version = "2.0.110", features = [ "full" ] } +quote = "1.0.42" diff --git a/yazi-codegen/README.md b/yazi-codegen/README.md new file mode 100644 index 00000000..f7a66057 --- /dev/null +++ b/yazi-codegen/README.md @@ -0,0 +1,5 @@ +# yazi-codegen + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-config/README.md b/yazi-config/README.md new file mode 100644 index 00000000..4ca9feee --- /dev/null +++ b/yazi-config/README.md @@ -0,0 +1,5 @@ +# yazi-config + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-core/README.md b/yazi-core/README.md new file mode 100644 index 00000000..2485ec26 --- /dev/null +++ b/yazi-core/README.md @@ -0,0 +1,5 @@ +# yazi-core + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-dds/README.md b/yazi-dds/README.md new file mode 100644 index 00000000..b60b5478 --- /dev/null +++ b/yazi-dds/README.md @@ -0,0 +1,5 @@ +# yazi-dds + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-ffi/README.md b/yazi-ffi/README.md new file mode 100644 index 00000000..9531636e --- /dev/null +++ b/yazi-ffi/README.md @@ -0,0 +1,5 @@ +# yazi-ffi + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-fm/README.md b/yazi-fm/README.md new file mode 100644 index 00000000..697151b4 --- /dev/null +++ b/yazi-fm/README.md @@ -0,0 +1,5 @@ +# yazi-fm + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-fs/README.md b/yazi-fs/README.md new file mode 100644 index 00000000..7838af5f --- /dev/null +++ b/yazi-fs/README.md @@ -0,0 +1,5 @@ +# yazi-fs + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-macro/README.md b/yazi-macro/README.md new file mode 100644 index 00000000..644f6fb7 --- /dev/null +++ b/yazi-macro/README.md @@ -0,0 +1,5 @@ +# yazi-macro + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-packing/README.md b/yazi-packing/README.md new file mode 100644 index 00000000..1bcb6f63 --- /dev/null +++ b/yazi-packing/README.md @@ -0,0 +1,5 @@ +# yazi-packing + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-parser/README.md b/yazi-parser/README.md new file mode 100644 index 00000000..147220d8 --- /dev/null +++ b/yazi-parser/README.md @@ -0,0 +1,5 @@ +# yazi-parser + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-plugin/README.md b/yazi-plugin/README.md new file mode 100644 index 00000000..08c0cb0f --- /dev/null +++ b/yazi-plugin/README.md @@ -0,0 +1,5 @@ +# yazi-plugin + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-proxy/README.md b/yazi-proxy/README.md new file mode 100644 index 00000000..bea35b9a --- /dev/null +++ b/yazi-proxy/README.md @@ -0,0 +1,5 @@ +# yazi-proxy + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-scheduler/README.md b/yazi-scheduler/README.md new file mode 100644 index 00000000..2da30c10 --- /dev/null +++ b/yazi-scheduler/README.md @@ -0,0 +1,5 @@ +# yazi-scheduler + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-scheduler/src/process/shell.rs b/yazi-scheduler/src/process/shell.rs index 25d1bfe3..73abe147 100644 --- a/yazi-scheduler/src/process/shell.rs +++ b/yazi-scheduler/src/process/shell.rs @@ -40,7 +40,7 @@ pub(crate) async fn shell(opt: ShellOpt) -> Result { .stderr(opt.stdio()) .arg("-c") .arg(opt.cmd) - .arg("--") + // .arg("--") // TODO: remove .args(opt.args.iter().skip(1).map(|u| u.as_url().unified_path_str())) .current_dir(cwd) diff --git a/yazi-shared/README.md b/yazi-shared/README.md new file mode 100644 index 00000000..5457e450 --- /dev/null +++ b/yazi-shared/README.md @@ -0,0 +1,5 @@ +# yazi-shared + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-shim/README.md b/yazi-shim/README.md new file mode 100644 index 00000000..66f18015 --- /dev/null +++ b/yazi-shim/README.md @@ -0,0 +1,5 @@ +# yazi-shim + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-term/README.md b/yazi-term/README.md new file mode 100644 index 00000000..965513dc --- /dev/null +++ b/yazi-term/README.md @@ -0,0 +1,5 @@ +# yazi-term + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-vfs/README.md b/yazi-vfs/README.md new file mode 100644 index 00000000..91c709f0 --- /dev/null +++ b/yazi-vfs/README.md @@ -0,0 +1,5 @@ +# yazi-vfs + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-watcher/README.md b/yazi-watcher/README.md new file mode 100644 index 00000000..851db65b --- /dev/null +++ b/yazi-watcher/README.md @@ -0,0 +1,5 @@ +# yazi-watcher + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-widgets/README.md b/yazi-widgets/README.md new file mode 100644 index 00000000..cb9f6b86 --- /dev/null +++ b/yazi-widgets/README.md @@ -0,0 +1,5 @@ +# yazi-widgets + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi From e5ee31cb7e4afbfba953a7e557a62c1070b84269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 24 Nov 2025 11:35:10 +0800 Subject: [PATCH 05/53] feat: new Unix path backend (#3359) --- Cargo.lock | 67 ++-- Cargo.toml | 6 +- yazi-actor/src/cmp/trigger.rs | 4 +- yazi-actor/src/lives/file.rs | 4 +- yazi-actor/src/mgr/bulk_rename.rs | 18 +- yazi-actor/src/mgr/copy.rs | 8 +- yazi-actor/src/mgr/quit.rs | 17 +- yazi-actor/src/mgr/search.rs | 2 +- yazi-actor/src/mgr/shell.rs | 2 +- yazi-adapter/src/drivers/sixel.rs | 2 +- yazi-binding/src/elements/line.rs | 2 +- yazi-binding/src/error.rs | 2 +- yazi-binding/src/path.rs | 111 ++++++- yazi-binding/src/url.rs | 144 ++++---- yazi-codegen/Cargo.toml | 2 +- yazi-config/src/popup/options.rs | 24 +- yazi-dds/src/sendable.rs | 10 +- yazi-fm/src/app/commands/plugin.rs | 4 +- yazi-fm/src/app/commands/quit.rs | 12 +- yazi-fs/Cargo.toml | 1 + yazi-fs/src/path/clean.rs | 34 +- yazi-fs/src/path/expand.rs | 7 +- yazi-fs/src/path/path.rs | 10 +- yazi-fs/src/path/percent.rs | 2 + yazi-fs/src/path/relative.rs | 12 +- yazi-fs/src/provider/local/local.rs | 2 +- yazi-fs/src/sorter.rs | 7 +- yazi-fs/src/url.rs | 15 +- yazi-plugin/src/fs/op.rs | 2 +- yazi-plugin/src/isolate/peek.rs | 2 +- yazi-plugin/src/isolate/seek.rs | 2 +- yazi-plugin/src/process/command.rs | 10 +- yazi-plugin/src/runtime/plugin.rs | 8 +- yazi-plugin/src/utils/text.rs | 2 +- yazi-shared/Cargo.toml | 1 + yazi-shared/src/data/data.rs | 6 +- yazi-shared/src/data/key.rs | 2 +- yazi-shared/src/event/event.rs | 6 +- yazi-shared/src/lib.rs | 2 +- yazi-shared/src/osstr.rs | 60 ---- yazi-shared/src/path/buf.rs | 63 +++- yazi-shared/src/path/component.rs | 75 +++++ yazi-shared/src/path/components.rs | 49 +++ yazi-shared/src/path/conversion.rs | 19 +- yazi-shared/src/path/cow.rs | 20 +- yazi-shared/src/path/display.rs | 12 + yazi-shared/src/path/error.rs | 30 +- yazi-shared/src/path/kind.rs | 1 + yazi-shared/src/path/like.rs | 6 +- yazi-shared/src/path/mod.rs | 2 +- yazi-shared/src/path/path.rs | 79 +++-- yazi-shared/src/path/view.rs | 27 -- yazi-shared/src/rand.rs | 27 -- yazi-shared/src/scheme/cow.rs | 11 +- yazi-shared/src/scheme/scheme.rs | 10 + yazi-shared/src/strand/buf.rs | 86 ++++- yazi-shared/src/strand/conversion.rs | 47 ++- yazi-shared/src/strand/cow.rs | 36 +- yazi-shared/src/strand/error.rs | 16 +- yazi-shared/src/strand/extensions.rs | 61 ++++ yazi-shared/src/strand/kind.rs | 29 +- yazi-shared/src/strand/mod.rs | 2 +- yazi-shared/src/strand/{dyn.rs => strand.rs} | 64 +++- yazi-shared/src/strand/view.rs | 26 -- yazi-shared/src/string.rs | 37 --- yazi-shared/src/url/buf.rs | 22 +- yazi-shared/src/url/component.rs | 202 +++--------- yazi-shared/src/url/components.rs | 196 +++++++++++ yazi-shared/src/url/display.rs | 14 +- yazi-shared/src/url/encode.rs | 1 - yazi-shared/src/url/like.rs | 75 +++++ yazi-shared/src/url/mod.rs | 2 +- yazi-shared/src/url/traits.rs | 91 +---- yazi-shared/src/url/url.rs | 329 ++++++++++--------- yazi-vfs/src/fns.rs | 6 +- yazi-vfs/src/provider/sftp/sftp.rs | 2 +- 76 files changed, 1430 insertions(+), 979 deletions(-) delete mode 100644 yazi-shared/src/osstr.rs create mode 100644 yazi-shared/src/path/display.rs delete mode 100644 yazi-shared/src/rand.rs create mode 100644 yazi-shared/src/strand/extensions.rs rename yazi-shared/src/strand/{dyn.rs => strand.rs} (74%) delete mode 100644 yazi-shared/src/string.rs create mode 100644 yazi-shared/src/url/components.rs create mode 100644 yazi-shared/src/url/like.rs diff --git a/Cargo.lock b/Cargo.lock index 6eaccab4..184ceeea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,9 +505,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.46" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "jobserver", @@ -1544,14 +1544,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] @@ -1712,12 +1713,12 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -2040,7 +2041,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" dependencies = [ - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] @@ -3468,9 +3469,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -3633,9 +3634,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -3961,6 +3962,12 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "typed-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7922f2cdc51280d47b491af9eafc41eb0cdab85eabcb390c854412fcbf26dbe8" + [[package]] name = "typeid" version = "1.0.3" @@ -4645,7 +4652,7 @@ dependencies = [ "anyhow", "crossterm 0.29.0", "futures", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "libc", "mlua", "paste", @@ -4698,7 +4705,7 @@ name = "yazi-binding" version = "25.9.15" dependencies = [ "ansi-to-tui", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "mlua", "paste", "ratatui", @@ -4722,7 +4729,7 @@ dependencies = [ "clap_complete_fig", "clap_complete_nushell", "futures", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "regex", "serde", "vergen-gitcl", @@ -4781,7 +4788,7 @@ dependencies = [ "crossterm 0.29.0", "dirs", "globset", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "indexmap", "ratatui", "regex", @@ -4802,7 +4809,7 @@ version = "25.9.15" dependencies = [ "anyhow", "crossterm 0.29.0", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "indexmap", "parking_lot", "ratatui", @@ -4831,7 +4838,7 @@ name = "yazi-dds" version = "25.9.15" dependencies = [ "anyhow", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "mlua", "ordered-float 5.1.0", "parking_lot", @@ -4913,11 +4920,12 @@ dependencies = [ "core-foundation-sys", "dirs", "foldhash 0.2.0", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "libc", "objc2", "parking_lot", "percent-encoding", + "rand 0.9.2", "regex", "scopeguard", "serde", @@ -4947,7 +4955,7 @@ dependencies = [ "anyhow", "bitflags 2.10.0", "crossterm 0.29.0", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "mlua", "ordered-float 5.1.0", "serde", @@ -4967,7 +4975,7 @@ dependencies = [ "anyhow", "crossterm 0.29.0", "futures", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "libc", "mlua", "parking_lot", @@ -5025,7 +5033,7 @@ dependencies = [ "async-priority-channel", "foldhash 0.2.0", "futures", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "libc", "lru 0.16.2", "mlua", @@ -5066,7 +5074,7 @@ dependencies = [ "crossterm 0.29.0", "foldhash 0.2.0", "futures", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "libc", "memchr", "ordered-float 5.1.0", @@ -5075,6 +5083,7 @@ dependencies = [ "serde", "thiserror 2.0.17", "tokio", + "typed-path", "uzers", "windows-sys 0.61.2", "yazi-macro", @@ -5108,7 +5117,7 @@ dependencies = [ "anyhow", "deadpool", "futures", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "parking_lot", "russh", "tokio", @@ -5125,7 +5134,7 @@ name = "yazi-watcher" version = "25.9.15" dependencies = [ "anyhow", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "notify", "parking_lot", "percent-encoding", @@ -5164,18 +5173,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 09e0246c..5b25ac8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,8 @@ dirs = "6.0.0" foldhash = "0.2.0" futures = "0.3.31" globset = "0.4.18" -hashbrown = { version = "0.16.0", features = [ "serde" ] } -indexmap = { version = "2.12.0", features = [ "serde" ] } +hashbrown = { version = "0.16.1", features = [ "serde" ] } +indexmap = { version = "2.12.1", features = [ "serde" ] } libc = "0.2.177" lru = "0.16.2" mlua = { version = "0.11.4", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } @@ -41,6 +41,7 @@ ordered-float = { version = "5.1.0", features = [ "serde" ] } parking_lot = "0.12.5" paste = "1.0.15" percent-encoding = "2.3.2" +rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] } ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] } regex = "1.12.2" russh = { version = "0.54.6", default-features = false, features = [ "ring", "rsa" ] } @@ -55,6 +56,7 @@ tokio-util = "0.7.17" toml = { version = "0.9.8" } tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } +typed-path = "0.12.0" unicode-width = { version = "0.2.0", default-features = false } uzers = "0.12.1" diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index c092099f..c2f36b73 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -81,7 +81,7 @@ impl Trigger { Some(match path.rsplit_pred(sep) { Some((p, c)) if p.is_empty() => { - let root = PathDyn::with(scheme.kind(), MAIN_SEPARATOR_STR).expect("valid root"); + let root = PathDyn::with_str(scheme.kind(), MAIN_SEPARATOR_STR); (UrlCow::try_from((scheme, root)).ok()?.into_owned(), c.into()) } Some((p, c)) => (expand_url(UrlCow::try_from((scheme, p)).ok()?), c.into()), @@ -99,7 +99,7 @@ mod tests { fn compare(s: &str, parent: &str, child: &str) { let (mut p, c) = Trigger::split_url(s).unwrap(); if let Ok(u) = p.try_strip_prefix(yazi_fs::CWD.load().as_ref()) { - p = UrlBuf::from(u); + p = UrlBuf::Regular(u.as_os().unwrap().into()); } assert_eq!((p, c.to_str().unwrap()), (parent.parse().unwrap(), child)); } diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index ac978929..d3c22917 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -4,7 +4,7 @@ use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods, Valu use yazi_binding::Style; use yazi_config::THEME; use yazi_plugin::bindings::Range; -use yazi_shared::url::UrlLike; +use yazi_shared::{path::AsPath, url::UrlLike}; use super::Lives; use crate::lives::PtrCell; @@ -91,7 +91,7 @@ impl UserData for File { let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components(); comp.next_back(); - Some(lua.create_string(comp.as_path().as_os_str().as_encoded_bytes())).transpose() + Some(lua.create_string(comp.as_path().encoded_bytes())).transpose() }); methods.add_method("style", |lua, me, ()| { lua.named_registry_value::("cx")?.borrow_scoped(|core: &yazi_core::Core| { diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 2742c867..c8308417 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -1,4 +1,4 @@ -use std::{ffi::{OsStr, OsString}, hash::Hash, io::{Read, Write}, ops::Deref, path::Path}; +use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path}; use anyhow::{Result, anyhow}; use crossterm::{execute, style::Print}; @@ -11,7 +11,7 @@ use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider use yazi_macro::{err, succ}; use yazi_parser::VoidOpt; use yazi_proxy::{AppProxy, HIDER, TasksProxy}; -use yazi_shared::{OsStrJoin, data::Data, path::PathDyn, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_term::tty::TTY; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; @@ -47,7 +47,7 @@ impl Actor for BulkRename { .create_new(true) .open(&tmp) .await? - .write_all(old.join(OsStr::new("\n")).as_encoded_bytes()) + .write_all(old.join(Strand::Utf8("\n")).encoded_bytes()) .await?; defer! { @@ -160,7 +160,7 @@ impl BulkRename { YAZI.opener.block(YAZI.open.all(Path::new("bulk-rename.txt"), "text/plain")) } - fn replace_url(url: &UrlBuf, take: usize, rep: &OsStr) -> Result { + fn replace_url(url: &UrlBuf, take: usize, rep: &StrandBuf) -> Result { Ok(url.try_replace(take, PathDyn::with(url.kind(), rep)?)?.into_owned()) } @@ -220,10 +220,10 @@ impl BulkRename { // --- Tuple #[derive(Clone, Debug)] -struct Tuple(usize, OsString); +struct Tuple(usize, StrandBuf); impl Deref for Tuple { - type Target = OsStr; + type Target = StrandBuf; fn deref(&self) -> &Self::Target { &self.1 } } @@ -238,12 +238,12 @@ impl Hash for Tuple { fn hash(&self, state: &mut H) { self.1.hash(state); } } -impl AsRef for Tuple { - fn as_ref(&self) -> &OsStr { &self.1 } +impl AsStrand for &Tuple { + fn as_strand(&self) -> Strand<'_> { self.1.as_strand() } } impl Tuple { - fn new(index: usize, inner: impl Into) -> Self { Self(index, inner.into()) } + fn new(index: usize, inner: impl Into) -> Self { Self(index, inner.into()) } } // --- Tests diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index 88402c93..0ccf4ff9 100644 --- a/yazi-actor/src/mgr/copy.rs +++ b/yazi-actor/src/mgr/copy.rs @@ -1,7 +1,7 @@ use anyhow::{Result, bail}; use yazi_macro::{act, succ}; use yazi_parser::mgr::CopyOpt; -use yazi_shared::{data::Data, url::UrlLike}; +use yazi_shared::{data::Data, strand::ToStrand, url::UrlLike}; use yazi_widgets::CLIPBOARD; use crate::{Actor, Ctx}; @@ -28,11 +28,11 @@ impl Actor for Copy { match opt.r#type.as_ref() { // TODO: rename to "url" "path" => { - s.extend_from_slice(&opt.separator.transform(&u.os_str())); + s.extend_from_slice(&opt.separator.transform(&u.to_strand())); } "dirname" => { if let Some(p) = u.parent() { - s.extend_from_slice(&opt.separator.transform(&p.os_str())); + s.extend_from_slice(&opt.separator.transform(&p.to_strand())); } } "filename" => { @@ -50,7 +50,7 @@ impl Actor for Copy { // Copy the CWD path regardless even if the directory is empty if s.is_empty() && opt.r#type == "dirname" { - s.extend_from_slice(&opt.separator.transform(&cx.cwd().os_str())); + s.extend_from_slice(&opt.separator.transform(&cx.cwd().to_strand())); } futures::executor::block_on(CLIPBOARD.set(s)); diff --git a/yazi-actor/src/mgr/quit.rs b/yazi-actor/src/mgr/quit.rs index 9eb28e5a..3014c7ef 100644 --- a/yazi-actor/src/mgr/quit.rs +++ b/yazi-actor/src/mgr/quit.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsString, time::Duration}; +use std::time::Duration; use anyhow::Result; use tokio::{select, time}; @@ -7,7 +7,7 @@ use yazi_dds::spark::SparkKind; use yazi_macro::{emit, succ}; use yazi_parser::mgr::QuitOpt; use yazi_proxy::ConfirmProxy; -use yazi_shared::{data::Data, event::EventQuit, url::{AsUrl, UrlCow}}; +use yazi_shared::{data::Data, event::EventQuit, strand::{Strand, StrandLike, ToStrandJoin}, url::AsUrl}; use crate::{Actor, Ctx}; @@ -66,17 +66,12 @@ impl Actor for Quit { } impl Quit { - pub(super) fn with_selected<'a, I, T>(selected: I) + pub(super) fn with_selected(selected: I) where - I: IntoIterator, - T: Into>, + I: IntoIterator, + I::Item: AsUrl, { - let paths = selected.into_iter().fold(OsString::new(), |mut s, u| { - s.push(u.into().as_url().os_str()); - s.push("\n"); - s - }); - + let paths = selected.into_iter().join(Strand::Utf8("\n")); if !paths.is_empty() { emit!(Quit(EventQuit { selected: Some(paths), ..Default::default() })); } diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index 44b7571b..33850f03 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -52,7 +52,7 @@ impl Actor for SearchDo { handle.abort(); } - let cwd = tab.cwd().to_search(opt.subject.as_ref())?; + let cwd = tab.cwd().to_search(&opt.subject)?; let hidden = tab.pref.show_hidden; tab.search = Some(tokio::spawn(async move { diff --git a/yazi-actor/src/mgr/shell.rs b/yazi-actor/src/mgr/shell.rs index 722788d1..2d1a0437 100644 --- a/yazi-actor/src/mgr/shell.rs +++ b/yazi-actor/src/mgr/shell.rs @@ -40,7 +40,7 @@ impl Actor for Shell { TasksProxy::open_shell_compat(ProcessOpenOpt { cwd: cwd.into(), - cmd: Splatter::new(&selected).splat(opt.run.as_ref()), + cmd: Splatter::new(&selected).splat(&*opt.run), args: selected, block: opt.block, orphan: opt.orphan, diff --git a/yazi-adapter/src/drivers/sixel.rs b/yazi-adapter/src/drivers/sixel.rs index 8c80d7e8..bcb4bfd8 100644 --- a/yazi-adapter/src/drivers/sixel.rs +++ b/yazi-adapter/src/drivers/sixel.rs @@ -55,7 +55,7 @@ impl Sixel { tokio::task::spawn_blocking(move || { let mut buf = vec![]; - write!(buf, "{START}P0;1;8q\"1;1;{};{}", img.width(), img.height())?; + write!(buf, "{START}P9;1q\"1;1;{};{}", img.width(), img.height())?; // Palette for (i, c) in qo.palette.iter().enumerate() { diff --git a/yazi-binding/src/elements/line.rs b/yazi-binding/src/elements/line.rs index 69df7adb..63e8a623 100644 --- a/yazi-binding/src/elements/line.rs +++ b/yazi-binding/src/elements/line.rs @@ -227,7 +227,7 @@ mod tests { .call(()) .unwrap(); - line.spans.iter().map(|s| s.content.as_ref()).collect() + line.spans.iter().map(|s| &*s.content).collect() } #[test] diff --git a/yazi-binding/src/error.rs b/yazi-binding/src/error.rs index 382326f9..eef599a3 100644 --- a/yazi-binding/src/error.rs +++ b/yazi-binding/src/error.rs @@ -66,7 +66,7 @@ impl UserData for Error { methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| { Ok(match me { Self::Io(_) | Self::Fs(_) | Self::Serde(_) => lua.create_string(me.to_string()), - Self::Custom(s) => lua.create_string(s.as_ref()), + Self::Custom(s) => lua.create_string(&**s), }) }); methods.add_meta_function(MetaMethod::Concat, |lua, (lhs, rhs): (Value, Value)| { diff --git a/yazi-binding/src/path.rs b/yazi-binding/src/path.rs index 03288668..a18dbd5e 100644 --- a/yazi-binding/src/path.rs +++ b/yazi-binding/src/path.rs @@ -1,18 +1,97 @@ use std::ops::Deref; -use mlua::{ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataMethods, Value}; -use yazi_shared::path::{PathBufDyn, PathLike}; +use mlua::{ExternalError, ExternalResult, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; +use yazi_shared::{path::{PathBufDyn, PathLike, StripPrefixError}, strand::{AsStrand, Strand, StrandCow}}; -pub struct Path(pub PathBufDyn); +use crate::cached_field; + +pub type PathRef = UserDataRef; + +pub struct Path { + inner: PathBufDyn, + + v_ext: Option, + v_name: Option, + v_parent: Option, + v_stem: Option, +} impl Deref for Path { type Target = PathBufDyn; - fn deref(&self) -> &Self::Target { &self.0 } + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl From for PathBufDyn { + fn from(value: Path) -> Self { value.inner } +} + +impl AsStrand for Path { + fn as_strand(&self) -> Strand<'_> { self.inner.as_strand() } +} + +impl AsStrand for &Path { + fn as_strand(&self) -> Strand<'_> { self.inner.as_strand() } } impl Path { - pub fn new(urn: impl Into) -> Self { Self(urn.into()) } + pub fn new(path: impl Into) -> Self { + Self { + inner: path.into(), + + v_ext: None, + v_name: None, + v_parent: None, + v_stem: None, + } + } + + fn ends_with(&self, child: Value) -> mlua::Result { + match child { + Value::String(s) => { + self.try_ends_with(StrandCow::with(self.kind(), &*s.as_bytes())?).into_lua_err() + } + Value::UserData(ud) => self.try_ends_with(&*ud.borrow::()?).into_lua_err(), + _ => Err("must be a string or Path".into_lua_err())?, + } + } + + fn join(&self, other: Value) -> mlua::Result { + Ok(Self::new(match other { + Value::String(s) => { + self.try_join(StrandCow::with(self.kind(), &*s.as_bytes())?).into_lua_err()? + } + Value::UserData(ref ud) => { + let path = ud.borrow::()?; + self.try_join(&*path).into_lua_err()? + } + _ => Err("must be a string or Path".into_lua_err())?, + })) + } + + fn starts_with(&self, base: Value) -> mlua::Result { + match base { + Value::String(s) => { + self.try_starts_with(StrandCow::with(self.kind(), &*s.as_bytes())?).into_lua_err() + } + Value::UserData(ud) => self.try_starts_with(&*ud.borrow::()?).into_lua_err(), + _ => Err("must be a string or Path".into_lua_err())?, + } + } + + fn strip_prefix(&self, base: Value) -> mlua::Result> { + let strip = match base { + Value::String(s) => self.try_strip_prefix(StrandCow::with(self.kind(), &*s.as_bytes())?), + Value::UserData(ud) => self.try_strip_prefix(&*ud.borrow::()?), + _ => Err("must be a string or Path".into_lua_err())?, + }; + + Ok(match strip { + Ok(p) => Some(Path::new(p)), + Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, + Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, + }) + } } impl FromLua for Path { @@ -25,7 +104,29 @@ impl FromLua for Path { } impl UserData for Path { + fn add_fields>(fields: &mut F) { + cached_field!(fields, ext, |lua, me| { + me.ext().map(|s| lua.create_string(s.encoded_bytes())).transpose() + }); + cached_field!(fields, name, |lua, me| { + me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() + }); + cached_field!(fields, parent, |_, me| Ok(me.parent().map(Self::new))); + cached_field!(fields, stem, |lua, me| { + me.stem().map(|s| lua.create_string(s.encoded_bytes())).transpose() + }); + } + fn add_methods>(methods: &mut M) { + methods.add_method("ends_with", |_, me, child: Value| me.ends_with(child)); + methods.add_method("join", |_, me, other: Value| me.join(other)); + methods.add_method("starts_with", |_, me, base: Value| me.starts_with(base)); + methods.add_method("strip_prefix", |_, me, base: Value| me.strip_prefix(base)); + + methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: mlua::String| { + lua.create_string([lhs.encoded_bytes(), &rhs.as_bytes()].concat()) + }); + methods.add_meta_method(MetaMethod::Eq, |_, me, other: PathRef| Ok(me.inner == other.inner)); methods .add_meta_method(MetaMethod::ToString, |lua, me, ()| lua.create_string(me.encoded_bytes())); } diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 7577d8b0..f787c7f3 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -1,16 +1,17 @@ use std::ops::Deref; -use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use yazi_fs::{FsHash64, FsHash128}; -use yazi_shared::{path::StripPrefixError, strand::StrandCow, url::{AsUrl, UrlCow, UrlLike}}; +use yazi_shared::{path::StripPrefixError, scheme::SchemeCow, strand::{StrandLike, ToStrand}, url::{AsUrl, UrlCow, UrlLike}}; -use crate::{Scheme, cached_field, deprecate}; +use crate::{Path, Scheme, cached_field, deprecate}; pub type UrlRef = UserDataRef; pub struct Url { inner: yazi_shared::url::UrlBuf, + v_path: Option, v_name: Option, v_stem: Option, v_ext: Option, @@ -61,6 +62,7 @@ impl Url { Self { inner: url.into(), + v_path: None, v_name: None, v_stem: None, v_ext: None, @@ -78,13 +80,74 @@ impl Url { "Url", lua.create_function(|_, value: Value| { Ok(match value { - Value::String(s) => Self::try_from(s.as_bytes().as_ref())?, - Value::UserData(ud) => Self::new(ud.borrow::()?.inner.clone()), + Value::String(s) => Self::try_from(&*s.as_bytes())?, + Value::UserData(ud) => Self::new(&ud.borrow::()?.inner), _ => Err("Expected a string or a Url".into_lua_err())?, }) })?, ) } + + fn ends_with(&self, child: Value) -> mlua::Result { + match child { + Value::String(s) => self.try_ends_with(UrlCow::try_from(&*s.as_bytes())?).into_lua_err(), + Value::UserData(ud) => self.try_ends_with(&*ud.borrow::()?).into_lua_err(), + _ => Err("must be a string or Url".into_lua_err())?, + } + } + + fn hash(&self, long: Option) -> mlua::Result { + Ok(if long.unwrap_or(false) { + format!("{:x}", self.hash_u128()) + } else { + format!("{:x}", self.hash_u64()) + }) + } + + fn join(&self, lua: &Lua, other: Value) -> mlua::Result { + match other { + Value::String(s) => { + let b = s.as_bytes(); + let (scheme, path) = SchemeCow::parse(&*b)?; + if scheme == self.scheme() { + Self::new(self.try_join(path).into_lua_err()?).into_lua(lua) + } else { + Self::new(UrlCow::try_from((scheme, path))?).into_lua(lua) + } + } + Value::UserData(ref ud) => { + let url = ud.borrow::()?; + if url.scheme() == self.scheme() { + Self::new(self.try_join(url.loc()).into_lua_err()?).into_lua(lua) + } else { + Ok(other) + } + } + _ => Err("must be a string or Url".into_lua_err())?, + } + } + + fn starts_with(&self, base: Value) -> mlua::Result { + match base { + Value::String(s) => self.try_starts_with(UrlCow::try_from(&*s.as_bytes())?).into_lua_err(), + Value::UserData(ud) => self.try_starts_with(&*ud.borrow::()?).into_lua_err(), + _ => Err("must be a string or Url".into_lua_err())?, + } + } + + fn strip_prefix(&self, base: Value) -> mlua::Result> { + let strip = match base { + Value::String(s) => self.try_strip_prefix(UrlCow::try_from(&*s.as_bytes())?), + Value::UserData(ud) => self.try_strip_prefix(&*ud.borrow::()?), + _ => Err("must be a string or Url".into_lua_err())?, + }; + + Ok(match strip { + Ok(p) => Some(Path::new(p)), + Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, + Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, + }) + } } impl FromLua for Url { @@ -98,6 +161,7 @@ impl FromLua for Url { impl UserData for Url { fn add_fields>(fields: &mut F) { + cached_field!(fields, path, |_, me| Ok(Path::new(me.loc()))); cached_field!(fields, name, |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); @@ -108,7 +172,7 @@ impl UserData for Url { me.ext().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, parent, |_, me| Ok(me.parent().map(Self::new))); - cached_field!(fields, urn, |_, me| Ok(super::Path::new(me.urn()))); + cached_field!(fields, urn, |_, me| Ok(Path::new(me.urn()))); cached_field!(fields, base, |_, me| { Ok(Some(me.base()).filter(|u| !u.loc().is_empty()).map(Self::new)) }); @@ -131,65 +195,11 @@ impl UserData for Url { } fn add_methods>(methods: &mut M) { - methods.add_method("hash", |_, me, long: Option| { - Ok(if long.unwrap_or(false) { - format!("{:x}", me.hash_u128()) - } else { - format!("{:x}", me.hash_u64()) - }) - }); - methods.add_method("join", |_, me, other: Value| { - Ok(Self::new(match other { - Value::String(s) => { - let b = s.as_bytes(); - me.try_join(StrandCow::with(me.kind(), &*b).into_lua_err()?).into_lua_err()? - } - Value::UserData(ud) => { - let url = ud.borrow::()?; - if !me.scheme().covariant(url.scheme()) { - return Err("cannot join Urls with different schemes".into_lua_err()); - } - me.try_join(url.loc()).into_lua_err()? - } - _ => Err("must be a string or Url".into_lua_err())?, - })) - }); - methods.add_method("starts_with", |_, me, base: Value| { - Ok(match base { - Value::String(s) => { - let b = s.as_bytes(); - me.loc().try_starts_with(&StrandCow::with(me.kind(), &*b).into_lua_err()?).into_lua_err() - } - Value::UserData(ud) => me.try_starts_with(&*ud.borrow::()?).into_lua_err(), - _ => Err("must be a string or Url".into_lua_err())?, - }) - }); - methods.add_method("ends_with", |_, me, child: Value| { - Ok(match child { - Value::String(s) => { - let b = s.as_bytes(); - me.loc().try_ends_with(&StrandCow::with(me.kind(), &*b).into_lua_err()?).into_lua_err() - } - Value::UserData(ud) => me.try_ends_with(&*ud.borrow::()?).into_lua_err(), - _ => Err("must be a string or Url".into_lua_err())?, - }) - }); - methods.add_method("strip_prefix", |_, me, base: Value| { - let strip = match base { - Value::String(s) => { - let b = s.as_bytes(); - me.loc().try_strip_prefix(&StrandCow::with(me.kind(), &*b).into_lua_err()?) - } - Value::UserData(ud) => me.try_strip_prefix(&*ud.borrow::()?), - _ => Err("must be a string or Url".into_lua_err())?, - }; - - Ok(match strip { - Ok(p) => Some(Self::new(p)), // TODO: return `Path` instead of `Url` - Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, - Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, - }) - }); + methods.add_method("ends_with", |_, me, child: Value| me.ends_with(child)); + methods.add_method("hash", |_, me, long: Option| me.hash(long)); + methods.add_method("join", |lua, me, other: Value| me.join(lua, other)); + methods.add_method("starts_with", |_, me, base: Value| me.starts_with(base)); + methods.add_method("strip_prefix", |_, me, base: Value| me.strip_prefix(base)); methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { let url = ud.take::()?.inner.into_search(domain.to_str()?).into_lua_err()?; @@ -198,10 +208,10 @@ impl UserData for Url { methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner)); methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| { - lua.create_string(me.os_str().as_encoded_bytes()) + lua.create_string(me.to_strand().encoded_bytes()) }); methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: mlua::String| { - lua.create_string([lhs.os_str().as_encoded_bytes(), &rhs.as_bytes()].concat()) + lua.create_string([lhs.to_strand().encoded_bytes(), &rhs.as_bytes()].concat()) }); } } diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index c39c5fce..b22d5f82 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -13,5 +13,5 @@ proc-macro = true [dependencies] # External dependencies -syn = { version = "2.0.110", features = [ "full" ] } +syn = { version = "2.0.111", features = [ "full" ] } quote = "1.0.42" diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs index 85fd42f7..17dcf238 100644 --- a/yazi-config/src/popup/options.rs +++ b/yazi-config/src/popup/options.rs @@ -1,5 +1,5 @@ use ratatui::{text::{Line, Text}, widgets::{Paragraph, Wrap}}; -use yazi_shared::{IntoStringLossy, url::UrlBuf}; +use yazi_shared::{strand::ToStrand, url::UrlBuf}; use super::{Offset, Position}; use crate::YAZI; @@ -123,7 +123,7 @@ impl ConfirmCfg { Self::replace_number(&YAZI.confirm.trash_title, urls.len()), YAZI.confirm.trash_position(), None, - Self::truncate_list(urls.iter(), urls.len(), 100), + Self::truncate_list(urls, urls.len(), 100), ) } @@ -132,7 +132,7 @@ impl ConfirmCfg { Self::replace_number(&YAZI.confirm.delete_title, urls.len()), YAZI.confirm.delete_position(), None, - Self::truncate_list(urls.iter(), urls.len(), 100), + Self::truncate_list(urls, urls.len(), 100), ) } @@ -141,7 +141,7 @@ impl ConfirmCfg { YAZI.confirm.overwrite_title.clone(), YAZI.confirm.overwrite_position(), Some(Text::raw(&YAZI.confirm.overwrite_body)), - Some(url.into_string_lossy().into()), + Some(url.to_strand().into_string_lossy().into()), ) } @@ -150,7 +150,7 @@ impl ConfirmCfg { Self::replace_number(&YAZI.confirm.quit_title, len), YAZI.confirm.quit_position(), Some(Text::raw(&YAZI.confirm.quit_body)), - Self::truncate_list(names.into_iter(), len, 10), + Self::truncate_list(names, len, 10), ) } @@ -158,18 +158,18 @@ impl ConfirmCfg { tpl.replace("{n}", &n.to_string()).replace("{s}", if n > 1 { "s" } else { "" }) } - fn truncate_list( - it: impl Iterator, - len: usize, - max: usize, - ) -> Option> { + fn truncate_list(it: I, len: usize, max: usize) -> Option> + where + I: IntoIterator, + I::Item: ToStrand, + { let mut lines = Vec::with_capacity(len.min(max + 1)); - for (i, s) in it.enumerate() { + for (i, s) in it.into_iter().enumerate() { if i >= max { lines.push(format!("... and {} more", len - max)); break; } - lines.push(s.into_string_lossy()); + lines.push(s.to_strand().into_string_lossy()); } Some(Text::from_iter(lines)) } diff --git a/yazi-dds/src/sendable.rs b/yazi-dds/src/sendable.rs index c04b6af7..3b8953ed 100644 --- a/yazi-dds/src/sendable.rs +++ b/yazi-dds/src/sendable.rs @@ -47,7 +47,7 @@ impl Sendable { Data::Url(ud.take::()?.into()) } Some(t) if t == TypeId::of::() => { - Data::Path(ud.take::()?.0) + Data::Path(ud.take::()?.into()) } Some(t) if t == TypeId::of::() => { Data::Id(**ud.borrow::()?) @@ -103,7 +103,7 @@ impl Sendable { Data::Boolean(b) => Value::Boolean(*b), Data::Integer(i) => Value::Integer(*i), Data::Number(n) => Value::Number(*n), - Data::String(s) => Value::String(lua.create_string(s.as_ref())?), + Data::String(s) => Value::String(lua.create_string(&**s)?), Data::List(l) => { let mut vec = Vec::with_capacity(l.len()); for v in l { @@ -175,7 +175,7 @@ impl Sendable { match k { DataKey::Integer(i) => tbl.raw_set(i + 1, Self::data_to_value_ref(lua, v)?), DataKey::String(s) => { - tbl.raw_set(replace_cow(s.as_ref(), "-", "_"), Self::data_to_value_ref(lua, v)?) + tbl.raw_set(replace_cow(&**s, "-", "_"), Self::data_to_value_ref(lua, v)?) } _ => Err("invalid key in Data".into_lua_err()), }?; @@ -215,7 +215,7 @@ impl Sendable { DataKey::Url(ud.take::()?.into()) } Some(t) if t == TypeId::of::() => { - DataKey::Path(ud.take::()?.0) + DataKey::Path(ud.take::()?.into()) } Some(t) if t == TypeId::of::() => { DataKey::Id(**ud.borrow::()?) @@ -241,7 +241,7 @@ impl Sendable { DataKey::Boolean(b) => Value::Boolean(*b), DataKey::Integer(i) => Value::Integer(*i), DataKey::Number(n) => Value::Number(n.0), - DataKey::String(s) => Value::String(lua.create_string(s.as_ref())?), + DataKey::String(s) => Value::String(lua.create_string(&**s)?), DataKey::Id(i) => yazi_binding::Id(*i).into_lua(lua)?, DataKey::Url(u) => yazi_binding::Url::new(u.clone()).into_lua(lua)?, DataKey::Path(u) => yazi_binding::Path::new(u).into_lua(lua)?, diff --git a/yazi-fm/src/app/commands/plugin.rs b/yazi-fm/src/app/commands/plugin.rs index e8986f19..48bc16ff 100644 --- a/yazi-fm/src/app/commands/plugin.rs +++ b/yazi-fm/src/app/commands/plugin.rs @@ -16,7 +16,7 @@ use crate::app::App; impl App { pub(crate) fn plugin(&mut self, mut opt: PluginOpt) -> Result { let mut hits = false; - if let Some(chunk) = LOADER.read().get(opt.id.as_ref()) { + if let Some(chunk) = LOADER.read().get(&*opt.id) { hits = true; opt.mode = opt.mode.auto_then(chunk.sync_entry); } @@ -38,7 +38,7 @@ impl App { pub(crate) fn plugin_do(&mut self, opt: PluginOpt) -> Result { let loader = LOADER.read(); - let Some(chunk) = loader.get(opt.id.as_ref()) else { + let Some(chunk) = loader.get(&*opt.id) else { succ!(warn!("plugin `{}` not found", opt.id)); }; diff --git a/yazi-fm/src/app/commands/quit.rs b/yazi-fm/src/app/commands/quit.rs index ecdc1342..807f0b99 100644 --- a/yazi-fm/src/app/commands/quit.rs +++ b/yazi-fm/src/app/commands/quit.rs @@ -1,8 +1,6 @@ -use std::ffi::OsString; - use yazi_boot::ARGS; use yazi_fs::provider::{Provider, local::Local}; -use yazi_shared::{event::EventQuit, url::UrlLike}; +use yazi_shared::{event::EventQuit, strand::{StrandBuf, StrandLike, ToStrand}}; use crate::{Term, app::App}; @@ -25,14 +23,14 @@ impl App { async fn cwd_to_file(&self, no: bool) { if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) { - let cwd = self.core.mgr.cwd().os_str(); - Local::regular(p).write(cwd.as_encoded_bytes()).await.ok(); + let cwd = self.core.mgr.cwd().to_strand(); + Local::regular(p).write(cwd.encoded_bytes()).await.ok(); } } - async fn selected_to_file(&self, selected: Option) { + async fn selected_to_file(&self, selected: Option) { if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) { - Local::regular(p).write(s.as_encoded_bytes()).await.ok(); + Local::regular(p).write(s.encoded_bytes()).await.ok(); } } } diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index deba1893..2fb51961 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -26,6 +26,7 @@ foldhash = { workspace = true } hashbrown = { workspace = true } parking_lot = { workspace = true } percent-encoding = { workspace = true } +rand = { workspace = true } regex = { workspace = true } scopeguard = { workspace = true } serde = { workspace = true } diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs index 7e5b2e9a..a5b9bc29 100644 --- a/yazi-fs/src/path/clean.rs +++ b/yazi-fs/src/path/clean.rs @@ -1,6 +1,4 @@ -use std::path::PathBuf; - -use yazi_shared::{loc::LocBuf, path::PathDyn, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{path::{PathBufDyn, PathDyn}, url::{UrlBuf, UrlCow, UrlLike}}; pub fn clean_url<'a>(url: impl Into>) -> UrlBuf { let cow: UrlCow = url.into(); @@ -10,25 +8,12 @@ pub fn clean_url<'a>(url: impl Into>) -> UrlBuf { cow.trail().components().count() - 1, ); - match cow.as_url() { - Url::Regular(_) => UrlBuf::Regular(path.into()), - Url::Search { domain, .. } => UrlBuf::Search { - loc: LocBuf::::with(path, uri, urn).expect("create Loc from cleaned path"), - domain: domain.intern(), - }, - Url::Archive { domain, .. } => UrlBuf::Archive { - loc: LocBuf::::with(path, uri, urn).expect("create Loc from cleaned path"), - domain: domain.intern(), - }, - Url::Sftp { domain, .. } => UrlBuf::Sftp { - loc: LocBuf::::with(path, uri, urn).expect("create Loc from cleaned path"), - domain: domain.intern(), - }, - } + let scheme = cow.scheme().to_owned().with_ports(uri, urn); + (scheme, path).try_into().expect("UrlBuf from cleaned path") } -fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBuf, usize, usize) { - use std::path::Component::*; +fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usize, usize) { + use yazi_shared::path::Component::*; let mut out = vec![]; let mut uri_count = 0; @@ -58,7 +43,14 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBuf, usize, } } - (if out.is_empty() { PathBuf::from(".") } else { out.iter().collect() }, uri_count, urn_count) + let kind = path.kind(); + let path = if out.is_empty() { + PathBufDyn::with_str(kind, ".") + } else { + PathBufDyn::from_components(kind, out).expect("components with same kind") + }; + + (path, uri_count, urn_count) } #[cfg(test)] diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index 562f06db..f5bde9ff 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, path::PathBuf}; -use yazi_shared::{FromWtf8Vec, loc::LocBuf, path::{PathBufDyn, PathCow, PathDyn, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{FromWtf8Vec, loc::LocBuf, path::{PathBufDyn, PathCow, PathDyn, PathKind, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use crate::{CWD, path::clean_url}; @@ -60,11 +60,12 @@ fn expand_variables<'a>(p: PathDyn<'a>) -> PathCow<'a> { .map_or_else(|| caps.get(0).unwrap().as_bytes().to_owned(), |s| s.into_encoded_bytes()) }); - match (b, p) { + match (b, p.kind()) { (Cow::Borrowed(_), _) => p.into(), - (Cow::Owned(b), PathDyn::Os(_)) => { + (Cow::Owned(b), PathKind::Os) => { PathBufDyn::Os(std::path::PathBuf::from_wtf8_vec(b).expect("valid WTF-8 path")).into() } + (Cow::Owned(b), PathKind::Unix) => PathBufDyn::Unix(b.into()).into(), } } diff --git a/yazi-fs/src/path/path.rs b/yazi-fs/src/path/path.rs index 088cf438..f59eb489 100644 --- a/yazi-fs/src/path/path.rs +++ b/yazi-fs/src/path/path.rs @@ -1,15 +1,13 @@ -use std::{borrow::Cow, ffi::OsStr}; +use yazi_shared::{strand::StrandCow, url::{UrlBuf, UrlLike}}; -use yazi_shared::url::{UrlBuf, UrlLike}; - -pub fn skip_url(url: &UrlBuf, n: usize) -> Cow<'_, OsStr> { +pub fn skip_url(url: &UrlBuf, n: usize) -> StrandCow<'_> { let mut it = url.components(); for _ in 0..n { if it.next().is_none() { - return OsStr::new("").into(); + return StrandCow::default(); } } - it.os_str() + it.strand() } #[cfg(test)] diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs index 98a5e299..6728d086 100644 --- a/yazi-fs/src/path/percent.rs +++ b/yazi-fs/src/path/percent.rs @@ -32,12 +32,14 @@ impl PercentEncoding for PathDyn<'_> { fn percent_encode(&self) -> Cow<'_, Path> { match self { PathDyn::Os(p) => p.percent_encode(), + PathDyn::Unix(_) => todo!(), } } fn percent_decode(&self) -> Cow<'_, [u8]> { match self { PathDyn::Os(p) => p.percent_decode(), + PathDyn::Unix(_) => todo!(), } } } diff --git a/yazi-fs/src/path/relative.rs b/yazi-fs/src/path/relative.rs index 674ccb69..172ec187 100644 --- a/yazi-fs/src/path/relative.rs +++ b/yazi-fs/src/path/relative.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use anyhow::{Result, bail}; use yazi_shared::{path::PathBufDyn, url::{UrlCow, UrlLike}}; @@ -23,7 +21,10 @@ fn url_relative_to_<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> } if from.covariant(&to) { - return UrlCow::try_from((to.scheme().zeroed().to_owned(), PathBufDyn::with(to.kind(), ".")?)); + return UrlCow::try_from(( + to.scheme().zeroed().to_owned(), + PathBufDyn::with_str(to.kind(), "."), + )); } let (mut f_it, mut t_it) = (from.components(), to.components()); @@ -44,7 +45,8 @@ fn url_relative_to_<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir); let rest = t_head.into_iter().chain(t_it); - let buf: PathBuf = dots.chain(rest).collect(); // FIXME: remove PathBuf - let buf = PathBufDyn::from(buf); + let iter = dots.chain(rest).map(|c| c.downgrade().expect("path component from dot or normal")); + let buf = PathBufDyn::from_components(to.kind(), iter)?; + UrlCow::try_from((to.scheme().zeroed().to_owned(), buf)) } diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index abfaaf71..8c51fed0 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -4,7 +4,7 @@ use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, url::{Url, Url use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Provider}}; -#[derive(Clone, Copy)] +#[derive(Clone)] pub struct Local<'a> { url: Url<'a>, path: &'a Path, diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index debac466..2d745dc7 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -1,7 +1,8 @@ use std::cmp::Ordering; use hashbrown::HashMap; -use yazi_shared::{LcgRng, natsort, path::PathBufDyn, translit::Transliterator, url::UrlLike}; +use rand::{RngCore, SeedableRng, rngs::SmallRng}; +use yazi_shared::{natsort, path::PathBufDyn, translit::Transliterator, url::UrlLike}; use crate::{File, SortBy}; @@ -59,8 +60,8 @@ impl FilesSorter { if ord == Ordering::Equal { by_alphabetical(a, b) } else { ord } }), SortBy::Random => { - let mut rng = LcgRng::default(); - items.sort_unstable_by(|a, b| self.cmp(rng.next(), rng.next(), self.promote(a, b))) + let mut rng = SmallRng::from_os_rng(); + items.sort_unstable_by(|a, b| self.cmp(rng.next_u64(), rng.next_u64(), self.promote(a, b))) } } } diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index 958fbfd2..b8ff40da 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; use yazi_shared::{path::PathDyn, url::{AsUrl, Url, UrlBuf, UrlCow}}; -use crate::{FsHash128, FsScheme, path::PercentEncoding}; +use crate::{FsHash128, FsScheme}; pub trait FsUrl<'a> { fn cache(&self) -> Option; @@ -26,12 +26,13 @@ impl<'a> FsUrl<'a> for Url<'a> { fn cache(&self) -> Option { fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { let mut it = loc.components(); - if it.next() == Some(std::path::Component::RootDir) { - root.push(it.as_path().percent_encode()); - } else { - root.push(".%2F"); - root.push(loc.percent_encode()); - } + todo!(); + // if it.next() == Some(std::path::Component::RootDir) { + // root.push(it.as_path().percent_encode()); + // } else { + // root.push(".%2F"); + // root.push(loc.percent_encode()); + // } root } diff --git a/yazi-plugin/src/fs/op.rs b/yazi-plugin/src/fs/op.rs index e2cb6c7d..4888e92a 100644 --- a/yazi-plugin/src/fs/op.rs +++ b/yazi-plugin/src/fs/op.rs @@ -35,7 +35,7 @@ impl FilesOp { url.into(), sizes .pairs::() - .map(|r| r.map(|(urn, size)| (urn.0, size))) + .map(|r| r.map(|(urn, size)| (urn.into(), size))) .collect::>()?, ))) } diff --git a/yazi-plugin/src/isolate/peek.rs b/yazi-plugin/src/isolate/peek.rs index 9c212f90..146061a0 100644 --- a/yazi-plugin/src/isolate/peek.rs +++ b/yazi-plugin/src/isolate/peek.rs @@ -64,7 +64,7 @@ fn peek_sync(cmd: &'static Cmd, file: yazi_fs::File, mime: Symbol, skip: us plugin.call_method("peek", job) }); - AppProxy::plugin(PluginOpt::new_callback(cmd.name.as_ref(), cb)); + AppProxy::plugin(PluginOpt::new_callback(&*cmd.name, cb)); } fn peek_async( diff --git a/yazi-plugin/src/isolate/seek.rs b/yazi-plugin/src/isolate/seek.rs index e51a4b5f..9085b82d 100644 --- a/yazi-plugin/src/isolate/seek.rs +++ b/yazi-plugin/src/isolate/seek.rs @@ -16,5 +16,5 @@ pub fn seek_sync(cmd: &'static Cmd, file: yazi_fs::File, units: i16) { plugin.call_method("seek", job) }); - AppProxy::plugin(PluginOpt::new_callback(cmd.name.as_ref(), cb)); + AppProxy::plugin(PluginOpt::new_callback(&*cmd.name, cb)); } diff --git a/yazi-plugin/src/process/command.rs b/yazi-plugin/src/process/command.rs index ac66cd71..1e63f64f 100644 --- a/yazi-plugin/src/process/command.rs +++ b/yazi-plugin/src/process/command.rs @@ -1,9 +1,9 @@ -use std::{any::TypeId, io, process::Stdio}; +use std::{any::TypeId, ffi::OsStr, io, process::Stdio}; use mlua::{AnyUserData, ExternalError, IntoLuaMulti, Lua, MetaMethod, Table, UserData, Value}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; use yazi_binding::Error; -use yazi_shared::IntoOsStr; +use yazi_shared::FromWtf8; use super::{Child, output::Output}; use crate::process::Status; @@ -144,11 +144,11 @@ impl UserData for Command { let mut me = ud.borrow_mut::()?; match arg { Value::String(s) => { - me.inner.arg(s.as_bytes().into_os_str()?); + me.inner.arg(OsStr::from_wtf8(&*s.as_bytes())?); } Value::Table(t) => { for s in t.sequence_values::() { - me.inner.arg(s?.as_bytes().into_os_str()?); + me.inner.arg(OsStr::from_wtf8(&*s?.as_bytes())?); } } _ => return Err("arg must be a string or table of strings".into_lua_err()), @@ -165,7 +165,7 @@ impl UserData for Command { |_, (ud, key, value): (AnyUserData, mlua::String, mlua::String)| { ud.borrow_mut::()? .inner - .env(key.as_bytes().into_os_str()?, value.as_bytes().into_os_str()?); + .env(OsStr::from_wtf8(&*key.as_bytes())?, OsStr::from_wtf8(&*value.as_bytes())?); Ok(ud) }, ); diff --git a/yazi-plugin/src/runtime/plugin.rs b/yazi-plugin/src/runtime/plugin.rs index 3978e6cc..58760bde 100644 --- a/yazi-plugin/src/runtime/plugin.rs +++ b/yazi-plugin/src/runtime/plugin.rs @@ -56,7 +56,7 @@ impl Fetcher { impl UserData for Fetcher { fn add_fields>(fields: &mut F) { - cached_field!(fields, cmd, |lua, me| lua.create_string(me.inner.run.name.as_ref())); + cached_field!(fields, cmd, |lua, me| lua.create_string(&*me.inner.run.name)); } } @@ -73,7 +73,7 @@ impl Spotter { impl UserData for Spotter { fn add_fields>(fields: &mut F) { - cached_field!(fields, cmd, |lua, me| lua.create_string(me.inner.run.name.as_ref())); + cached_field!(fields, cmd, |lua, me| lua.create_string(&*me.inner.run.name)); } } @@ -90,7 +90,7 @@ impl Preloader { impl UserData for Preloader { fn add_fields>(fields: &mut F) { - cached_field!(fields, cmd, |lua, me| lua.create_string(me.inner.run.name.as_ref())); + cached_field!(fields, cmd, |lua, me| lua.create_string(&*me.inner.run.name)); } } @@ -107,6 +107,6 @@ impl Previewer { impl UserData for Previewer { fn add_fields>(fields: &mut F) { - cached_field!(fields, cmd, |lua, me| lua.create_string(me.inner.run.name.as_ref())); + cached_field!(fields, cmd, |lua, me| lua.create_string(&*me.inner.run.name)); } } diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index 296d0d1f..22b861e2 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -21,7 +21,7 @@ impl Utils { Some(false) => yazi_shared::shell::escape_windows(s.as_ref()), None => yazi_shared::shell::escape_native(s.as_ref()), }; - lua.create_string(s.as_ref()) + lua.create_string(&*s) }) } diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 5544bcb2..7a4cece6 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -28,6 +28,7 @@ percent-encoding = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } +typed-path = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } diff --git a/yazi-shared/src/data/data.rs b/yazi-shared/src/data/data.rs index 261322ed..6f77c30a 100644 --- a/yazi-shared/src/data/data.rs +++ b/yazi-shared/src/data/data.rs @@ -20,7 +20,7 @@ pub enum Data { Id(Id), #[serde(skip_deserializing)] Url(UrlBuf), - #[serde(skip_deserializing)] + #[serde(skip)] Path(PathBufDyn), #[serde(skip)] Bytes(Vec), @@ -150,7 +150,7 @@ impl<'a> TryFrom<&'a Data> for Cow<'a, str> { fn try_from(value: &'a Data) -> Result { match value { - Data::String(s) => Ok(s.as_ref().into()), + Data::String(s) => Ok(Cow::Borrowed(s)), _ => bail!("not a string"), } } @@ -193,7 +193,7 @@ impl<'a> TryFrom<&'a Data> for UrlCow<'a> { fn try_from(value: &'a Data) -> Result { match value { - Data::String(s) => s.as_ref().try_into(), + Data::String(s) => Self::try_from(&**s), Data::Url(u) => Ok(u.into()), Data::Bytes(b) => b.as_slice().try_into(), _ => bail!("not a URL"), diff --git a/yazi-shared/src/data/key.rs b/yazi-shared/src/data/key.rs index 005416ac..61b6c5e1 100644 --- a/yazi-shared/src/data/key.rs +++ b/yazi-shared/src/data/key.rs @@ -17,7 +17,7 @@ pub enum DataKey { Id(Id), #[serde(skip_deserializing)] Url(UrlBuf), - #[serde(skip_deserializing)] + #[serde(skip)] Path(PathBufDyn), #[serde(skip)] Bytes(Vec), diff --git a/yazi-shared/src/event/event.rs b/yazi-shared/src/event/event.rs index 4ca69480..5a6c3606 100644 --- a/yazi-shared/src/event/event.rs +++ b/yazi-shared/src/event/event.rs @@ -1,10 +1,8 @@ -use std::ffi::OsString; - use crossterm::event::{KeyEvent, MouseEvent}; use tokio::sync::mpsc; use super::CmdCow; -use crate::RoCell; +use crate::{RoCell, strand::StrandBuf}; static TX: RoCell> = RoCell::new(); static RX: RoCell> = RoCell::new(); @@ -25,7 +23,7 @@ pub enum Event { pub struct EventQuit { pub code: i32, pub no_cwd_file: bool, - pub selected: Option, + pub selected: Option, } impl Event { diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 3b1af6dc..aff60ffa 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -2,7 +2,7 @@ yazi_macro::mod_pub!(data errors event loc path pool scheme shell strand translit url); -yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os osstr predictor rand ro_cell source string sync_cell terminal tests throttle time utf8 wtf8); +yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os predictor ro_cell source sync_cell terminal tests throttle time utf8 wtf8); pub fn init() { pool::init(); diff --git a/yazi-shared/src/osstr.rs b/yazi-shared/src/osstr.rs deleted file mode 100644 index 7fc560af..00000000 --- a/yazi-shared/src/osstr.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}}; - -pub trait IntoOsStr<'a> { - type Error; - - fn into_os_str(self) -> Result, Self::Error>; -} - -impl<'a> IntoOsStr<'a> for Cow<'a, [u8]> { - type Error = anyhow::Error; - - fn into_os_str(self) -> Result, Self::Error> { - #[cfg(unix)] - { - use std::os::unix::ffi::{OsStrExt, OsStringExt}; - Ok(match self { - Cow::Borrowed(b) => Cow::Borrowed(OsStr::from_bytes(b)), - Cow::Owned(b) => Cow::Owned(OsString::from_vec(b)), - }) - } - #[cfg(windows)] - { - Ok(match self { - Cow::Borrowed(b) => Cow::Borrowed(OsStr::new(str::from_utf8(b)?)), - Cow::Owned(b) => Cow::Owned(OsString::from(String::from_utf8(b)?)), - }) - } - } -} - -impl<'a> IntoOsStr<'a> for &'a [u8] { - type Error = anyhow::Error; - - fn into_os_str(self) -> Result, Self::Error> { Cow::Borrowed(self).into_os_str() } -} - -// --- OsStrJoin -pub trait OsStrJoin { - fn join(&self, sep: &OsStr) -> OsString; -} - -impl OsStrJoin for Vec -where - T: AsRef, -{ - fn join(&self, sep: &OsStr) -> OsString { - if self.is_empty() { - return OsString::new(); - } - - let mut result = OsString::new(); - for (i, item) in self.iter().enumerate() { - if i > 0 { - result.push(sep); - } - result.push(item.as_ref()); - } - result - } -} diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index d9273700..9d42ca69 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -1,15 +1,14 @@ use std::ffi::{OsStr, OsString}; use hashbrown::Equivalent; -use serde::Serialize; -use crate::{FromWtf8, path::{AsPath, PathBufDynError, PathDyn, PathKind, SetNameError}, strand::{AsStrand, Strand, StrandError}}; +use crate::{FromWtf8, FromWtf8Vec, path::{AsPath, Component, PathDyn, PathDynError, PathKind, SetNameError}, strand::{AsStrand, Strand}}; // --- PathBufDyn -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] -#[serde(untagged)] +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum PathBufDyn { Os(std::path::PathBuf), + Unix(typed_path::UnixPathBuf), } impl From for PathBufDyn { @@ -21,7 +20,7 @@ impl From> for PathBufDyn { } impl TryFrom for std::path::PathBuf { - type Error = PathBufDynError; + type Error = PathDynError; fn try_from(value: PathBufDyn) -> Result { value.into_os() } } @@ -46,19 +45,34 @@ impl PathBufDyn { { match kind.into() { PathKind::Os => Self::Os(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }.into()), + PathKind::Unix => Self::Unix(bytes.into()), } } + #[inline] + pub fn from_components<'a, K, I>(kind: K, iter: I) -> Result + where + K: Into, + I: IntoIterator>, + { + Ok(match kind.into() { + PathKind::Os => Self::Os(iter.into_iter().collect::>()?), + PathKind::Unix => Self::Unix(iter.into_iter().collect::>()?), + }) + } + pub fn into_encoded_bytes(self) -> Vec { match self { Self::Os(p) => p.into_os_string().into_encoded_bytes(), + Self::Unix(p) => p.into_vec(), } } #[inline] - pub fn into_os(self) -> Result { + pub fn into_os(self) -> Result { Ok(match self { Self::Os(p) => p, + Self::Unix(_) => Err(PathDynError::AsOs)?, }) } @@ -66,13 +80,11 @@ impl PathBufDyn { pub fn new(kind: PathKind) -> Self { match kind { PathKind::Os => Self::Os(std::path::PathBuf::new()), + PathKind::Unix => Self::Unix(typed_path::UnixPathBuf::new()), } } - #[inline] - pub fn os(path: impl Into) -> Self { Self::Os(path.into()) } - - pub fn try_extend(&mut self, paths: T) -> Result<(), StrandError> + pub fn try_extend(&mut self, paths: T) -> Result<(), PathDynError> where T: IntoIterator, T::Item: AsPath, @@ -83,13 +95,14 @@ impl PathBufDyn { Ok(()) } - pub fn try_push(&mut self, path: T) -> Result<(), StrandError> + pub fn try_push(&mut self, path: T) -> Result<(), PathDynError> where T: AsPath, { let path = path.as_path(); Ok(match self { Self::Os(p) => p.push(path.as_os()?), + Self::Unix(p) => p.push(path.encoded_bytes()), }) } @@ -98,22 +111,25 @@ impl PathBufDyn { T: AsStrand, { Ok(match (self, name.as_strand()) { - (Self::Os(p), Strand::Os(q)) => p.set_file_name(q), - (Self::Os(p), Strand::Utf8(q)) => p.set_file_name(q), + (Self::Os(p), Strand::Os(s)) => p.set_file_name(s), + (Self::Os(p), Strand::Utf8(s)) => p.set_file_name(s), (Self::Os(p), Strand::Bytes(b)) => { p.set_file_name(OsStr::from_wtf8(b).map_err(|_| SetNameError::FromWtf8)?) } + + (Self::Unix(p), s) => p.set_file_name(s.encoded_bytes()), }) } - pub fn with(kind: K, strand: T) -> Result + pub fn with(kind: K, bytes: Vec) -> Result where K: Into, - T: AsStrand, { - let s = strand.as_strand(); Ok(match kind.into() { - PathKind::Os => Self::Os(std::path::PathBuf::from(s.as_os()?)), + PathKind::Os => { + Self::Os(std::path::PathBuf::from_wtf8_vec(bytes).map_err(|_| PathDynError::AsOs)?) + } + PathKind::Unix => Self::Unix(bytes.into()), }) } @@ -123,6 +139,19 @@ impl PathBufDyn { { match kind.into() { PathKind::Os => Self::Os(std::path::PathBuf::with_capacity(capacity)), + PathKind::Unix => Self::Unix(typed_path::UnixPathBuf::with_capacity(capacity)), + } + } + + pub fn with_str(kind: K, s: S) -> Self + where + K: Into, + S: Into, + { + let s = s.into(); + match kind.into() { + PathKind::Os => Self::Os(s.into()), + PathKind::Unix => Self::Unix(s.into()), } } } diff --git a/yazi-shared/src/path/component.rs b/yazi-shared/src/path/component.rs index e69de29b..98b73bdb 100644 --- a/yazi-shared/src/path/component.rs +++ b/yazi-shared/src/path/component.rs @@ -0,0 +1,75 @@ +use std::path::PrefixComponent; + +use crate::{path::PathDynError, strand::Strand}; + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Component<'a> { + Prefix(PrefixComponent<'a>), + RootDir, + CurDir, + ParentDir, + Normal(Strand<'a>), +} + +impl<'a> From> for Component<'a> { + fn from(value: std::path::Component<'a>) -> Self { + match value { + std::path::Component::Prefix(p) => Self::Prefix(p), + std::path::Component::RootDir => Self::RootDir, + std::path::Component::CurDir => Self::CurDir, + std::path::Component::ParentDir => Self::ParentDir, + std::path::Component::Normal(s) => Self::Normal(Strand::Os(s)), + } + } +} + +impl<'a> From> for Component<'a> { + fn from(value: typed_path::UnixComponent<'a>) -> Self { + match value { + typed_path::UnixComponent::RootDir => Self::RootDir, + typed_path::UnixComponent::CurDir => Self::CurDir, + typed_path::UnixComponent::ParentDir => Self::ParentDir, + typed_path::UnixComponent::Normal(b) => Self::Normal(Strand::Bytes(b)), + } + } +} + +impl<'a> TryFrom> for std::path::Component<'a> { + type Error = PathDynError; + + fn try_from(value: Component<'a>) -> Result { + Ok(match value { + Component::Prefix(p) => Self::Prefix(p), + Component::RootDir => Self::RootDir, + Component::CurDir => Self::CurDir, + Component::ParentDir => Self::ParentDir, + Component::Normal(s) => Self::Normal(s.as_os()?), + }) + } +} + +impl<'a> TryFrom> for typed_path::UnixComponent<'a> { + type Error = PathDynError; + + fn try_from(value: Component<'a>) -> Result { + Ok(match value { + Component::Prefix(_) => Err(PathDynError::AsUnix)?, + Component::RootDir => Self::RootDir, + Component::CurDir => Self::CurDir, + Component::ParentDir => Self::ParentDir, + Component::Normal(s) => Self::Normal(s.encoded_bytes()), + }) + } +} + +impl<'a> FromIterator> for Result { + fn from_iter>>(iter: I) -> Self { + iter.into_iter().map(std::path::Component::try_from).collect() + } +} + +impl<'a> FromIterator> for Result { + fn from_iter>>(iter: I) -> Self { + iter.into_iter().map(typed_path::UnixComponent::try_from).collect() + } +} diff --git a/yazi-shared/src/path/components.rs b/yazi-shared/src/path/components.rs index e69de29b..37273b3b 100644 --- a/yazi-shared/src/path/components.rs +++ b/yazi-shared/src/path/components.rs @@ -0,0 +1,49 @@ +use std::iter::FusedIterator; + +use typed_path::Components as _; + +use crate::{path::{Component, PathDyn}, strand::Strand}; + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum Components<'a> { + Os(std::path::Components<'a>), + Unix(typed_path::UnixComponents<'a>), +} + +impl<'a> Components<'a> { + pub fn path(&self) -> PathDyn<'a> { + match self { + Self::Os(c) => PathDyn::Os(c.as_path()), + Self::Unix(c) => PathDyn::Unix(c.as_path()), + } + } + + pub fn strand(&self) -> Strand<'a> { + match self { + Self::Os(c) => Strand::Os(c.as_path().as_os_str()), + Self::Unix(c) => Strand::Bytes(c.as_bytes()), + } + } +} + +impl<'a> Iterator for Components<'a> { + type Item = Component<'a>; + + fn next(&mut self) -> Option> { + match self { + Self::Os(c) => c.next().map(Into::into), + Self::Unix(c) => c.next().map(Into::into), + } + } +} + +impl<'a> DoubleEndedIterator for Components<'a> { + fn next_back(&mut self) -> Option> { + match self { + Self::Os(c) => c.next_back().map(Into::into), + Self::Unix(c) => c.next_back().map(Into::into), + } + } +} + +impl FusedIterator for Components<'_> {} diff --git a/yazi-shared/src/path/conversion.rs b/yazi-shared/src/path/conversion.rs index 963fef8b..9dbccb4d 100644 --- a/yazi-shared/src/path/conversion.rs +++ b/yazi-shared/src/path/conversion.rs @@ -1,5 +1,3 @@ -use std::ffi::OsStr; - use super::{PathBufDyn, PathDyn}; use crate::path::PathCow; @@ -8,14 +6,6 @@ pub trait AsPath { fn as_path(&self) -> PathDyn<'_>; } -impl AsPath for OsStr { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self.as_ref()) } -} - -impl AsPath for &OsStr { - fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self.as_ref()) } -} - impl AsPath for std::path::Path { fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } } @@ -32,14 +22,11 @@ impl AsPath for PathBufDyn { fn as_path(&self) -> PathDyn<'_> { match self { Self::Os(p) => PathDyn::Os(p), + Self::Unix(p) => PathDyn::Unix(p), } } } -impl AsPath for &PathBufDyn { - fn as_path(&self) -> PathDyn<'_> { (*self).as_path() } -} - impl AsPath for PathCow<'_> { fn as_path(&self) -> PathDyn<'_> { match self { @@ -49,6 +36,10 @@ impl AsPath for PathCow<'_> { } } +impl AsPath for super::Components<'_> { + fn as_path(&self) -> PathDyn<'_> { self.path() } +} + // --- AsPathRef pub trait AsPathRef<'a> { fn as_path_ref(self) -> PathDyn<'a>; diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs index a2523944..3e1414b7 100644 --- a/yazi-shared/src/path/cow.rs +++ b/yazi-shared/src/path/cow.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use anyhow::Result; -use crate::{IntoOsStr, path::{AsPath, PathBufDyn, PathDyn}}; +use crate::path::{AsPath, PathBufDyn, PathDyn, PathKind}; // --- PathCow pub enum PathCow<'a> { @@ -32,17 +32,21 @@ impl PartialEq<&str> for PathCow<'_> { } impl<'a> PathCow<'a> { - pub fn from_os_bytes(bytes: impl Into>) -> Result { - Ok(match bytes.into().into_os_str()? { - Cow::Borrowed(s) => PathDyn::os(s).into(), - Cow::Owned(s) => PathBufDyn::os(s).into(), - }) - } - pub fn into_owned(self) -> PathBufDyn { match self { Self::Borrowed(s) => s.to_owned(), Self::Owned(s) => s, } } + + pub fn with(kind: K, bytes: T) -> Result + where + K: Into, + T: Into>, + { + Ok(match bytes.into() { + Cow::Borrowed(b) => PathDyn::with(kind, b)?.into(), + Cow::Owned(b) => PathBufDyn::with(kind, b)?.into(), + }) + } } diff --git a/yazi-shared/src/path/display.rs b/yazi-shared/src/path/display.rs new file mode 100644 index 00000000..dbdb5df3 --- /dev/null +++ b/yazi-shared/src/path/display.rs @@ -0,0 +1,12 @@ +use crate::path::PathDyn; + +pub struct Display<'a>(pub PathDyn<'a>); + +impl std::fmt::Display for Display<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + PathDyn::Os(p) => write!(f, "{}", p.display()), + PathDyn::Unix(p) => write!(f, "{}", p.display()), + } + } +} diff --git a/yazi-shared/src/path/error.rs b/yazi-shared/src/path/error.rs index 8b28d274..772d7250 100644 --- a/yazi-shared/src/path/error.rs +++ b/yazi-shared/src/path/error.rs @@ -12,7 +12,7 @@ pub struct EndsWithError; #[error("calling join on paths with different encodings")] pub enum JoinError { FromWtf8, - FromPathBufDyn(#[from] PathBufDynError), + FromPathDyn(#[from] PathDynError), } impl From for std::io::Error { @@ -22,27 +22,33 @@ impl From for std::io::Error { // --- PathDynError #[derive(Debug, Error)] pub enum PathDynError { - #[error("conversion to OsStr failed")] + #[error("conversion to OS path failed")] AsOs, + #[error("conversion to Unix path failed")] + AsUnix, + #[error("conversion to UTF-8 path failed")] + AsUtf8, +} + +impl From for PathDynError { + fn from(err: StrandError) -> Self { + match err { + StrandError::AsOs => Self::AsOs, + StrandError::AsUtf8 => Self::AsUtf8, + } + } } impl From for std::io::Error { fn from(err: PathDynError) -> Self { Self::other(err) } } -// --- PathBufDynError -#[derive(Debug, Error)] -pub enum PathBufDynError { - #[error("conversion to OsString failed")] - IntoOs, -} - // --- SetNameError #[derive(Debug, Error)] #[error("calling set_name on paths with different encodings")] pub enum SetNameError { FromWtf8, - FromStrandDyn(#[from] StrandError), + FromStrand(#[from] StrandError), } impl From for std::io::Error { @@ -89,3 +95,7 @@ pub enum StripPrefixError { impl From for StripPrefixError { fn from(_: std::path::StripPrefixError) -> Self { Self::NotPrefix } } + +impl From for StripPrefixError { + fn from(_: typed_path::StripPrefixError) -> Self { Self::NotPrefix } +} diff --git a/yazi-shared/src/path/kind.rs b/yazi-shared/src/path/kind.rs index 4c9e1920..a0dd82ba 100644 --- a/yazi-shared/src/path/kind.rs +++ b/yazi-shared/src/path/kind.rs @@ -2,6 +2,7 @@ use crate::scheme::SchemeKind; pub enum PathKind { Os, + Unix, } impl From for PathKind { diff --git a/yazi-shared/src/path/like.rs b/yazi-shared/src/path/like.rs index 99ee4b5e..0c8ca02c 100644 --- a/yazi-shared/src/path/like.rs +++ b/yazi-shared/src/path/like.rs @@ -2,14 +2,14 @@ use std::borrow::Cow; use anyhow::Result; -use crate::{Utf8BytePredictor, path::{AsPath, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError}, strand::{AsStrand, Strand}}; +use crate::{Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError}, strand::{AsStrand, Strand}}; pub trait PathLike: AsPath { fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.as_path().as_os() } - fn components(&self) -> std::path::Components<'_> { self.as_path().components() } + fn components(&self) -> Components<'_> { self.as_path().components() } - fn display(&self) -> std::path::Display<'_> { self.as_path().display() } + fn display(&self) -> Display<'_> { self.as_path().display() } fn encoded_bytes(&self) -> &[u8] { self.as_path().encoded_bytes() } diff --git a/yazi-shared/src/path/mod.rs b/yazi-shared/src/path/mod.rs index e7691f0c..a3d7f658 100644 --- a/yazi-shared/src/path/mod.rs +++ b/yazi-shared/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(buf component components conversion cow error kind like path view); +yazi_macro::mod_flat!(buf component components conversion cow display error kind like path view); diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index 3ac95899..eb895296 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -4,17 +4,22 @@ use anyhow::Result; use hashbrown::Equivalent; use super::{RsplitOnceError, StartsWithError}; -use crate::{BytesExt, FromWtf8, Utf8BytePredictor, path::{AsPath, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError}, strand::{AsStrand, Strand, StrandError}}; +use crate::{BytesExt, FromWtf8, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError}, strand::{AsStrand, Strand, StrandError}}; #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum PathDyn<'p> { Os(&'p std::path::Path), + Unix(&'p typed_path::UnixPath), } impl<'a> From<&'a std::path::Path> for PathDyn<'a> { fn from(value: &'a std::path::Path) -> Self { Self::Os(value) } } +impl<'a> From<&'a typed_path::UnixPath> for PathDyn<'a> { + fn from(value: &'a typed_path::UnixPath) -> Self { Self::Unix(value) } +} + impl<'a> From<&'a PathBufDyn> for PathDyn<'a> { fn from(value: &'a PathBufDyn) -> Self { value.as_path() } } @@ -33,8 +38,9 @@ impl PartialEq<&std::path::Path> for PathDyn<'_> { impl PartialEq<&str> for PathDyn<'_> { fn eq(&self, other: &&str) -> bool { - match self { - PathDyn::Os(p) => p == other, + match *self { + PathDyn::Os(p) => p == *other, + PathDyn::Unix(p) => p == typed_path::UnixPath::new(other), } } } @@ -48,30 +54,30 @@ impl<'p> PathDyn<'p> { pub fn as_os(self) -> Result<&'p std::path::Path, PathDynError> { match self { Self::Os(p) => Ok(p), + Self::Unix(_) => Err(PathDynError::AsOs), } } - pub fn components(self) -> std::path::Components<'p> { + pub fn components(self) -> Components<'p> { match self { - Self::Os(p) => p.components(), + Self::Os(p) => Components::Os(p.components()), + Self::Unix(p) => Components::Unix(p.components()), } } - pub fn display(self) -> std::path::Display<'p> { - match self { - Self::Os(p) => p.display(), - } - } + pub fn display(self) -> Display<'p> { Display(self) } pub fn encoded_bytes(self) -> &'p [u8] { match self { Self::Os(p) => p.as_os_str().as_encoded_bytes(), + Self::Unix(p) => p.as_bytes(), } } pub fn ext(self) -> Option> { Some(match self { Self::Os(p) => p.extension()?.into(), + Self::Unix(p) => p.extension()?.into(), }) } @@ -82,18 +88,21 @@ impl<'p> PathDyn<'p> { { match kind.into() { PathKind::Os => Self::Os(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }.as_ref()), + PathKind::Unix => Self::Unix(typed_path::UnixPath::new(bytes)), } } pub fn has_root(self) -> bool { match self { Self::Os(p) => p.has_root(), + Self::Unix(p) => p.has_root(), } } pub fn is_absolute(self) -> bool { match self { Self::Os(p) => p.is_absolute(), + Self::Unix(p) => p.is_absolute(), } } @@ -107,6 +116,7 @@ impl<'p> PathDyn<'p> { pub fn kind(self) -> PathKind { match self { Self::Os(_) => PathKind::Os, + Self::Unix(_) => PathKind::Unix, } } @@ -115,20 +125,14 @@ impl<'p> PathDyn<'p> { pub fn name(self) -> Option> { Some(match self { Self::Os(p) => p.file_name()?.into(), + Self::Unix(p) => p.file_name()?.into(), }) } - #[inline] - pub fn os(path: &'p T) -> Self - where - T: ?Sized + AsRef, - { - Self::Os(path.as_ref()) - } - pub fn parent(self) -> Option { Some(match self { Self::Os(p) => Self::Os(p.parent()?), + Self::Unix(p) => Self::Unix(p.parent()?), }) } @@ -145,6 +149,7 @@ impl<'p> PathDyn<'p> { pub fn stem(self) -> Option> { Some(match self { Self::Os(p) => p.file_stem()?.into(), + Self::Unix(p) => p.file_stem()?.into(), }) } @@ -152,12 +157,14 @@ impl<'p> PathDyn<'p> { pub fn to_os_owned(self) -> Result { match self { Self::Os(p) => Ok(p.to_owned()), + Self::Unix(_) => Err(PathDynError::AsOs), } } pub fn to_owned(self) -> PathBufDyn { match self { - Self::Os(p) => PathBufDyn::Os(p.to_path_buf()), + Self::Os(p) => PathBufDyn::Os(p.to_owned()), + Self::Unix(p) => PathBufDyn::Unix(p.to_owned()), } } @@ -177,6 +184,8 @@ impl<'p> PathDyn<'p> { (Self::Os(p), Strand::Bytes(b)) => { p.ends_with(OsStr::from_wtf8(b).map_err(|_| EndsWithError)?) } + + (Self::Unix(p), s) => p.ends_with(s.encoded_bytes()), }) } @@ -190,6 +199,8 @@ impl<'p> PathDyn<'p> { (Self::Os(p), Strand::Bytes(b)) => { PathBufDyn::Os(p.join(OsStr::from_wtf8(b).map_err(|_| JoinError::FromWtf8)?)) } + + (Self::Unix(p), s) => PathBufDyn::Unix(p.join(s.encoded_bytes())), }) } @@ -203,6 +214,7 @@ impl<'p> PathDyn<'p> { PathDyn::Os(p) => { p.as_os_str().as_encoded_bytes().rsplit_seq_once(pat.as_os()?.as_encoded_bytes()) } + PathDyn::Unix(p) => p.as_bytes().rsplit_seq_once(pat.encoded_bytes()), } .ok_or(RsplitOnceError::NotFound)?; @@ -216,11 +228,13 @@ impl<'p> PathDyn<'p> { T: AsStrand, { Ok(match (self, base.as_strand()) { - (Self::Os(p), Strand::Os(q)) => p.starts_with(q), - (Self::Os(p), Strand::Utf8(q)) => p.starts_with(q), + (Self::Os(p), Strand::Os(s)) => p.starts_with(s), + (Self::Os(p), Strand::Utf8(s)) => p.starts_with(s), (Self::Os(p), Strand::Bytes(b)) => { p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) } + + (Self::Unix(p), s) => p.starts_with(s.encoded_bytes()), }) } @@ -229,22 +243,37 @@ impl<'p> PathDyn<'p> { T: AsStrand, { Ok(match (self, base.as_strand()) { - (Self::Os(p), Strand::Os(q)) => Self::Os(p.strip_prefix(q)?), - (Self::Os(p), Strand::Utf8(q)) => Self::Os(p.strip_prefix(q)?), + (Self::Os(p), Strand::Os(s)) => Self::Os(p.strip_prefix(s)?), + (Self::Os(p), Strand::Utf8(s)) => Self::Os(p.strip_prefix(s)?), (Self::Os(p), Strand::Bytes(b)) => { Self::Os(p.strip_prefix(OsStr::from_wtf8(b).map_err(|_| StripPrefixError::WrongEncoding)?)?) } + + (Self::Unix(p), s) => Self::Unix(p.strip_prefix(s.encoded_bytes())?), }) } - pub fn with(kind: K, strand: &'p T) -> Result + pub fn with(kind: K, strand: &'p S) -> Result where K: Into, - T: ?Sized + AsStrand, + S: ?Sized + AsStrand, { let s = strand.as_strand(); Ok(match kind.into() { PathKind::Os => Self::Os(std::path::Path::new(s.as_os()?)), + PathKind::Unix => Self::Unix(typed_path::UnixPath::new(s.encoded_bytes())), }) } + + pub fn with_str(kind: K, s: &'p S) -> Self + where + K: Into, + S: ?Sized + AsRef, + { + let s = s.as_ref(); + match kind.into() { + PathKind::Os => Self::Os(s.as_ref()), + PathKind::Unix => Self::Unix(s.as_ref()), + } + } } diff --git a/yazi-shared/src/path/view.rs b/yazi-shared/src/path/view.rs index 883977d9..601682a5 100644 --- a/yazi-shared/src/path/view.rs +++ b/yazi-shared/src/path/view.rs @@ -1,16 +1,8 @@ -use std::{borrow::Cow, ffi::OsStr}; - -use crate::path::{AsPath, PathDyn}; - // --- AsPathView pub trait AsPathView<'a, T> { fn as_path_view(self) -> T; } -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a OsStr { - fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } -} - impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { fn as_path_view(self) -> &'a std::path::Path { self } } @@ -18,22 +10,3 @@ impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { fn as_path_view(self) -> &'a std::path::Path { self } } - -impl<'a> AsPathView<'a, &'a std::path::Path> for std::path::Components<'a> { - fn as_path_view(self) -> &'a std::path::Path { self.as_path() } -} - -impl<'a, T> AsPathView<'a, PathDyn<'a>> for &'a T -where - T: AsPath, -{ - fn as_path_view(self) -> PathDyn<'a> { self.as_path() } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a Cow<'_, OsStr> { - fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for crate::loc::Loc<'a, &'a std::path::Path> { - fn as_path_view(self) -> &'a std::path::Path { *self } -} diff --git a/yazi-shared/src/rand.rs b/yazi-shared/src/rand.rs deleted file mode 100644 index 20271405..00000000 --- a/yazi-shared/src/rand.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -pub struct LcgRng { - seed: u64, -} - -impl LcgRng { - const A: u64 = 6364136223846793005; - const C: u64 = 1; - const M: u64 = u64::MAX; -} - -impl Iterator for LcgRng { - type Item = u64; - - fn next(&mut self) -> Option { - self.seed = Self::A.wrapping_mul(self.seed).wrapping_add(Self::C) % Self::M; - Some(self.seed) - } -} - -impl Default for LcgRng { - fn default() -> Self { - let time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards"); - Self { seed: time.as_secs() ^ time.subsec_nanos() as u64 } - } -} diff --git a/yazi-shared/src/scheme/cow.rs b/yazi-shared/src/scheme/cow.rs index 641c298d..7c92c371 100644 --- a/yazi-shared/src/scheme/cow.rs +++ b/yazi-shared/src/scheme/cow.rs @@ -30,6 +30,10 @@ impl From> for Scheme { fn from(value: SchemeCow<'_>) -> Self { value.into_owned() } } +impl PartialEq> for SchemeCow<'_> { + fn eq(&self, other: &SchemeRef) -> bool { self.as_scheme() == *other } +} + impl<'a> SchemeCow<'a> { pub fn regular(uri: usize, urn: usize) -> Self { SchemeRef::Regular { uri, urn }.into() } @@ -130,12 +134,7 @@ impl<'a> SchemeCow<'a> { fn decode_path(kind: SchemeKind, tilde: bool, bytes: &'a [u8]) -> Result> { let bytes: Cow<_> = if tilde { percent_decode(bytes).into() } else { bytes.into() }; - Ok(match kind { - SchemeKind::Regular => PathCow::from_os_bytes(bytes)?, - SchemeKind::Search => PathCow::from_os_bytes(bytes)?, - SchemeKind::Archive => PathCow::from_os_bytes(bytes)?, - SchemeKind::Sftp => PathCow::from_os_bytes(bytes)?, - }) + PathCow::with(kind, bytes) } fn normalize_ports( diff --git a/yazi-shared/src/scheme/scheme.rs b/yazi-shared/src/scheme/scheme.rs index 886dbca0..71d6a84f 100644 --- a/yazi-shared/src/scheme/scheme.rs +++ b/yazi-shared/src/scheme/scheme.rs @@ -28,4 +28,14 @@ impl Scheme { } } } + + #[inline] + pub fn with_ports(self, uri: usize, urn: usize) -> Self { + match self { + Self::Regular { .. } => Self::Regular { uri, urn }, + Self::Search { domain, .. } => Self::Search { domain, uri, urn }, + Self::Archive { domain, .. } => Self::Archive { domain, uri, urn }, + Self::Sftp { domain, .. } => Self::Sftp { domain, uri, urn }, + } + } } diff --git a/yazi-shared/src/strand/buf.rs b/yazi-shared/src/strand/buf.rs index f5b53849..f75f093d 100644 --- a/yazi-shared/src/strand/buf.rs +++ b/yazi-shared/src/strand/buf.rs @@ -1,8 +1,9 @@ -use std::ffi::OsString; +use std::{borrow::Cow, ffi::OsString}; +use anyhow::Result; use serde::Serialize; -use crate::{path::PathDyn, scheme::SchemeKind, strand::{AsStrand, Strand, StrandError, StrandKind}}; +use crate::{FromWtf8Vec, path::PathDyn, strand::{AsStrand, Strand, StrandCow, StrandError, StrandKind}}; // --- StrandBuf #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] @@ -21,6 +22,10 @@ impl From for StrandBuf { fn from(value: OsString) -> Self { Self::Os(value) } } +impl From<&str> for StrandBuf { + fn from(value: &str) -> Self { Self::Utf8(value.to_owned()) } +} + impl From for StrandBuf { fn from(value: String) -> Self { Self::Utf8(value) } } @@ -29,10 +34,15 @@ impl From> for StrandBuf { fn from(value: PathDyn) -> Self { match value { PathDyn::Os(p) => Self::Os(p.as_os_str().to_owned()), + PathDyn::Unix(p) => Self::Bytes(p.as_bytes().to_owned()), } } } +impl From> for StrandBuf { + fn from(value: StrandCow<'_>) -> Self { value.into_owned() } +} + impl PartialEq> for StrandBuf { fn eq(&self, other: &Strand<'_>) -> bool { self.as_strand() == *other } } @@ -49,8 +59,8 @@ impl StrandBuf { #[inline] pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: Vec) -> Self { match kind.into() { - StrandKind::Os => Self::Os(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), StrandKind::Utf8 => Self::Utf8(unsafe { String::from_utf8_unchecked(bytes) }), + StrandKind::Os => Self::Os(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), StrandKind::Bytes => Self::Bytes(bytes), } } @@ -63,6 +73,39 @@ impl StrandBuf { } } + pub fn into_string_lossy(self) -> String { + match self { + Self::Os(s) => match s.to_string_lossy() { + Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(s.into_encoded_bytes()) }, + Cow::Owned(s) => s, + }, + Self::Utf8(s) => s, + Self::Bytes(b) => match String::from_utf8_lossy(&b) { + Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(b) }, + Cow::Owned(s) => s, + }, + } + } + + pub fn new(kind: impl Into) -> Self { Self::with_str(kind, "") } + + pub fn push_str(&mut self, s: impl AsRef) { + let s = s.as_ref(); + match self { + Self::Os(buf) => buf.push(s), + Self::Utf8(buf) => buf.push_str(s), + Self::Bytes(buf) => buf.extend(s.as_bytes()), + } + } + + pub fn reserve_exact(&mut self, additional: usize) { + match self { + Self::Os(buf) => buf.reserve_exact(additional), + Self::Utf8(buf) => buf.reserve_exact(additional), + Self::Bytes(buf) => buf.reserve_exact(additional), + } + } + pub fn try_push(&mut self, s: T) -> Result<(), StrandError> where T: AsStrand, @@ -75,11 +118,38 @@ impl StrandBuf { }) } - pub fn with_capacity(kind: SchemeKind, capacity: usize) -> Self { - use SchemeKind as K; - match kind { - K::Regular | K::Search | K::Archive => Self::Os(OsString::with_capacity(capacity)), - K::Sftp => Self::Os(OsString::with_capacity(capacity)), // FIXME + pub fn with(kind: K, bytes: Vec) -> Result + where + K: Into, + { + Ok(match kind.into() { + StrandKind::Utf8 => Self::Utf8(String::from_utf8(bytes)?), + StrandKind::Os => Self::Os(OsString::from_wtf8_vec(bytes)?), + StrandKind::Bytes => Self::Bytes(bytes), + }) + } + + pub fn with_capacity(kind: K, capacity: usize) -> Self + where + K: Into, + { + match kind.into() { + StrandKind::Utf8 => Self::Utf8(String::with_capacity(capacity)), + StrandKind::Os => Self::Os(OsString::with_capacity(capacity)), + StrandKind::Bytes => Self::Bytes(Vec::with_capacity(capacity)), + } + } + + pub fn with_str(kind: K, s: S) -> Self + where + K: Into, + S: Into, + { + let s = s.into(); + match kind.into() { + StrandKind::Utf8 => Self::Utf8(s), + StrandKind::Os => Self::Os(s.into()), + StrandKind::Bytes => Self::Bytes(s.into_bytes()), } } } diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs index 5f21c101..e8e628e6 100644 --- a/yazi-shared/src/strand/conversion.rs +++ b/yazi-shared/src/strand/conversion.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}}; -use crate::{path::{PathBufDyn, PathDyn}, strand::{Strand, StrandBuf, StrandCow}}; +use crate::{path::{PathBufDyn, PathCow, PathDyn}, strand::{Strand, StrandBuf, StrandCow}, url::AsUrl}; // --- AsStrand pub trait AsStrand { @@ -11,14 +11,6 @@ impl AsStrand for [u8] { fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } } -impl AsStrand for &[u8] { - fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } -} - -impl AsStrand for Vec { - fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } -} - impl AsStrand for str { fn as_strand(&self) -> Strand<'_> { Strand::Utf8(self) } } @@ -51,6 +43,10 @@ impl AsStrand for &std::path::Path { fn as_strand(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } } +impl AsStrand for crate::path::Components<'_> { + fn as_strand(&self) -> Strand<'_> { self.strand() } +} + impl AsStrand for Cow<'_, OsStr> { fn as_strand(&self) -> Strand<'_> { Strand::Os(self) } } @@ -59,14 +55,29 @@ impl AsStrand for PathDyn<'_> { fn as_strand(&self) -> Strand<'_> { match self { Self::Os(p) => Strand::Os(p.as_os_str()), + Self::Unix(p) => Strand::Bytes(p.as_bytes()), + } + } +} + +impl AsStrand for PathBufDyn { + fn as_strand(&self) -> Strand<'_> { + match self { + PathBufDyn::Os(p) => Strand::Os(p.as_os_str()), + PathBufDyn::Unix(p) => Strand::Bytes(p.as_bytes()), } } } impl AsStrand for &PathBufDyn { + fn as_strand(&self) -> Strand<'_> { (**self).as_strand() } +} + +impl AsStrand for PathCow<'_> { fn as_strand(&self) -> Strand<'_> { match self { - PathBufDyn::Os(p) => Strand::Os(p.as_os_str()), + Self::Borrowed(p) => p.as_strand(), + Self::Owned(p) => p.as_strand(), } } } @@ -101,3 +112,19 @@ impl AsStrand for StrandCow<'_> { impl AsStrand for &StrandCow<'_> { fn as_strand(&self) -> Strand<'_> { (**self).as_strand() } } + +// --- ToStrand +pub trait ToStrand { + fn to_strand(&self) -> StrandCow<'_>; +} + +impl ToStrand for String { + fn to_strand(&self) -> StrandCow<'_> { self.as_strand().into() } +} + +impl ToStrand for T +where + T: AsUrl, +{ + fn to_strand(&self) -> StrandCow<'_> { self.as_url().components().strand() } +} diff --git a/yazi-shared/src/strand/cow.rs b/yazi-shared/src/strand/cow.rs index 58b85aa1..f18f099b 100644 --- a/yazi-shared/src/strand/cow.rs +++ b/yazi-shared/src/strand/cow.rs @@ -2,14 +2,17 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}}; use anyhow::Result; -use crate::{IntoOsStr, scheme::SchemeKind, strand::{Strand, StrandBuf}}; +use crate::strand::{Strand, StrandBuf, StrandKind}; -// --- StrandCow pub enum StrandCow<'a> { Borrowed(Strand<'a>), Owned(StrandBuf), } +impl Default for StrandCow<'_> { + fn default() -> Self { Self::Borrowed(Strand::default()) } +} + impl From for StrandCow<'_> { fn from(value: OsString) -> Self { Self::Owned(StrandBuf::Os(value)) } } @@ -41,13 +44,6 @@ impl PartialEq> for StrandCow<'_> { } impl<'a> StrandCow<'a> { - pub fn from_os_bytes(bytes: impl Into>) -> Result { - Ok(match bytes.into().into_os_str()? { - Cow::Borrowed(s) => Strand::Os(s).into(), - Cow::Owned(s) => StrandBuf::Os(s).into(), - }) - } - pub fn into_owned(self) -> StrandBuf { match self { Self::Borrowed(s) => s.to_owned(), @@ -55,13 +51,21 @@ impl<'a> StrandCow<'a> { } } - pub fn with(kind: SchemeKind, bytes: T) -> Result - where - T: Into>, - { - match kind { - SchemeKind::Regular | SchemeKind::Search | SchemeKind::Archive => Self::from_os_bytes(bytes), - SchemeKind::Sftp => Self::from_os_bytes(bytes), // FIXME + pub fn into_string_lossy(self) -> String { + match self { + Self::Borrowed(s) => s.to_string_lossy().into_owned(), + Self::Owned(s) => s.into_string_lossy(), } } + + pub fn with(kind: K, bytes: T) -> Result + where + K: Into, + T: Into>, + { + Ok(match bytes.into() { + Cow::Borrowed(b) => Strand::with(kind, b)?.into(), + Cow::Owned(b) => StrandBuf::with(kind, b)?.into(), + }) + } } diff --git a/yazi-shared/src/strand/error.rs b/yazi-shared/src/strand/error.rs index 83e521da..d66e86d1 100644 --- a/yazi-shared/src/strand/error.rs +++ b/yazi-shared/src/strand/error.rs @@ -1,24 +1,14 @@ use thiserror::Error; -use crate::path::PathDynError; - -// --- StrandDynError +// --- StrandError #[derive(Debug, Error)] pub enum StrandError { - #[error("conversion to OsStr failed")] + #[error("conversion to OS string failed")] AsOs, - #[error("conversion to UTF-8 str failed")] + #[error("conversion to UTF-8 string failed")] AsUtf8, } -impl From for StrandError { - fn from(err: PathDynError) -> Self { - match err { - PathDynError::AsOs => Self::AsOs, - } - } -} - impl From for std::io::Error { fn from(err: StrandError) -> Self { Self::other(err) } } diff --git a/yazi-shared/src/strand/extensions.rs b/yazi-shared/src/strand/extensions.rs new file mode 100644 index 00000000..24e53cf4 --- /dev/null +++ b/yazi-shared/src/strand/extensions.rs @@ -0,0 +1,61 @@ +use crate::strand::{AsStrand, Strand, StrandBuf, StrandLike, ToStrand}; + +// --- StrandJoin +pub trait AsStrandJoin { + fn join(self, sep: Strand) -> StrandBuf; +} + +impl AsStrandJoin for T +where + T: IntoIterator, + T::Item: AsStrand, +{ + fn join(self, sep: Strand) -> StrandBuf { + let mut kind = sep.kind(); + let mut buf = Vec::new(); + for (i, item) in self.into_iter().enumerate() { + if i > 0 { + buf.extend(sep.encoded_bytes()); + } + + let s = item.as_strand(); + buf.extend(s.encoded_bytes()); + + if s.kind() > kind { + kind = s.kind(); + } + } + + unsafe { StrandBuf::from_encoded_bytes(kind, buf) } + } +} + +// --- ToStrandJoin +pub trait ToStrandJoin { + fn join(self, sep: Strand) -> StrandBuf; +} + +impl ToStrandJoin for T +where + T: IntoIterator, + T::Item: ToStrand, +{ + fn join(self, sep: Strand) -> StrandBuf { + let mut kind = sep.kind(); + let mut buf = Vec::new(); + for (i, item) in self.into_iter().enumerate() { + if i > 0 { + buf.extend(sep.encoded_bytes()); + } + + let s = item.to_strand(); + buf.extend(s.encoded_bytes()); + + if s.kind() > kind { + kind = s.kind(); + } + } + + unsafe { StrandBuf::from_encoded_bytes(kind, buf) } + } +} diff --git a/yazi-shared/src/strand/kind.rs b/yazi-shared/src/strand/kind.rs index 77c050aa..dd2e0883 100644 --- a/yazi-shared/src/strand/kind.rs +++ b/yazi-shared/src/strand/kind.rs @@ -1,5 +1,28 @@ +use crate::{path::PathKind, scheme::SchemeKind}; + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum StrandKind { - Os, - Utf8, - Bytes, + Utf8 = 0, + Os = 1, + Bytes = 2, +} + +impl From for StrandKind { + fn from(value: PathKind) -> Self { + match value { + PathKind::Os => StrandKind::Os, + PathKind::Unix => StrandKind::Bytes, + } + } +} + +impl From for StrandKind { + fn from(value: SchemeKind) -> Self { + match value { + SchemeKind::Regular => Self::Os, + SchemeKind::Search => Self::Os, + SchemeKind::Archive => Self::Os, + SchemeKind::Sftp => Self::Os, // FIXME + } + } } diff --git a/yazi-shared/src/strand/mod.rs b/yazi-shared/src/strand/mod.rs index ca1ffd07..af6a08c4 100644 --- a/yazi-shared/src/strand/mod.rs +++ b/yazi-shared/src/strand/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(buf conversion cow error kind like r#dyn view); +yazi_macro::mod_flat!(buf conversion cow error extensions kind like strand view); diff --git a/yazi-shared/src/strand/dyn.rs b/yazi-shared/src/strand/strand.rs similarity index 74% rename from yazi-shared/src/strand/dyn.rs rename to yazi-shared/src/strand/strand.rs index 5512cb7a..92dc418f 100644 --- a/yazi-shared/src/strand/dyn.rs +++ b/yazi-shared/src/strand/strand.rs @@ -1,9 +1,11 @@ use std::{borrow::Cow, ffi::OsStr, fmt::Display}; +use anyhow::Result; + use crate::{BytesExt, FromWtf8, strand::{AsStrand, StrandBuf, StrandError, StrandKind}}; // --- Strand -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialOrd)] pub enum Strand<'p> { Os(&'p OsStr), Utf8(&'p str), @@ -22,6 +24,10 @@ impl<'a> From<&'a str> for Strand<'a> { fn from(value: &'a str) -> Self { Self::Utf8(value) } } +impl<'a> From<&'a [u8]> for Strand<'a> { + fn from(value: &'a [u8]) -> Self { Self::Bytes(value) } +} + impl<'a> From<&'a StrandBuf> for Strand<'a> { fn from(value: &'a StrandBuf) -> Self { match value { @@ -32,12 +38,42 @@ impl<'a> From<&'a StrandBuf> for Strand<'a> { } } +impl PartialEq for Strand<'_> { + fn eq(&self, other: &Self) -> bool { + match *other { + Self::Os(s) => *self == s, + Self::Utf8(s) => *self == s, + Self::Bytes(b) => *self == b, + } + } +} + +impl PartialEq<&OsStr> for Strand<'_> { + fn eq(&self, other: &&OsStr) -> bool { + match *self { + Self::Os(s) => s == *other, + Self::Utf8(s) => s == *other, + Self::Bytes(b) => b == other.as_encoded_bytes(), + } + } +} + impl PartialEq<&str> for Strand<'_> { fn eq(&self, other: &&str) -> bool { - match self { - Self::Os(s) => s == other, - Self::Utf8(s) => s == other, - Self::Bytes(b) => *b == other.as_bytes(), + match *self { + Self::Os(s) => s == *other, + Self::Utf8(s) => s == *other, + Self::Bytes(b) => b == other.as_bytes(), + } + } +} + +impl PartialEq<&[u8]> for Strand<'_> { + fn eq(&self, other: &&[u8]) -> bool { + match *self { + Self::Os(s) => s.as_encoded_bytes() == *other, + Self::Utf8(s) => s.as_bytes() == *other, + Self::Bytes(b) => b == *other, } } } @@ -102,6 +138,7 @@ impl<'a> Strand<'a> { D(self) } + #[inline] pub fn encoded_bytes(self) -> &'a [u8] { match self { Self::Os(s) => s.as_encoded_bytes(), @@ -117,8 +154,8 @@ impl<'a> Strand<'a> { #[inline] pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: &'a [u8]) -> Self { match kind.into() { - StrandKind::Os => Self::Os(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }), StrandKind::Utf8 => Self::Utf8(unsafe { str::from_utf8_unchecked(bytes) }), + StrandKind::Os => Self::Os(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }), StrandKind::Bytes => Self::Bytes(bytes), } } @@ -127,8 +164,8 @@ impl<'a> Strand<'a> { pub fn kind(self) -> StrandKind { match self { - Self::Os(_) => StrandKind::Os, Self::Utf8(_) => StrandKind::Utf8, + Self::Os(_) => StrandKind::Os, Self::Bytes(_) => StrandKind::Bytes, } } @@ -152,4 +189,17 @@ impl<'a> Strand<'a> { } pub fn to_string_lossy(self) -> Cow<'a, str> { String::from_utf8_lossy(self.encoded_bytes()) } + + pub fn with(kind: K, strand: &'a S) -> Result + where + K: Into, + S: ?Sized + AsStrand, + { + let strand = strand.as_strand(); + Ok(match kind.into() { + StrandKind::Utf8 => Self::Utf8(strand.as_utf8()?), + StrandKind::Os => Self::Os(strand.as_os()?), + StrandKind::Bytes => Self::Bytes(strand.encoded_bytes()), + }) + } } diff --git a/yazi-shared/src/strand/view.rs b/yazi-shared/src/strand/view.rs index 454f713b..3a3e7a35 100644 --- a/yazi-shared/src/strand/view.rs +++ b/yazi-shared/src/strand/view.rs @@ -1,16 +1,10 @@ use std::ffi::OsStr; -use crate::{path::PathDyn, strand::Strand}; - // --- AsStrandView pub trait AsStrandView<'a, T> { fn as_strand_view(self) -> T; } -impl<'a> AsStrandView<'a, &'a OsStr> for &'a str { - fn as_strand_view(self) -> &'a OsStr { OsStr::new(self) } -} - impl<'a> AsStrandView<'a, &'a OsStr> for &'a OsStr { fn as_strand_view(self) -> &'a OsStr { self } } @@ -22,23 +16,3 @@ impl<'a> AsStrandView<'a, &'a OsStr> for &'a std::path::Path { impl<'a> AsStrandView<'a, &'a OsStr> for std::path::Components<'a> { fn as_strand_view(self) -> &'a OsStr { self.as_path().as_os_str() } } - -impl<'a> AsStrandView<'a, Strand<'a>> for &'a str { - fn as_strand_view(self) -> Strand<'a> { Strand::Utf8(self) } -} - -impl<'a> AsStrandView<'a, Strand<'a>> for &'a std::path::Path { - fn as_strand_view(self) -> Strand<'a> { Strand::Os(self.as_os_str()) } -} - -impl<'a> AsStrandView<'a, Strand<'a>> for std::path::Components<'a> { - fn as_strand_view(self) -> Strand<'a> { Strand::Os(self.as_path().as_os_str()) } -} - -impl<'a> AsStrandView<'a, Strand<'a>> for PathDyn<'a> { - fn as_strand_view(self) -> Strand<'a> { - match self { - Self::Os(p) => Strand::Os(p.as_os_str()), - } - } -} diff --git a/yazi-shared/src/string.rs b/yazi-shared/src/string.rs deleted file mode 100644 index 20f961a5..00000000 --- a/yazi-shared/src/string.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}}; - -use crate::url::{UrlBuf, UrlLike}; - -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 &UrlBuf { - fn into_string_lossy(self) -> String { self.os_str().into_string_lossy() } -} diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 4206a177..9986ab46 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt::{Debug, Formatter}, path::{Path, PathBuf}, str::From use anyhow::Result; use serde::{Deserialize, Serialize}; -use crate::{loc::LocBuf, path::{PathBufDyn, PathBufDynError, PathDyn, PathDynError, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::SchemeKind, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlLike}}; +use crate::{loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::{Scheme, SchemeKind}, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlLike}}; #[derive(Clone, Eq, Hash, PartialEq)] pub enum UrlBuf { @@ -53,14 +53,6 @@ impl From<&Path> for UrlBuf { fn from(path: &Path) -> Self { path.to_path_buf().into() } } -impl From> for UrlBuf { - fn from(path: PathDyn) -> Self { - match path { - PathDyn::Os(p) => p.to_path_buf().into(), - } - } -} - impl FromStr for UrlBuf { type Err = anyhow::Error; @@ -75,6 +67,14 @@ impl TryFrom for UrlBuf { } } +impl TryFrom<(Scheme, PathBufDyn)> for UrlBuf { + type Error = anyhow::Error; + + fn try_from(value: (Scheme, PathBufDyn)) -> Result { + Ok(UrlCow::try_from(value)?.into_owned()) + } +} + impl AsRef for UrlBuf { fn as_ref(&self) -> &Self { self } } @@ -155,7 +155,7 @@ impl UrlBuf { pub fn to_regular(&self) -> Result { Ok(self.as_url().as_regular()?.into()) } #[inline] - pub fn into_regular(self) -> Result { + pub fn into_regular(self) -> Result { Ok(Self::Regular(self.into_loc().into_os()?.into())) } @@ -172,7 +172,7 @@ impl UrlBuf { } #[inline] - pub fn into_search(self, domain: impl AsRef) -> Result { + pub fn into_search(self, domain: impl AsRef) -> Result { Ok(Self::Search { loc: LocBuf::::zeroed(self.into_loc().into_os()?), domain: Pool::::intern(domain), diff --git a/yazi-shared/src/url/component.rs b/yazi-shared/src/url/component.rs index fdba0425..37835a61 100644 --- a/yazi-shared/src/url/component.rs +++ b/yazi-shared/src/url/component.rs @@ -1,8 +1,8 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, path::{self, PathBuf, PrefixComponent}}; +use std::path::PrefixComponent; use anyhow::Result; -use crate::{path::{PathBufDyn, PathCow}, scheme::SchemeRef, url::{Encode, Url, UrlBuf, UrlCow}}; +use crate::{path::{self, PathDynError}, scheme::SchemeRef, strand::Strand}; #[derive(Clone, Copy, Debug, PartialEq)] pub enum Component<'a> { @@ -11,171 +11,67 @@ pub enum Component<'a> { RootDir, CurDir, ParentDir, - Normal(&'a OsStr), + Normal(Strand<'a>), } impl<'a> From> for Component<'a> { - fn from(comp: path::Component<'a>) -> Self { - match comp { - path::Component::Prefix(p) => Component::Prefix(p), - path::Component::RootDir => Component::RootDir, - path::Component::CurDir => Component::CurDir, - path::Component::ParentDir => Component::ParentDir, - path::Component::Normal(s) => Component::Normal(s), + fn from(value: path::Component<'a>) -> Self { + match value { + path::Component::Prefix(p) => Self::Prefix(p), + path::Component::RootDir => Self::RootDir, + path::Component::CurDir => Self::CurDir, + path::Component::ParentDir => Self::ParentDir, + path::Component::Normal(s) => Self::Normal(s), } } } -impl<'a> FromIterator> for Result { - fn from_iter>>(iter: I) -> Self { - let mut buf = PathBuf::new(); - let mut scheme = None; - iter.into_iter().for_each(|c| match c { - Component::Scheme(s) => scheme = Some(s), - Component::Prefix(p) => buf.push(path::Component::Prefix(p)), - Component::RootDir => buf.push(path::Component::RootDir), - Component::CurDir => buf.push(path::Component::CurDir), - Component::ParentDir => buf.push(path::Component::ParentDir), - Component::Normal(s) => buf.push(path::Component::Normal(s)), - }); - - Ok(if let Some(s) = scheme { - UrlCow::try_from((s, PathCow::Owned(PathBufDyn::Os(buf))))?.into_owned() - } else { - buf.into() +impl<'a> Component<'a> { + pub fn downgrade(self) -> Option> { + Some(match self { + Self::Scheme(_) => None?, + Self::Prefix(p) => path::Component::Prefix(p), + Self::RootDir => path::Component::RootDir, + Self::CurDir => path::Component::CurDir, + Self::ParentDir => path::Component::ParentDir, + Self::Normal(s) => path::Component::Normal(s), }) } } -impl<'a> FromIterator> for PathBuf { +// impl<'a> FromIterator> for Result { +// fn from_iter>>(iter: I) -> Self { +// let mut buf = PathBuf::new(); +// let mut scheme = None; +// iter.into_iter().for_each(|c| match c { +// Component::Scheme(s) => scheme = Some(s), +// Component::Prefix(p) => buf.push(path::Component::Prefix(p)), +// Component::RootDir => buf.push(path::Component::RootDir), +// Component::CurDir => buf.push(path::Component::CurDir), +// Component::ParentDir => buf.push(path::Component::ParentDir), +// Component::Normal(s) => buf.push(path::Component::Normal(s)), +// }); + +// Ok(if let Some(s) = scheme { +// UrlCow::try_from((s, PathCow::Owned(PathBufDyn::Os(buf))))?.into_owned() +// } else { +// buf.into() +// }) +// } +// } + +impl<'a> FromIterator> for Result { fn from_iter>>(iter: I) -> Self { - let mut buf = Self::new(); - iter.into_iter().for_each(|c| match c { - Component::Scheme(_) => {} - Component::Prefix(p) => buf.push(path::Component::Prefix(p)), - Component::RootDir => buf.push(path::Component::RootDir), - Component::CurDir => buf.push(path::Component::CurDir), - Component::ParentDir => buf.push(path::Component::ParentDir), - Component::Normal(s) => buf.push(path::Component::Normal(s)), - }); - buf + iter.into_iter().filter_map(|c| c.downgrade()).map(std::path::Component::try_from).collect() } } -// --- Components -#[derive(Clone)] -pub struct Components<'a> { - inner: path::Components<'a>, - url: Url<'a>, - scheme_yielded: bool, -} - -impl<'a> From> for Components<'a> { - fn from(value: Url<'a>) -> Self { - Self { inner: value.loc().components(), url: value, scheme_yielded: false } - } -} - -impl<'a> Components<'a> { - pub fn os_str(&self) -> Cow<'a, OsStr> { - let path = self.inner.as_path(); - if self.url.kind().is_local() || self.scheme_yielded { - return path.as_os_str().into(); - } - - let mut s = OsString::from(Encode(self.url).to_string()); - s.reserve_exact(path.as_os_str().len()); - s.push(path); - s.into() - } - - pub fn covariant(&self, other: &Self) -> bool { - match (self.scheme_yielded, other.scheme_yielded) { - (false, false) => {} - (true, true) if self.url.scheme().covariant(other.url.scheme()) => {} - _ => return false, - } - self.inner == other.inner - } -} - -impl<'a> Iterator for Components<'a> { - type Item = Component<'a>; - - fn next(&mut self) -> Option { - if !self.scheme_yielded { - self.scheme_yielded = true; - Some(Component::Scheme(self.url.scheme())) - } else { - self.inner.next().map(Into::into) - } - } - - fn size_hint(&self) -> (usize, Option) { - let (min, max) = self.inner.size_hint(); - let scheme = self.scheme_yielded.not() as usize; - - (min + scheme, max.map(|n| n + scheme)) - } -} - -impl<'a> DoubleEndedIterator for Components<'a> { - fn next_back(&mut self) -> Option { - if let Some(comp) = self.inner.next_back() { - Some(comp.into()) - } else if !self.scheme_yielded { - self.scheme_yielded = true; - Some(Component::Scheme(self.url.scheme())) - } else { - None - } - } -} - -impl<'a> FusedIterator for Components<'a> {} - -impl<'a> PartialEq for Components<'a> { - fn eq(&self, other: &Self) -> bool { - Some(self.url.scheme()).filter(|_| !self.scheme_yielded) - == Some(other.url.scheme()).filter(|_| !other.scheme_yielded) - && self.inner == other.inner - } -} - -// --- Tests -#[cfg(test)] -mod tests { - use std::path::Path; - - use anyhow::Result; - - use super::*; - use crate::url::UrlLike; - - #[test] - fn test_collect() -> Result<()> { - crate::init_tests(); - - let search: UrlBuf = "search://keyword//root/projects/yazi".parse()?; - assert_eq!(search.uri(), ""); - assert_eq!(search.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 }); - - let item = search.try_join("main.rs")?; - assert_eq!(item.uri(), "main.rs"); - assert_eq!(item.scheme(), SchemeRef::Search { domain: "keyword", uri: 1, urn: 1 }); - - let u: UrlBuf = item.components().take(4).collect::>()?; - assert_eq!(u.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 }); - assert_eq!(u.loc(), Path::new("/root/projects")); - - let u: UrlBuf = item - .components() - .take(5) - .chain([Component::Normal(OsStr::new("target/release/yazi"))]) - .collect::>()?; - assert_eq!(u.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 }); - assert_eq!(u.loc(), Path::new("/root/projects/yazi/target/release/yazi")); - - Ok(()) +impl<'a> FromIterator> for Result { + fn from_iter>>(iter: I) -> Self { + iter + .into_iter() + .filter_map(|c| c.downgrade()) + .map(typed_path::UnixComponent::try_from) + .collect() } } diff --git a/yazi-shared/src/url/components.rs b/yazi-shared/src/url/components.rs new file mode 100644 index 00000000..987bbf53 --- /dev/null +++ b/yazi-shared/src/url/components.rs @@ -0,0 +1,196 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not}; + +use crate::{loc::Loc, path, scheme::{Encode as EncodeScheme, SchemeCow, SchemeRef}, strand::{StrandBuf, StrandCow}, url::{Component, Encode as EncodeUrl, Url}}; + +#[derive(Clone)] +pub struct Components<'a> { + inner: path::Components<'a>, + url: Url<'a>, + back_yields: usize, + scheme_yielded: bool, +} + +impl<'a> From> for Components<'a> { + fn from(value: Url<'a>) -> Self { + Self { + inner: value.loc().components(), + url: value, + back_yields: 0, + scheme_yielded: false, + } + } +} + +impl<'a> Components<'a> { + pub fn covariant(&self, other: &Self) -> bool { + match (self.scheme_yielded, other.scheme_yielded) { + (true, true) => {} + (false, false) if self.scheme().covariant(other.scheme()) => {} + _ => return false, + } + self.inner == other.inner + } + + pub fn os_str(&self) -> Cow<'a, OsStr> { + let Ok(path) = self.inner.path().as_os() else { + return OsString::from(EncodeUrl(self.url()).to_string()).into(); + }; + + if self.url.is_regular() || self.scheme_yielded { + return path.as_os_str().into(); + } + + let mut s = OsString::from(EncodeScheme(self.url()).to_string()); + s.reserve_exact(path.as_os_str().len()); + s.push(path); + s.into() + } + + pub fn scheme(&self) -> SchemeRef<'a> { + let left = self.inner.clone().count(); + + let (uri, urn) = SchemeCow::retrieve_ports(self.url); + let (uri, urn) = ( + uri.saturating_sub(self.back_yields).min(left), + urn.saturating_sub(self.back_yields).min(left), + ); + + match self.url { + Url::Regular(_) => SchemeRef::Regular { uri, urn }, + Url::Search { domain, .. } => SchemeRef::Search { domain, uri, urn }, + Url::Archive { domain, .. } => SchemeRef::Archive { domain, uri, urn }, + Url::Sftp { domain, .. } => SchemeRef::Sftp { domain, uri, urn }, + } + } + + pub fn strand(&self) -> StrandCow<'a> { + let s = self.inner.strand(); + if self.url.is_regular() || self.scheme_yielded { + return s.into(); + } + + let mut buf = StrandBuf::with_str(s.kind(), EncodeScheme(self.url()).to_string()); + buf.reserve_exact(s.len()); + buf.try_push(s).expect("strand with same kind"); + buf.into() + } + + pub fn url(&self) -> Url<'a> { + let path = self.inner.path(); + let (uri, urn) = self.scheme().ports(); + match self.url { + Url::Regular(_) => Url::Regular(Loc::with(path.as_os().unwrap(), uri, urn).unwrap()), + Url::Search { domain, .. } => { + Url::Search { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), domain } + } + Url::Archive { domain, .. } => { + Url::Archive { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), domain } + } + Url::Sftp { domain, .. } => { + Url::Sftp { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), domain } + } + } + } +} + +impl<'a> Iterator for Components<'a> { + type Item = Component<'a>; + + fn next(&mut self) -> Option { + if !self.scheme_yielded { + self.scheme_yielded = true; + Some(Component::Scheme(self.scheme())) + } else { + self.inner.next().map(Into::into) + } + } + + fn size_hint(&self) -> (usize, Option) { + let (min, max) = self.inner.size_hint(); + let scheme = self.scheme_yielded.not() as usize; + + (min + scheme, max.map(|n| n + scheme)) + } +} + +impl<'a> DoubleEndedIterator for Components<'a> { + fn next_back(&mut self) -> Option { + if let Some(c) = self.inner.next_back() { + self.back_yields += 1; + Some(c.into()) + } else if !self.scheme_yielded { + self.scheme_yielded = true; + Some(Component::Scheme(self.scheme())) + } else { + None + } + } +} + +impl<'a> FusedIterator for Components<'a> {} + +impl<'a> PartialEq for Components<'a> { + fn eq(&self, other: &Self) -> bool { + if self.inner != other.inner { + return false; + } + match (self.scheme_yielded, other.scheme_yielded) { + (true, true) => true, + (false, false) if self.scheme() == other.scheme() => true, + _ => false, + } + } +} + +// --- Tests +#[cfg(test)] +mod tests { + use anyhow::Result; + + use crate::{scheme::SchemeRef, url::{Component, UrlBuf, UrlLike}}; + + #[test] + fn test_url() -> Result<()> { + use Component::*; + use SchemeRef as S; + + crate::init_tests(); + + let search: UrlBuf = "search://keyword//root/projects/yazi".parse()?; + assert_eq!(search.uri(), ""); + assert_eq!(search.scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + + let src = search.try_join("src")?; + assert_eq!(src.uri(), "src"); + assert_eq!(src.scheme(), S::Search { domain: "keyword", uri: 1, urn: 1 }); + + let main = src.try_join("main.rs")?; + assert_eq!(main.urn(), "src/main.rs"); + assert_eq!(main.scheme(), S::Search { domain: "keyword", uri: 2, urn: 2 }); + + let mut it = main.components(); + assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 2, urn: 2 }); + assert_eq!(it.next_back(), Some(Normal("main.rs".into()))); + assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 1, urn: 1 }); + assert_eq!(it.next_back(), Some(Normal("src".into()))); + assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(it.next_back(), Some(Normal("yazi".into()))); + assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + + let mut it = main.components(); + assert_eq!(it.next(), Some(Scheme(S::Search { domain: "keyword", uri: 2, urn: 2 }))); + assert_eq!(it.next(), Some(RootDir)); + assert_eq!(it.next(), Some(Normal("root".into()))); + assert_eq!(it.next(), Some(Normal("projects".into()))); + assert_eq!(it.next(), Some(Normal("yazi".into()))); + assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 2, urn: 2 }); + assert_eq!(it.next(), Some(Normal("src".into()))); + assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 1, urn: 1 }); + assert_eq!(it.next_back(), Some(Normal("main.rs".into()))); + assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(it.next(), None); + assert_eq!(it.url().scheme(), S::Search { domain: "keyword", uri: 0, urn: 0 }); + + Ok(()) + } +} diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs index 176600b6..ad797709 100644 --- a/yazi-shared/src/url/display.rs +++ b/yazi-shared/src/url/display.rs @@ -1,18 +1,12 @@ use crate::{scheme::Encode, url::Url}; -pub struct Display<'a> { - inner: Url<'a>, -} +pub struct Display<'a>(pub Url<'a>); -impl<'a> From> for Display<'a> { - fn from(value: Url<'a>) -> Self { Self { inner: value } } -} - -impl<'a> std::fmt::Display for Display<'a> { +impl std::fmt::Display for Display<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let (kind, loc) = (self.inner.kind(), self.inner.loc()); + let (kind, loc) = (self.0.kind(), self.0.loc()); if kind.is_virtual() { - Encode(self.inner).fmt(f)?; + Encode(self.0).fmt(f)?; } loc.display().fmt(f) } diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index fe2a9d3f..1695e50e 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -4,7 +4,6 @@ use percent_encoding::{CONTROLS, percent_encode}; use crate::url::Url; -// --- Tilded #[derive(Clone, Copy)] pub struct Encode<'a>(pub Url<'a>); diff --git a/yazi-shared/src/url/like.rs b/yazi-shared/src/url/like.rs new file mode 100644 index 00000000..46c3544f --- /dev/null +++ b/yazi-shared/src/url/like.rs @@ -0,0 +1,75 @@ +use std::{borrow::Cow, ffi::OsStr, path::Path}; + +use anyhow::Result; + +use crate::{path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, scheme::{SchemeKind, SchemeRef}, strand::{AsStrand, Strand}, url::{AsUrl, Components, Display, Url, UrlBuf, UrlCow}}; + +pub trait UrlLike +where + Self: AsUrl, +{ + fn as_local(&self) -> Option<&Path> { self.as_url().as_local() } + + fn base(&self) -> Url<'_> { self.as_url().base() } + + fn components(&self) -> Components<'_> { self.as_url().into() } + + fn covariant(&self, other: impl AsUrl) -> bool { self.as_url().covariant(other) } + + fn display(&self) -> Display<'_> { Display(self.as_url()) } + + fn ext(&self) -> Option> { self.as_url().ext() } + + fn has_base(&self) -> bool { self.as_url().has_base() } + + fn has_root(&self) -> bool { self.as_url().has_root() } + + fn has_trail(&self) -> bool { self.as_url().has_trail() } + + fn is_absolute(&self) -> bool { self.as_url().is_absolute() } + + fn kind(&self) -> SchemeKind { self.as_url().kind() } + + fn loc(&self) -> PathDyn<'_> { self.as_url().loc() } + + fn name(&self) -> Option> { self.as_url().name() } + + fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() } + + fn pair(&self) -> Option<(Url<'_>, PathDyn<'_>)> { self.as_url().pair() } + + fn parent(&self) -> Option> { self.as_url().parent() } + + fn scheme(&self) -> SchemeRef<'_> { self.as_url().scheme() } + + fn stem(&self) -> Option> { self.as_url().stem() } + + fn trail(&self) -> Url<'_> { self.as_url().trail() } + + fn try_ends_with(&self, child: impl AsUrl) -> Result { + self.as_url().try_ends_with(child) + } + + fn try_join(&self, path: impl AsStrand) -> Result { + self.as_url().try_join(path) + } + + fn try_replace<'a>(&self, take: usize, path: impl AsPathRef<'a>) -> Result> { + self.as_url().try_replace(take, path) + } + + fn try_starts_with(&self, base: impl AsUrl) -> Result { + self.as_url().try_starts_with(base) + } + + fn try_strip_prefix(&self, base: impl AsUrl) -> Result, StripPrefixError> { + self.as_url().try_strip_prefix(base) + } + + fn uri(&self) -> PathDyn<'_> { self.as_url().uri() } + + fn urn(&self) -> PathDyn<'_> { self.as_url().urn() } +} + +impl UrlLike for UrlBuf {} +impl UrlLike for UrlCow<'_> {} diff --git a/yazi-shared/src/url/mod.rs b/yazi-shared/src/url/mod.rs index c94d033a..207f15eb 100644 --- a/yazi-shared/src/url/mod.rs +++ b/yazi-shared/src/url/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(buf component cov cow display encode traits url); +yazi_macro::mod_flat!(buf component components cov cow display encode like traits url); diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index 717cb0b7..0b0c5d73 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -1,8 +1,6 @@ -use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; +use std::path::{Path, PathBuf}; -use anyhow::Result; - -use crate::{loc::Loc, path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, scheme::{SchemeKind, SchemeRef}, strand::{AsStrand, Strand}, url::{Components, Display, Url, UrlBuf, UrlCow}}; +use crate::{loc::Loc, url::{Url, UrlBuf, UrlCow}}; // --- AsUrl pub trait AsUrl { @@ -29,15 +27,6 @@ impl AsUrl for &PathBuf { fn as_url(&self) -> Url<'_> { (*self).as_path().as_url() } } -impl AsUrl for PathDyn<'_> { - #[inline] - fn as_url(&self) -> Url<'_> { - match *self { - Self::Os(p) => p.as_url(), - } - } -} - impl AsUrl for Url<'_> { #[inline] fn as_url(&self) -> Url<'_> { *self } @@ -82,10 +71,13 @@ impl AsUrl for UrlCow<'_> { } impl AsUrl for &UrlCow<'_> { - #[inline] fn as_url(&self) -> Url<'_> { (**self).as_url() } } +impl AsUrl for super::Components<'_> { + fn as_url(&self) -> Url<'_> { self.url() } +} + impl<'a, T> From<&'a T> for Url<'a> where T: AsUrl + ?Sized, @@ -99,74 +91,3 @@ where { fn from(value: &'a mut T) -> Self { value.as_url() } } - -// UrlLike -pub trait UrlLike -where - Self: AsUrl, -{ - fn as_local(&self) -> Option<&Path> { self.as_url().as_local() } - - fn base(&self) -> Url<'_> { self.as_url().base() } - - fn components(&self) -> Components<'_> { self.as_url().into() } - - fn covariant(&self, other: impl AsUrl) -> bool { self.as_url().covariant(other) } - - fn display(&self) -> Display<'_> { self.as_url().into() } - - fn ext(&self) -> Option> { self.as_url().ext() } - - fn has_base(&self) -> bool { self.as_url().has_base() } - - fn has_root(&self) -> bool { self.as_url().has_root() } - - fn has_trail(&self) -> bool { self.as_url().has_trail() } - - fn is_absolute(&self) -> bool { self.as_url().is_absolute() } - - fn kind(&self) -> SchemeKind { self.as_url().kind() } - - fn loc(&self) -> PathDyn<'_> { self.as_url().loc() } - - fn name(&self) -> Option> { self.as_url().name() } - - fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() } - - fn pair(&self) -> Option<(Url<'_>, PathDyn<'_>)> { self.as_url().pair() } - - fn parent(&self) -> Option> { self.as_url().parent() } - - fn scheme(&self) -> SchemeRef<'_> { self.as_url().scheme() } - - fn stem(&self) -> Option> { self.as_url().stem() } - - fn trail(&self) -> Url<'_> { self.as_url().trail() } - - fn try_ends_with(&self, child: impl AsUrl) -> Result { - self.as_url().try_ends_with(child) - } - - fn try_join(&self, path: impl AsStrand) -> Result { - self.as_url().try_join(path) - } - - fn try_replace<'a>(&self, take: usize, path: impl AsPathRef<'a>) -> Result> { - self.as_url().try_replace(take, path) - } - - fn try_starts_with(&self, base: impl AsUrl) -> Result { - self.as_url().try_starts_with(base) - } - - fn try_strip_prefix(&self, base: impl AsUrl) -> Result, StripPrefixError> { - self.as_url().try_strip_prefix(base) - } - - fn uri(&self) -> PathDyn<'_> { self.as_url().uri() } - - fn urn(&self) -> PathDyn<'_> { self.as_url().urn() } -} - -impl UrlLike for UrlBuf {} -impl UrlLike for UrlCow<'_> {} diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index d0e86fbb..86795dbf 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -39,15 +39,85 @@ impl Debug for Url<'_> { impl Serialize for Url<'_> { fn serialize(&self, serializer: S) -> Result { let (kind, loc) = (self.kind(), self.loc()); - match (kind.is_virtual(), loc.to_str()) { - (false, Ok(s)) => serializer.serialize_str(s), - (true, Ok(s)) => serializer.serialize_str(&format!("{}{s}", EncodeScheme(*self))), + match (kind == SchemeKind::Regular, loc.to_str()) { + (true, Ok(s)) => serializer.serialize_str(s), + (false, Ok(s)) => serializer.serialize_str(&format!("{}{s}", EncodeScheme(*self))), (_, Err(_)) => serializer.collect_str(&EncodeUrl(*self)), } } } impl<'a> Url<'a> { + #[inline] + pub fn as_local(self) -> Option<&'a Path> { + self.loc().as_os().ok().filter(|_| self.kind().is_local()) + } + + #[inline] + pub fn as_regular(self) -> Result { + Ok(Self::Regular(Loc::bare(self.loc().as_os()?))) + } + + pub fn base(self) -> Self { + match self { + Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())), + Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.base()), domain }, + Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.base()), domain }, + Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.base()), domain }, + } + } + + #[inline] + pub fn components(self) -> Components<'a> { Components::from(self) } + + #[inline] + pub fn covariant(self, other: impl AsUrl) -> bool { + let other = other.as_url(); + self.loc() == other.loc() && self.scheme().covariant(other.scheme()) + } + + #[inline] + pub fn ext(self) -> Option> { + Some(match self { + Self::Regular(loc) => loc.extension()?.as_strand(), + Self::Search { loc, .. } => loc.extension()?.as_strand(), + Self::Archive { loc, .. } => loc.extension()?.as_strand(), + Self::Sftp { loc, .. } => loc.extension()?.as_strand(), + }) + } + + #[inline] + pub fn has_base(self) -> bool { + match self { + Self::Regular(loc) => loc.has_base(), + Self::Search { loc, .. } => loc.has_base(), + Self::Archive { loc, .. } => loc.has_base(), + Self::Sftp { loc, .. } => loc.has_base(), + } + } + + #[inline] + pub fn has_root(self) -> bool { self.loc().has_root() } + + #[inline] + pub fn has_trail(self) -> bool { + match self { + Self::Regular(loc) => loc.has_trail(), + Self::Search { loc, .. } => loc.has_trail(), + Self::Archive { loc, .. } => loc.has_trail(), + Self::Sftp { loc, .. } => loc.has_trail(), + } + } + + #[inline] + pub fn is_absolute(self) -> bool { self.loc().is_absolute() } + + #[inline] + pub fn is_regular(self) -> bool { matches!(self, Self::Regular(_)) } + + #[inline] + pub fn is_search(self) -> bool { matches!(self, Self::Search { .. }) } + #[inline] pub fn kind(&self) -> SchemeKind { match self { @@ -68,6 +138,54 @@ impl<'a> Url<'a> { } } + #[inline] + pub fn name(self) -> Option> { + Some(match self { + Self::Regular(loc) => loc.file_name()?.as_strand(), + Self::Search { loc, .. } => loc.file_name()?.as_strand(), + Self::Archive { loc, .. } => loc.file_name()?.as_strand(), + Self::Sftp { loc, .. } => loc.file_name()?.as_strand(), + }) + } + + #[inline] + pub fn os_str(self) -> Cow<'a, OsStr> { self.components().os_str() } + + #[inline] + pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.urn())) } + + pub fn parent(self) -> Option { + let uri = self.uri(); + + Some(match self { + // Regular + Self::Regular(loc) => Self::regular(loc.parent()?), + + // Search + Self::Search { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), + Self::Search { loc, domain } => { + Self::Search { loc: Loc::new(loc.parent()?, loc.base(), loc.base()), domain } + } + + // Archive + Self::Archive { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), + Self::Archive { loc, domain } if uri.components().nth(1).is_none() => { + Self::Archive { loc: Loc::zeroed(loc.parent()?), domain } + } + Self::Archive { loc, domain } => { + Self::Archive { loc: Loc::floated(loc.parent()?, loc.base()), domain } + } + + // SFTP + Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.parent()?), domain }, + }) + } + + #[inline] + pub fn regular + ?Sized>(path: &'a T) -> Self { + Self::Regular(Loc::bare(path.as_ref())) + } + #[inline] pub fn scheme(self) -> SchemeRef<'a> { let (uri, urn) = SchemeCow::retrieve_ports(self); @@ -80,30 +198,47 @@ impl<'a> Url<'a> { } #[inline] - pub fn regular + ?Sized>(path: &'a T) -> Self { - Self::Regular(Loc::bare(path.as_ref())) + pub fn stem(self) -> Option> { + Some(match self { + Self::Regular(loc) => loc.file_stem()?.as_strand(), + Self::Search { loc, .. } => loc.file_stem()?.as_strand(), + Self::Archive { loc, .. } => loc.file_stem()?.as_strand(), + Self::Sftp { loc, .. } => loc.file_stem()?.as_strand(), + }) } - #[inline] - pub fn is_regular(self) -> bool { matches!(self, Self::Regular(_)) } - - #[inline] - pub fn as_regular(self) -> Result { - Ok(Self::Regular(Loc::bare(self.loc().as_os()?))) - } - - #[inline] - pub fn is_search(self) -> bool { matches!(self, Self::Search { .. }) } - - #[inline] - pub fn is_absolute(self) -> bool { self.loc().is_absolute() } - - #[inline] - pub fn has_root(self) -> bool { self.loc().has_root() } - #[inline] pub fn to_owned(self) -> UrlBuf { self.into() } + #[inline] + pub fn trail(self) -> Self { + match self { + Self::Regular(loc) => Self::Regular(Loc::bare(loc.trail())), + Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.trail()), domain }, + Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.trail()), domain }, + Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.trail()), domain }, + } + } + + pub fn triple(self) -> (PathDyn<'a>, PathDyn<'a>, PathDyn<'a>) { + match self { + Self::Regular(loc) | Self::Search { loc, .. } | Self::Archive { loc, .. } => { + let (base, rest, urn) = loc.triple(); + (base.as_path(), rest.as_path(), urn.as_path()) + } + Self::Sftp { loc, .. } => { + let (base, rest, urn) = loc.triple(); + (base.as_path(), rest.as_path(), urn.as_path()) + } + } + } + + #[inline] + pub fn try_ends_with(self, child: impl AsUrl) -> Result { + let child = child.as_url(); + Ok(self.loc().try_ends_with(child.loc())? && self.scheme().covariant(child.scheme())) + } + pub fn try_join(self, path: impl AsStrand) -> Result { let joined = self.loc().try_join(path)?; @@ -135,7 +270,8 @@ impl<'a> Url<'a> { return UrlCow::try_from(b); } - let mut path = PathBufDyn::Os(self.loc().components().take(take - 1).collect()); // FIXME + let loc = self.loc(); + let mut path = PathBufDyn::from_components(loc.kind(), loc.components().take(take - 1))?; path.try_push(rep)?; let url = match self { @@ -171,6 +307,12 @@ impl<'a> Url<'a> { Ok(url.into()) } + #[inline] + pub fn try_starts_with(self, base: impl AsUrl) -> Result { + let base = base.as_url(); + Ok(self.loc().try_starts_with(base.loc())? && self.scheme().covariant(base.scheme())) + } + pub fn try_strip_prefix(self, base: impl AsUrl) -> Result, StripPrefixError> { use StripPrefixError::{Exotic, NotPrefix}; use Url as U; @@ -236,145 +378,4 @@ impl<'a> Url<'a> { Self::Sftp { loc, .. } => loc.urn().as_path(), } } - - #[inline] - pub fn name(self) -> Option> { - Some(match self { - Self::Regular(loc) => loc.file_name()?.as_strand(), - Self::Search { loc, .. } => loc.file_name()?.as_strand(), - Self::Archive { loc, .. } => loc.file_name()?.as_strand(), - Self::Sftp { loc, .. } => loc.file_name()?.as_strand(), - }) - } - - #[inline] - pub fn stem(self) -> Option> { - Some(match self { - Self::Regular(loc) => loc.file_stem()?.as_strand(), - Self::Search { loc, .. } => loc.file_stem()?.as_strand(), - Self::Archive { loc, .. } => loc.file_stem()?.as_strand(), - Self::Sftp { loc, .. } => loc.file_stem()?.as_strand(), - }) - } - - #[inline] - pub fn ext(self) -> Option> { - Some(match self { - Self::Regular(loc) => loc.extension()?.as_strand(), - Self::Search { loc, .. } => loc.extension()?.as_strand(), - Self::Archive { loc, .. } => loc.extension()?.as_strand(), - Self::Sftp { loc, .. } => loc.extension()?.as_strand(), - }) - } - - pub fn base(self) -> Self { - match self { - Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())), - Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.base()), domain }, - Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.base()), domain }, - Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.base()), domain }, - } - } - - #[inline] - pub fn trail(self) -> Self { - match self { - Self::Regular(loc) => Self::Regular(Loc::bare(loc.trail())), - Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.trail()), domain }, - Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.trail()), domain }, - Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.trail()), domain }, - } - } - - pub fn triple(self) -> (PathDyn<'a>, PathDyn<'a>, PathDyn<'a>) { - match self { - Self::Regular(loc) | Self::Search { loc, .. } | Self::Archive { loc, .. } => { - let (base, rest, urn) = loc.triple(); - (base.as_path(), rest.as_path(), urn.as_path()) - } - Self::Sftp { loc, .. } => { - let (base, rest, urn) = loc.triple(); - (base.as_path(), rest.as_path(), urn.as_path()) - } - } - } - - pub fn parent(self) -> Option { - let uri = self.uri(); - - Some(match self { - // Regular - Self::Regular(loc) => Self::regular(loc.parent()?), - - // Search - Self::Search { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), - Self::Search { loc, domain } => { - Self::Search { loc: Loc::new(loc.parent()?, loc.base(), loc.base()), domain } - } - - // Archive - Self::Archive { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), - Self::Archive { loc, domain } if uri.components().nth(1).is_none() => { - Self::Archive { loc: Loc::zeroed(loc.parent()?), domain } - } - Self::Archive { loc, domain } => { - Self::Archive { loc: Loc::floated(loc.parent()?, loc.base()), domain } - } - - // SFTP - Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.parent()?), domain }, - }) - } - - #[inline] - pub fn try_starts_with(self, base: impl AsUrl) -> Result { - let base = base.as_url(); - Ok(self.loc().try_starts_with(base.loc())? && self.scheme().covariant(base.scheme())) - } - - #[inline] - pub fn try_ends_with(self, child: impl AsUrl) -> Result { - let child = child.as_url(); - Ok(self.loc().try_ends_with(child.loc())? && self.scheme().covariant(child.scheme())) - } - - #[inline] - pub fn components(self) -> Components<'a> { Components::from(self) } - - #[inline] - pub fn os_str(self) -> Cow<'a, OsStr> { self.components().os_str() } - - #[inline] - pub fn covariant(self, other: impl AsUrl) -> bool { - let other = other.as_url(); - self.loc() == other.loc() && self.scheme().covariant(other.scheme()) - } - - #[inline] - pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.urn())) } - - #[inline] - pub fn as_local(self) -> Option<&'a Path> { - self.loc().as_os().ok().filter(|_| self.kind().is_local()) - } - - #[inline] - pub fn has_base(self) -> bool { - match self { - Self::Regular(loc) => loc.has_base(), - Self::Search { loc, .. } => loc.has_base(), - Self::Archive { loc, .. } => loc.has_base(), - Self::Sftp { loc, .. } => loc.has_base(), - } - } - - #[inline] - pub fn has_trail(self) -> bool { - match self { - Self::Regular(loc) => loc.has_trail(), - Self::Search { loc, .. } => loc.has_trail(), - Self::Archive { loc, .. } => loc.has_trail(), - Self::Sftp { loc, .. } => loc.has_trail(), - } - } } diff --git a/yazi-vfs/src/fns.rs b/yazi-vfs/src/fns.rs index 25115c72..a1f3cb6c 100644 --- a/yazi-vfs/src/fns.rs +++ b/yazi-vfs/src/fns.rs @@ -39,7 +39,7 @@ async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result { let dot_ext = match url.ext() { Some(e) => { let mut s = StrandBuf::with_capacity(url.kind(), e.len() + 1); - s.try_push(".")?; + s.push_str("."); s.try_push(e)?; s } @@ -53,9 +53,9 @@ async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result { if append { name.try_push(&dot_ext)?; - name.try_push(format!("_{i}"))?; + name.push_str(format!("_{i}")); } else { - name.try_push(format!("_{i}"))?; + name.push_str(format!("_{i}")); name.try_push(&dot_ext)?; } diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 6bb37475..531edc75 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -9,7 +9,7 @@ use yazi_shared::{path::{AsPath, PathBufDyn}, pool::InternStr, url::{Url, UrlBuf use super::Cha; use crate::provider::sftp::Conn; -#[derive(Clone, Copy)] +#[derive(Clone)] pub struct Sftp<'a> { url: Url<'a>, path: &'a Path, From 76fe6d7244eb67068ed5a6c608b6605b9bbf6a89 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Mon, 24 Nov 2025 12:30:52 +0800 Subject: [PATCH 06/53] fix: user-prepended open rules do not override presets (#3360) --- CHANGELOG.md | 2 ++ yazi-config/src/open/open.rs | 3 ++- yazi-scheduler/src/process/shell.rs | 3 +-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c2366d..aded5892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Fixed +- User-prepended open rules do not override presets ([#3360]) - Respect user's system media opener instead of hardcoding `mpv` ([#2959]) - Incorrect `$0` and `$@` parameters in `shell` command under empty directories ([#3225]) - Avoid appending a newline when reading clipboard contents ([#3059]) @@ -1537,3 +1538,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3290]: https://github.com/sxyazi/yazi/pull/3290 [#3313]: https://github.com/sxyazi/yazi/pull/3313 [#3317]: https://github.com/sxyazi/yazi/pull/3317 +[#3360]: https://github.com/sxyazi/yazi/pull/3360 diff --git a/yazi-config/src/open/open.rs b/yazi-config/src/open/open.rs index 05953e16..731fcda6 100644 --- a/yazi-config/src/open/open.rs +++ b/yazi-config/src/open/open.rs @@ -34,10 +34,11 @@ impl Open { self .rules .iter() - .filter(move |&r| { + .find(move |&r| { r.mime.as_ref().is_some_and(|p| p.match_mime(&mime)) || r.url.as_ref().is_some_and(|p| p.match_url(url.as_url(), is_dir)) }) + .into_iter() .flat_map(|r| &r.r#use) .map(String::as_str) } diff --git a/yazi-scheduler/src/process/shell.rs b/yazi-scheduler/src/process/shell.rs index 73abe147..1b6c704f 100644 --- a/yazi-scheduler/src/process/shell.rs +++ b/yazi-scheduler/src/process/shell.rs @@ -40,9 +40,8 @@ pub(crate) async fn shell(opt: ShellOpt) -> Result { .stderr(opt.stdio()) .arg("-c") .arg(opt.cmd) - // .arg("--") // TODO: remove - .args(opt.args.iter().skip(1).map(|u| u.as_url().unified_path_str())) + .args(opt.args.iter().map(|u| u.as_url().unified_path_str())) .current_dir(cwd) .kill_on_drop(!opt.orphan) .pre_exec(move || { From 449450d0db1cb8e30ddc94c7b1d8ef42aa2a0d48 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Mon, 24 Nov 2025 15:23:06 +0800 Subject: [PATCH 07/53] feat!: introduce `Path` as a subset of `Url` to enforce type safety (#3361) --- .github/workflows/cachix.yml | 2 +- .github/workflows/check.yml | 6 +++--- .github/workflows/draft.yml | 10 +++++----- .github/workflows/test.yml | 2 +- .github/workflows/validate-form.yml | 2 +- CHANGELOG.md | 1 + yazi-binding/src/path.rs | 3 +++ yazi-boot/src/boot.rs | 3 +-- yazi-shared/src/strand/buf.rs | 14 ++++++++++---- yazi-shared/src/strand/cow.rs | 9 ++------- 10 files changed, 28 insertions(+), 24 deletions(-) diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 2d1c89e3..1bde031f 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -12,7 +12,7 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install Nix uses: cachix/install-nix-action@v31 diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 56e8e14d..43466367 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,7 +14,7 @@ jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Rust toolchain run: | @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Rust toolchain run: | @@ -47,7 +47,7 @@ jobs: stylua: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: JohnnyMorganz/stylua-action@v4 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml index 15f6a43d..f95b4562 100644 --- a/.github/workflows/draft.yml +++ b/.github/workflows/draft.yml @@ -39,7 +39,7 @@ jobs: CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install gcc if: matrix.gcc != '' @@ -78,7 +78,7 @@ jobs: CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: @@ -117,7 +117,7 @@ jobs: container: image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: @@ -147,7 +147,7 @@ jobs: arch: arm64 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -229,7 +229,7 @@ jobs: echo "Generated on: $(date -u +"%Y-%m-%d %H:%M") UTC" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/download-artifact@v6 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e524a55a..17ed52e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Rust toolchain run: rustup toolchain install stable --profile minimal diff --git a/.github/workflows/validate-form.yml b/.github/workflows/validate-form.yml index 2d48f16a..b9416d4b 100644 --- a/.github/workflows/validate-form.yml +++ b/.github/workflows/validate-form.yml @@ -12,7 +12,7 @@ jobs: permissions: issues: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index aded5892..0a216a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Rename `name` to `url` for open, fetchers, spotters, preloaders, previewers, filetype, and `globs` icon rules to support virtual file system ([#3034]) - Rename `mime` fetcher to `mime.local`, and introduce `mime.dir` fetcher to support folder MIME types ([#3222]) - Remove `$0` parameter in opener rules to make the `open` command work under empty directories ([#3226]) +- Return `Path` instead of `Url` from `Url:strip_prefix()` to enforce type safety ([#3361]) - Use `body` instead of the term `content` in confirmations ([#2921]) - Use `u16` instead of `u32` as the type of `max_width` and `max_height` options to avoid memory exhaustion ([#3313]) - Implement `__pairs` metamethod instead of `__index` for the callback argument of the `@yank` DDS event ([#2997]) diff --git a/yazi-binding/src/path.rs b/yazi-binding/src/path.rs index a18dbd5e..be37b940 100644 --- a/yazi-binding/src/path.rs +++ b/yazi-binding/src/path.rs @@ -115,6 +115,9 @@ impl UserData for Path { cached_field!(fields, stem, |lua, me| { me.stem().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); + + fields.add_field_method_get("is_absolute", |_, me| Ok(me.is_absolute())); + fields.add_field_method_get("has_root", |_, me| Ok(me.has_root())); } fn add_methods>(methods: &mut M) { diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index 2c123084..2be24d9e 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -2,12 +2,11 @@ use std::path::PathBuf; use futures::executor::block_on; use hashbrown::HashSet; -use serde::Serialize; use yazi_fs::{CWD, Xdg, path::expand_url}; use yazi_shared::{strand::StrandBuf, url::{UrlBuf, UrlLike}}; use yazi_vfs::provider; -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default)] pub struct Boot { pub cwds: Vec, pub files: Vec, diff --git a/yazi-shared/src/strand/buf.rs b/yazi-shared/src/strand/buf.rs index f75f093d..e5a28479 100644 --- a/yazi-shared/src/strand/buf.rs +++ b/yazi-shared/src/strand/buf.rs @@ -1,13 +1,11 @@ -use std::{borrow::Cow, ffi::OsString}; +use std::{borrow::Cow, ffi::OsString, hash::{Hash, Hasher}}; use anyhow::Result; -use serde::Serialize; use crate::{FromWtf8Vec, path::PathDyn, strand::{AsStrand, Strand, StrandCow, StrandError, StrandKind}}; // --- StrandBuf -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] -#[serde(untagged)] +#[derive(Clone, Debug, Eq)] pub enum StrandBuf { Os(OsString), Utf8(String), @@ -43,10 +41,18 @@ impl From> for StrandBuf { fn from(value: StrandCow<'_>) -> Self { value.into_owned() } } +impl PartialEq for StrandBuf { + fn eq(&self, other: &Self) -> bool { self.as_strand() == other.as_strand() } +} + impl PartialEq> for StrandBuf { fn eq(&self, other: &Strand<'_>) -> bool { self.as_strand() == *other } } +impl Hash for StrandBuf { + fn hash(&self, state: &mut H) { self.as_strand().hash(state); } +} + impl StrandBuf { pub fn clear(&mut self) { match self { diff --git a/yazi-shared/src/strand/cow.rs b/yazi-shared/src/strand/cow.rs index f18f099b..74d6b906 100644 --- a/yazi-shared/src/strand/cow.rs +++ b/yazi-shared/src/strand/cow.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}}; use anyhow::Result; -use crate::strand::{Strand, StrandBuf, StrandKind}; +use crate::strand::{AsStrand, Strand, StrandBuf, StrandKind}; pub enum StrandCow<'a> { Borrowed(Strand<'a>), @@ -35,12 +35,7 @@ impl From for StrandCow<'_> { } impl PartialEq> for StrandCow<'_> { - fn eq(&self, other: &Strand) -> bool { - match self { - Self::Borrowed(s) => s == other, - Self::Owned(s) => s == other, - } - } + fn eq(&self, other: &Strand) -> bool { self.as_strand() == *other } } impl<'a> StrandCow<'a> { From c725c91df17f3c131082865d21922e808a8816d3 Mon Sep 17 00:00:00 2001 From: XYenon Date: Mon, 24 Nov 2025 19:55:41 +0800 Subject: [PATCH 08/53] fix(nix): shell env (#3363) --- flake.lock | 12 ++++++------ flake.nix | 7 +++++-- nix/shell.nix | 24 ++++++++++++------------ nix/yazi.nix | 2 ++ 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/flake.lock b/flake.lock index a69f3da9..509429d9 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1762286042, - "narHash": "sha256-OD5HsZ+sN7VvNucbrjiCz7CHF5zf9gP51YVJvPwYIH8=", + "lastModified": 1763806073, + "narHash": "sha256-FHsEKDvfWpzdADWj99z7vBk4D716Ujdyveo5+A048aI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "12c1f0253aa9a54fdf8ec8aecaafada64a111e24", + "rev": "878e468e02bfabeda08c79250f7ad583037f2227", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1762396738, - "narHash": "sha256-BarSecuxtzp1boERdABLkkoxQTi6s/V33lJwUbWLrLY=", + "lastModified": 1763952169, + "narHash": "sha256-+PeDBD8P+NKauH+w7eO/QWCIp8Cx4mCfWnh9sJmy9CM=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "c63598992afd54d215d54f2b764adc0484c2b159", + "rev": "ab726555a9a72e6dc80649809147823a813fa95b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 07fb4afb..7c10f7c7 100644 --- a/flake.nix +++ b/flake.nix @@ -52,10 +52,13 @@ }; devShells = { - default = pkgs.callPackage ./nix/shell.nix { }; + default = pkgs.callPackage ./nix/shell.nix { + inherit toolchain; + inherit (self.packages.${system}) yazi yazi-unwrapped; + }; }; - formatter = pkgs.nixfmt-rfc-style; + formatter = pkgs.nixfmt-tree; } ) // { diff --git a/nix/shell.nix b/nix/shell.nix index f39538dc..3207d8e3 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,14 +1,14 @@ { - callPackage, - rust-bin, + mkShell, + yazi, + toolchain, nodePackages, + yazi-unwrapped, }: -let - mainPkg = callPackage ./yazi.nix { }; -in -mainPkg.overrideAttrs (oa: { - nativeBuildInputs = [ - (rust-bin.nightly.latest.default.override { + +mkShell { + packages = yazi.passthru.runtimePaths ++ [ + (toolchain.override { extensions = [ "rust-src" "rustfmt" @@ -16,10 +16,10 @@ mainPkg.overrideAttrs (oa: { "clippy" ]; }) - nodePackages.cspell - ] - ++ (oa.nativeBuildInputs or [ ]); + ]; + + inputsFrom = [ yazi-unwrapped ]; env.RUST_BACKTRACE = "1"; -}) +} diff --git a/nix/yazi.nix b/nix/yazi.nix index 1eacd22c..5bac0fc7 100644 --- a/nix/yazi.nix +++ b/nix/yazi.nix @@ -96,6 +96,8 @@ runCommand yazi-unwrapped.name inherit (yazi-unwrapped) pname version meta; nativeBuildInputs = [ makeWrapper ]; + + passthru.runtimePaths = runtimePaths; } '' mkdir -p $out/bin From 2d55c9427dd67a359cd1e9908303bbf6156053d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Tue, 25 Nov 2025 09:10:11 +0800 Subject: [PATCH 09/53] fix: prevent quotes in file(1) arguments from being stripped under MSYS2 (#3364) --- CHANGELOG.md | 2 ++ Cargo.lock | 17 +++++++------- Cargo.toml | 2 +- yazi-plugin/preset/plugins/mime-local.lua | 28 ++++++++++++++++++++--- yazi-plugin/src/utils/app.rs | 16 +++++++++++++ yazi-plugin/src/utils/utils.rs | 1 + 6 files changed, 54 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a216a76..f964c3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Force Git checkout for plugin cache repositories ([#3169]) - Check compatibility when reusing previewer bytecode cache ([#3190]) - Disable kitty keyboard protocol on Windows due to `crossterm` inability to handle it ([#3250]) +- Prevent quotes in file(1) arguments from being stripped under MSYS2 ([#3364]) - Expose `ya` CLI in the Snap build ([#2904]) - Fallback to `PollWatcher` for file changes watching on NetBSD ([#2941]) - Generate unique image IDs for Kgp to tolerate tmux ([#3038]) @@ -1540,3 +1541,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3313]: https://github.com/sxyazi/yazi/pull/3313 [#3317]: https://github.com/sxyazi/yazi/pull/3317 [#3360]: https://github.com/sxyazi/yazi/pull/3360 +[#3364]: https://github.com/sxyazi/yazi/pull/3364 diff --git a/Cargo.lock b/Cargo.lock index 184ceeea..40b612b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2123,15 +2123,16 @@ dependencies = [ [[package]] name = "mlua" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be1c2bfc684b8a228fbaebf954af7a47a98ec27721986654a4cc2c40a20cc7e" +checksum = "935ac67539907efcd7198137eb7358e052555f77fe1b2916600a2249351f2b33" dependencies = [ "anyhow", "bstr", "either", "erased-serde", "futures-util", + "libc", "mlua-sys", "mlua_derive", "num-traits", @@ -2144,9 +2145,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4dc9cfc5a7698899802e97480617d9726f7da78c910db989d4d0fd4991d900" +checksum = "8c968af21bf6b19fc9ca8e7b85ee16f86e4c9e3d0591de101a5608086bda0ad8" dependencies = [ "cc", "cfg-if", @@ -5173,18 +5174,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.28" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.28" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5b25ac8f..de637afa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ hashbrown = { version = "0.16.1", features = [ "serde" ] } indexmap = { version = "2.12.1", features = [ "serde" ] } libc = "0.2.177" lru = "0.16.2" -mlua = { version = "0.11.4", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } +mlua = { version = "0.11.5", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } objc2 = "0.6.3" ordered-float = { version = "5.1.0", features = [ "serde" ] } parking_lot = "0.12.5" diff --git a/yazi-plugin/preset/plugins/mime-local.lua b/yazi-plugin/preset/plugins/mime-local.lua index c7b9d087..3d8e8d18 100644 --- a/yazi-plugin/preset/plugins/mime-local.lua +++ b/yazi-plugin/preset/plugins/mime-local.lua @@ -15,6 +15,29 @@ local function match_mimetype(line) end end +local function spawn_file1(paths) + local bin = os.getenv("YAZI_FILE_ONE") or "file" + local windows = ya.target_family() == "windows" + + local cmd = Command(bin):arg({ "-bL", "--mime-type" }):stdout(Command.PIPED) + if windows then + cmd:arg({ "-f", "-" }):stdin(Command.PIPED) + else + cmd:arg("--"):arg(paths) + end + + local child, err = cmd:spawn() + if not child then + return nil, Err("Failed to start `%s`, error: %s", bin, err) + elseif windows then + child:write_all(table.concat(paths, "\n")) + child:flush() + ya.drop(child:take_stdin()) + end + + return child +end + function M:fetch(job) local urls, paths = {}, {} for i, file in ipairs(job.files) do @@ -25,10 +48,9 @@ function M:fetch(job) end end - local cmd = os.getenv("YAZI_FILE_ONE") or "file" - local child, err = Command(cmd):arg({ "-bL", "--mime-type", "--" }):arg(paths):stdout(Command.PIPED):spawn() + local child, err = spawn_file1(paths) if not child then - return true, Err("Failed to start `%s`, error: %s", cmd, err) + return true, err end local updates, last = {}, ya.time() diff --git a/yazi-plugin/src/utils/app.rs b/yazi-plugin/src/utils/app.rs index c3d07048..e87759af 100644 --- a/yazi-plugin/src/utils/app.rs +++ b/yazi-plugin/src/utils/app.rs @@ -1,4 +1,7 @@ +use std::any::TypeId; + use mlua::{AnyUserData, ExternalError, Function, Lua}; +use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; use yazi_binding::{Id, Permit, PermitRef, deprecate}; use yazi_proxy::{AppProxy, HIDER}; @@ -15,6 +18,19 @@ impl Utils { }) } + pub(super) fn drop(lua: &Lua) -> mlua::Result { + lua.create_function(|_, ud: AnyUserData| { + match ud.type_id() { + Some(t) if t == TypeId::of::() => {} + Some(t) if t == TypeId::of::() => {} + Some(t) if t == TypeId::of::() => {} + Some(t) => Err(format!("Cannot drop userdata of type {t:?}").into_lua_err())?, + None => Err("Cannot drop scoped userdata".into_lua_err())?, + }; + ud.destroy() + }) + } + pub(super) fn hide(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, ()| async move { deprecate!(lua, "`ya.hide()` is deprecated, use `ui.hide()` instead, in your {}\nSee #2939 for more details: https://github.com/sxyazi/yazi/pull/2939"); diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index 5e94aac8..ca4e2083 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -10,6 +10,7 @@ pub fn compose( match key { // App b"id" => Utils::id(lua)?, + b"drop" => Utils::drop(lua)?, b"hide" => Utils::hide(lua)?, // Cache From a1fb206a591be9a6937e606cbb85cfcd9158bc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 26 Nov 2025 08:57:35 +0800 Subject: [PATCH 10/53] feat: `ya.quote()` support escaping strings containing invalid UTF-8 (#3369) --- CHANGELOG.md | 3 +- yazi-binding/src/path.rs | 4 +- yazi-binding/src/url.rs | 2 +- yazi-parser/src/app/plugin.rs | 2 +- yazi-parser/src/mgr/search.rs | 2 +- yazi-plugin/src/process/command.rs | 6 +- yazi-plugin/src/utils/text.rs | 8 +- yazi-scheduler/src/process/shell.rs | 7 +- yazi-shared/src/event/cmd.rs | 2 +- yazi-shared/src/shell/error.rs | 11 ++ yazi-shared/src/shell/mod.rs | 43 ++---- yazi-shared/src/shell/unix.rs | 109 ++++++-------- yazi-shared/src/shell/windows.rs | 213 ++++++++++++--------------- yazi-shared/src/strand/conversion.rs | 4 +- yazi-shared/src/strand/kind.rs | 4 +- 15 files changed, 179 insertions(+), 241 deletions(-) create mode 100644 yazi-shared/src/shell/error.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f964c3a6..07ed6fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Allow dynamic adjustment of layout ratio via `rt.mgr.ratio` ([#2964]) - Support `.deb` packages ([#2807], [#3128], [#3209]) - Port several widespread GUI keys to the input component ([#2849]) -- Support invalid UTF-8 paths throughout the codebase ([#2884], [#2889], [#2890], [#2895], [#3023], [#3290]) +- Support invalid UTF-8 paths throughout the codebase ([#2884], [#2889], [#2890], [#2895], [#3023], [#3290], [#3369]) - Allow upgrading only specific packages with `ya pkg` ([#2841]) - Respect the user's `image_filter` setting in the preset ImageMagick previewer ([#3286]) - Allow custom mouse click behavior for individual files ([#2925]) @@ -1542,3 +1542,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3317]: https://github.com/sxyazi/yazi/pull/3317 [#3360]: https://github.com/sxyazi/yazi/pull/3360 [#3364]: https://github.com/sxyazi/yazi/pull/3364 +[#3369]: https://github.com/sxyazi/yazi/pull/3369 diff --git a/yazi-binding/src/path.rs b/yazi-binding/src/path.rs index be37b940..137a1d84 100644 --- a/yazi-binding/src/path.rs +++ b/yazi-binding/src/path.rs @@ -79,7 +79,7 @@ impl Path { } } - fn strip_prefix(&self, base: Value) -> mlua::Result> { + fn strip_prefix(&self, base: Value) -> mlua::Result> { let strip = match base { Value::String(s) => self.try_strip_prefix(StrandCow::with(self.kind(), &*s.as_bytes())?), Value::UserData(ud) => self.try_strip_prefix(&*ud.borrow::()?), @@ -87,7 +87,7 @@ impl Path { }; Ok(match strip { - Ok(p) => Some(Path::new(p)), + Ok(p) => Some(Self::new(p)), Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, }) diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index f787c7f3..9def6a38 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -108,7 +108,7 @@ impl Url { match other { Value::String(s) => { let b = s.as_bytes(); - let (scheme, path) = SchemeCow::parse(&*b)?; + let (scheme, path) = SchemeCow::parse(&b)?; if scheme == self.scheme() { Self::new(self.try_join(path).into_lua_err()?).into_lua(lua) } else { diff --git a/yazi-parser/src/app/plugin.rs b/yazi-parser/src/app/plugin.rs index 2413a573..5232ea8f 100644 --- a/yazi-parser/src/app/plugin.rs +++ b/yazi-parser/src/app/plugin.rs @@ -29,7 +29,7 @@ impl TryFrom for PluginOpt { }; let args = if let Ok(s) = c.second() { - let (words, last) = yazi_shared::shell::split_unix(s, true)?; + let (words, last) = yazi_shared::shell::unix::split(s, true)?; Cmd::parse_args(words, last)? } else { Default::default() diff --git a/yazi-parser/src/mgr/search.rs b/yazi-parser/src/mgr/search.rs index ffef434e..10459077 100644 --- a/yazi-parser/src/mgr/search.rs +++ b/yazi-parser/src/mgr/search.rs @@ -24,7 +24,7 @@ impl TryFrom for SearchOpt { (c.str(0).parse()?, "".into()) }; - let Ok(args) = yazi_shared::shell::split_unix(c.str("args"), false) else { + let Ok(args) = yazi_shared::shell::unix::split(c.str("args"), false) else { bail!("Invalid 'args' argument in SearchOpt"); }; diff --git a/yazi-plugin/src/process/command.rs b/yazi-plugin/src/process/command.rs index 1e63f64f..72a41303 100644 --- a/yazi-plugin/src/process/command.rs +++ b/yazi-plugin/src/process/command.rs @@ -144,11 +144,11 @@ impl UserData for Command { let mut me = ud.borrow_mut::()?; match arg { Value::String(s) => { - me.inner.arg(OsStr::from_wtf8(&*s.as_bytes())?); + me.inner.arg(OsStr::from_wtf8(&s.as_bytes())?); } Value::Table(t) => { for s in t.sequence_values::() { - me.inner.arg(OsStr::from_wtf8(&*s?.as_bytes())?); + me.inner.arg(OsStr::from_wtf8(&s?.as_bytes())?); } } _ => return Err("arg must be a string or table of strings".into_lua_err()), @@ -165,7 +165,7 @@ impl UserData for Command { |_, (ud, key, value): (AnyUserData, mlua::String, mlua::String)| { ud.borrow_mut::()? .inner - .env(OsStr::from_wtf8(&*key.as_bytes())?, OsStr::from_wtf8(&*value.as_bytes())?); + .env(OsStr::from_wtf8(&key.as_bytes())?, OsStr::from_wtf8(&value.as_bytes())?); Ok(ud) }, ); diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index 22b861e2..0b34d0e2 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -15,11 +15,11 @@ impl Utils { pub(super) fn quote(lua: &Lua) -> mlua::Result { lua.create_function(|lua, (s, unix): (mlua::String, Option)| { - let s = s.to_str()?; + let b = s.as_bytes(); let s = match unix { - Some(true) => yazi_shared::shell::escape_unix(s.as_ref()), - Some(false) => yazi_shared::shell::escape_windows(s.as_ref()), - None => yazi_shared::shell::escape_native(s.as_ref()), + Some(true) => yazi_shared::shell::unix::escape_os_bytes(&b), + Some(false) => yazi_shared::shell::windows::escape_os_bytes(&b), + None => yazi_shared::shell::escape_os_bytes(&b), }; lua.create_string(&*s) }) diff --git a/yazi-scheduler/src/process/shell.rs b/yazi-scheduler/src/process/shell.rs index 1b6c704f..0266a111 100644 --- a/yazi-scheduler/src/process/shell.rs +++ b/yazi-scheduler/src/process/shell.rs @@ -2,7 +2,7 @@ use std::{ffi::OsString, process::Stdio}; use anyhow::Result; use tokio::process::{Child, Command}; -use yazi_fs::{Cwd, FsUrl}; +use yazi_fs::Cwd; use yazi_shared::url::{AsUrl, UrlCow}; pub(crate) struct ShellOpt { @@ -32,6 +32,7 @@ pub(crate) async fn shell(opt: ShellOpt) -> Result { #[cfg(unix)] return Ok(unsafe { + use yazi_fs::FsUrl; use yazi_shared::url::AsUrl; Command::new("sh") @@ -59,8 +60,10 @@ pub(crate) async fn shell(opt: ShellOpt) -> Result { .stdin(opt.stdio()) .stdout(opt.stdio()) .stderr(opt.stdio()) - .raw_arg("/C") + .env("=", r#""^\n\n""#) + .raw_arg(r#"/Q /S /D /V:OFF /E:ON /C ""#) .raw_arg(opt.cmd) + .raw_arg(r#"""#) .current_dir(cwd) .kill_on_drop(!opt.orphan) .spawn()?, diff --git a/yazi-shared/src/event/cmd.rs b/yazi-shared/src/event/cmd.rs index 352765c1..e7ad0469 100644 --- a/yazi-shared/src/event/cmd.rs +++ b/yazi-shared/src/event/cmd.rs @@ -231,7 +231,7 @@ impl FromStr for Cmd { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let (mut words, last) = crate::shell::split_unix(s, true)?; + let (mut words, last) = crate::shell::unix::split(s, true)?; if words.is_empty() || words[0].is_empty() { bail!("command name cannot be empty"); } diff --git a/yazi-shared/src/shell/error.rs b/yazi-shared/src/shell/error.rs new file mode 100644 index 00000000..a151602d --- /dev/null +++ b/yazi-shared/src/shell/error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SplitError { + #[error("missing closing single quote")] + MissingSingleQuote, + #[error("missing closing double quote")] + MissingDoubleQuote, + #[error("missing quote after escape slash")] + MissingQuoteAfterSlash, +} diff --git a/yazi-shared/src/shell/mod.rs b/yazi-shared/src/shell/mod.rs index c2e4abcc..69ecf016 100644 --- a/yazi-shared/src/shell/mod.rs +++ b/yazi-shared/src/shell/mod.rs @@ -1,31 +1,27 @@ //! Escape characters that may have special meaning in a shell, including -//! spaces. This is a modified version of the [`shell-escape`], [`shell-words`] -//! and [`this PR`]. +//! spaces. This is a modified version of the [`shell-escape`], [`shell-words`], +//! [Rust std] and [this PR]. //! //! [`shell-escape`]: https://crates.io/crates/shell-escape //! [`shell-words`]: https://crates.io/crates/shell-words -//! [`this PR`]: https://github.com/sfackler/shell-escape/pull/9 +//! [Rust std]: https://github.com/rust-lang/rust/blob/main/library/std/src/sys/args/windows.rs#L220 +//! [this PR]: https://github.com/sfackler/shell-escape/pull/9 use std::{borrow::Cow, ffi::OsStr}; -mod unix; -mod windows; +yazi_macro::mod_pub!(unix, windows); + +yazi_macro::mod_flat!(error); #[inline] -pub fn escape_unix(s: &str) -> Cow<'_, str> { unix::escape_str(s) } - -#[inline] -pub fn escape_windows(s: &str) -> Cow<'_, str> { windows::escape_str(s) } - -#[inline] -pub fn escape_native(s: &str) -> Cow<'_, str> { +pub fn escape_os_bytes(b: &[u8]) -> Cow<'_, [u8]> { #[cfg(unix)] { - escape_unix(s) + unix::escape_os_bytes(b) } #[cfg(windows)] { - escape_windows(s) + windows::escape_os_bytes(b) } } @@ -40,22 +36,3 @@ pub fn escape_os_str(s: &OsStr) -> Cow<'_, OsStr> { windows::escape_os_str(s) } } - -#[inline] -pub fn split_unix(s: &str, eoo: bool) -> anyhow::Result<(Vec, Option)> { - unix::split(s, eoo).map_err(|()| anyhow::anyhow!("missing closing quote")) -} - -#[cfg(windows)] -pub fn split_windows(s: &str) -> anyhow::Result> { Ok(windows::split(s)?) } - -pub fn split_native(s: &str) -> anyhow::Result> { - #[cfg(unix)] - { - Ok(split_unix(s, false)?.0) - } - #[cfg(windows)] - { - split_windows(s) - } -} diff --git a/yazi-shared/src/shell/unix.rs b/yazi-shared/src/shell/unix.rs index 75d1b47e..42364c81 100644 --- a/yazi-shared/src/shell/unix.rs +++ b/yazi-shared/src/shell/unix.rs @@ -1,40 +1,25 @@ use std::{borrow::Cow, mem}; -pub fn escape_str(s: &str) -> Cow<'_, str> { - match escape_slice(s.as_bytes()) { - Cow::Borrowed(_) => Cow::Borrowed(s), - Cow::Owned(v) => String::from_utf8(v).expect("Invalid bytes returned by escape_slice()").into(), - } -} +use crate::shell::SplitError; -#[cfg(unix)] -pub fn escape_os_str(s: &std::ffi::OsStr) -> Cow<'_, std::ffi::OsStr> { - use std::os::unix::ffi::{OsStrExt, OsStringExt}; - - match escape_slice(s.as_bytes()) { - Cow::Borrowed(_) => Cow::Borrowed(s), - Cow::Owned(v) => std::ffi::OsString::from_vec(v).into(), - } -} - -fn escape_slice(s: &[u8]) -> Cow<'_, [u8]> { - if !s.is_empty() && s.iter().copied().all(allowed) { - return Cow::Borrowed(s); +pub fn escape_os_bytes(b: &[u8]) -> Cow<'_, [u8]> { + if !b.is_empty() && b.iter().copied().all(allowed) { + return Cow::Borrowed(b); } - let mut escaped = Vec::with_capacity(s.len() + 2); + let mut escaped = Vec::with_capacity(b.len() + 2); escaped.push(b'\''); - for &b in s { - match b { + for &c in b { + match c { b'\'' | b'!' => { escaped.reserve(4); escaped.push(b'\''); escaped.push(b'\\'); - escaped.push(b); + escaped.push(c); escaped.push(b'\''); } - _ => escaped.push(b), + _ => escaped.push(c), } } @@ -42,11 +27,21 @@ fn escape_slice(s: &[u8]) -> Cow<'_, [u8]> { escaped.into() } +#[cfg(unix)] +pub fn escape_os_str(s: &std::ffi::OsStr) -> Cow<'_, std::ffi::OsStr> { + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + + match escape_os_bytes(s.as_bytes()) { + Cow::Borrowed(_) => Cow::Borrowed(s), + Cow::Owned(v) => std::ffi::OsString::from_vec(v).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'+') } -pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), ()> { +pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), SplitError> { enum State { /// Within a delimiter. Delimiter, @@ -138,7 +133,7 @@ pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), ()> { } }, SingleQuoted => match c { - None => return Err(()), + None => return Err(SplitError::MissingSingleQuote), Some('\'') => Unquoted, Some(c) => { word.push(c); @@ -146,7 +141,7 @@ pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), ()> { } }, DoubleQuoted => match c { - None => return Err(()), + None => return Err(SplitError::MissingDoubleQuote), Some('\"') => Unquoted, Some('\\') => DoubleQuotedBackslash, Some(c) => { @@ -155,7 +150,7 @@ pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), ()> { } }, DoubleQuotedBackslash => match c { - None => return Err(()), + None => return Err(SplitError::MissingQuoteAfterSlash), Some('\n') => DoubleQuoted, Some(c @ '$') | Some(c @ '`') | Some(c @ '"') | Some(c @ '\\') => { word.push(c); @@ -183,47 +178,25 @@ mod tests { use super::*; #[test] - fn test_escape_str() { - assert_eq!(escape_str(""), r#"''"#); - assert_eq!(escape_str(" "), r#"' '"#); - assert_eq!(escape_str("*"), r#"'*'"#); + fn test_escape_os_bytes() { + let cases: &[(&[u8], &[u8])] = &[ + (b"", br#"''"#), + (b" ", br#"' '"#), + (b"*", br#"'*'"#), + (b"--aaa=bbb-ccc", b"--aaa=bbb-ccc"), + (br#"--features="default""#, br#"'--features="default"'"#), + (b"linker=gcc -L/foo -Wl,bar", br#"'linker=gcc -L/foo -Wl,bar'"#), + ( + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", + ), + (br#"'!\$`\\\n "#, br#"''\'''\!'\$`\\\n '"#), + (&[0x66, 0x6f, 0x80, 0x6f], &[b'\'', 0x66, 0x6f, 0x80, 0x6f, b'\'']), + ]; - assert_eq!(escape_str("--aaa=bbb-ccc"), "--aaa=bbb-ccc"); - assert_eq!(escape_str(r#"--features="default""#), r#"'--features="default"'"#); - assert_eq!(escape_str("linker=gcc -L/foo -Wl,bar"), r#"'linker=gcc -L/foo -Wl,bar'"#); - - assert_eq!( - escape_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"), - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", - ); - assert_eq!(escape_str(r#"'!\$`\\\n "#), r#"''\'''\!'\$`\\\n '"#); - } - - #[cfg(unix)] - #[test] - fn test_escape_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!(escape_os_str(OsStr::from_bytes(input)), OsStr::from_bytes(expected)); + for &(input, expected) in cases { + let escaped = escape_os_bytes(input); + assert_eq!(escaped, expected, "Failed to escape: {:?}", String::from_utf8_lossy(input)); } - - 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'\'']); } } diff --git a/yazi-shared/src/shell/windows.rs b/yazi-shared/src/shell/windows.rs index aa142e39..29ddc83b 100644 --- a/yazi-shared/src/shell/windows.rs +++ b/yazi-shared/src/shell/windows.rs @@ -1,82 +1,68 @@ -use std::{borrow::Cow, iter::repeat_n}; +use std::borrow::Cow; -pub fn escape_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); +pub fn escape_os_bytes(b: &[u8]) -> Cow<'_, [u8]> { + let quote = needs_quotes(b); + let mut buf = Vec::with_capacity(b.len() + quote as usize * 2); + + if quote { + buf.push(b'"'); } - let mut escaped = Vec::with_capacity(bytes.len() + 2); - escaped.push(b'"'); - - 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_n(b'\\', slashes * 2 + 1)); - escaped.push(b'"'); - } - Some(b) => { - escaped.reserve(slashes + 1); - escaped.extend(repeat_n(b'\\', slashes)); - escaped.push(b); - } - None => { - escaped.reserve(slashes * 2); - escaped.extend(repeat_n(b'\\', slashes * 2)); - break; - } + // Loop through the string, escaping `\` only if followed by `"`. + // And escaping `"` by doubling them. + let mut backslashes: usize = 0; + for &c in b { + if c == b'\\' { + backslashes += 1; + } else { + if c == b'"' { + buf.extend((0..backslashes).map(|_| b'\\')); + buf.push(b'"'); + } else if c == b'%' { + buf.extend_from_slice(b"%%cd:~,"); + } else if c == b'\r' || c == b'\n' { + buf.extend_from_slice(b"%=%"); + backslashes = 0; + continue; + } + backslashes = 0; } + buf.push(c); } - escaped.push(b'"'); - Cow::Owned(unsafe { String::from_utf8_unchecked(escaped) }) + if quote { + buf.extend((0..backslashes).map(|_| b'\\')); + buf.push(b'"'); + } + + buf.into() } #[cfg(windows)] pub fn escape_os_str(s: &std::ffi::OsStr) -> Cow<'_, std::ffi::OsStr> { - use std::os::windows::ffi::{OsStrExt, OsStringExt}; + use crate::FromWtf8Vec; - let wide = s.encode_wide(); - if !s.is_empty() && !wide.clone().into_iter().any(disallowed) { - return Cow::Borrowed(s); + match escape_os_bytes(s.as_encoded_bytes()) { + Cow::Borrowed(_) => Cow::Borrowed(s), + Cow::Owned(v) => std::ffi::OsString::from_wtf8_vec(v).expect("valid WTF-8").into(), + } +} + +fn needs_quotes(arg: &[u8]) -> bool { + static UNQUOTED: &[u8] = br"#$*+-./:?@\_"; + + if arg.is_empty() || arg.last() == Some(&b'\\') { + return true; } - let mut escaped: Vec = 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_n(b'\\' as u16, slashes * 2 + 1)); - escaped.push(b'"' as _); - } - Some(c) => { - escaped.reserve(slashes + 1); - escaped.extend(repeat_n(b'\\' as u16, slashes)); - escaped.push(c); - } - None => { - escaped.reserve(slashes * 2); - escaped.extend(repeat_n(b'\\' as u16, slashes * 2)); - break; - } + for c in arg { + if c.is_ascii_control() { + return true; + } else if c.is_ascii() && !(c.is_ascii_alphanumeric() || UNQUOTED.contains(c)) { + return true; } } - - escaped.push(b'"' as _); - std::ffi::OsString::from_wide(&escaped).into() + false } #[cfg(windows)] @@ -84,11 +70,11 @@ pub fn split(s: &str) -> std::io::Result> { use std::os::windows::ffi::OsStrExt; let s: Vec<_> = std::ffi::OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect(); - split_slice(&s) + split_wide(&s) } #[cfg(windows)] -fn split_slice(s: &[u16]) -> std::io::Result> { +fn split_wide(s: &[u16]) -> std::io::Result> { use std::mem::MaybeUninit; use windows_sys::{Win32::{Foundation::LocalFree, UI::Shell::CommandLineToArgvW}, core::PCWSTR}; @@ -114,31 +100,36 @@ fn split_slice(s: &[u16]) -> std::io::Result> { Ok(res) } -#[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_escape_str() { - assert_eq!(escape_str(""), r#""""#); - assert_eq!(escape_str(r#""""#), r#""\"\"""#); + fn test_escape_os_bytes() { + let cases: &[(&[u8], &[u8])] = &[ + // Empty string + (b"", br#""""#), + (br#""""#, br#""""""""#), + // No escaping needed + (b"--aaa=bbb-ccc", br#""--aaa=bbb-ccc""#), + // Paths with spaces + (br#"\path\to\my documents\"#, br#""\path\to\my documents\\""#), + // Strings with quotes + (br#"--features="default""#, br#""--features=""default""""#), + // Nested quotes + (br#""--features=\"default\"""#, br#""""--features=\\""default\\""""""#), + // Complex command + (b"linker=gcc -L/foo -Wl,bar", br#""linker=gcc -L/foo -Wl,bar""#), + // Variable expansion + (b"%APPDATA%.txt", br#""%%cd:~,%APPDATA%%cd:~,%.txt""#), + // Unicode characters + ("이것은 테스트".as_bytes(), r#""이것은 테스트""#.as_bytes()), + ]; - assert_eq!(escape_str("--aaa=bbb-ccc"), "--aaa=bbb-ccc"); - assert_eq!(escape_str(r#"\path\to\my documents\"#), r#""\path\to\my documents\\""#); - - assert_eq!(escape_str(r#"--features="default""#), r#""--features=\"default\"""#); - assert_eq!(escape_str(r#""--features=\"default\"""#), r#""\"--features=\\\"default\\\"\"""#); - assert_eq!(escape_str("linker=gcc -L/foo -Wl,bar"), r#""linker=gcc -L/foo -Wl,bar""#); - - assert_eq!(escape_str("이것은 테스트"), r#""이것은 테스트""#); + for &(input, expected) in cases { + let escaped = escape_os_bytes(input); + assert_eq!(escaped, expected, "Failed to escape: {:?}", String::from_utf8_lossy(input)); + } } #[cfg(windows)] @@ -146,41 +137,23 @@ mod tests { fn test_escape_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!(escape_os_str(observed.as_os_str()), expected.as_os_str()); + #[rustfmt::skip] + let cases: &[(OsString, OsString)] = &[ + // Surrogate pairs and special characters + ( + OsString::from_wide(&[0x1055, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065]), + OsString::from_wide(&[0x1055, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065]), + ), + // Surrogate pair with quotes + ( + OsString::from_wide(&[0xd801, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065]), + OsString::from_wide(&[0xd801, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065]), + ), + ]; + + for (input, expected) in cases { + let escaped = escape_os_str(&input); + assert_eq!(&*escaped, expected, "Failed to escape: {:?}", input.to_string_lossy()); } - - fn from_bytes(input: &[u16], expected: &[u16]) { - let observed = OsString::from_wide(input); - let expected = OsString::from_wide(expected); - assert_eq!(escape_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, - ]); } } diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs index e8e628e6..35258cfa 100644 --- a/yazi-shared/src/strand/conversion.rs +++ b/yazi-shared/src/strand/conversion.rs @@ -63,8 +63,8 @@ impl AsStrand for PathDyn<'_> { impl AsStrand for PathBufDyn { fn as_strand(&self) -> Strand<'_> { match self { - PathBufDyn::Os(p) => Strand::Os(p.as_os_str()), - PathBufDyn::Unix(p) => Strand::Bytes(p.as_bytes()), + Self::Os(p) => Strand::Os(p.as_os_str()), + Self::Unix(p) => Strand::Bytes(p.as_bytes()), } } } diff --git a/yazi-shared/src/strand/kind.rs b/yazi-shared/src/strand/kind.rs index dd2e0883..e1034715 100644 --- a/yazi-shared/src/strand/kind.rs +++ b/yazi-shared/src/strand/kind.rs @@ -10,8 +10,8 @@ pub enum StrandKind { impl From for StrandKind { fn from(value: PathKind) -> Self { match value { - PathKind::Os => StrandKind::Os, - PathKind::Unix => StrandKind::Bytes, + PathKind::Os => Self::Os, + PathKind::Unix => Self::Bytes, } } } From 329c80cbee5bb9e527ffa475bc0df2477f71ed93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 26 Nov 2025 17:27:52 +0800 Subject: [PATCH 11/53] feat: support Unix path backend for the SFTP provider (#3371) --- Cargo.lock | 2 + yazi-fs/src/file.rs | 2 +- yazi-fs/src/path/expand.rs | 5 +- yazi-sftp/Cargo.toml | 1 + yazi-sftp/src/byte_str.rs | 114 ------------------------- yazi-sftp/src/fs/dir_entry.rs | 40 +++------ yazi-sftp/src/fs/read_dir.rs | 12 +-- yazi-sftp/src/lib.rs | 6 +- yazi-sftp/src/operator.rs | 79 ++++++++--------- yazi-sftp/src/path.rs | 83 ++++++++++++++++++ yazi-sftp/src/requests/extended.rs | 26 +++--- yazi-sftp/src/requests/lstat.rs | 10 +-- yazi-sftp/src/requests/mkdir.rs | 10 +-- yazi-sftp/src/requests/open.rs | 10 +-- yazi-sftp/src/requests/open_dir.rs | 10 +-- yazi-sftp/src/requests/readlink.rs | 10 +-- yazi-sftp/src/requests/realpath.rs | 10 +-- yazi-sftp/src/requests/remove.rs | 10 +-- yazi-sftp/src/requests/rename.rs | 14 +-- yazi-sftp/src/requests/rmdir.rs | 10 +-- yazi-sftp/src/requests/set_stat.rs | 10 +-- yazi-sftp/src/requests/stat.rs | 10 +-- yazi-sftp/src/requests/symlink.rs | 14 +-- yazi-sftp/src/responses/extended.rs | 2 +- yazi-sftp/src/responses/name.rs | 8 +- yazi-sftp/src/wtf.rs | 87 ------------------- yazi-shared/src/loc/able.rs | 62 ++++++++++++++ yazi-shared/src/loc/buf.rs | 16 ++-- yazi-shared/src/loc/loc.rs | 4 +- yazi-shared/src/path/buf.rs | 18 ++++ yazi-shared/src/path/conversion.rs | 8 ++ yazi-shared/src/path/kind.rs | 2 +- yazi-shared/src/path/like.rs | 2 + yazi-shared/src/path/path.rs | 8 ++ yazi-shared/src/path/view.rs | 8 ++ yazi-shared/src/strand/conversion.rs | 8 ++ yazi-shared/src/strand/cow.rs | 7 ++ yazi-shared/src/strand/kind.rs | 2 +- yazi-shared/src/strand/view.rs | 12 +++ yazi-shared/src/url/buf.rs | 9 +- yazi-shared/src/url/components.rs | 2 +- yazi-shared/src/url/cow.rs | 12 +-- yazi-shared/src/url/url.rs | 14 +-- yazi-vfs/Cargo.toml | 1 + yazi-vfs/src/provider/sftp/metadata.rs | 11 ++- yazi-vfs/src/provider/sftp/sftp.rs | 10 +-- 46 files changed, 410 insertions(+), 401 deletions(-) delete mode 100644 yazi-sftp/src/byte_str.rs create mode 100644 yazi-sftp/src/path.rs delete mode 100644 yazi-sftp/src/wtf.rs diff --git a/Cargo.lock b/Cargo.lock index 40b612b2..ec94c648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5065,6 +5065,7 @@ dependencies = [ "russh", "serde", "tokio", + "typed-path", ] [[package]] @@ -5123,6 +5124,7 @@ dependencies = [ "russh", "tokio", "tracing", + "typed-path", "yazi-config", "yazi-fs", "yazi-macro", diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index 17d46159..399ca4c0 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -60,7 +60,7 @@ impl File { Url::Archive { loc: Loc::saturated(to.as_os().ok()?, kind), domain } } UrlBuf::Sftp { domain, .. } => { - Url::Sftp { loc: Loc::saturated(to.as_os().ok()?, kind), domain } + Url::Sftp { loc: Loc::saturated(to.as_unix().ok()?, kind), domain } } }) } diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index f5bde9ff..cec4b1fd 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -36,7 +36,10 @@ fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> { Url::Regular(_) => UrlBuf::Regular(loc), Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }, Url::Archive { domain, .. } => UrlBuf::Archive { loc, domain: domain.intern() }, - Url::Sftp { domain, .. } => UrlBuf::Sftp { loc, domain: domain.intern() }, + Url::Sftp { domain, .. } => { + todo!(); + // UrlBuf::Sftp { loc, domain: domain.intern() } + } }; absolute_url(expanded) diff --git a/yazi-sftp/Cargo.toml b/yazi-sftp/Cargo.toml index 20939e46..b9f2606a 100644 --- a/yazi-sftp/Cargo.toml +++ b/yazi-sftp/Cargo.toml @@ -17,3 +17,4 @@ parking_lot = { workspace = true } russh = { workspace = true } serde = { workspace = true } tokio = { workspace = true } +typed-path = { workspace = true } diff --git a/yazi-sftp/src/byte_str.rs b/yazi-sftp/src/byte_str.rs deleted file mode 100644 index 97f2d156..00000000 --- a/yazi-sftp/src/byte_str.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}, ops::Deref, path::{Path, PathBuf}}; - -use serde::{Deserialize, Serialize}; - -use crate::Error; - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct ByteStr<'a>(Cow<'a, [u8]>); - -impl Deref for ByteStr<'_> { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl<'a> From<&'a str> for ByteStr<'a> { - fn from(value: &'a str) -> Self { ByteStr(Cow::Borrowed(value.as_bytes())) } -} - -impl<'a> From<&'a Self> for ByteStr<'a> { - fn from(value: &'a ByteStr) -> Self { ByteStr(Cow::Borrowed(&value.0)) } -} - -impl PartialEq<&str> for ByteStr<'_> { - fn eq(&self, other: &&str) -> bool { self.0 == other.as_bytes() } -} - -impl<'a> ByteStr<'a> { - pub fn to_os_str(&self) -> Cow<'_, OsStr> { - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - OsStr::from_bytes(&self.0).into() - } - #[cfg(windows)] - { - super::wtf::bytes_to_wide(self.0.as_ref()) - } - } - - pub fn into_os_string(self) -> OsString { - #[cfg(unix)] - { - use std::os::unix::ffi::OsStringExt; - OsString::from_vec(self.0.into_owned()) - } - #[cfg(windows)] - { - match super::wtf::bytes_to_wide(self.0.as_ref()) { - Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(self.0.into_owned()) }.into(), - Cow::Owned(s) => s, - } - } - } - - pub fn to_path(&self) -> Cow<'_, Path> { - match self.to_os_str() { - Cow::Borrowed(s) => Path::new(s).into(), - Cow::Owned(s) => PathBuf::from(s).into(), - } - } - - pub fn into_path(self) -> PathBuf { - match self.0 { - Cow::Borrowed(_) => self.to_path().into_owned(), - Cow::Owned(_) => self.into_os_string().into(), - } - } - - pub fn join(&self, other: impl Into) -> PathBuf { - let other = other.into(); - match self.to_path() { - Cow::Borrowed(p) => p.join(other.to_path()), - Cow::Owned(mut p) => { - p.push(other.to_path()); - p - } - } - } - - pub fn into_owned(self) -> ByteStr<'static> { ByteStr(Cow::Owned(self.0.into_owned())) } - - pub(super) unsafe fn from_str_bytes_unchecked(bytes: &'a [u8]) -> Self { - Self(Cow::Borrowed(bytes)) - } -} - -// --- Traits -pub trait ToByteStr<'a> { - fn to_byte_str(self) -> Result, Error>; -} - -impl<'a, T> ToByteStr<'a> for &'a T -where - T: AsRef + ?Sized, -{ - fn to_byte_str(self) -> Result, Error> { - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - Ok(ByteStr(Cow::Borrowed(self.as_ref().as_os_str().as_bytes()))) - } - #[cfg(windows)] - { - super::wtf::wide_to_bytes(self.as_ref().as_os_str()) - .ok_or(Error::custom("failed to convert wide path to bytes")) - .map(ByteStr) - } - } -} - -impl<'a> ToByteStr<'a> for &'a ByteStr<'a> { - fn to_byte_str(self) -> Result, Error> { Ok(ByteStr(Cow::Borrowed(&self.0))) } -} diff --git a/yazi-sftp/src/fs/dir_entry.rs b/yazi-sftp/src/fs/dir_entry.rs index 7ae8cb85..66ea6a02 100644 --- a/yazi-sftp/src/fs/dir_entry.rs +++ b/yazi-sftp/src/fs/dir_entry.rs @@ -1,47 +1,31 @@ -use std::{borrow::Cow, ffi::OsStr, path::PathBuf, sync::Arc}; +use std::sync::Arc; -use crate::{ByteStr, fs::Attrs}; +use typed_path::UnixPathBuf; + +use crate::fs::Attrs; pub struct DirEntry { - pub(super) dir: Arc>, - pub(super) name: ByteStr<'static>, - pub(super) long_name: ByteStr<'static>, + pub(super) dir: Arc, + pub(super) name: Vec, + pub(super) long_name: Vec, pub(super) attrs: Attrs, } impl DirEntry { #[must_use] - pub fn path(&self) -> PathBuf { self.dir.join(&self.name) } + pub fn path(&self) -> UnixPathBuf { self.dir.join(&self.name) } - #[must_use] - pub fn name(&self) -> Cow<'_, OsStr> { self.name.to_os_str() } + pub fn name(&self) -> &[u8] { &self.name } - #[must_use] - pub fn long_name(&self) -> Cow<'_, OsStr> { self.long_name.to_os_str() } + pub fn long_name(&self) -> &[u8] { &self.long_name } pub fn attrs(&self) -> &Attrs { &self.attrs } pub fn nlink(&self) -> Option { str::from_utf8(self.long_name_field(1)?).ok()?.parse().ok() } - pub fn user(&self) -> Option> { - let b = self.long_name_field(2)?; - Some(unsafe { - match ByteStr::from_str_bytes_unchecked(b).to_os_str() { - Cow::Borrowed(_) => OsStr::from_encoded_bytes_unchecked(b).into(), - Cow::Owned(s) => s.into(), - } - }) - } + pub fn user(&self) -> Option<&[u8]> { self.long_name_field(2) } - pub fn group(&self) -> Option> { - let b = self.long_name_field(3)?; - Some(unsafe { - match ByteStr::from_str_bytes_unchecked(b).to_os_str() { - Cow::Borrowed(_) => OsStr::from_encoded_bytes_unchecked(b).into(), - Cow::Owned(s) => s.into(), - } - }) - } + pub fn group(&self) -> Option<&[u8]> { self.long_name_field(3) } fn long_name_field(&self, n: usize) -> Option<&[u8]> { self.long_name.split(|b| b.is_ascii_whitespace()).filter(|s| !s.is_empty()).nth(n) diff --git a/yazi-sftp/src/fs/read_dir.rs b/yazi-sftp/src/fs/read_dir.rs index 8386f428..22e0a354 100644 --- a/yazi-sftp/src/fs/read_dir.rs +++ b/yazi-sftp/src/fs/read_dir.rs @@ -1,10 +1,10 @@ use std::{mem, sync::Arc}; -use crate::{ByteStr, Error, Session, fs::DirEntry, requests, responses}; +use crate::{Error, Session, SftpPath, fs::DirEntry, requests, responses}; pub struct ReadDir { session: Arc, - dir: Arc>, + dir: Arc, handle: String, name: responses::Name<'static>, @@ -13,7 +13,7 @@ pub struct ReadDir { } impl ReadDir { - pub(crate) fn new(session: &Arc, dir: ByteStr, handle: String) -> Self { + pub(crate) fn new(session: &Arc, dir: SftpPath, handle: String) -> Self { Self { session: session.clone(), dir: Arc::new(dir.into_owned()), @@ -33,11 +33,11 @@ impl ReadDir { }; self.cursor += 1; - if item.name != "." && item.name != ".." { + if &*item.name != b"." && &*item.name != b".." { return Ok(Some(DirEntry { dir: self.dir.clone(), - name: item.name, - long_name: item.long_name, + name: item.name.into_owned(), + long_name: item.long_name.into_owned(), attrs: item.attrs, })); } diff --git a/yazi-sftp/src/lib.rs b/yazi-sftp/src/lib.rs index e7e7b9d2..9a0e87cc 100644 --- a/yazi-sftp/src/lib.rs +++ b/yazi-sftp/src/lib.rs @@ -4,23 +4,21 @@ pub mod fs; pub mod requests; pub mod responses; -mod byte_str; mod de; mod error; mod id; mod macros; mod operator; mod packet; +mod path; mod ser; mod session; -#[cfg(windows)] -mod wtf; -pub use byte_str::*; pub(crate) use de::*; pub use error::*; pub(crate) use id::*; pub use operator::*; pub use packet::*; +pub use path::*; pub(crate) use ser::*; pub use session::*; diff --git a/yazi-sftp/src/operator.rs b/yazi-sftp/src/operator.rs index 1620d4f4..b28cb181 100644 --- a/yazi-sftp/src/operator.rs +++ b/yazi-sftp/src/operator.rs @@ -1,9 +1,10 @@ -use std::{ops::Deref, path::PathBuf, sync::Arc}; +use std::{ops::Deref, sync::Arc}; use russh::{ChannelStream, client::Msg}; use tokio::sync::oneshot; +use typed_path::UnixPathBuf; -use crate::{ByteStr, Error, Packet, Session, ToByteStr, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; +use crate::{AsSftpPath, Error, Packet, Session, SftpPath, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; pub struct Operator(Arc); @@ -28,9 +29,9 @@ impl Operator { pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: &'a Attrs) -> Result where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let handle: responses::Handle = self.send(requests::Open::new(path, flags, attrs)?).await?; + let handle: responses::Handle = self.send(requests::Open::new(path, flags, attrs)).await?; Ok(File::new(&self.0, handle.handle)) } @@ -59,9 +60,9 @@ impl Operator { pub async fn lstat<'a, P>(&self, path: P) -> Result where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let attrs: responses::Attrs = self.send(requests::Lstat::new(path)?).await?; + let attrs: responses::Attrs = self.send(requests::Lstat::new(path)).await?; Ok(attrs.attrs) } @@ -72,9 +73,9 @@ impl Operator { pub async fn setstat<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let status: responses::Status = self.send(requests::SetStat::new(path, attrs)?).await?; + let status: responses::Status = self.send(requests::SetStat::new(path, attrs)).await?; status.into() } @@ -85,100 +86,100 @@ impl Operator { pub async fn read_dir<'a, P>(&'a self, dir: P) -> Result where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let dir: ByteStr = dir.to_byte_str()?; - let handle: responses::Handle = self.send(requests::OpenDir::new(&dir)?).await?; + let dir: SftpPath = dir.as_sftp_path(); + let handle: responses::Handle = self.send(requests::OpenDir::new(&dir)).await?; Ok(ReadDir::new(&self.0, dir, handle.handle)) } pub async fn remove<'a, P>(&self, path: P) -> Result<(), Error> where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let status: responses::Status = self.send(requests::Remove::new(path)?).await?; + let status: responses::Status = self.send(requests::Remove::new(path)).await?; status.into() } pub async fn mkdir<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let status: responses::Status = self.send(requests::Mkdir::new(path, attrs)?).await?; + let status: responses::Status = self.send(requests::Mkdir::new(path, attrs)).await?; status.into() } pub async fn rmdir<'a, P>(&self, path: P) -> Result<(), Error> where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let status: responses::Status = self.send(requests::Rmdir::new(path)?).await?; + let status: responses::Status = self.send(requests::Rmdir::new(path)).await?; status.into() } - pub async fn realpath<'a, P>(&self, path: P) -> Result + pub async fn realpath<'a, P>(&self, path: P) -> Result where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let mut name: responses::Name = self.send(requests::Realpath::new(path)?).await?; + let mut name: responses::Name = self.send(requests::Realpath::new(path)).await?; if name.items.is_empty() { Err(Error::custom("realpath returned no names")) } else { - Ok(name.items.swap_remove(0).name.into_path()) + Ok(name.items.swap_remove(0).name.into_owned().into()) } } pub async fn stat<'a, P>(&self, path: P) -> Result where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let attrs: responses::Attrs = self.send(requests::Stat::new(path)?).await?; + let attrs: responses::Attrs = self.send(requests::Stat::new(path)).await?; Ok(attrs.attrs) } pub async fn rename<'a, F, T>(&self, from: F, to: T) -> Result<(), Error> where - F: ToByteStr<'a>, - T: ToByteStr<'a>, + F: AsSftpPath<'a>, + T: AsSftpPath<'a>, { - let status: responses::Status = self.send(requests::Rename::new(from, to)?).await?; + let status: responses::Status = self.send(requests::Rename::new(from, to)).await?; status.into() } pub async fn rename_posix<'a, F, T>(&self, from: F, to: T) -> Result<(), Error> where - F: ToByteStr<'a>, - T: ToByteStr<'a>, + F: AsSftpPath<'a>, + T: AsSftpPath<'a>, { if self.extensions.lock().get("posix-rename@openssh.com").is_none_or(|s| s != "1") { return Err(Error::Unsupported); } - let data = requests::ExtendedRename::new(from, to)?; + let data = requests::ExtendedRename::new(from, to); let status: responses::Status = self.send(requests::Extended::new("posix-rename@openssh.com", data)).await?; status.into() } - pub async fn readlink<'a, P>(&self, path: P) -> Result + pub async fn readlink<'a, P>(&self, path: P) -> Result where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - let mut name: responses::Name = self.send(requests::Readlink::new(path)?).await?; + let mut name: responses::Name = self.send(requests::Readlink::new(path)).await?; if name.items.is_empty() { Err(Error::custom("readlink returned no names")) } else { - Ok(name.items.swap_remove(0).name.into_path()) + Ok(name.items.swap_remove(0).name.into_owned().into()) } } pub async fn symlink<'a, L, O>(&self, original: O, link: L) -> Result<(), Error> where - O: ToByteStr<'a>, - L: ToByteStr<'a>, + O: AsSftpPath<'a>, + L: AsSftpPath<'a>, { - let status: responses::Status = self.send(requests::Symlink::new(original, link)?).await?; + let status: responses::Status = self.send(requests::Symlink::new(original, link)).await?; status.into() } @@ -193,14 +194,14 @@ impl Operator { pub async fn hardlink<'a, O, L>(&self, original: O, link: L) -> Result<(), Error> where - O: ToByteStr<'a>, - L: ToByteStr<'a>, + O: AsSftpPath<'a>, + L: AsSftpPath<'a>, { if self.extensions.lock().get("hardlink@openssh.com").is_none_or(|s| s != "1") { return Err(Error::Unsupported); } - let data = requests::ExtendedHardlink::new(original, link)?; + let data = requests::ExtendedHardlink::new(original, link); let status: responses::Status = self.send(requests::Extended::new("hardlink@openssh.com", data)).await?; status.into() diff --git a/yazi-sftp/src/path.rs b/yazi-sftp/src/path.rs new file mode 100644 index 00000000..39f20d7c --- /dev/null +++ b/yazi-sftp/src/path.rs @@ -0,0 +1,83 @@ +use std::{borrow::Cow, ops::Deref}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug)] +pub enum SftpPath<'a> { + Borrowed(&'a typed_path::UnixPath), + Owned(typed_path::UnixPathBuf), +} + +impl Deref for SftpPath<'_> { + type Target = typed_path::UnixPath; + + fn deref(&self) -> &Self::Target { + match self { + SftpPath::Borrowed(p) => p, + SftpPath::Owned(p) => p.as_path(), + } + } +} + +impl Default for SftpPath<'_> { + fn default() -> Self { SftpPath::Borrowed(typed_path::UnixPath::new("")) } +} + +impl<'a> From<&'a Self> for SftpPath<'a> { + fn from(value: &'a SftpPath) -> Self { SftpPath::Borrowed(value) } +} + +impl<'a> From> for SftpPath<'a> { + fn from(value: Cow<'a, [u8]>) -> Self { + match value { + Cow::Borrowed(b) => Self::Borrowed(typed_path::UnixPath::new(b)), + Cow::Owned(b) => SftpPath::Owned(typed_path::UnixPathBuf::from(b)), + } + } +} + +impl Serialize for SftpPath<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(self.as_bytes()) + } +} + +impl<'de> Deserialize<'de> for SftpPath<'_> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let cow = >::deserialize(deserializer)?; + Ok(Self::Owned(cow.into_owned().into())) + } +} + +impl<'a> SftpPath<'a> { + pub fn len(&self) -> usize { self.as_bytes().len() } + + pub fn into_owned(self) -> typed_path::UnixPathBuf { + match self { + SftpPath::Borrowed(p) => p.to_owned(), + SftpPath::Owned(p) => p, + } + } +} + +// --- Traits +pub trait AsSftpPath<'a> { + fn as_sftp_path(self) -> SftpPath<'a>; +} + +impl<'a, T> AsSftpPath<'a> for &'a T +where + T: ?Sized + AsRef, +{ + fn as_sftp_path(self) -> SftpPath<'a> { SftpPath::Borrowed(self.as_ref()) } +} + +impl<'a> AsSftpPath<'a> for &'a SftpPath<'a> { + fn as_sftp_path(self) -> SftpPath<'a> { SftpPath::Borrowed(self) } +} diff --git a/yazi-sftp/src/requests/extended.rs b/yazi-sftp/src/requests/extended.rs index 7b310b03..de29a665 100644 --- a/yazi-sftp/src/requests/extended.rs +++ b/yazi-sftp/src/requests/extended.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, fmt::Debug}; use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct Extended<'a, D> { @@ -30,17 +30,17 @@ pub trait ExtendedData: Debug + Serialize + for<'de> Deserialize<'de> { // --- POSIX Rename #[derive(Debug, Deserialize, Serialize)] pub struct ExtendedRename<'a> { - pub from: ByteStr<'a>, - pub to: ByteStr<'a>, + pub from: SftpPath<'a>, + pub to: SftpPath<'a>, } impl<'a> ExtendedRename<'a> { - pub fn new(from: F, to: T) -> Result + pub fn new(from: F, to: T) -> Self where - F: ToByteStr<'a>, - T: ToByteStr<'a>, + F: AsSftpPath<'a>, + T: AsSftpPath<'a>, { - Ok(Self { from: from.to_byte_str()?, to: to.to_byte_str()? }) + Self { from: from.as_sftp_path(), to: to.as_sftp_path() } } } @@ -65,17 +65,17 @@ impl ExtendedData for ExtendedFsync<'_> { // --- Hardlink #[derive(Debug, Deserialize, Serialize)] pub struct ExtendedHardlink<'a> { - pub original: ByteStr<'a>, - pub link: ByteStr<'a>, + pub original: SftpPath<'a>, + pub link: SftpPath<'a>, } impl<'a> ExtendedHardlink<'a> { - pub fn new(original: O, link: L) -> Result + pub fn new(original: O, link: L) -> Self where - O: ToByteStr<'a>, - L: ToByteStr<'a>, + O: AsSftpPath<'a>, + L: AsSftpPath<'a>, { - Ok(Self { original: original.to_byte_str()?, link: link.to_byte_str()? }) + Self { original: original.as_sftp_path(), link: link.as_sftp_path() } } } diff --git a/yazi-sftp/src/requests/lstat.rs b/yazi-sftp/src/requests/lstat.rs index 55f87891..ae6539f7 100644 --- a/yazi-sftp/src/requests/lstat.rs +++ b/yazi-sftp/src/requests/lstat.rs @@ -1,19 +1,19 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct Lstat<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, } impl Lstat<'_> { - pub fn new<'a, P>(path: P) -> Result, Error> + pub fn new<'a, P>(path: P) -> Lstat<'a> where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Lstat { id: 0, path: path.to_byte_str()? }) + Lstat { id: 0, path: path.as_sftp_path() } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } diff --git a/yazi-sftp/src/requests/mkdir.rs b/yazi-sftp/src/requests/mkdir.rs index 4d37563e..0dd8de51 100644 --- a/yazi-sftp/src/requests/mkdir.rs +++ b/yazi-sftp/src/requests/mkdir.rs @@ -1,20 +1,20 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr, fs::Attrs}; +use crate::{AsSftpPath, SftpPath, fs::Attrs}; #[derive(Debug, Deserialize, Serialize)] pub struct Mkdir<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, pub attrs: Attrs, } impl<'a> Mkdir<'a> { - pub fn new

(path: P, attrs: Attrs) -> Result + pub fn new

(path: P, attrs: Attrs) -> Self where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Self { id: 0, path: path.to_byte_str()?, attrs }) + Self { id: 0, path: path.as_sftp_path(), attrs } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() } diff --git a/yazi-sftp/src/requests/open.rs b/yazi-sftp/src/requests/open.rs index 22f22861..55b2eacd 100644 --- a/yazi-sftp/src/requests/open.rs +++ b/yazi-sftp/src/requests/open.rs @@ -2,22 +2,22 @@ use std::borrow::Cow; use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr, fs::{Attrs, Flags}}; +use crate::{AsSftpPath, SftpPath, fs::{Attrs, Flags}}; #[derive(Debug, Deserialize, Serialize)] pub struct Open<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, pub flags: Flags, pub attrs: Cow<'a, Attrs>, } impl<'a> Open<'a> { - pub fn new

(path: P, flags: Flags, attrs: &'a Attrs) -> Result + pub fn new

(path: P, flags: Flags, attrs: &'a Attrs) -> Self where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Self { id: 0, path: path.to_byte_str()?, flags, attrs: Cow::Borrowed(attrs) }) + Self { id: 0, path: path.as_sftp_path(), flags, attrs: Cow::Borrowed(attrs) } } pub fn len(&self) -> usize { diff --git a/yazi-sftp/src/requests/open_dir.rs b/yazi-sftp/src/requests/open_dir.rs index 3d29b54f..71eaf8a8 100644 --- a/yazi-sftp/src/requests/open_dir.rs +++ b/yazi-sftp/src/requests/open_dir.rs @@ -1,19 +1,19 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct OpenDir<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, } impl<'a> OpenDir<'a> { - pub fn new

(path: P) -> Result + pub fn new

(path: P) -> Self where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Self { id: 0, path: path.to_byte_str()? }) + Self { id: 0, path: path.as_sftp_path() } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } diff --git a/yazi-sftp/src/requests/readlink.rs b/yazi-sftp/src/requests/readlink.rs index 9f9ed259..dfdc34f2 100644 --- a/yazi-sftp/src/requests/readlink.rs +++ b/yazi-sftp/src/requests/readlink.rs @@ -1,19 +1,19 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct Readlink<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, } impl<'a> Readlink<'a> { - pub fn new

(path: P) -> Result + pub fn new

(path: P) -> Self where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Self { id: 0, path: path.to_byte_str()? }) + Self { id: 0, path: path.as_sftp_path() } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } diff --git a/yazi-sftp/src/requests/realpath.rs b/yazi-sftp/src/requests/realpath.rs index bad53344..05760f98 100644 --- a/yazi-sftp/src/requests/realpath.rs +++ b/yazi-sftp/src/requests/realpath.rs @@ -1,19 +1,19 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct Realpath<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, } impl<'a> Realpath<'a> { - pub fn new

(path: P) -> Result + pub fn new

(path: P) -> Self where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Self { id: 0, path: path.to_byte_str()? }) + Self { id: 0, path: path.as_sftp_path() } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } diff --git a/yazi-sftp/src/requests/remove.rs b/yazi-sftp/src/requests/remove.rs index db9ea2d5..ffc7122f 100644 --- a/yazi-sftp/src/requests/remove.rs +++ b/yazi-sftp/src/requests/remove.rs @@ -1,19 +1,19 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct Remove<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, } impl<'a> Remove<'a> { - pub fn new

(path: P) -> Result + pub fn new

(path: P) -> Self where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Self { id: 0, path: path.to_byte_str()? }) + Self { id: 0, path: path.as_sftp_path() } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } diff --git a/yazi-sftp/src/requests/rename.rs b/yazi-sftp/src/requests/rename.rs index 749495f0..9b734630 100644 --- a/yazi-sftp/src/requests/rename.rs +++ b/yazi-sftp/src/requests/rename.rs @@ -1,21 +1,21 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct Rename<'a> { pub id: u32, - pub from: ByteStr<'a>, - pub to: ByteStr<'a>, + pub from: SftpPath<'a>, + pub to: SftpPath<'a>, } impl<'a> Rename<'a> { - pub fn new(from: F, to: T) -> Result + pub fn new(from: F, to: T) -> Self where - F: ToByteStr<'a>, - T: ToByteStr<'a>, + F: AsSftpPath<'a>, + T: AsSftpPath<'a>, { - Ok(Self { id: 0, from: from.to_byte_str()?, to: to.to_byte_str()? }) + Self { id: 0, from: from.as_sftp_path(), to: to.as_sftp_path() } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.from.len() + 4 + self.to.len() } diff --git a/yazi-sftp/src/requests/rmdir.rs b/yazi-sftp/src/requests/rmdir.rs index 3d9a26c6..13fb990b 100644 --- a/yazi-sftp/src/requests/rmdir.rs +++ b/yazi-sftp/src/requests/rmdir.rs @@ -1,19 +1,19 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct Rmdir<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, } impl<'a> Rmdir<'a> { - pub fn new

(path: P) -> Result + pub fn new

(path: P) -> Self where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Self { id: 0, path: path.to_byte_str()? }) + Self { id: 0, path: path.as_sftp_path() } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } diff --git a/yazi-sftp/src/requests/set_stat.rs b/yazi-sftp/src/requests/set_stat.rs index 4e618489..9734f3fc 100644 --- a/yazi-sftp/src/requests/set_stat.rs +++ b/yazi-sftp/src/requests/set_stat.rs @@ -2,21 +2,21 @@ use std::borrow::Cow; use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr, fs::Attrs}; +use crate::{AsSftpPath, SftpPath, fs::Attrs}; #[derive(Debug, Deserialize, Serialize)] pub struct SetStat<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, pub attrs: Attrs, } impl<'a> SetStat<'a> { - pub fn new

(path: P, attrs: Attrs) -> Result + pub fn new

(path: P, attrs: Attrs) -> Self where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Self { id: 0, path: path.to_byte_str()?, attrs }) + Self { id: 0, path: path.as_sftp_path(), attrs } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() } diff --git a/yazi-sftp/src/requests/stat.rs b/yazi-sftp/src/requests/stat.rs index 58addd47..dfbcdf04 100644 --- a/yazi-sftp/src/requests/stat.rs +++ b/yazi-sftp/src/requests/stat.rs @@ -1,19 +1,19 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct Stat<'a> { pub id: u32, - pub path: ByteStr<'a>, + pub path: SftpPath<'a>, } impl<'a> Stat<'a> { - pub fn new

(path: P) -> Result + pub fn new

(path: P) -> Self where - P: ToByteStr<'a>, + P: AsSftpPath<'a>, { - Ok(Self { id: 0, path: path.to_byte_str()? }) + Self { id: 0, path: path.as_sftp_path() } } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } diff --git a/yazi-sftp/src/requests/symlink.rs b/yazi-sftp/src/requests/symlink.rs index 4f88ce2a..7dffd335 100644 --- a/yazi-sftp/src/requests/symlink.rs +++ b/yazi-sftp/src/requests/symlink.rs @@ -1,21 +1,21 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, Error, ToByteStr}; +use crate::{AsSftpPath, SftpPath}; #[derive(Debug, Deserialize, Serialize)] pub struct Symlink<'a> { pub id: u32, - pub link: ByteStr<'a>, - pub original: ByteStr<'a>, + pub link: SftpPath<'a>, + pub original: SftpPath<'a>, } impl<'a> Symlink<'a> { - pub fn new(link: L, original: O) -> Result + pub fn new(link: L, original: O) -> Self where - L: ToByteStr<'a>, - O: ToByteStr<'a>, + L: AsSftpPath<'a>, + O: AsSftpPath<'a>, { - Ok(Self { id: 0, link: link.to_byte_str()?, original: original.to_byte_str()? }) + Self { id: 0, link: link.as_sftp_path(), original: original.as_sftp_path() } } pub fn len(&self) -> usize { diff --git a/yazi-sftp/src/responses/extended.rs b/yazi-sftp/src/responses/extended.rs index 8be4fafc..6f772b14 100644 --- a/yazi-sftp/src/responses/extended.rs +++ b/yazi-sftp/src/responses/extended.rs @@ -30,7 +30,7 @@ impl Serialize for ExtendedData<'_> { S: serde::Serializer, { let mut seq = serializer.serialize_seq(None)?; - for b in self.0.as_ref() { + for b in &*self.0 { seq.serialize_element(b)?; } seq.end() diff --git a/yazi-sftp/src/responses/name.rs b/yazi-sftp/src/responses/name.rs index 6126cd22..6982e02d 100644 --- a/yazi-sftp/src/responses/name.rs +++ b/yazi-sftp/src/responses/name.rs @@ -1,6 +1,8 @@ +use std::borrow::Cow; + use serde::{Deserialize, Serialize}; -use crate::{ByteStr, fs::Attrs}; +use crate::fs::Attrs; #[derive(Debug, Default, Deserialize, Serialize)] pub struct Name<'a> { @@ -10,8 +12,8 @@ pub struct Name<'a> { #[derive(Debug, Default, Deserialize, Serialize)] pub struct NameItem<'a> { - pub name: ByteStr<'a>, - pub long_name: ByteStr<'a>, + pub name: Cow<'a, [u8]>, + pub long_name: Cow<'a, [u8]>, pub attrs: Attrs, } diff --git a/yazi-sftp/src/wtf.rs b/yazi-sftp/src/wtf.rs deleted file mode 100644 index bc097032..00000000 --- a/yazi-sftp/src/wtf.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}, os::windows::ffi::{OsStrExt, OsStringExt}}; - -pub(super) fn bytes_to_wide(mut bytes: &[u8]) -> Cow<'_, OsStr> { - let mut wide: Option> = None; - while !bytes.is_empty() { - match (str::from_utf8(bytes), &mut wide) { - (Ok(valid), None) => { - return OsStr::new(valid).into(); - } - (Ok(valid), Some(wide)) => { - for ch in valid.chars() { - wide.extend(ch.encode_utf16(&mut [0u16; 2]).iter()); - } - break; - } - (Err(err), _) => { - let wide = wide.get_or_insert_with(|| Vec::with_capacity(bytes.len())); - - let valid = unsafe { str::from_utf8_unchecked(&bytes[..err.valid_up_to()]) }; - for c in valid.chars() { - wide.extend(c.encode_utf16(&mut [0u16; 2]).iter()); - } - bytes = &bytes[valid.len()..]; - - let invalid = err.error_len().unwrap_or(bytes.len()); - for &b in &bytes[..invalid] { - wide.push(0xdc00 + b as u16); - } - bytes = &bytes[invalid..]; - } - } - } - OsString::from_wide(&wide.unwrap_or_default()).into() -} - -pub(super) fn wide_to_bytes(wide: &OsStr) -> Option> { - if let Some(s) = wide.to_str() { - return Some(s.as_bytes().into()); - } - - let mut it = wide.encode_wide(); - let mut out = Vec::with_capacity(wide.len()); - - while let Some(w) = it.next() { - if (0xdc00..=0xdcff).contains(&w) { - out.push((w - 0xdc00) as u8); - } else if (0xd800..=0xdbff).contains(&w) { - let x = it.next().filter(|x| (0xdc00..=0xdfff).contains(x))?; - let c = char::from_u32(0x10000 + (((w as u32 - 0xd800) << 10) | (x as u32 - 0xdc00)))?; - out.extend_from_slice(c.encode_utf8(&mut [0u8; 4]).as_bytes()); - } else if (0xdc00..=0xdfff).contains(&w) { - return None; - } else { - let c = char::from_u32(w as u32)?; - out.extend_from_slice(c.encode_utf8(&mut [0u8; 4]).as_bytes()); - } - } - - Some(out.into()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn wtf8_roundtrip() { - let b = &[ - b'\0', // NUL - 0xff, // 0xFF - b'a', b'b', b'c', // abc - 0xf0, 0x9f, 0x98, 0x80, // 😀 - 0xc3, 0x28, // illegal UTF-8 - ]; - assert_eq!(&*wide_to_bytes(&bytes_to_wide(b)).unwrap(), b); - } - - #[test] - #[cfg(windows)] - fn low_surrogates_for_non_utf8() { - use std::os::windows::ffi::OsStrExt; - - let os = bytes_to_wide(b"\xFF"); - let wide: Vec = os.encode_wide().collect(); - assert_eq!(wide, vec![0xdc00 + 0xff]); - } -} diff --git a/yazi-shared/src/loc/able.rs b/yazi-shared/src/loc/able.rs index 4b5b2cd2..2421fdec 100644 --- a/yazi-shared/src/loc/able.rs +++ b/yazi-shared/src/loc/able.rs @@ -18,6 +18,12 @@ impl<'p> LocAble<'p> for &'p std::path::Path { type Strand<'a> = &'a OsStr; } +impl<'p> LocAble<'p> for &'p typed_path::UnixPath { + type Components<'a> = typed_path::UnixComponents<'a>; + type Owned = typed_path::UnixPathBuf; + type Strand<'a> = &'a [u8]; +} + // --- LocBufAble pub trait LocBufAble where @@ -36,11 +42,18 @@ impl LocBufAble for std::path::PathBuf { type Strand<'a> = &'a OsStr; } +impl LocBufAble for typed_path::UnixPathBuf { + type Borrowed<'a> = &'a typed_path::UnixPath; + type Strand<'a> = &'a [u8]; +} + // --- StrandAble pub trait StrandAble<'a>: Copy {} impl<'a> StrandAble<'a> for &'a OsStr {} +impl<'a> StrandAble<'a> for &'a [u8] {} + // --- LocAbleImpl pub(super) trait LocAbleImpl<'p>: LocAble<'p> { fn as_encoded_bytes(self) -> &'p [u8]; @@ -96,6 +109,36 @@ impl<'p> LocAbleImpl<'p> for &'p std::path::Path { fn to_path_buf(self) -> Self::Owned { self.to_path_buf() } } +impl<'p> LocAbleImpl<'p> for &'p typed_path::UnixPath { + fn as_encoded_bytes(self) -> &'p [u8] { self.as_bytes() } + + fn components(self) -> Self::Components<'p> { self.components() } + + fn empty() -> Self { typed_path::UnixPath::new("") } + + fn file_name(self) -> Option> { self.file_name() } + + unsafe fn from_encoded_bytes_unchecked(bytes: &'p [u8]) -> Self { + typed_path::UnixPath::new(bytes) + } + + fn join<'a, T>(self, path: T) -> Self::Owned + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + self.join(path.as_strand_view()) + } + + fn strip_prefix<'a, T>(self, base: T) -> Option + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + self.strip_prefix(base.as_strand_view()).ok() + } + + fn to_path_buf(self) -> Self::Owned { self.to_path_buf() } +} + // --- LocBufAbleImpl pub(super) trait LocBufAbleImpl: LocBufAble { fn as_encoded_bytes(&self) -> &[u8] { self.borrow().as_encoded_bytes() } @@ -130,6 +173,21 @@ impl LocBufAbleImpl for std::path::PathBuf { } } +impl LocBufAbleImpl for typed_path::UnixPathBuf { + fn borrow(&self) -> Self::Borrowed<'_> { self.as_path() } + + unsafe fn from_encoded_bytes_unchecked(bytes: Vec) -> Self { bytes.into() } + + fn into_encoded_bytes(self) -> Vec { self.into_vec() } + + fn set_file_name<'a, T>(&mut self, name: T) + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + self.set_file_name(name.as_strand_view()) + } +} + // --- StrandAbleImpl pub(super) trait StrandAbleImpl<'a>: StrandAble<'a> { fn as_encoded_bytes(self) -> &'a [u8]; @@ -140,3 +198,7 @@ pub(super) trait StrandAbleImpl<'a>: StrandAble<'a> { impl<'a> StrandAbleImpl<'a> for &'a OsStr { fn as_encoded_bytes(self) -> &'a [u8] { self.as_encoded_bytes() } } + +impl<'a> StrandAbleImpl<'a> for &'a [u8] { + fn as_encoded_bytes(self) -> &'a [u8] { self } +} diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index 171f9247..029866a2 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -1,11 +1,11 @@ -use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, marker::PhantomData, mem, ops::Deref, path::PathBuf}; +use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, marker::PhantomData, mem, ops::Deref}; use anyhow::Result; use crate::{loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{AsPath, AsPathView, PathDyn, SetNameError}, scheme::SchemeKind, strand::AsStrandView}; #[derive(Clone, Default, Eq, PartialEq)] -pub struct LocBuf

{ +pub struct LocBuf

{ pub(super) inner: P, pub(super) uri: usize, pub(super) urn: usize, @@ -21,7 +21,7 @@ where } // FIXME: remove -impl AsRef for LocBuf { +impl AsRef for LocBuf { fn as_ref(&self) -> &std::path::Path { self.inner.as_ref() } } @@ -93,8 +93,8 @@ where } } -impl> From<&T> for LocBuf { - fn from(value: &T) -> Self { Self::from(PathBuf::from(value)) } +impl> From<&T> for LocBuf { + fn from(value: &T) -> Self { Self::from(std::path::PathBuf::from(value)) } } impl

LocBuf

@@ -249,13 +249,13 @@ where pub fn has_trail(&self) -> bool { self.as_loc().has_trail() } } -impl LocBuf { - pub const fn empty() -> Self { Self { inner: PathBuf::new(), uri: 0, urn: 0 } } +impl LocBuf { + pub const fn empty() -> Self { Self { inner: std::path::PathBuf::new(), uri: 0, urn: 0 } } } #[cfg(test)] mod tests { - use std::path::Path; + use std::path::{Path, PathBuf}; use super::*; use crate::url::{UrlBuf, UrlLike}; diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 182284d7..6d9c27c6 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -1,4 +1,4 @@ -use std::{hash::{Hash, Hasher}, marker::PhantomData, ops::Deref, path::Path}; +use std::{hash::{Hash, Hasher}, marker::PhantomData, ops::Deref}; use anyhow::{Result, bail}; @@ -6,7 +6,7 @@ use super::LocAbleImpl; use crate::{loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{AsPath, AsPathView, PathDyn}, scheme::SchemeKind, strand::AsStrandView}; #[derive(Clone, Copy, Debug)] -pub struct Loc<'p, P = &'p Path> { +pub struct Loc<'p, P = &'p std::path::Path> { pub(super) inner: P, pub(super) uri: usize, pub(super) urn: usize, diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 9d42ca69..4f282cf8 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -15,6 +15,10 @@ impl From for PathBufDyn { fn from(value: std::path::PathBuf) -> Self { Self::Os(value) } } +impl From for PathBufDyn { + fn from(value: typed_path::UnixPathBuf) -> Self { Self::Unix(value) } +} + impl From> for PathBufDyn { fn from(value: PathDyn<'_>) -> Self { value.to_owned() } } @@ -25,6 +29,12 @@ impl TryFrom for std::path::PathBuf { fn try_from(value: PathBufDyn) -> Result { value.into_os() } } +impl TryFrom for typed_path::UnixPathBuf { + type Error = PathDynError; + + fn try_from(value: PathBufDyn) -> Result { value.into_unix() } +} + impl PartialEq> for PathBufDyn { fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } } @@ -76,6 +86,14 @@ impl PathBufDyn { }) } + #[inline] + pub fn into_unix(self) -> Result { + Ok(match self { + Self::Os(_) => Err(PathDynError::AsUnix)?, + Self::Unix(p) => p, + }) + } + #[inline] pub fn new(kind: PathKind) -> Self { match kind { diff --git a/yazi-shared/src/path/conversion.rs b/yazi-shared/src/path/conversion.rs index 9dbccb4d..5478b1ff 100644 --- a/yazi-shared/src/path/conversion.rs +++ b/yazi-shared/src/path/conversion.rs @@ -14,6 +14,14 @@ impl AsPath for std::path::PathBuf { fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) } } +impl AsPath for typed_path::UnixPath { + fn as_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } +} + +impl AsPath for typed_path::UnixPathBuf { + fn as_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) } +} + impl AsPath for PathDyn<'_> { fn as_path(&self) -> PathDyn<'_> { *self } } diff --git a/yazi-shared/src/path/kind.rs b/yazi-shared/src/path/kind.rs index a0dd82ba..c66781cd 100644 --- a/yazi-shared/src/path/kind.rs +++ b/yazi-shared/src/path/kind.rs @@ -11,7 +11,7 @@ impl From for PathKind { SchemeKind::Regular => Self::Os, SchemeKind::Search => Self::Os, SchemeKind::Archive => Self::Os, - SchemeKind::Sftp => Self::Os, // FIXME + SchemeKind::Sftp => Self::Unix, } } } diff --git a/yazi-shared/src/path/like.rs b/yazi-shared/src/path/like.rs index 0c8ca02c..bf50a271 100644 --- a/yazi-shared/src/path/like.rs +++ b/yazi-shared/src/path/like.rs @@ -7,6 +7,8 @@ use crate::{Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError pub trait PathLike: AsPath { fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.as_path().as_os() } + fn as_unix(&self) -> Result<&typed_path::UnixPath, PathDynError> { self.as_path().as_unix() } + fn components(&self) -> Components<'_> { self.as_path().components() } fn display(&self) -> Display<'_> { self.as_path().display() } diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index eb895296..6ed324d7 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -58,6 +58,14 @@ impl<'p> PathDyn<'p> { } } + #[inline] + pub fn as_unix(self) -> Result<&'p typed_path::UnixPath, PathDynError> { + match self { + Self::Os(_) => Err(PathDynError::AsUnix), + Self::Unix(p) => Ok(p), + } + } + pub fn components(self) -> Components<'p> { match self { Self::Os(p) => Components::Os(p.components()), diff --git a/yazi-shared/src/path/view.rs b/yazi-shared/src/path/view.rs index 601682a5..31a2fdf0 100644 --- a/yazi-shared/src/path/view.rs +++ b/yazi-shared/src/path/view.rs @@ -10,3 +10,11 @@ impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { fn as_path_view(self) -> &'a std::path::Path { self } } + +impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPath { + fn as_path_view(self) -> &'a typed_path::UnixPath { self } +} + +impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPathBuf { + fn as_path_view(self) -> &'a typed_path::UnixPath { self } +} diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs index 35258cfa..b527c5c7 100644 --- a/yazi-shared/src/strand/conversion.rs +++ b/yazi-shared/src/strand/conversion.rs @@ -11,6 +11,10 @@ impl AsStrand for [u8] { fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } } +impl AsStrand for &[u8] { + fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } +} + impl AsStrand for str { fn as_strand(&self) -> Strand<'_> { Strand::Utf8(self) } } @@ -43,6 +47,10 @@ impl AsStrand for &std::path::Path { fn as_strand(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } } +impl AsStrand for &typed_path::UnixPath { + fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self.as_bytes()) } +} + impl AsStrand for crate::path::Components<'_> { fn as_strand(&self) -> Strand<'_> { self.strand() } } diff --git a/yazi-shared/src/strand/cow.rs b/yazi-shared/src/strand/cow.rs index 74d6b906..4a2de3fe 100644 --- a/yazi-shared/src/strand/cow.rs +++ b/yazi-shared/src/strand/cow.rs @@ -34,6 +34,13 @@ impl From for StrandCow<'_> { fn from(value: StrandBuf) -> Self { Self::Owned(value) } } +impl<'a, T> From<&'a T> for StrandCow<'a> +where + T: ?Sized + AsStrand, +{ + fn from(value: &'a T) -> Self { Self::Borrowed(value.as_strand()) } +} + impl PartialEq> for StrandCow<'_> { fn eq(&self, other: &Strand) -> bool { self.as_strand() == *other } } diff --git a/yazi-shared/src/strand/kind.rs b/yazi-shared/src/strand/kind.rs index e1034715..103afc20 100644 --- a/yazi-shared/src/strand/kind.rs +++ b/yazi-shared/src/strand/kind.rs @@ -22,7 +22,7 @@ impl From for StrandKind { SchemeKind::Regular => Self::Os, SchemeKind::Search => Self::Os, SchemeKind::Archive => Self::Os, - SchemeKind::Sftp => Self::Os, // FIXME + SchemeKind::Sftp => Self::Bytes, } } } diff --git a/yazi-shared/src/strand/view.rs b/yazi-shared/src/strand/view.rs index 3a3e7a35..9303486a 100644 --- a/yazi-shared/src/strand/view.rs +++ b/yazi-shared/src/strand/view.rs @@ -16,3 +16,15 @@ impl<'a> AsStrandView<'a, &'a OsStr> for &'a std::path::Path { impl<'a> AsStrandView<'a, &'a OsStr> for std::path::Components<'a> { fn as_strand_view(self) -> &'a OsStr { self.as_path().as_os_str() } } + +impl<'a> AsStrandView<'a, &'a [u8]> for &'a [u8] { + fn as_strand_view(self) -> &'a [u8] { self } +} + +impl<'a> AsStrandView<'a, &'a [u8]> for &'a typed_path::UnixPath { + fn as_strand_view(self) -> &'a [u8] { self.as_bytes() } +} + +impl<'a> AsStrandView<'a, &'a [u8]> for typed_path::UnixComponents<'a> { + fn as_strand_view(self) -> &'a [u8] { self.as_path::().as_bytes() } +} diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 9986ab46..4811d866 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -10,7 +10,7 @@ pub enum UrlBuf { Regular(LocBuf), Search { loc: LocBuf, domain: Symbol }, Archive { loc: LocBuf, domain: Symbol }, - Sftp { loc: LocBuf, domain: Symbol }, + Sftp { loc: LocBuf, domain: Symbol }, } // FIXME: remove @@ -128,7 +128,7 @@ impl UrlBuf { Self::Regular(loc) => loc.try_set_name(name.as_os()?)?, Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?, Self::Archive { loc, .. } => loc.try_set_name(name.as_os()?)?, - Self::Sftp { loc, .. } => loc.try_set_name(name.as_os()?)?, + Self::Sftp { loc, .. } => loc.try_set_name(name.encoded_bytes())?, }) } @@ -141,7 +141,10 @@ impl UrlBuf { Self::Archive { loc, domain } => { Self::Archive { loc: loc.rebase(base), domain: domain.clone() } } - Self::Sftp { loc, domain } => Self::Sftp { loc: loc.rebase(base), domain: domain.clone() }, + Self::Sftp { loc, domain } => { + todo!(); + // Self::Sftp { loc: loc.rebase(base), domain: domain.clone() } + } } } } diff --git a/yazi-shared/src/url/components.rs b/yazi-shared/src/url/components.rs index 987bbf53..3c164c56 100644 --- a/yazi-shared/src/url/components.rs +++ b/yazi-shared/src/url/components.rs @@ -87,7 +87,7 @@ impl<'a> Components<'a> { Url::Archive { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), domain } } Url::Sftp { domain, .. } => { - Url::Sftp { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), domain } + Url::Sftp { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), domain } } } } diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index 99fbadad..2abf6bf6 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -10,12 +10,12 @@ pub enum UrlCow<'a> { Regular(LocBuf), Search { loc: LocBuf, domain: SymbolCow<'a, str> }, Archive { loc: LocBuf, domain: SymbolCow<'a, str> }, - Sftp { loc: LocBuf, domain: SymbolCow<'a, str> }, + Sftp { loc: LocBuf, domain: SymbolCow<'a, str> }, RegularRef(Loc<'a>), SearchRef { loc: Loc<'a>, domain: SymbolCow<'a, str> }, ArchiveRef { loc: Loc<'a>, domain: SymbolCow<'a, str> }, - SftpRef { loc: Loc<'a>, domain: SymbolCow<'a, str> }, + SftpRef { loc: Loc<'a, &'a typed_path::UnixPath>, domain: SymbolCow<'a, str> }, } // FIXME: remove @@ -148,7 +148,7 @@ impl<'a> TryFrom<(SchemeCow<'a>, PathDyn<'a>)> for UrlCow<'a> { domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?, }, SchemeKind::Sftp => Self::SftpRef { - loc: Loc::with(path.as_os()?, uri, urn)?, + loc: Loc::with(path.as_unix()?, uri, urn)?, domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?, }, }) @@ -165,15 +165,15 @@ impl<'a> TryFrom<(SchemeCow<'a>, PathBufDyn)> for UrlCow<'a> { Ok(match kind { SchemeKind::Regular => Self::Regular(path.into_os()?.into()), SchemeKind::Search => Self::Search { - loc: LocBuf::::with(path.try_into()?, uri, urn)?, + loc: LocBuf::::with(path.try_into()?, uri, urn)?, domain: domain.ok_or_else(|| anyhow!("missing domain for search scheme"))?, }, SchemeKind::Archive => Self::Archive { - loc: LocBuf::::with(path.try_into()?, uri, urn)?, + loc: LocBuf::::with(path.try_into()?, uri, urn)?, domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?, }, SchemeKind::Sftp => Self::Sftp { - loc: LocBuf::::with(path.try_into()?, uri, urn)?, + loc: LocBuf::::with(path.try_into()?, uri, urn)?, domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?, }, }) diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 86795dbf..bd971a85 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -12,7 +12,7 @@ pub enum Url<'a> { Regular(Loc<'a>), Search { loc: Loc<'a>, domain: &'a str }, Archive { loc: Loc<'a>, domain: &'a str }, - Sftp { loc: Loc<'a>, domain: &'a str }, + Sftp { loc: Loc<'a, &'a typed_path::UnixPath>, domain: &'a str }, } // --- Eq @@ -253,7 +253,7 @@ impl<'a> Url<'a> { domain: domain.intern(), }, Self::Sftp { domain, .. } => { - UrlBuf::Sftp { loc: joined.into_os()?.into(), domain: domain.intern() } + UrlBuf::Sftp { loc: joined.into_unix()?.into(), domain: domain.intern() } } }) } @@ -282,24 +282,24 @@ impl<'a> Url<'a> { domain: domain.intern(), }, Self::Archive { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Archive { - loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), domain: domain.intern(), }, Self::Sftp { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Sftp { - loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + loc: LocBuf::::new(path.into_unix()?, loc.base(), loc.trail()), domain: domain.intern(), }, Self::Search { domain, .. } => UrlBuf::Search { - loc: LocBuf::::saturated(path.into_os()?, self.kind()), + loc: LocBuf::::saturated(path.into_os()?, self.kind()), domain: domain.intern(), }, Self::Archive { domain, .. } => UrlBuf::Archive { - loc: LocBuf::::saturated(path.into_os()?, self.kind()), + loc: LocBuf::::saturated(path.into_os()?, self.kind()), domain: domain.intern(), }, Self::Sftp { domain, .. } => UrlBuf::Sftp { - loc: LocBuf::::saturated(path.into_os()?, self.kind()), + loc: LocBuf::::saturated(path.into_unix()?, self.kind()), domain: domain.intern(), }, }; diff --git a/yazi-vfs/Cargo.toml b/yazi-vfs/Cargo.toml index 3bed8acd..53c524b0 100644 --- a/yazi-vfs/Cargo.toml +++ b/yazi-vfs/Cargo.toml @@ -27,3 +27,4 @@ parking_lot = { workspace = true } russh = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +typed-path = { workspace = true } diff --git a/yazi-vfs/src/provider/sftp/metadata.rs b/yazi-vfs/src/provider/sftp/metadata.rs index 23c2ee9e..82473e94 100644 --- a/yazi-vfs/src/provider/sftp/metadata.rs +++ b/yazi-vfs/src/provider/sftp/metadata.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsStr, io, time::{Duration, UNIX_EPOCH}}; +use std::{io, time::{Duration, UNIX_EPOCH}}; use yazi_fs::cha::ChaKind; @@ -26,18 +26,17 @@ impl TryFrom<&yazi_sftp::fs::DirEntry> for Cha { type Error = io::Error; fn try_from(ent: &yazi_sftp::fs::DirEntry) -> Result { - let mut cha = Self::try_from((ent.name().as_ref(), ent.attrs()))?; + let mut cha = Self::try_from((ent.name(), ent.attrs()))?; cha.0.nlink = ent.nlink().unwrap_or_default(); Ok(cha) } } -impl TryFrom<(&OsStr, &yazi_sftp::fs::Attrs)> for Cha { +impl TryFrom<(&[u8], &yazi_sftp::fs::Attrs)> for Cha { type Error = io::Error; - fn try_from((name, attrs): (&OsStr, &yazi_sftp::fs::Attrs)) -> Result { - let kind = - if name.as_encoded_bytes().starts_with(b".") { ChaKind::HIDDEN } else { ChaKind::empty() }; + fn try_from((name, attrs): (&[u8], &yazi_sftp::fs::Attrs)) -> Result { + let kind = if name.starts_with(b".") { ChaKind::HIDDEN } else { ChaKind::empty() }; Ok(Self(yazi_fs::cha::Cha { kind, diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 531edc75..e31fbc76 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -12,7 +12,7 @@ use crate::provider::sftp::Conn; #[derive(Clone)] pub struct Sftp<'a> { url: Url<'a>, - path: &'a Path, + path: &'a typed_path::UnixPath, name: &'static str, config: &'static ProviderSftp, @@ -71,7 +71,7 @@ impl<'a> Provider for Sftp<'a> { where P: AsPath, { - let to = to.as_path().as_os()?; + let to = to.as_path().as_unix()?; let attrs = Attrs::from(super::Attrs(attrs)); let op = self.op().await?; @@ -96,7 +96,7 @@ impl<'a> Provider for Sftp<'a> { where P: AsPath, { - let to = to.as_path().as_os()?; + let to = to.as_path().as_unix()?; Ok(self.op().await?.hardlink(self.path, to).await?) } @@ -137,7 +137,7 @@ impl<'a> Provider for Sftp<'a> { where P: AsPath, { - let to = to.as_path().as_os()?; + let to = to.as_path().as_unix()?; let op = self.op().await?; match op.rename_posix(self.path, &to).await { @@ -156,7 +156,7 @@ impl<'a> Provider for Sftp<'a> { P: AsPath, F: AsyncFnOnce() -> io::Result, { - let original = original.as_path().as_os()?; + let original = original.as_path().as_unix()?; Ok(self.op().await?.symlink(&original, self.path).await?) } From 3561867b99605ddba338343fbaa4e6d303e61d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Thu, 27 Nov 2025 14:22:14 +0800 Subject: [PATCH 12/53] feat: WTF-8 validator (#3374) --- Cargo.lock | 20 ++--- yazi-actor/src/mgr/refresh.rs | 10 +-- yazi-boot/Cargo.toml | 1 - yazi-fm/Cargo.toml | 2 +- yazi-fs/Cargo.toml | 1 + yazi-fs/src/path/expand.rs | 32 ++++---- yazi-fs/src/path/percent.rs | 45 ++++------- yazi-fs/src/url.rs | 17 ++-- yazi-plugin/src/process/command.rs | 2 +- yazi-shared/src/lib.rs | 4 +- yazi-shared/src/path/buf.rs | 28 ++++--- yazi-shared/src/path/component.rs | 9 +++ yazi-shared/src/path/error.rs | 19 ++++- yazi-shared/src/path/path.rs | 52 +++++------- yazi-shared/src/shell/windows.rs | 2 +- yazi-shared/src/strand/buf.rs | 2 +- yazi-shared/src/strand/conversion.rs | 8 ++ yazi-shared/src/strand/strand.rs | 2 +- yazi-shared/src/url/buf.rs | 13 ++- yazi-shared/src/url/components.rs | 8 +- yazi-shared/src/wtf8/mod.rs | 1 + yazi-shared/src/wtf8/validator.rs | 116 +++++++++++++++++++++++++++ yazi-shared/src/{ => wtf8}/wtf8.rs | 14 +++- yazi-vfs/src/provider/sftp/sftp.rs | 2 +- yazi-watcher/src/reporter.rs | 26 +++--- yazi-watcher/src/watched.rs | 38 ++++----- 26 files changed, 308 insertions(+), 166 deletions(-) create mode 100644 yazi-shared/src/wtf8/mod.rs create mode 100644 yazi-shared/src/wtf8/validator.rs rename yazi-shared/src/{ => wtf8}/wtf8.rs (78%) diff --git a/Cargo.lock b/Cargo.lock index ec94c648..6ce50e62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3876,21 +3876,21 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3899,9 +3899,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -4615,9 +4615,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] name = "winsafe" @@ -4732,7 +4732,6 @@ dependencies = [ "futures", "hashbrown 0.16.1", "regex", - "serde", "vergen-gitcl", "yazi-adapter", "yazi-config", @@ -4933,6 +4932,7 @@ dependencies = [ "tokio", "tracing", "trash", + "typed-path", "uzers", "windows-sys 0.61.2", "yazi-ffi", diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index b1580cda..b174d9a8 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -48,12 +48,12 @@ impl Refresh { // TODO: performance improvement fn trigger_dirs(folders: &[&Folder]) { - async fn go(cwd: UrlBuf, cha: Cha) { - let Some(cha) = Files::assert_stale(&cwd, cha).await else { return }; + async fn go(dir: UrlBuf, cha: Cha) { + let Some(cha) = Files::assert_stale(&dir, cha).await else { return }; - match Files::from_dir_bulk(&cwd).await { - Ok(files) => FilesOp::Full(cwd, files, cha).emit(), - Err(e) => FilesOp::issue_error(&cwd, e).await, + match Files::from_dir_bulk(&dir).await { + Ok(files) => FilesOp::Full(dir, files, cha).emit(), + Err(e) => FilesOp::issue_error(&dir, e).await, } } diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 724e99a3..1130dcc7 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -21,7 +21,6 @@ clap = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } regex = { workspace = true } -serde = { workspace = true } [build-dependencies] yazi-shared = { path = "../yazi-shared", version = "25.9.15" } diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 177a063b..d5a7cb36 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -59,7 +59,7 @@ tokio-stream = { workspace = true } # Logging tracing = { workspace = true } -tracing-appender = "0.2.3" +tracing-appender = "0.2.4" tracing-subscriber = { version = "0.3.20", features = [ "env-filter" ] } [target."cfg(unix)".dependencies] diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index 2fb51961..00633926 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -32,6 +32,7 @@ scopeguard = { workspace = true } serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +typed-path = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index cec4b1fd..200f1665 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, path::PathBuf}; -use yazi_shared::{FromWtf8Vec, loc::LocBuf, path::{PathBufDyn, PathCow, PathDyn, PathKind, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{loc::LocBuf, path::{PathBufDyn, PathCow, PathDyn, PathKind, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}, wtf8::FromWtf8Vec}; use crate::{CWD, path::clean_url}; @@ -25,21 +25,25 @@ fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> { let mut path = PathBufDyn::with_capacity(url.kind(), n_base.len() + n_rest.len() + n_urn.len()); path.try_extend([n_base, n_rest, n_urn]).expect("extend original parts should not fail"); - let loc = LocBuf::::with( - path.into_os().expect("Failed to convert PathBufDyn to PathBuf"), - (uri_count + rest_diff + urn_diff) as usize, - (urn_count + urn_diff) as usize, - ) - .expect("Failed to create Loc from expanded path"); + let uri = (uri_count + rest_diff + urn_diff) as usize; + let urn = (urn_count + urn_diff) as usize; let expanded = match url { - Url::Regular(_) => UrlBuf::Regular(loc), - Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }, - Url::Archive { domain, .. } => UrlBuf::Archive { loc, domain: domain.intern() }, - Url::Sftp { domain, .. } => { - todo!(); - // UrlBuf::Sftp { loc, domain: domain.intern() } - } + Url::Regular(_) => UrlBuf::Regular( + LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + ), + Url::Search { domain, .. } => UrlBuf::Search { + loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + domain: domain.intern(), + }, + Url::Archive { domain, .. } => UrlBuf::Archive { + loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + domain: domain.intern(), + }, + Url::Sftp { domain, .. } => UrlBuf::Sftp { + loc: LocBuf::::with(path.into_unix().unwrap(), uri, urn).unwrap(), + domain: domain.intern(), + }, }; absolute_url(expanded) diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs index 6728d086..0656a347 100644 --- a/yazi-fs/src/path/percent.rs +++ b/yazi-fs/src/path/percent.rs @@ -1,45 +1,32 @@ use std::{borrow::Cow, path::{Path, PathBuf}}; +use anyhow::Result; use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; -use yazi_shared::path::PathDyn; +use yazi_shared::path::{PathCow, PathDyn, PathKind}; const SET: &AsciiSet = &CONTROLS.add(b'"').add(b'*').add(b':').add(b'<').add(b'>').add(b'?').add(b'\\').add(b'|'); -pub trait PercentEncoding { - fn percent_encode(&self) -> Cow<'_, Path>; +pub trait PercentEncoding<'a> { + fn percent_encode(self) -> Cow<'a, Path>; - fn percent_decode(&self) -> Cow<'_, [u8]>; + fn percent_decode(self, kind: K) -> Result> + where + K: Into; } -impl PercentEncoding for Path { - fn percent_encode(&self) -> Cow<'_, Path> { - match percent_encode(self.as_os_str().as_encoded_bytes(), SET).into() { - Cow::Borrowed(_) => self.into(), +impl<'a> PercentEncoding<'a> for PathDyn<'a> { + fn percent_encode(self) -> Cow<'a, Path> { + match percent_encode(self.encoded_bytes(), SET).into() { + Cow::Borrowed(s) => Path::new(s).into(), Cow::Owned(s) => PathBuf::from(s).into(), } } - fn percent_decode(&self) -> Cow<'_, [u8]> { - match percent_decode(self.as_os_str().as_encoded_bytes()).into() { - Cow::Borrowed(_) => self.as_os_str().as_encoded_bytes().into(), - Cow::Owned(s) => s.into(), - } - } -} - -impl PercentEncoding for PathDyn<'_> { - fn percent_encode(&self) -> Cow<'_, Path> { - match self { - PathDyn::Os(p) => p.percent_encode(), - PathDyn::Unix(_) => todo!(), - } - } - - fn percent_decode(&self) -> Cow<'_, [u8]> { - match self { - PathDyn::Os(p) => p.percent_decode(), - PathDyn::Unix(_) => todo!(), - } + fn percent_decode(self, kind: K) -> Result> + where + K: Into, + { + PathCow::with(kind, percent_decode(self.encoded_bytes())) } } diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index b8ff40da..ab23e9fc 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,8 +1,8 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; -use yazi_shared::{path::PathDyn, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::{AsPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlCow}}; -use crate::{FsHash128, FsScheme}; +use crate::{FsHash128, FsScheme, path::PercentEncoding}; pub trait FsUrl<'a> { fn cache(&self) -> Option; @@ -26,13 +26,12 @@ impl<'a> FsUrl<'a> for Url<'a> { fn cache(&self) -> Option { fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { let mut it = loc.components(); - todo!(); - // if it.next() == Some(std::path::Component::RootDir) { - // root.push(it.as_path().percent_encode()); - // } else { - // root.push(".%2F"); - // root.push(loc.percent_encode()); - // } + if it.next() == Some(yazi_shared::path::Component::RootDir) { + root.push(it.as_path().percent_encode()); + } else { + root.push(".%2F"); + root.push(loc.percent_encode()); + } root } diff --git a/yazi-plugin/src/process/command.rs b/yazi-plugin/src/process/command.rs index 72a41303..93fb9a48 100644 --- a/yazi-plugin/src/process/command.rs +++ b/yazi-plugin/src/process/command.rs @@ -3,7 +3,7 @@ use std::{any::TypeId, ffi::OsStr, io, process::Stdio}; use mlua::{AnyUserData, ExternalError, IntoLuaMulti, Lua, MetaMethod, Table, UserData, Value}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; use yazi_binding::Error; -use yazi_shared::FromWtf8; +use yazi_shared::wtf8::FromWtf8; use super::{Child, output::Output}; use crate::process::Status; diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index aff60ffa..6c3406f6 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -1,8 +1,8 @@ #![allow(clippy::option_map_unit_fn)] -yazi_macro::mod_pub!(data errors event loc path pool scheme shell strand translit url); +yazi_macro::mod_pub!(data errors event loc path pool scheme shell strand translit url wtf8); -yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os predictor ro_cell source sync_cell terminal tests throttle time utf8 wtf8); +yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os predictor ro_cell source sync_cell terminal tests throttle time utf8); pub fn init() { pool::init(); diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 4f282cf8..807c65a9 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -1,11 +1,11 @@ -use std::ffi::{OsStr, OsString}; +use std::{ffi::OsString, hash::{Hash, Hasher}}; use hashbrown::Equivalent; -use crate::{FromWtf8, FromWtf8Vec, path::{AsPath, Component, PathDyn, PathDynError, PathKind, SetNameError}, strand::{AsStrand, Strand}}; +use crate::{path::{AsPath, Component, PathDyn, PathDynError, PathKind, SetNameError}, strand::AsStrand, wtf8::FromWtf8Vec}; // --- PathBufDyn -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Eq)] pub enum PathBufDyn { Os(std::path::PathBuf), Unix(typed_path::UnixPathBuf), @@ -35,6 +35,16 @@ impl TryFrom for typed_path::UnixPathBuf { fn try_from(value: PathBufDyn) -> Result { value.into_unix() } } +// --- Hash +impl Hash for PathBufDyn { + fn hash(&self, state: &mut H) { self.as_path().hash(state) } +} + +// --- PartialEq +impl PartialEq for PathBufDyn { + fn eq(&self, other: &PathBufDyn) -> bool { self.as_path() == other.as_path() } +} + impl PartialEq> for PathBufDyn { fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } } @@ -128,14 +138,10 @@ impl PathBufDyn { where T: AsStrand, { - Ok(match (self, name.as_strand()) { - (Self::Os(p), Strand::Os(s)) => p.set_file_name(s), - (Self::Os(p), Strand::Utf8(s)) => p.set_file_name(s), - (Self::Os(p), Strand::Bytes(b)) => { - p.set_file_name(OsStr::from_wtf8(b).map_err(|_| SetNameError::FromWtf8)?) - } - - (Self::Unix(p), s) => p.set_file_name(s.encoded_bytes()), + let s = name.as_strand(); + Ok(match self { + Self::Os(p) => p.set_file_name(s.as_os()?), + Self::Unix(p) => p.set_file_name(s.encoded_bytes()), }) } diff --git a/yazi-shared/src/path/component.rs b/yazi-shared/src/path/component.rs index 98b73bdb..ec60377b 100644 --- a/yazi-shared/src/path/component.rs +++ b/yazi-shared/src/path/component.rs @@ -11,6 +11,15 @@ pub enum Component<'a> { Normal(Strand<'a>), } +impl<'a> Component<'a> { + pub fn as_normal(&self) -> Option> { + match self { + Self::Normal(s) => Some(*s), + _ => None, + } + } +} + impl<'a> From> for Component<'a> { fn from(value: std::path::Component<'a>) -> Self { match value { diff --git a/yazi-shared/src/path/error.rs b/yazi-shared/src/path/error.rs index 772d7250..175ec67a 100644 --- a/yazi-shared/src/path/error.rs +++ b/yazi-shared/src/path/error.rs @@ -5,13 +5,15 @@ use crate::strand::StrandError; // --- EndsWithError #[derive(Debug, Error)] #[error("calling ends_with on paths with different encodings")] -pub struct EndsWithError; +pub enum EndsWithError { + FromStrand(#[from] StrandError), +} // --- JoinError #[derive(Debug, Error)] #[error("calling join on paths with different encodings")] pub enum JoinError { - FromWtf8, + FromStrand(#[from] StrandError), FromPathDyn(#[from] PathDynError), } @@ -47,7 +49,6 @@ impl From for std::io::Error { #[derive(Debug, Error)] #[error("calling set_name on paths with different encodings")] pub enum SetNameError { - FromWtf8, FromStrand(#[from] StrandError), } @@ -79,7 +80,9 @@ impl From for RsplitOnceError { // --- StartsWithError #[derive(Error, Debug)] #[error("calling starts_with on paths with different encodings")] -pub struct StartsWithError; +pub enum StartsWithError { + FromStrand(#[from] StrandError), +} // --- StripPrefixError #[derive(Debug, Error)] @@ -92,6 +95,14 @@ pub enum StripPrefixError { WrongEncoding, } +impl From for StripPrefixError { + fn from(err: StrandError) -> Self { + match err { + StrandError::AsOs | StrandError::AsUtf8 => Self::WrongEncoding, + } + } +} + impl From for StripPrefixError { fn from(_: std::path::StripPrefixError) -> Self { Self::NotPrefix } } diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index 6ed324d7..01c3cca6 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -4,9 +4,9 @@ use anyhow::Result; use hashbrown::Equivalent; use super::{RsplitOnceError, StartsWithError}; -use crate::{BytesExt, FromWtf8, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError}, strand::{AsStrand, Strand, StrandError}}; +use crate::{BytesExt, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError}, strand::{AsStrand, Strand, StrandError}}; -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum PathDyn<'p> { Os(&'p std::path::Path), Unix(&'p typed_path::UnixPath), @@ -186,14 +186,10 @@ impl<'p> PathDyn<'p> { where T: AsStrand, { - Ok(match (self, child.as_strand()) { - (Self::Os(p), Strand::Os(q)) => p.ends_with(q), - (Self::Os(p), Strand::Utf8(q)) => p.ends_with(q), - (Self::Os(p), Strand::Bytes(b)) => { - p.ends_with(OsStr::from_wtf8(b).map_err(|_| EndsWithError)?) - } - - (Self::Unix(p), s) => p.ends_with(s.encoded_bytes()), + let s = child.as_strand(); + Ok(match self { + Self::Os(p) => p.ends_with(s.as_os()?), + Self::Unix(p) => p.ends_with(s.encoded_bytes()), }) } @@ -201,14 +197,10 @@ impl<'p> PathDyn<'p> { where T: AsStrand, { - Ok(match (self, path.as_strand()) { - (Self::Os(p), Strand::Os(q)) => PathBufDyn::Os(p.join(q)), - (Self::Os(p), Strand::Utf8(q)) => PathBufDyn::Os(p.join(q)), - (Self::Os(p), Strand::Bytes(b)) => { - PathBufDyn::Os(p.join(OsStr::from_wtf8(b).map_err(|_| JoinError::FromWtf8)?)) - } - - (Self::Unix(p), s) => PathBufDyn::Unix(p.join(s.encoded_bytes())), + let s = path.as_strand(); + Ok(match self { + Self::Os(p) => PathBufDyn::Os(p.join(s.as_os()?)), + Self::Unix(p) => PathBufDyn::Unix(p.join(s.encoded_bytes())), }) } @@ -235,14 +227,10 @@ impl<'p> PathDyn<'p> { where T: AsStrand, { - Ok(match (self, base.as_strand()) { - (Self::Os(p), Strand::Os(s)) => p.starts_with(s), - (Self::Os(p), Strand::Utf8(s)) => p.starts_with(s), - (Self::Os(p), Strand::Bytes(b)) => { - p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) - } - - (Self::Unix(p), s) => p.starts_with(s.encoded_bytes()), + let s = base.as_strand(); + Ok(match self { + Self::Os(p) => p.starts_with(s.as_os()?), + Self::Unix(p) => p.starts_with(s.encoded_bytes()), }) } @@ -250,14 +238,10 @@ impl<'p> PathDyn<'p> { where T: AsStrand, { - Ok(match (self, base.as_strand()) { - (Self::Os(p), Strand::Os(s)) => Self::Os(p.strip_prefix(s)?), - (Self::Os(p), Strand::Utf8(s)) => Self::Os(p.strip_prefix(s)?), - (Self::Os(p), Strand::Bytes(b)) => { - Self::Os(p.strip_prefix(OsStr::from_wtf8(b).map_err(|_| StripPrefixError::WrongEncoding)?)?) - } - - (Self::Unix(p), s) => Self::Unix(p.strip_prefix(s.encoded_bytes())?), + let s = base.as_strand(); + Ok(match self { + Self::Os(p) => Self::Os(p.strip_prefix(s.as_os()?)?), + Self::Unix(p) => Self::Unix(p.strip_prefix(s.encoded_bytes())?), }) } diff --git a/yazi-shared/src/shell/windows.rs b/yazi-shared/src/shell/windows.rs index 29ddc83b..ad25beee 100644 --- a/yazi-shared/src/shell/windows.rs +++ b/yazi-shared/src/shell/windows.rs @@ -40,7 +40,7 @@ pub fn escape_os_bytes(b: &[u8]) -> Cow<'_, [u8]> { #[cfg(windows)] pub fn escape_os_str(s: &std::ffi::OsStr) -> Cow<'_, std::ffi::OsStr> { - use crate::FromWtf8Vec; + use crate::wtf8::FromWtf8Vec; match escape_os_bytes(s.as_encoded_bytes()) { Cow::Borrowed(_) => Cow::Borrowed(s), diff --git a/yazi-shared/src/strand/buf.rs b/yazi-shared/src/strand/buf.rs index e5a28479..7b513bf0 100644 --- a/yazi-shared/src/strand/buf.rs +++ b/yazi-shared/src/strand/buf.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsString, hash::{Hash, Hasher}}; use anyhow::Result; -use crate::{FromWtf8Vec, path::PathDyn, strand::{AsStrand, Strand, StrandCow, StrandError, StrandKind}}; +use crate::{path::PathDyn, strand::{AsStrand, Strand, StrandCow, StrandError, StrandKind}, wtf8::FromWtf8Vec}; // --- StrandBuf #[derive(Clone, Debug, Eq)] diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs index b527c5c7..846f139c 100644 --- a/yazi-shared/src/strand/conversion.rs +++ b/yazi-shared/src/strand/conversion.rs @@ -47,6 +47,10 @@ impl AsStrand for &std::path::Path { fn as_strand(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } } +impl AsStrand for &std::path::PathBuf { + fn as_strand(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } +} + impl AsStrand for &typed_path::UnixPath { fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self.as_bytes()) } } @@ -55,6 +59,10 @@ impl AsStrand for crate::path::Components<'_> { fn as_strand(&self) -> Strand<'_> { self.strand() } } +impl AsStrand for Cow<'_, [u8]> { + fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } +} + impl AsStrand for Cow<'_, OsStr> { fn as_strand(&self) -> Strand<'_> { Strand::Os(self) } } diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs index 92dc418f..a1cc5711 100644 --- a/yazi-shared/src/strand/strand.rs +++ b/yazi-shared/src/strand/strand.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, fmt::Display}; use anyhow::Result; -use crate::{BytesExt, FromWtf8, strand::{AsStrand, StrandBuf, StrandError, StrandKind}}; +use crate::{BytesExt, strand::{AsStrand, StrandBuf, StrandError, StrandKind}, wtf8::FromWtf8}; // --- Strand #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialOrd)] diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 4811d866..fbfabae9 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -1,11 +1,11 @@ -use std::{borrow::Cow, fmt::{Debug, Formatter}, path::{Path, PathBuf}, str::FromStr}; +use std::{borrow::Cow, fmt::{Debug, Formatter}, hash::{Hash, Hasher}, path::{Path, PathBuf}, str::FromStr}; use anyhow::Result; use serde::{Deserialize, Serialize}; use crate::{loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::{Scheme, SchemeKind}, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlLike}}; -#[derive(Clone, Eq, Hash, PartialEq)] +#[derive(Clone, Eq)] pub enum UrlBuf { Regular(LocBuf), Search { loc: LocBuf, domain: Symbol }, @@ -92,6 +92,10 @@ impl From> for UrlBuf { } // --- Eq +impl PartialEq for UrlBuf { + fn eq(&self, other: &Self) -> bool { self.as_url() == other.as_url() } +} + impl PartialEq> for UrlBuf { fn eq(&self, other: &Url) -> bool { self.as_url() == *other } } @@ -100,6 +104,11 @@ impl PartialEq> for &UrlBuf { fn eq(&self, other: &Url) -> bool { self.as_url() == *other } } +// --- Hash +impl Hash for UrlBuf { + fn hash(&self, state: &mut H) { self.as_url().hash(state) } +} + impl UrlBuf { #[inline] pub fn new() -> &'static Self { diff --git a/yazi-shared/src/url/components.rs b/yazi-shared/src/url/components.rs index 3c164c56..50778f81 100644 --- a/yazi-shared/src/url/components.rs +++ b/yazi-shared/src/url/components.rs @@ -32,17 +32,17 @@ impl<'a> Components<'a> { } pub fn os_str(&self) -> Cow<'a, OsStr> { - let Ok(path) = self.inner.path().as_os() else { + let Ok(os) = self.inner.strand().as_os() else { return OsString::from(EncodeUrl(self.url()).to_string()).into(); }; if self.url.is_regular() || self.scheme_yielded { - return path.as_os_str().into(); + return os.into(); } let mut s = OsString::from(EncodeScheme(self.url()).to_string()); - s.reserve_exact(path.as_os_str().len()); - s.push(path); + s.reserve_exact(os.len()); + s.push(os); s.into() } diff --git a/yazi-shared/src/wtf8/mod.rs b/yazi-shared/src/wtf8/mod.rs new file mode 100644 index 00000000..3a7d6a45 --- /dev/null +++ b/yazi-shared/src/wtf8/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(validator wtf8); diff --git a/yazi-shared/src/wtf8/validator.rs b/yazi-shared/src/wtf8/validator.rs new file mode 100644 index 00000000..247589e2 --- /dev/null +++ b/yazi-shared/src/wtf8/validator.rs @@ -0,0 +1,116 @@ +#[cfg(windows)] +pub(super) fn valid_wtf8(bytes: &[u8]) -> bool { + let mut i = 0; + while i < bytes.len() { + let b = bytes[i]; + if b < 0b1000_0000 { + // ASCII + i += 1; + continue; + } + + // 2-byte: 110x_xxxx 10xx_xxxx + // first byte must be >= 0b1100_0010 to forbid overlongs + if (b & 0b1110_0000) == 0b1100_0000 { + if b < 0b1100_0010 { + return false; + } + if i + 1 >= bytes.len() { + return false; + } + if (bytes[i + 1] & 0b1100_0000) != 0b1000_0000 { + return false; + } + i += 2; + continue; + } + + // 3-byte: 1110_xxxx 10xx_xxxx 10xx_xxxx + if (b & 0b1111_0000) == 0b1110_0000 { + if i + 2 >= bytes.len() { + return false; + } + let (b1, b2) = (bytes[i + 1], bytes[i + 2]); + if (b1 & 0b1100_0000) != 0b1000_0000 || (b2 & 0b1100_0000) != 0b1000_0000 { + return false; + } + if b == 0b1110_0000 && b1 < 0b1010_0000 { + // to forbid overlongs, second byte must be >= 0xA0 + return false; + } + i += 3; + continue; + } + + // 4-byte: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if (b & 0b1111_1000) == 0b1111_0000 { + if b > 0b1111_0100 { + return false; + } + if i + 3 >= bytes.len() { + return false; + } + let (b1, b2, b3) = (bytes[i + 1], bytes[i + 2], bytes[i + 3]); + if (b1 & 0b1100_0000) != 0b1000_0000 + || (b2 & 0b1100_0000) != 0b1000_0000 + || (b3 & 0b1100_0000) != 0b1000_0000 + { + return false; + } + if b == 0b1111_0000 && b1 < 0b1001_0000 { + // to forbid overlongs for > U+FFFF, second byte must be >= 0x90 + return false; + } else if b == 0b1111_0100 && b1 > 0b1000_1111 { + // to stay <= U+10FFFF, second byte must be <= 0x8F + return false; + } + i += 4; + continue; + } + + return false; + } + true +} + +#[cfg(windows)] +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_wtf8() { + let cases: &[(&[u8], bool)] = &[ + // Valid ASCII + (b"hello", true), + // Valid 2-byte UTF-8 + (&[0xc2, 0xa0], true), // U+00A0 + // Invalid 2-byte: overlong encoding + (&[0xc0, 0x80], false), // overlong for U+0000 + (&[0xc1, 0xbf], false), // overlong for U+007F + // Valid 3-byte UTF-8 + (&[0xe0, 0xa0, 0x80], true), // U+0800 + // Invalid 3-byte: overlong encoding + (&[0xe0, 0x9f, 0xbf], false), // overlong for U+07FF + // WTF-8 specific: unpaired surrogates (should be valid in WTF-8) + ((&[0xed, 0xa0, 0x80]), true), // U+D800 = ED A0 80 (high surrogate) + ((&[0xed, 0xbf, 0xbf]), true), // U+DFFF = ED BF BF (low surrogate) + // Valid 4-byte UTF-8 + ((&[0xf0, 0x90, 0x80, 0x80]), true), // U+10000 + // Invalid 4-byte: overlong + ((&[0xf0, 0x8f, 0xbf, 0xbf]), false), // overlong for U+FFFF + // Invalid 4-byte: beyond U+10FFFF + ((&[0xf4, 0x90, 0x80, 0x80]), false), // U+110000 + // Invalid continuation byte + ((&[0xc2, 0x00]), false), + // Incomplete sequence + ((&[0xc2]), false), + ((&[0xe0, 0xa0]), false), + ((&[0xf0, 0x90, 0x80]), false), + ]; + + for &(input, expected) in cases { + assert_eq!(valid_wtf8(input), expected, "input: {:?}", input); + } + } +} diff --git a/yazi-shared/src/wtf8.rs b/yazi-shared/src/wtf8/wtf8.rs similarity index 78% rename from yazi-shared/src/wtf8.rs rename to yazi-shared/src/wtf8/wtf8.rs index cf785aba..5ab86e7e 100644 --- a/yazi-shared/src/wtf8.rs +++ b/yazi-shared/src/wtf8/wtf8.rs @@ -35,8 +35,11 @@ impl FromWtf8 for OsStr { } #[cfg(windows)] { - // FIXME: validate WTF-8 - Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) + if super::valid_wtf8(wtf8) { + Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) + } else { + Err(anyhow::anyhow!("Invalid WTF-8 sequence")) + } } } } @@ -61,8 +64,11 @@ impl FromWtf8Vec for OsString { } #[cfg(windows)] { - // FIXME: validate WTF-8 - Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) + if super::valid_wtf8(&wtf8) { + Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) + } else { + Err(anyhow::anyhow!("Invalid WTF-8 sequence")) + } } } } diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index e31fbc76..9cc6143b 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -1,4 +1,4 @@ -use std::{io, path::Path, sync::Arc}; +use std::{io, sync::Arc}; use tokio::io::{AsyncWriteExt, BufReader, BufWriter}; use yazi_config::vfs::{ProviderSftp, Vfs}; diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index 5b2f4ac0..abda3763 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -1,3 +1,6 @@ +use std::borrow::Cow; + +use percent_encoding::percent_decode; use tokio::sync::mpsc; use yazi_shared::{scheme::SchemeKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; @@ -24,8 +27,8 @@ impl Reporter { } } - fn report_local<'a>(&self, url: UrlCow<'a>) { - let Some((parent, name)) = url.pair() else { return }; + fn report_local(&self, url: UrlCow) { + let Some((parent, urn)) = url.pair() else { return }; // FIXME: LINKED should return Url instead of Path let linked = LINKED.read(); @@ -37,19 +40,22 @@ impl Reporter { self.local_tx.send(url.to_owned()).ok(); self.local_tx.send(parent.to_owned()).ok(); } - if name.ext().is_some_and(|e| e == "%tmp") { + + if urn.ext().is_some_and(|e| e == "%tmp") { continue; } - // SFTP caches - // todo!(); - // if let Some(dir) = watched.find_by_cache(&parent.loc()) { - // self.remote_tx.send(dir.join(name)).ok(); - // self.remote_tx.send(dir.to_owned()).ok(); - // } + + // Virtual caches + let Some(dir) = watched.find_by_cache(parent.loc()) else { continue }; + let Some(name) = url.name() else { continue }; + if let Ok(u) = dir.try_join(Cow::from(percent_decode(name.encoded_bytes()))) { + self.remote_tx.send(u).ok(); + } + self.remote_tx.send(dir).ok(); } } - fn report_remote<'a>(&self, url: UrlCow<'a>) { + fn report_remote(&self, url: UrlCow) { let Some(parent) = url.parent() else { return }; if !WATCHED.read().contains(parent) { return; diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index bfe322c6..cdb69a34 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -3,7 +3,7 @@ use std::path::Path; use hashbrown::HashSet; use percent_encoding::percent_decode_str; use yazi_fs::{Xdg, path::PercentEncoding}; -use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; +use yazi_shared::{path::{Component, PathBufDyn, PathDyn, PathLike}, pool::InternStr, scheme::SchemeKind, url::{AsUrl, UrlBuf, UrlLike}}; #[derive(Debug, Default)] pub struct Watched(HashSet); @@ -28,28 +28,24 @@ impl Watched { #[inline] pub(crate) fn remove(&mut self, url: impl AsUrl) { self.0.remove(&url.as_url()); } - pub(super) fn find_by_cache(&self, cache: &Path) -> Option<&UrlBuf> { - let mut it = cache.strip_prefix(Xdg::cache_dir()).ok()?.components(); + pub(super) fn find_by_cache(&self, cache: PathDyn) -> Option { + let mut it = cache.try_strip_prefix(Xdg::cache_dir()).ok()?.components(); - let l1 = it.next()?.as_os_str().to_str()?; - let (l2, rel) = - if let Ok(p) = it.as_path().strip_prefix(".%2F") { (p, true) } else { (it.as_path(), false) }; + // Parse domain + let domain = it.next()?.as_normal()?.to_str().ok()?; + let domain = percent_decode_str(domain.strip_prefix("sftp-")?).decode_utf8().ok()?.intern(); - let domain = percent_decode_str(l1.strip_prefix("sftp-")?).decode_utf8().ok()?; - let loc = l2.percent_decode(); + // Parse path + let (path, abs) = + if let Ok(p) = it.path().try_strip_prefix(".%2F") { (p, false) } else { (it.path(), true) }; + let path = path.percent_decode(SchemeKind::Sftp).ok()?; + let path = PathBufDyn::from_components( + SchemeKind::Sftp, + Some(Component::RootDir).filter(|_| abs).into_iter().chain(path.components()), + ) + .ok()?; - self.0.iter().find(|u| { - todo!() - // if u.scheme != SchemeRef::Sftp(&domain) { - // return false; - // } - - // let mut it = u.loc.components(); - // if it.next() == Some(std::path::Component::RootDir) { - // !rel && it.as_path().as_os_str().as_encoded_bytes() == loc.as_ref() - // } else { - // rel && u.loc.as_os_str().as_encoded_bytes() == loc.as_ref() - // } - }) + let url = UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), domain }; + if self.contains(&url) { Some(url) } else { None } } } From a8f2498471f4b300e2cab2b1f694a8d82fb332fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sat, 29 Nov 2025 05:27:17 +0800 Subject: [PATCH 13/53] feat: new `stash` command to let users customize `back` and `forward` command behavior (#3380) --- yazi-actor/src/context.rs | 10 +- yazi-actor/src/mgr/back.rs | 2 +- yazi-actor/src/mgr/cd.rs | 18 +- yazi-actor/src/mgr/displace.rs | 59 ++++ yazi-actor/src/mgr/forward.rs | 6 +- yazi-actor/src/mgr/mod.rs | 2 + yazi-actor/src/mgr/search.rs | 15 +- yazi-actor/src/mgr/stash.rs | 31 ++ yazi-binding/src/url.rs | 4 + yazi-config/src/which/sorting.rs | 2 +- yazi-dds/src/ember/ember.rs | 6 +- yazi-dds/src/spark/kind.rs | 4 + yazi-dds/src/spark/spark.rs | 10 + yazi-fm/src/executor.rs | 1 + yazi-fs/src/path/expand.rs | 6 +- yazi-parser/src/mgr/cd.rs | 11 +- yazi-parser/src/mgr/displace_do.rs | 29 ++ yazi-parser/src/mgr/mod.rs | 2 + yazi-parser/src/mgr/stash.rs | 44 +++ yazi-parser/src/void.rs | 6 +- yazi-plugin/preset/plugins/mime-local.lua | 2 +- yazi-proxy/src/mgr.rs | 40 +-- yazi-shared/src/data/data.rs | 337 +++++++++++++++++++--- yazi-shared/src/event/cmd.rs | 8 + yazi-shared/src/event/cow.rs | 12 + yazi-shared/src/loc/able.rs | 6 + yazi-shared/src/loc/buf.rs | 5 +- yazi-shared/src/loc/loc.rs | 233 +++++++-------- yazi-shared/src/path/cow.rs | 1 + yazi-shared/src/path/path.rs | 4 +- yazi-shared/src/source.rs | 10 +- yazi-shared/src/url/buf.rs | 14 +- 32 files changed, 702 insertions(+), 238 deletions(-) create mode 100644 yazi-actor/src/mgr/displace.rs create mode 100644 yazi-actor/src/mgr/stash.rs create mode 100644 yazi-parser/src/mgr/displace_do.rs create mode 100644 yazi-parser/src/mgr/stash.rs diff --git a/yazi-actor/src/context.rs b/yazi-actor/src/context.rs index 9361ae92..259c3160 100644 --- a/yazi-actor/src/context.rs +++ b/yazi-actor/src/context.rs @@ -118,13 +118,5 @@ impl<'a> Ctx<'a> { pub fn tasks(&self) -> &Tasks { &self.tasks } #[inline] - pub fn source(&self) -> Source { - use Source::*; - match self.source { - Key if self.level != 1 => Ind, - Emit if self.level != 1 => EmitInd, - Relay if self.level != 1 => RelayInd, - s => s, - } - } + pub fn source(&self) -> Source { if self.level != 1 { Source::Ind } else { self.source } } } diff --git a/yazi-actor/src/mgr/back.rs b/yazi-actor/src/mgr/back.rs index 95f744f3..b8ba672d 100644 --- a/yazi-actor/src/mgr/back.rs +++ b/yazi-actor/src/mgr/back.rs @@ -14,7 +14,7 @@ impl Actor for Back { fn act(cx: &mut Ctx, _: Self::Options) -> Result { if let Some(u) = cx.tab_mut().backstack.shift_backward().cloned() { - return act!(mgr:cd, cx, (u, CdSource::Back)); + act!(mgr:cd, cx, (u, CdSource::Back))?; } succ!(); } diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 73003575..244c8e22 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -9,7 +9,7 @@ use yazi_fs::{File, FilesOp, path::expand_url}; use yazi_macro::{act, err, render, succ}; use yazi_parser::mgr::CdOpt; use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; -use yazi_shared::{Debounce, data::Data, errors::InputError, url::{AsUrl, UrlBuf, UrlLike}}; +use yazi_shared::{Debounce, data::Data, errors::InputError, url::{UrlBuf, UrlLike}}; use yazi_vfs::VfsFile; use crate::{Actor, Ctx}; @@ -33,18 +33,8 @@ impl Actor for Cd { } // Take parent to history - if let Some(rep) = tab.parent.take() { - tab.history.insert(rep.url.to_owned(), rep); - } - - // Backstack - if opt.source.big_jump() { - if tab.current.url.is_regular() { - tab.backstack.push(tab.current.url.as_url()); - } - if opt.target.is_regular() { - tab.backstack.push(opt.target.as_url()); - } + if let Some(t) = tab.parent.take() { + tab.history.insert(t.url.to_owned(), t); } // Current @@ -58,10 +48,12 @@ impl Actor for Cd { } err!(Pubsub::pub_after_cd(tab.id, tab.cwd())); + act!(mgr:displace, cx)?; act!(mgr:hidden, cx)?; act!(mgr:sort, cx)?; act!(mgr:hover, cx)?; act!(mgr:refresh, cx)?; + act!(mgr:stash, cx, opt).ok(); succ!(render!()); } } diff --git a/yazi-actor/src/mgr/displace.rs b/yazi-actor/src/mgr/displace.rs new file mode 100644 index 00000000..a23f1f68 --- /dev/null +++ b/yazi-actor/src/mgr/displace.rs @@ -0,0 +1,59 @@ +use anyhow::{Result, bail}; +use yazi_macro::{act, succ}; +use yazi_parser::{VoidOpt, mgr::{CdSource, DisplaceDoOpt}}; +use yazi_proxy::MgrProxy; +use yazi_shared::{data::Data, url::UrlLike}; +use yazi_vfs::provider; + +use crate::{Actor, Ctx}; + +pub struct Displace; + +impl Actor for Displace { + type Options = VoidOpt; + + const NAME: &str = "displace"; + + fn act(cx: &mut Ctx, _: Self::Options) -> Result { + if cx.cwd().is_absolute() { + succ!(); + } + + let tab = cx.tab().id; + let from = cx.cwd().to_owned(); + tokio::spawn(async move { + if let Ok(to) = provider::absolute(&from).await + && to.is_owned() + { + MgrProxy::displace_do(tab, DisplaceDoOpt { to: to.into(), from }); + } + }); + + succ!(); + } +} + +// --- Do +pub struct DisplaceDo; + +impl Actor for DisplaceDo { + type Options = DisplaceDoOpt; + + const NAME: &str = "displace_do"; + + fn act(cx: &mut Ctx, opt: Self::Options) -> Result { + if !opt.to.is_absolute() { + bail!("Target URL must be absolute"); + } + + if cx.cwd() != opt.from { + succ!() + } else if let Some(hovered) = cx.hovered() + && let Ok(url) = opt.to.try_join(hovered.urn()) + { + act!(mgr:reveal, cx, (url, CdSource::Displace)) + } else { + act!(mgr:cd, cx, (opt.to, CdSource::Displace)) + } + } +} diff --git a/yazi-actor/src/mgr/forward.rs b/yazi-actor/src/mgr/forward.rs index a6642df5..b0a10732 100644 --- a/yazi-actor/src/mgr/forward.rs +++ b/yazi-actor/src/mgr/forward.rs @@ -13,9 +13,9 @@ impl Actor for Forward { const NAME: &str = "forward"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { - match cx.tab_mut().backstack.shift_forward().cloned() { - Some(u) => act!(mgr:cd, cx, (u, CdSource::Forward)), - None => succ!(), + if let Some(u) = cx.tab_mut().backstack.shift_forward().cloned() { + act!(mgr:cd, cx, (u, CdSource::Forward))?; } + succ!() } } diff --git a/yazi-actor/src/mgr/mod.rs b/yazi-actor/src/mgr/mod.rs index 29842efb..68c0a218 100644 --- a/yazi-actor/src/mgr/mod.rs +++ b/yazi-actor/src/mgr/mod.rs @@ -6,6 +6,7 @@ yazi_macro::mod_flat!( close copy create + displace download enter escape @@ -36,6 +37,7 @@ yazi_macro::mod_flat!( shell sort spot + stash suspend tab_close tab_create diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index 33850f03..b559f3e4 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, mem, time::Duration}; +use std::{borrow::Cow, time::Duration}; use anyhow::Result; use tokio::pin; @@ -6,7 +6,7 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; use yazi_fs::{FilesOp, cha::Cha}; use yazi_macro::{act, succ}; -use yazi_parser::{VoidOpt, mgr::{SearchOpt, SearchOptVia}}; +use yazi_parser::{VoidOpt, mgr::{CdSource, SearchOpt, SearchOptVia}}; use yazi_plugin::external; use yazi_proxy::{InputProxy, MgrProxy}; use yazi_shared::data::Data; @@ -107,15 +107,10 @@ impl Actor for SearchStop { handle.abort(); } - if !tab.cwd().is_search() { - succ!(); + if tab.cwd().is_search() { + act!(mgr:cd, cx, (tab.cwd().to_regular()?, CdSource::Escape))?; } - let rep = tab.history.remove_or(tab.cwd().to_regular()?); - drop(mem::replace(&mut tab.current, rep)); - - act!(mgr:hidden, cx)?; - act!(mgr:sort, cx)?; - act!(mgr:refresh, cx) + succ!(); } } diff --git a/yazi-actor/src/mgr/stash.rs b/yazi-actor/src/mgr/stash.rs new file mode 100644 index 00000000..eb1bd502 --- /dev/null +++ b/yazi-actor/src/mgr/stash.rs @@ -0,0 +1,31 @@ +use anyhow::Result; +use yazi_dds::spark::SparkKind; +use yazi_macro::succ; +use yazi_parser::mgr::StashOpt; +use yazi_shared::{Source, data::Data, url::AsUrl}; + +use crate::{Actor, Ctx}; + +pub struct Stash; + +impl Actor for Stash { + type Options = StashOpt; + + const NAME: &str = "stash"; + + fn act(cx: &mut Ctx, opt: Self::Options) -> Result { + if opt.target.is_regular() { + cx.tab_mut().backstack.push(opt.target.as_url()); + } + + succ!() + } + + fn hook(cx: &Ctx, _opt: &Self::Options) -> Option { + match cx.source() { + Source::Relay => Some(SparkKind::RelayStash), + Source::Ind => Some(SparkKind::IndStash), + _ => None, + } + } +} diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 9def6a38..06f3d7b5 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -49,6 +49,10 @@ impl From for yazi_shared::url::UrlBufCov { fn from(value: Url) -> Self { Self(value.inner) } } +impl From for yazi_shared::url::UrlCow<'_> { + fn from(value: Url) -> Self { value.inner.into() } +} + impl TryFrom<&[u8]> for Url { type Error = mlua::Error; diff --git a/yazi-config/src/which/sorting.rs b/yazi-config/src/which/sorting.rs index 2a4c475a..8c81be8b 100644 --- a/yazi-config/src/which/sorting.rs +++ b/yazi-config/src/which/sorting.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "kebab-case")] pub enum SortBy { #[default] diff --git a/yazi-dds/src/ember/ember.rs b/yazi-dds/src/ember/ember.rs index b71d7d6a..477acb07 100644 --- a/yazi-dds/src/ember/ember.rs +++ b/yazi-dds/src/ember/ember.rs @@ -67,12 +67,10 @@ impl Ember<'static> { | "trash" | "delete" | "mount" - ) || kind.starts_with("emit-") - || kind.starts_with("emit-ind-") + ) || kind.starts_with("key-") || kind.starts_with("ind-") - || kind.starts_with("key-") + || kind.starts_with("emit-") || kind.starts_with("relay-") - || kind.starts_with("relay-ind-") { bail!("Cannot construct system event"); } diff --git a/yazi-dds/src/spark/kind.rs b/yazi-dds/src/spark/kind.rs index 6d37a9e8..3cefc874 100644 --- a/yazi-dds/src/spark/kind.rs +++ b/yazi-dds/src/spark/kind.rs @@ -2,13 +2,17 @@ use std::fmt::Display; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SparkKind { + IndStash, KeyQuit, + RelayStash, } impl AsRef for SparkKind { fn as_ref(&self) -> &str { match self { + Self::IndStash => "ind-stash", Self::KeyQuit => "key-quit", + Self::RelayStash => "relay-stash", } } } diff --git a/yazi-dds/src/spark/spark.rs b/yazi-dds/src/spark/spark.rs index 992f58c9..1eaf6e24 100644 --- a/yazi-dds/src/spark/spark.rs +++ b/yazi-dds/src/spark/spark.rs @@ -15,6 +15,8 @@ pub enum Spark<'a> { Close(yazi_parser::mgr::CloseOpt), Copy(yazi_parser::mgr::CopyOpt), Create(yazi_parser::mgr::CreateOpt), + Displace(yazi_parser::VoidOpt), + DisplaceDo(yazi_parser::mgr::DisplaceDoOpt), Download(yazi_parser::mgr::DownloadOpt), Enter(yazi_parser::VoidOpt), Escape(yazi_parser::mgr::EscapeOpt), @@ -53,6 +55,7 @@ pub enum Spark<'a> { Shell(yazi_parser::mgr::ShellOpt), Sort(yazi_parser::mgr::SortOpt), Spot(yazi_parser::mgr::SpotOpt), + Stash(yazi_parser::mgr::StashOpt), Suspend(yazi_parser::VoidOpt), TabClose(yazi_parser::mgr::TabCloseOpt), TabCreate(yazi_parser::mgr::TabCreateOpt), @@ -119,7 +122,9 @@ pub enum Spark<'a> { impl<'a> Spark<'a> { pub fn from_lua(lua: &Lua, kind: SparkKind, value: Value) -> mlua::Result { Ok(match kind { + SparkKind::IndStash => Self::Stash(<_>::from_lua(value, lua)?), SparkKind::KeyQuit => Self::Quit(<_>::from_lua(value, lua)?), + SparkKind::RelayStash => Self::Stash(<_>::from_lua(value, lua)?), }) } } @@ -138,6 +143,8 @@ impl<'a> IntoLua for Spark<'a> { Self::Close(b) => b.into_lua(lua), Self::Copy(b) => b.into_lua(lua), Self::Create(b) => b.into_lua(lua), + Self::Displace(b) => b.into_lua(lua), + Self::DisplaceDo(b) => b.into_lua(lua), Self::Download(b) => b.into_lua(lua), Self::Enter(b) => b.into_lua(lua), Self::Escape(b) => b.into_lua(lua), @@ -176,6 +183,7 @@ impl<'a> IntoLua for Spark<'a> { Self::Shell(b) => b.into_lua(lua), Self::Sort(b) => b.into_lua(lua), Self::Spot(b) => b.into_lua(lua), + Self::Stash(b) => b.into_lua(lua), Self::Suspend(b) => b.into_lua(lua), Self::TabClose(b) => b.into_lua(lua), Self::TabCreate(b) => b.into_lua(lua), @@ -282,6 +290,7 @@ try_from_spark!(mgr::CdOpt, mgr:cd); try_from_spark!(mgr::CloseOpt, mgr:close); try_from_spark!(mgr::CopyOpt, mgr:copy); try_from_spark!(mgr::CreateOpt, mgr:create); +try_from_spark!(mgr::DisplaceDoOpt, mgr:displace_do); try_from_spark!(mgr::DownloadOpt, mgr:download); try_from_spark!(mgr::EscapeOpt, mgr:escape); try_from_spark!(mgr::FilterOpt, mgr:filter, mgr:filter_do); @@ -306,6 +315,7 @@ try_from_spark!(mgr::SeekOpt, mgr:seek); try_from_spark!(mgr::ShellOpt, mgr:shell); try_from_spark!(mgr::SortOpt, mgr:sort); try_from_spark!(mgr::SpotOpt, mgr:spot); +try_from_spark!(mgr::StashOpt, mgr:stash); try_from_spark!(mgr::TabCloseOpt, mgr:tab_close); try_from_spark!(mgr::TabCreateOpt, mgr:tab_create); try_from_spark!(mgr::TabSwitchOpt, mgr:tab_switch); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 60a8b2f2..70416d9b 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -137,6 +137,7 @@ impl<'a> Executor<'a> { // VFS on!(download); on!(upload); + on!(displace_do); match cmd.name.as_ref() { // Help diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index 200f1665..4c5f2342 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -92,7 +92,7 @@ fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> { if url.has_base() { 0 } else { 2 }, if url.has_trail() { 0 } else { 2 }, ) - .expect("Failed to create Loc from drive letter") + .expect("Loc from drive letter") } else if let Ok(rest) = path.strip_prefix("~/") && let Some(home) = dirs::home_dir() && home.is_absolute() @@ -103,7 +103,7 @@ fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> { url.uri().components().count() + if url.has_base() { 0 } else { add }, url.urn().components().count() + if url.has_trail() { 0 } else { add }, ) - .expect("Failed to create Loc from home directory") + .expect("Loc from home directory") } else if !url.is_absolute() { let cwd = CWD.path(); LocBuf::::with( @@ -111,7 +111,7 @@ fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> { url.uri().components().count(), url.urn().components().count(), ) - .expect("Failed to create Loc from relative path") + .expect("Loc from relative path") } else { return url; }; diff --git a/yazi-parser/src/mgr/cd.rs b/yazi-parser/src/mgr/cd.rs index d26c3baa..ee83f606 100644 --- a/yazi-parser/src/mgr/cd.rs +++ b/yazi-parser/src/mgr/cd.rs @@ -1,4 +1,5 @@ use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use serde::{Deserialize, Serialize}; use yazi_fs::path::expand_url; use yazi_shared::{event::CmdCow, url::{Url, UrlBuf, UrlCow}}; @@ -46,7 +47,8 @@ impl IntoLua for CdOpt { } // --- Source -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] pub enum CdSource { Tab, Cd, @@ -55,9 +57,6 @@ pub enum CdSource { Leave, Forward, Back, -} - -impl CdSource { - #[inline] - pub fn big_jump(self) -> bool { self == Self::Cd || self == Self::Reveal } + Escape, + Displace, } diff --git a/yazi-parser/src/mgr/displace_do.rs b/yazi-parser/src/mgr/displace_do.rs new file mode 100644 index 00000000..49d25409 --- /dev/null +++ b/yazi-parser/src/mgr/displace_do.rs @@ -0,0 +1,29 @@ +use anyhow::bail; +use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use yazi_shared::{event::CmdCow, url::UrlBuf}; + +#[derive(Debug)] +pub struct DisplaceDoOpt { + pub to: UrlBuf, + pub from: UrlBuf, +} + +impl TryFrom for DisplaceDoOpt { + type Error = anyhow::Error; + + fn try_from(mut c: CmdCow) -> Result { + if let Some(opt) = c.take_any2("opt") { + opt + } else { + bail!("'opt' is required for DisplaceDoOpt"); + } + } +} + +impl FromLua for DisplaceDoOpt { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for DisplaceDoOpt { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} diff --git a/yazi-parser/src/mgr/mod.rs b/yazi-parser/src/mgr/mod.rs index caec442e..085ae19e 100644 --- a/yazi-parser/src/mgr/mod.rs +++ b/yazi-parser/src/mgr/mod.rs @@ -3,6 +3,7 @@ yazi_macro::mod_flat!( close copy create + displace_do download escape filter @@ -27,6 +28,7 @@ yazi_macro::mod_flat!( shell sort spot + stash tab_close tab_create tab_switch diff --git a/yazi-parser/src/mgr/stash.rs b/yazi-parser/src/mgr/stash.rs new file mode 100644 index 00000000..70ef52fe --- /dev/null +++ b/yazi-parser/src/mgr/stash.rs @@ -0,0 +1,44 @@ +use anyhow::bail; +use mlua::{ExternalError, FromLua, IntoLua, Lua, LuaSerdeExt, Value}; +use serde::{Deserialize, Serialize}; +use yazi_binding::Url; +use yazi_shared::{event::CmdCow, url::UrlCow}; + +use crate::mgr::{CdOpt, CdSource}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct StashOpt { + pub target: UrlCow<'static>, + pub source: CdSource, +} + +impl TryFrom for StashOpt { + type Error = anyhow::Error; + + fn try_from(_: CmdCow) -> Result { bail!("unsupported") } +} + +impl From for StashOpt { + fn from(opt: CdOpt) -> Self { Self { target: opt.target, source: opt.source } } +} + +impl FromLua for StashOpt { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + let tbl = value.as_table().ok_or_else(|| "expected table".into_lua_err())?; + Ok(Self { + target: tbl.get::("target")?.into(), + source: lua.from_value(tbl.get("source")?)?, + }) + } +} + +impl IntoLua for StashOpt { + fn into_lua(self, lua: &Lua) -> mlua::Result { + lua + .create_table_from([ + ("target", Url::new(self.target).into_lua(lua)?), + ("source", lua.to_value(&self.source)?), + ])? + .into_lua(lua) + } +} diff --git a/yazi-parser/src/void.rs b/yazi-parser/src/void.rs index a2fd45a1..2b9f25cf 100644 --- a/yazi-parser/src/void.rs +++ b/yazi-parser/src/void.rs @@ -1,4 +1,4 @@ -use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use mlua::{FromLua, IntoLua, Lua, Value}; use yazi_shared::event::CmdCow; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -13,9 +13,9 @@ impl From<()> for VoidOpt { } impl FromLua for VoidOpt { - fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Ok(Self) } } impl IntoLua for VoidOpt { - fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } + fn into_lua(self, lua: &Lua) -> mlua::Result { lua.create_table()?.into_lua(lua) } } diff --git a/yazi-plugin/preset/plugins/mime-local.lua b/yazi-plugin/preset/plugins/mime-local.lua index 3d8e8d18..fb50e2d2 100644 --- a/yazi-plugin/preset/plugins/mime-local.lua +++ b/yazi-plugin/preset/plugins/mime-local.lua @@ -44,7 +44,7 @@ function M:fetch(job) if file.cache then urls[i], paths[i] = tostring(file.url), tostring(file.cache) else - paths[i] = tostring(file.url) + paths[i] = tostring(file.url.path) end end diff --git a/yazi-proxy/src/mgr.rs b/yazi-proxy/src/mgr.rs index 1977d8c7..37826c26 100644 --- a/yazi-proxy/src/mgr.rs +++ b/yazi-proxy/src/mgr.rs @@ -1,22 +1,30 @@ use std::borrow::Cow; use yazi_macro::{emit, relay}; -use yazi_parser::mgr::{FilterOpt, FindDoOpt, OpenDoOpt, OpenOpt, SearchOpt, UpdatePeekedOpt, UpdateSpottedOpt}; -use yazi_shared::{SStr, url::UrlBuf}; +use yazi_parser::mgr::{DisplaceDoOpt, FilterOpt, FindDoOpt, OpenDoOpt, OpenOpt, SearchOpt, UpdatePeekedOpt, UpdateSpottedOpt}; +use yazi_shared::{Id, SStr, url::UrlBuf}; pub struct MgrProxy; impl MgrProxy { + pub fn arrow(step: impl Into) { + emit!(Call(relay!(mgr:arrow, [step.into()]))); + } + pub fn cd(target: &UrlBuf) { emit!(Call(relay!(mgr:cd, [target]).with("raw", true))); } - pub fn reveal(target: &UrlBuf) { - emit!(Call(relay!(mgr:reveal, [target]).with("raw", true).with("no-dummy", true))); + pub fn displace_do(tab: Id, opt: DisplaceDoOpt) { + emit!(Call(relay!(mgr:displace_do).with("tab", tab).with_any("opt", opt))); } - pub fn arrow(step: impl Into) { - emit!(Call(relay!(mgr:arrow, [step.into()]))); + pub fn filter_do(opt: FilterOpt) { + emit!(Call(relay!(mgr:filter_do).with_any("opt", opt))); + } + + pub fn find_do(opt: FindDoOpt) { + emit!(Call(relay!(mgr:find_do).with_any("opt", opt))); } pub fn open(opt: OpenOpt) { @@ -33,12 +41,8 @@ impl MgrProxy { )); } - pub fn find_do(opt: FindDoOpt) { - emit!(Call(relay!(mgr:find_do).with_any("opt", opt))); - } - - pub fn filter_do(opt: FilterOpt) { - emit!(Call(relay!(mgr:filter_do).with_any("opt", opt))); + pub fn reveal(target: &UrlBuf) { + emit!(Call(relay!(mgr:reveal, [target]).with("raw", true).with("no-dummy", true))); } pub fn search_do(opt: SearchOpt) { @@ -50,8 +54,8 @@ impl MgrProxy { )); } - pub fn watch() { - emit!(Call(relay!(mgr:watch))); + pub fn update_paged_by(page: usize, only_if: &UrlBuf) { + emit!(Call(relay!(mgr:update_paged, [page]).with("only-if", only_if))); } pub fn update_peeked(opt: UpdatePeekedOpt) { @@ -62,14 +66,14 @@ impl MgrProxy { emit!(Call(relay!(mgr:update_spotted).with_any("opt", opt))); } - pub fn update_paged_by(page: usize, only_if: &UrlBuf) { - emit!(Call(relay!(mgr:update_paged, [page]).with("only-if", only_if))); - } - pub fn upload(urls: I) where I: IntoIterator, { emit!(Call(relay!(mgr:upload).with_seq(urls))); } + + pub fn watch() { + emit!(Call(relay!(mgr:watch))); + } } diff --git a/yazi-shared/src/data/data.rs b/yazi-shared/src/data/data.rs index 6f77c30a..7fab095e 100644 --- a/yazi-shared/src/data/data.rs +++ b/yazi-shared/src/data/data.rs @@ -28,40 +28,6 @@ pub enum Data { Any(Box), } -impl Data { - pub fn as_str(&self) -> Option<&str> { - match self { - Self::String(s) => Some(s), - _ => None, - } - } - - pub fn into_string(self) -> Option { - match self { - Self::String(s) => Some(s), - _ => None, - } - } - - pub fn into_any(self) -> Option { - match self { - Self::Any(b) => b.downcast::().ok().map(|b| *b), - _ => None, - } - } - - // FIXME: find a better name - pub fn into_any2(self) -> Result { - if let Self::Any(b) = self - && let Ok(t) = b.downcast::() - { - Ok(*t) - } else { - bail!("Failed to downcast Data into {}", std::any::type_name::()) - } - } -} - impl From<()> for Data { fn from(_: ()) -> Self { Self::Nil } } @@ -201,13 +167,291 @@ impl<'a> TryFrom<&'a Data> for UrlCow<'a> { } } +impl<'a> TryFrom<&'a Data> for &'a [u8] { + type Error = anyhow::Error; + + fn try_from(value: &'a Data) -> Result { + match value { + Data::Bytes(b) => Ok(b), + _ => bail!("not bytes"), + } + } +} + impl PartialEq for Data { fn eq(&self, other: &bool) -> bool { self.try_into().is_ok_and(|b| *other == b) } } +impl Data { + pub fn as_str(&self) -> Option<&str> { + match self { + Self::String(s) => Some(s), + _ => None, + } + } + + pub fn into_string(self) -> Option { + match self { + Self::String(s) => Some(s), + _ => None, + } + } + + pub fn into_any(self) -> Option { + match self { + Self::Any(b) => b.downcast::().ok().map(|b| *b), + _ => None, + } + } + + // FIXME: find a better name + pub fn into_any2(self) -> Result { + if let Self::Any(b) = self + && let Ok(t) = b.downcast::() + { + Ok(*t) + } else { + bail!("Failed to downcast Data into {}", std::any::type_name::()) + } + } +} + +impl<'de> serde::Deserializer<'de> for &Data { + type Error = serde::de::value::Error; + + fn deserialize_any(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("any not supported")) + } + + fn deserialize_bool(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_bool(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_i8(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_i8(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_i16(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_i16(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_i32(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_i32(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_i64(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_i64(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_u8(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_u8(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_u16(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_u16(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_u32(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_u32(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_u64(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_u64(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_f32(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_f32(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_f64(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_f64(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_char(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("not a char")) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_str(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_string(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("string not supported")) + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_bytes(self.try_into().map_err(serde::de::Error::custom)?) + } + + fn deserialize_byte_buf(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("byte buf not supported")) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + match self { + Data::Nil => visitor.visit_none(), + _ => visitor.visit_some(self), + } + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_newtype_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_seq(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("seq not supported")) + } + + fn deserialize_tuple(self, _len: usize, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("tuple not supported")) + } + + fn deserialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + _visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("tuple struct not supported")) + } + + fn deserialize_map(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("map not supported")) + } + + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + _visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("struct not supported")) + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + _visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("enum not supported")) + } + + fn deserialize_identifier(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("identifier not supported")) + } + + fn deserialize_ignored_any(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::custom("ignored any not supported")) + } +} + // --- Macros macro_rules! impl_into_integer { - ($t:ty, $name:ident) => { + ($t:ty) => { impl TryFrom<&Data> for $t { type Error = anyhow::Error; @@ -224,14 +468,14 @@ macro_rules! impl_into_integer { } macro_rules! impl_into_number { - ($t:ty, $name:ident) => { + ($t:ty) => { impl TryFrom<&Data> for $t { type Error = anyhow::Error; fn try_from(value: &Data) -> Result { Ok(match value { Data::Integer(i) if *i == (*i as $t as _) => *i as $t, - Data::Number(n) => <$t>::try_from(*n)?, + Data::Number(n) if *n == (*n as $t as _) => *n as $t, Data::String(s) => s.parse()?, Data::Id(i) if i.0 == (i.0 as $t as _) => i.0 as $t, _ => bail!("not a number"), @@ -241,10 +485,17 @@ macro_rules! impl_into_number { }; } -impl_into_integer!(usize, as_usize); -impl_into_integer!(isize, as_isize); -impl_into_integer!(i16, as_i16); -impl_into_integer!(i32, as_i32); -impl_into_integer!(crate::Id, as_id); +impl_into_integer!(i8); +impl_into_integer!(i16); +impl_into_integer!(i32); +impl_into_integer!(i64); +impl_into_integer!(isize); +impl_into_integer!(u8); +impl_into_integer!(u16); +impl_into_integer!(u32); +impl_into_integer!(u64); +impl_into_integer!(usize); +impl_into_integer!(crate::Id); -impl_into_number!(f64, as_f64); +impl_into_number!(f32); +impl_into_number!(f64); diff --git a/yazi-shared/src/event/cmd.rs b/yazi-shared/src/event/cmd.rs index e7ad0469..6d0c18d7 100644 --- a/yazi-shared/src/event/cmd.rs +++ b/yazi-shared/src/event/cmd.rs @@ -152,6 +152,14 @@ impl Cmd { self.take(0) } + pub fn take_second(&mut self) -> Result + where + T: TryFrom, + T::Error: Into, + { + self.take(1) + } + pub fn take_seq(&mut self) -> Vec where T: TryFrom, diff --git a/yazi-shared/src/event/cow.rs b/yazi-shared/src/event/cow.rs index 309b5782..5e510489 100644 --- a/yazi-shared/src/event/cow.rs +++ b/yazi-shared/src/event/cow.rs @@ -55,6 +55,18 @@ impl CmdCow { } } + pub fn take_second<'a, T>(&mut self) -> Result + where + T: TryFrom + TryFrom<&'a Data>, + >::Error: Into, + >::Error: Into, + { + match self { + Self::Owned(c) => c.take_second(), + Self::Borrowed(c) => c.second(), + } + } + pub fn take_seq<'a, T>(&mut self) -> Vec where T: TryFrom + TryFrom<&'a Data>, diff --git a/yazi-shared/src/loc/able.rs b/yazi-shared/src/loc/able.rs index 2421fdec..f25b9a3e 100644 --- a/yazi-shared/src/loc/able.rs +++ b/yazi-shared/src/loc/able.rs @@ -72,6 +72,8 @@ pub(super) trait LocAbleImpl<'p>: LocAble<'p> { fn len(self) -> usize { self.as_encoded_bytes().len() } + fn parent(self) -> Option; + fn strip_prefix<'a, T>(self, base: T) -> Option where T: AsStrandView<'a, Self::Strand<'a>>; @@ -99,6 +101,8 @@ impl<'p> LocAbleImpl<'p> for &'p std::path::Path { self.join(path.as_strand_view()) } + fn parent(self) -> Option { self.parent() } + fn strip_prefix<'a, T>(self, base: T) -> Option where T: AsStrandView<'a, Self::Strand<'a>>, @@ -129,6 +133,8 @@ impl<'p> LocAbleImpl<'p> for &'p typed_path::UnixPath { self.join(path.as_strand_view()) } + fn parent(self) -> Option { self.parent() } + fn strip_prefix<'a, T>(self, base: T) -> Option where T: AsStrandView<'a, Self::Strand<'a>>, diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index 029866a2..115ef646 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -182,7 +182,7 @@ where let old = self.inner.len(); self.mutate(|path| path.set_file_name(name)); - let new = self.len(); + let new = self.inner.len(); if new == old { return Ok(()); } @@ -215,6 +215,9 @@ where loc } + #[inline] + pub fn parent(&self) -> Option> { self.as_loc().parent() } + #[inline] fn mutate T>(&mut self, f: F) -> T { let mut inner = mem::take(&mut self.inner); diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 6d9c27c6..59492801 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -73,6 +73,64 @@ impl<'p, P> Loc<'p, P> where P: LocAble<'p> + LocAbleImpl<'p>, { + #[inline] + pub fn as_inner(self) -> P { self.inner } + + #[inline] + pub fn as_loc(self) -> Self { self } + + pub fn bare(path: T) -> Self + where + T: AsPathView<'p, P>, + { + let path = path.as_path_view(); + let Some(name) = path.file_name() else { + let uri = path.strip_prefix(P::empty()).unwrap().len(); + return Self { inner: path, uri, urn: 0, _phantom: PhantomData }; + }; + + let name_len = name.len(); + let prefix_len = unsafe { + name.as_encoded_bytes().as_ptr().offset_from_unsigned(path.as_encoded_bytes().as_ptr()) + }; + + let bytes = &path.as_encoded_bytes()[..prefix_len + name_len]; + Self { + inner: unsafe { P::from_encoded_bytes_unchecked(bytes) }, + uri: bytes.len(), + urn: name_len, + _phantom: PhantomData, + } + } + + #[inline] + pub fn base(self) -> P { + unsafe { + P::from_encoded_bytes_unchecked( + self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.uri), + ) + } + } + + pub fn floated<'a, T, S>(path: T, base: S) -> Self + where + T: AsPathView<'p, P>, + S: AsStrandView<'a, P::Strand<'a>>, + { + let mut loc = Self::bare(path); + loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len(); + loc + } + + #[inline] + pub fn has_base(self) -> bool { self.inner.len() != self.uri } + + #[inline] + pub fn has_trail(self) -> bool { self.inner.len() != self.urn } + + #[inline] + pub fn is_empty(self) -> bool { self.inner.len() == 0 } + pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self where T: AsPathView<'p, P>, @@ -84,6 +142,67 @@ where loc } + #[inline] + pub fn parent(self) -> Option

{ + self.inner.parent().filter(|p| !p.as_encoded_bytes().is_empty()) + } + + pub fn saturated<'a, T>(path: T, kind: SchemeKind) -> Self + where + T: AsPathView<'p, P>, + { + match kind { + SchemeKind::Regular => Self::bare(path), + SchemeKind::Search => Self::zeroed(path), + SchemeKind::Archive => Self::zeroed(path), + SchemeKind::Sftp => Self::bare(path), + } + } + + #[inline] + pub fn trail(self) -> P { + unsafe { + P::from_encoded_bytes_unchecked( + self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.urn), + ) + } + } + + #[inline] + pub fn triple(self) -> (P, P, P) { + let len = self.inner.len(); + + let base = ..len - self.uri; + let rest = len - self.uri..len - self.urn; + let urn = len - self.urn..; + + unsafe { + ( + P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(base)), + P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(rest)), + P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(urn)), + ) + } + } + + #[inline] + pub fn uri(self) -> P { + unsafe { + P::from_encoded_bytes_unchecked( + self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.uri..), + ) + } + } + + #[inline] + pub fn urn(self) -> P { + unsafe { + P::from_encoded_bytes_unchecked( + self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.urn..), + ) + } + } + pub fn with(path: T, uri: usize, urn: usize) -> Result where T: AsPathView<'p, P>, @@ -116,30 +235,6 @@ where Ok(loc) } - pub fn bare(path: T) -> Self - where - T: AsPathView<'p, P>, - { - let path = path.as_path_view(); - let Some(name) = path.file_name() else { - let uri = path.strip_prefix(P::empty()).unwrap().len(); - return Self { inner: path, uri, urn: 0, _phantom: PhantomData }; - }; - - let name_len = name.len(); - let prefix_len = unsafe { - name.as_encoded_bytes().as_ptr().offset_from_unsigned(path.as_encoded_bytes().as_ptr()) - }; - - let bytes = &path.as_encoded_bytes()[..prefix_len + name_len]; - Self { - inner: unsafe { P::from_encoded_bytes_unchecked(bytes) }, - uri: bytes.len(), - urn: name_len, - _phantom: PhantomData, - } - } - pub fn zeroed(path: T) -> Self where T: AsPathView<'p, P>, @@ -148,94 +243,4 @@ where (loc.uri, loc.urn) = (0, 0); loc } - - pub fn floated<'a, T, S>(path: T, base: S) -> Self - where - T: AsPathView<'p, P>, - S: AsStrandView<'a, P::Strand<'a>>, - { - let mut loc = Self::bare(path); - loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len(); - loc - } - - pub fn saturated<'a, T>(path: T, kind: SchemeKind) -> Self - where - T: AsPathView<'p, P>, - { - match kind { - SchemeKind::Regular => Self::bare(path), - SchemeKind::Search => Self::zeroed(path), - SchemeKind::Archive => Self::zeroed(path), - SchemeKind::Sftp => Self::bare(path), - } - } - - #[inline] - pub fn as_loc(self) -> Self { self } - - #[inline] - pub fn as_inner(self) -> P { self.inner } - - #[inline] - pub fn is_empty(self) -> bool { self.inner.len() == 0 } - - #[inline] - pub fn uri(self) -> P { - unsafe { - P::from_encoded_bytes_unchecked( - self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.uri..), - ) - } - } - - #[inline] - pub fn urn(self) -> P { - unsafe { - P::from_encoded_bytes_unchecked( - self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.urn..), - ) - } - } - - #[inline] - pub fn base(self) -> P { - unsafe { - P::from_encoded_bytes_unchecked( - self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.uri), - ) - } - } - - #[inline] - pub fn has_base(self) -> bool { self.inner.len() != self.uri } - - #[inline] - pub fn trail(self) -> P { - unsafe { - P::from_encoded_bytes_unchecked( - self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.urn), - ) - } - } - - #[inline] - pub fn has_trail(self) -> bool { self.inner.len() != self.urn } - - #[inline] - pub fn triple(self) -> (P, P, P) { - let len = self.inner.len(); - - let base = ..len - self.uri; - let rest = len - self.uri..len - self.urn; - let urn = len - self.urn..; - - unsafe { - ( - P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(base)), - P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(rest)), - P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(urn)), - ) - } - } } diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs index 3e1414b7..49b7ef4f 100644 --- a/yazi-shared/src/path/cow.rs +++ b/yazi-shared/src/path/cow.rs @@ -5,6 +5,7 @@ use anyhow::Result; use crate::path::{AsPath, PathBufDyn, PathDyn, PathKind}; // --- PathCow +#[derive(Debug)] pub enum PathCow<'a> { Borrowed(PathDyn<'a>), Owned(PathBufDyn), diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index 01c3cca6..0d33535f 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -139,8 +139,8 @@ impl<'p> PathDyn<'p> { pub fn parent(self) -> Option { Some(match self { - Self::Os(p) => Self::Os(p.parent()?), - Self::Unix(p) => Self::Unix(p.parent()?), + Self::Os(p) => Self::Os(p.parent().filter(|p| !p.as_os_str().is_empty())?), + Self::Unix(p) => Self::Unix(p.parent().filter(|p| !p.as_bytes().is_empty())?), }) } diff --git a/yazi-shared/src/source.rs b/yazi-shared/src/source.rs index 05d75447..f966af5a 100644 --- a/yazi-shared/src/source.rs +++ b/yazi-shared/src/source.rs @@ -6,16 +6,16 @@ pub enum Source { Unknown, Key, - Ind, - Emit, - EmitInd, - Relay, - RelayInd, + + Ind, } impl Source { #[inline] pub fn is_key(self) -> bool { self == Self::Key } + + #[inline] + pub fn is_ind(self) -> bool { self == Self::Ind } } diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index fbfabae9..b21688bc 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -96,6 +96,10 @@ impl PartialEq for UrlBuf { fn eq(&self, other: &Self) -> bool { self.as_url() == other.as_url() } } +impl PartialEq for &UrlBuf { + fn eq(&self, other: &UrlBuf) -> bool { self.as_url() == other.as_url() } +} + impl PartialEq> for UrlBuf { fn eq(&self, other: &Url) -> bool { self.as_url() == *other } } @@ -104,6 +108,14 @@ impl PartialEq> for &UrlBuf { fn eq(&self, other: &Url) -> bool { self.as_url() == *other } } +impl PartialEq> for UrlBuf { + fn eq(&self, other: &UrlCow) -> bool { self.as_url() == other.as_url() } +} + +impl PartialEq> for &UrlBuf { + fn eq(&self, other: &UrlCow) -> bool { self.as_url() == other.as_url() } +} + // --- Hash impl Hash for UrlBuf { fn hash(&self, state: &mut H) { self.as_url().hash(state) } @@ -293,7 +305,7 @@ mod tests { ("sftp://remote//", None), // Relative ("search://kw:2:2/a/b", Some("search://kw:1:1/a")), - ("search://kw:1:1/a", Some("search://kw/")), + ("search://kw:1:1/a", None), ("search://kw/", None), ]; From 8c2a6dba9060d3b4152f8a7d88467329a531bd3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sat, 29 Nov 2025 05:27:35 +0800 Subject: [PATCH 14/53] feat: check if already in visual mode before entering visual mode (#3381) --- yazi-widgets/src/input/commands/visual.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yazi-widgets/src/input/commands/visual.rs b/yazi-widgets/src/input/commands/visual.rs index fcfa3485..122ea128 100644 --- a/yazi-widgets/src/input/commands/visual.rs +++ b/yazi-widgets/src/input/commands/visual.rs @@ -12,10 +12,15 @@ impl Input { } let snap = self.snap_mut(); + if let InputOp::Select(_) = snap.op { + succ!(); + } + if !snap.value.is_empty() { snap.op = InputOp::Select(snap.cursor); render!(); } + succ!(); } } From 0b8aaba29bb7ceda912095746e1d64514a35b0a6 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Sat, 29 Nov 2025 07:39:04 +0800 Subject: [PATCH 15/53] refactor: make Clippy happy --- Cargo.lock | 48 +++++++-------- Cargo.toml | 3 +- yazi-actor/Cargo.toml | 3 + yazi-actor/src/lib.rs | 2 - yazi-actor/src/lives/folder.rs | 2 +- yazi-actor/src/lives/preference.rs | 2 +- yazi-actor/src/lives/selected.rs | 2 +- yazi-actor/src/lives/yanked.rs | 2 +- yazi-actor/src/mgr/cd.rs | 4 +- yazi-actor/src/mgr/hover.rs | 2 +- yazi-actor/src/mgr/refresh.rs | 7 +-- yazi-adapter/Cargo.toml | 3 + yazi-adapter/src/brand.rs | 96 ++++++++++++++---------------- yazi-adapter/src/lib.rs | 2 - yazi-binding/src/url.rs | 4 +- yazi-boot/Cargo.toml | 3 + yazi-build/Cargo.toml | 3 + yazi-cli/Cargo.toml | 3 + yazi-codegen/Cargo.toml | 3 + yazi-core/src/lib.rs | 2 - yazi-dds/src/lib.rs | 2 - yazi-dds/src/sendable.rs | 4 +- yazi-fm/Cargo.toml | 2 +- yazi-fm/src/main.rs | 2 - yazi-fs/src/lib.rs | 2 - yazi-plugin/src/lib.rs | 2 - yazi-scheduler/src/lib.rs | 2 - yazi-sftp/src/lib.rs | 2 - yazi-shared/src/lib.rs | 2 - yazi-shared/src/path/buf.rs | 2 +- yazi-shared/src/url/cov.rs | 4 ++ yazi-vfs/src/lib.rs | 2 - yazi-widgets/src/lib.rs | 2 - 33 files changed, 108 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ce50e62..d3a753f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,9 +505,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.47" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "jobserver", @@ -1856,9 +1856,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -2173,9 +2173,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608" dependencies = [ "num-traits", "pxfm", @@ -2798,9 +2798,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "b3502d6155304a4173a5f2c34b52b7ed0dd085890326cb50fd625fdf39e86b3b" dependencies = [ "num-traits", ] @@ -3865,9 +3865,9 @@ checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3920,9 +3920,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -4154,9 +4154,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -4167,9 +4167,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -4180,9 +4180,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4190,9 +4190,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -4203,18 +4203,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index de637afa..5ab7628e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ tokio = { version = "1.48.0", features = [ "full" ] } tokio-stream = "0.1.17" tokio-util = "0.7.17" toml = { version = "0.9.8" } -tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] } +tracing = { version = "0.1.43", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } typed-path = "0.12.0" unicode-width = { version = "0.2.0", default-features = false } @@ -67,5 +67,6 @@ implicit_clone = "warn" len_without_is_empty = "allow" missing_safety_doc = "allow" module_inception = "allow" +option_map_unit_fn = "allow" unit_arg = "allow" use_self = "warn" diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index 6cad1e14..42af4c39 100644 --- a/yazi-actor/Cargo.toml +++ b/yazi-actor/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi actor model" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [features] default = [ "vendored-lua" ] vendored-lua = [ "mlua/vendored" ] diff --git a/yazi-actor/src/lib.rs b/yazi-actor/src/lib.rs index def17563..95950dad 100644 --- a/yazi-actor/src/lib.rs +++ b/yazi-actor/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn)] - extern crate self as yazi_actor; yazi_macro::mod_pub!(cmp confirm core help input lives mgr pick spot tasks which); diff --git a/yazi-actor/src/lives/folder.rs b/yazi-actor/src/lives/folder.rs index 3ff86e7c..ab4ed872 100644 --- a/yazi-actor/src/lives/folder.rs +++ b/yazi-actor/src/lives/folder.rs @@ -54,7 +54,7 @@ impl Folder { impl UserData for Folder { fn add_fields>(fields: &mut F) { - cached_field!(fields, cwd, |_, me| Ok(Url::new(me.url.to_owned()))); + cached_field!(fields, cwd, |_, me| Ok(Url::new(&me.url))); cached_field!(fields, files, |_, me| Files::make(0..me.files.len(), me, &me.tab)); cached_field!(fields, stage, |_, me| Ok(FolderStage::new(me.stage.clone()))); cached_field!(fields, window, |_, me| Files::make(me.window.clone(), me, &me.tab)); diff --git a/yazi-actor/src/lives/preference.rs b/yazi-actor/src/lives/preference.rs index f77a30b5..a7236363 100644 --- a/yazi-actor/src/lives/preference.rs +++ b/yazi-actor/src/lives/preference.rs @@ -32,7 +32,7 @@ impl UserData for Preference { fields.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); fields.add_field_method_get("sort_translit", |_, me| Ok(me.sort_translit)); - cached_field!(fields, linemode, |_, me| Ok(me.linemode.to_string())); + cached_field!(fields, linemode, |lua, me| lua.create_string(&me.linemode)); fields.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden)); } } diff --git a/yazi-actor/src/lives/selected.rs b/yazi-actor/src/lives/selected.rs index b3569b96..2956dd37 100644 --- a/yazi-actor/src/lives/selected.rs +++ b/yazi-actor/src/lives/selected.rs @@ -11,7 +11,7 @@ impl Selected { let inner = PtrCell::from(inner); Lives::scoped_userdata(yazi_binding::Iter::new( - inner.as_static().values().cloned().map(yazi_binding::Url::new), + inner.as_static().values().map(yazi_binding::Url::new), Some(inner.len()), )) } diff --git a/yazi-actor/src/lives/yanked.rs b/yazi-actor/src/lives/yanked.rs index 8854936d..d5b9ee92 100644 --- a/yazi-actor/src/lives/yanked.rs +++ b/yazi-actor/src/lives/yanked.rs @@ -23,7 +23,7 @@ impl Yanked { Lives::scoped_userdata(Self { inner, iter: Lives::scoped_userdata(Iter::new( - inner.as_static().iter().cloned().map(yazi_binding::Url::new), + inner.as_static().iter().map(yazi_binding::Url::new), Some(inner.len()), ))?, }) diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 244c8e22..5025bc70 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -34,13 +34,13 @@ impl Actor for Cd { // Take parent to history if let Some(t) = tab.parent.take() { - tab.history.insert(t.url.to_owned(), t); + tab.history.insert(t.url.clone(), t); } // Current let rep = tab.history.remove_or(&opt.target); let rep = mem::replace(&mut tab.current, rep); - tab.history.insert(rep.url.to_owned(), rep); + tab.history.insert(rep.url.clone(), rep); // Parent if let Some(parent) = opt.target.parent() { diff --git a/yazi-actor/src/mgr/hover.rs b/yazi-actor/src/mgr/hover.rs index 51c3b379..bb16eab4 100644 --- a/yazi-actor/src/mgr/hover.rs +++ b/yazi-actor/src/mgr/hover.rs @@ -30,7 +30,7 @@ impl Actor for Hover { { // `hover(Some)` occurs after user actions, such as create, rename, reveal, etc. // At this point, it's intuitive to track the location of the file regardless. - tab.current.trace = Some(u.to_owned()); + tab.current.trace = Some(u.clone()); } // Publish through DDS diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index b174d9a8..a81599ae 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -57,11 +57,8 @@ impl Refresh { } } - let futs: Vec<_> = folders - .iter() - .filter(|&f| f.url.is_internal()) - .map(|&f| go(f.url.to_owned(), f.cha)) - .collect(); + let futs: Vec<_> = + folders.iter().filter(|&f| f.url.is_internal()).map(|&f| go(f.url.clone(), f.cha)).collect(); if !futs.is_empty() { tokio::spawn(futures::future::join_all(futs)); diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index 9a7bd550..b05d654a 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi image adapter" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-config = { path = "../yazi-config", version = "25.9.15" } yazi-fs = { path = "../yazi-fs", version = "25.9.15" } diff --git a/yazi-adapter/src/brand.rs b/yazi-adapter/src/brand.rs index 91c740f6..c180591c 100644 --- a/yazi-adapter/src/brand.rs +++ b/yazi-adapter/src/brand.rs @@ -44,42 +44,40 @@ impl Brand { } pub fn from_env() -> Option { - use Brand as B; - - let (term, program) = B::env(); + let (term, program) = Self::env(); let vars = [ - ("KITTY_WINDOW_ID", B::Kitty), - ("KONSOLE_VERSION", B::Konsole), - ("ITERM_SESSION_ID", B::Iterm2), - ("WEZTERM_EXECUTABLE", B::WezTerm), - ("GHOSTTY_RESOURCES_DIR", B::Ghostty), - ("WT_Session", B::Microsoft), - ("WARP_HONOR_PS1", B::Warp), - ("VSCODE_INJECTION", B::VSCode), - ("TABBY_CONFIG_DIRECTORY", B::Tabby), + ("KITTY_WINDOW_ID", Self::Kitty), + ("KONSOLE_VERSION", Self::Konsole), + ("ITERM_SESSION_ID", Self::Iterm2), + ("WEZTERM_EXECUTABLE", Self::WezTerm), + ("GHOSTTY_RESOURCES_DIR", Self::Ghostty), + ("WT_Session", Self::Microsoft), + ("WARP_HONOR_PS1", Self::Warp), + ("VSCODE_INJECTION", Self::VSCode), + ("TABBY_CONFIG_DIRECTORY", Self::Tabby), ]; match term.as_str() { - "xterm-kitty" => return Some(B::Kitty), - "foot" => return Some(B::Foot), - "foot-extra" => return Some(B::Foot), - "xterm-ghostty" => return Some(B::Ghostty), - "rio" => return Some(B::Rio), - "rxvt-unicode-256color" => return Some(B::Urxvt), + "xterm-kitty" => return Some(Self::Kitty), + "foot" => return Some(Self::Foot), + "foot-extra" => return Some(Self::Foot), + "xterm-ghostty" => return Some(Self::Ghostty), + "rio" => return Some(Self::Rio), + "rxvt-unicode-256color" => return Some(Self::Urxvt), _ => {} } match program.as_str() { - "iTerm.app" => return Some(B::Iterm2), - "WezTerm" => return Some(B::WezTerm), - "ghostty" => return Some(B::Ghostty), - "WarpTerminal" => return Some(B::Warp), - "rio" => return Some(B::Rio), - "BlackBox" => return Some(B::BlackBox), - "vscode" => return Some(B::VSCode), - "Tabby" => return Some(B::Tabby), - "Hyper" => return Some(B::Hyper), - "mintty" => return Some(B::Mintty), - "Apple_Terminal" => return Some(B::Apple), + "iTerm.app" => return Some(Self::Iterm2), + "WezTerm" => return Some(Self::WezTerm), + "ghostty" => return Some(Self::Ghostty), + "WarpTerminal" => return Some(Self::Warp), + "rio" => return Some(Self::Rio), + "BlackBox" => return Some(Self::BlackBox), + "vscode" => return Some(Self::VSCode), + "Tabby" => return Some(Self::Tabby), + "Hyper" => return Some(Self::Hyper), + "mintty" => return Some(Self::Mintty), + "Apple_Terminal" => return Some(Self::Apple), _ => {} } if let Some((var, brand)) = vars.into_iter().find(|&(s, _)| env_exists(s)) { @@ -91,30 +89,28 @@ impl Brand { } pub(super) fn adapters(self) -> &'static [crate::Adapter] { - use Brand as B; - use crate::Adapter as A; match self { - B::Kitty => &[A::Kgp], - B::Konsole => &[A::KgpOld], - B::Iterm2 => &[A::Iip, A::Sixel], - B::WezTerm => &[A::Iip, A::Sixel], - B::Foot => &[A::Sixel], - B::Ghostty => &[A::Kgp], - B::Microsoft => &[A::Sixel], - B::Warp => &[A::Iip, A::KgpOld], - B::Rio => &[A::Iip, A::Sixel], - B::BlackBox => &[A::Sixel], - B::VSCode => &[A::Iip, A::Sixel], - B::Tabby => &[A::Iip, A::Sixel], - B::Hyper => &[A::Iip, A::Sixel], - B::Mintty => &[A::Iip], - B::Tmux => &[], - B::VTerm => &[], - B::Apple => &[], - B::Urxvt => &[], - B::Bobcat => &[A::Iip, A::Sixel], + Self::Kitty => &[A::Kgp], + Self::Konsole => &[A::KgpOld], + Self::Iterm2 => &[A::Iip, A::Sixel], + Self::WezTerm => &[A::Iip, A::Sixel], + Self::Foot => &[A::Sixel], + Self::Ghostty => &[A::Kgp], + Self::Microsoft => &[A::Sixel], + Self::Warp => &[A::Iip, A::KgpOld], + Self::Rio => &[A::Iip, A::Sixel], + Self::BlackBox => &[A::Sixel], + Self::VSCode => &[A::Iip, A::Sixel], + Self::Tabby => &[A::Iip, A::Sixel], + Self::Hyper => &[A::Iip, A::Sixel], + Self::Mintty => &[A::Iip], + Self::Tmux => &[], + Self::VTerm => &[], + Self::Apple => &[], + Self::Urxvt => &[], + Self::Bobcat => &[A::Iip, A::Sixel], } } diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index cd0d3303..cf37ad5f 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::option_map_unit_fn)] - yazi_macro::mod_pub!(drivers); yazi_macro::mod_flat!(adapter brand dimension emulator image info mux unknown); diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 06f3d7b5..c80ae7c7 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -56,9 +56,7 @@ impl From for yazi_shared::url::UrlCow<'_> { impl TryFrom<&[u8]> for Url { type Error = mlua::Error; - fn try_from(value: &[u8]) -> mlua::Result { - Ok(Self::new(UrlCow::try_from(value)?.into_owned())) - } + fn try_from(value: &[u8]) -> mlua::Result { Ok(Self::new(UrlCow::try_from(value)?)) } } impl Url { diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 1130dcc7..a7aa5449 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi bootstrapper" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [dependencies] yazi-adapter = { path = "../yazi-adapter", version = "25.9.15" } yazi-config = { path = "../yazi-config", version = "25.9.15" } diff --git a/yazi-build/Cargo.toml b/yazi-build/Cargo.toml index d21e1cea..04bc8e69 100644 --- a/yazi-build/Cargo.toml +++ b/yazi-build/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi build system" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [profile.release] codegen-units = 1 lto = true diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 9ea61a80..8be1fcd0 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi command-line interface" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [profile.release] codegen-units = 1 lto = true diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index b22d5f82..08af4e41 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -8,6 +8,9 @@ description = "Yazi code generator" homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" +[lints] +workspace = true + [lib] proc-macro = true diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 4f94bf8b..b95d01c7 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::if_same_then_else, clippy::len_without_is_empty, clippy::option_map_unit_fn)] - yazi_macro::mod_pub!(cmp confirm help input mgr notify pick spot tab tasks which); yazi_macro::mod_flat!(core); diff --git a/yazi-dds/src/lib.rs b/yazi-dds/src/lib.rs index 095e15bb..652bf3c9 100644 --- a/yazi-dds/src/lib.rs +++ b/yazi-dds/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::option_map_unit_fn)] - mod macros; yazi_macro::mod_pub!(ember spark); diff --git a/yazi-dds/src/sendable.rs b/yazi-dds/src/sendable.rs index 3b8953ed..0fa95ff3 100644 --- a/yazi-dds/src/sendable.rs +++ b/yazi-dds/src/sendable.rs @@ -120,7 +120,7 @@ impl Sendable { Value::Table(tbl) } Data::Id(i) => yazi_binding::Id(*i).into_lua(lua)?, - Data::Url(u) => yazi_binding::Url::new(u.clone()).into_lua(lua)?, + Data::Url(u) => yazi_binding::Url::new(u).into_lua(lua)?, Data::Path(u) => yazi_binding::Path::new(u).into_lua(lua)?, Data::Bytes(b) => Value::String(lua.create_string(b)?), Data::Any(a) => { @@ -243,7 +243,7 @@ impl Sendable { DataKey::Number(n) => Value::Number(n.0), DataKey::String(s) => Value::String(lua.create_string(&**s)?), DataKey::Id(i) => yazi_binding::Id(*i).into_lua(lua)?, - DataKey::Url(u) => yazi_binding::Url::new(u.clone()).into_lua(lua)?, + DataKey::Url(u) => yazi_binding::Url::new(u).into_lua(lua)?, DataKey::Path(u) => yazi_binding::Path::new(u).into_lua(lua)?, DataKey::Bytes(b) => Value::String(lua.create_string(b)?), }) diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index d5a7cb36..3968ab5a 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -60,7 +60,7 @@ tokio-stream = { workspace = true } # Logging tracing = { workspace = true } tracing-appender = "0.2.4" -tracing-subscriber = { version = "0.3.20", features = [ "env-filter" ] } +tracing-subscriber = { version = "0.3.22", features = [ "env-filter" ] } [target."cfg(unix)".dependencies] libc = { workspace = true } diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index bd0032bb..9033317f 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -1,5 +1,3 @@ -#![allow(clippy::if_same_then_else)] - #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index 52fd93ce..36d5962a 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn)] - yazi_macro::mod_pub!(cha error mounts path provider); yazi_macro::mod_flat!(cwd file files filter fns hash op scheme sorter sorting splatter stage url xdg); diff --git a/yazi-plugin/src/lib.rs b/yazi-plugin/src/lib.rs index 9b767077..a07afcc1 100644 --- a/yazi-plugin/src/lib.rs +++ b/yazi-plugin/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::if_same_then_else)] - yazi_macro::mod_pub!(bindings elements external fs isolate loader process pubsub runtime theme utils); yazi_macro::mod_flat!(lua); diff --git a/yazi-scheduler/src/lib.rs b/yazi-scheduler/src/lib.rs index 6a288622..4120f1a8 100644 --- a/yazi-scheduler/src/lib.rs +++ b/yazi-scheduler/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::option_map_unit_fn)] - mod macros; yazi_macro::mod_pub!(file plugin prework process); diff --git a/yazi-sftp/src/lib.rs b/yazi-sftp/src/lib.rs index 9a0e87cc..2b6d05c0 100644 --- a/yazi-sftp/src/lib.rs +++ b/yazi-sftp/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::len_without_is_empty)] - pub mod fs; pub mod requests; pub mod responses; diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 6c3406f6..6471e522 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::option_map_unit_fn)] - yazi_macro::mod_pub!(data errors event loc path pool scheme shell strand translit url wtf8); yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os predictor ro_cell source sync_cell terminal tests throttle time utf8); diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 807c65a9..1699e061 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -42,7 +42,7 @@ impl Hash for PathBufDyn { // --- PartialEq impl PartialEq for PathBufDyn { - fn eq(&self, other: &PathBufDyn) -> bool { self.as_path() == other.as_path() } + fn eq(&self, other: &Self) -> bool { self.as_path() == other.as_path() } } impl PartialEq> for PathBufDyn { diff --git a/yazi-shared/src/url/cov.rs b/yazi-shared/src/url/cov.rs index f9439f85..42520ae9 100644 --- a/yazi-shared/src/url/cov.rs +++ b/yazi-shared/src/url/cov.rs @@ -54,6 +54,10 @@ impl From for UrlBuf { fn from(value: UrlBufCov) -> Self { value.0 } } +impl From<&UrlBufCov> for UrlBuf { + fn from(value: &UrlBufCov) -> Self { value.0.clone() } +} + impl From for UrlBufCov { fn from(value: UrlBuf) -> Self { Self(value) } } diff --git a/yazi-vfs/src/lib.rs b/yazi-vfs/src/lib.rs index 1b3ab656..8abc3e7d 100644 --- a/yazi-vfs/src/lib.rs +++ b/yazi-vfs/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::if_same_then_else)] - yazi_macro::mod_pub!(provider); yazi_macro::mod_flat!(cha file files fns op); diff --git a/yazi-widgets/src/lib.rs b/yazi-widgets/src/lib.rs index 2b44b317..aab44f98 100644 --- a/yazi-widgets/src/lib.rs +++ b/yazi-widgets/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::if_same_then_else)] - yazi_macro::mod_pub!(input); yazi_macro::mod_flat!(clipboard scrollable); From 81ccdd8b645ebfa9471c2d8bb13a7c23abc0fe74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sat, 29 Nov 2025 10:06:05 +0800 Subject: [PATCH 16/53] feat: include stderr along with existing error code when video preview fails (#3383) --- yazi-plugin/preset/plugins/video.lua | 25 ++++++++++++------------ yazi-plugin/src/process/command.rs | 29 ++++++++++++++-------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/yazi-plugin/preset/plugins/video.lua b/yazi-plugin/preset/plugins/video.lua index 3edefcc6..890c48cd 100644 --- a/yazi-plugin/preset/plugins/video.lua +++ b/yazi-plugin/preset/plugins/video.lua @@ -52,36 +52,35 @@ function M:preload(job) return false end - -- stylua: ignore - local cmd = Command("ffmpeg"):arg({ - "-v", "quiet", "-threads", 1, "-hwaccel", "auto", - "-skip_frame", "nokey", - "-an", "-sn", "-dn", - }) + local cmd = Command("ffmpeg") + :stderr(Command.PIPED) + :arg { "-v", "warning", "-hwaccel", "auto", "-threads", 1, "-an", "-sn", "-dn" } if percent ~= 0 then cmd:arg { "-ss", math.floor(meta.format.duration * percent / 100) } end - cmd:arg { "-i", tostring(job.file.url) } + cmd:arg { "-skip_frame", "nokey", "-i", tostring(job.file.url) } if percent == 0 then cmd:arg { "-map", "disp:attached_pic" } end -- stylua: ignore - local status, err = cmd:arg({ + local output, err = cmd:arg({ "-vframes", 1, "-q:v", 31 - math.floor(rt.preview.image_quality * 0.3), "-vf", string.format("scale='min(%d,iw)':'min(%d,ih)':force_original_aspect_ratio=decrease:flags=fast_bilinear", rt.preview.max_width, rt.preview.max_height), "-f", "image2", "-y", tostring(cache), - }):status() + }):output() - if not status then + if not output then return true, Err("Failed to start `ffmpeg`, error: %s", err) - elseif not status.success then - return false, Err("`ffmpeg` exited with error code: %s", status.code) - else + elseif output.status.success then return true + elseif output.stderr:find("No filtered frames for output stream", 1, true) then + return true, Err("No more keyframes available") + else + return false, Err("`ffmpeg` exited with error code %s: %s", output.status.code, output.stderr) end end diff --git a/yazi-plugin/src/process/command.rs b/yazi-plugin/src/process/command.rs index 93fb9a48..67d4b3d4 100644 --- a/yazi-plugin/src/process/command.rs +++ b/yazi-plugin/src/process/command.rs @@ -1,6 +1,6 @@ use std::{any::TypeId, ffi::OsStr, io, process::Stdio}; -use mlua::{AnyUserData, ExternalError, IntoLuaMulti, Lua, MetaMethod, Table, UserData, Value}; +use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Lua, MetaMethod, Table, UserData, Value}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; use yazi_binding::Error; use yazi_shared::wtf8::FromWtf8; @@ -139,22 +139,21 @@ impl UserData for Command { ) } - methods.add_function_mut("arg", |_, (ud, arg): (AnyUserData, Value)| { - { - let mut me = ud.borrow_mut::()?; - match arg { - Value::String(s) => { - me.inner.arg(OsStr::from_wtf8(&s.as_bytes())?); - } - Value::Table(t) => { - for s in t.sequence_values::() { - me.inner.arg(OsStr::from_wtf8(&s?.as_bytes())?); - } - } - _ => return Err("arg must be a string or table of strings".into_lua_err()), + methods.add_function_mut("arg", |lua, (ud, arg): (AnyUserData, Value)| { + let mut me = ud.borrow_mut::()?; + match arg { + Value::Nil => return lua.create_sequence_from(me.inner.as_std().get_args())?.into_lua(lua), + Value::String(s) => { + me.inner.arg(OsStr::from_wtf8(&s.as_bytes())?); } + Value::Table(t) => { + for s in t.sequence_values::() { + me.inner.arg(OsStr::from_wtf8(&s?.as_bytes())?); + } + } + _ => Err("arg must be a string or table of strings".into_lua_err())?, } - Ok(ud) + ud.into_lua(lua) }); methods.add_function_mut("cwd", |_, (ud, dir): (AnyUserData, mlua::String)| { ud.borrow_mut::()?.inner.current_dir(dir.to_str()?.as_ref()); From ade1025a7426028a668307b6241caa79c41f8eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sat, 29 Nov 2025 15:02:25 +0800 Subject: [PATCH 17/53] feat: timeouts for SFTP operations (#3384) --- yazi-sftp/src/error.rs | 4 +++ yazi-sftp/src/fs/file.rs | 12 ++++---- yazi-sftp/src/lib.rs | 2 ++ yazi-sftp/src/operator.rs | 23 ++++----------- yazi-sftp/src/receiver.rs | 47 +++++++++++++++++++++++++++++++ yazi-sftp/src/responses/status.rs | 9 ++++++ yazi-sftp/src/session.rs | 27 ++++++++++-------- 7 files changed, 90 insertions(+), 34 deletions(-) create mode 100644 yazi-sftp/src/receiver.rs diff --git a/yazi-sftp/src/error.rs b/yazi-sftp/src/error.rs index 1fd74de9..0fe10a89 100644 --- a/yazi-sftp/src/error.rs +++ b/yazi-sftp/src/error.rs @@ -59,6 +59,10 @@ impl From for Error { fn from(_: tokio::sync::oneshot::error::RecvError) -> Self { Self::custom("channel closed") } } +impl From for Error { + fn from(_: tokio::time::error::Elapsed) -> Self { Self::Timeout } +} + impl std::error::Error for Error {} impl std::fmt::Display for Error { diff --git a/yazi-sftp/src/fs/file.rs b/yazi-sftp/src/fs/file.rs index fca67713..71b53c77 100644 --- a/yazi-sftp/src/fs/file.rs +++ b/yazi-sftp/src/fs/file.rs @@ -1,8 +1,8 @@ use std::{io, pin::Pin, sync::Arc, task::{Context, Poll, ready}, time::Duration}; -use tokio::{io::{AsyncRead, AsyncWrite, ReadBuf}, sync::oneshot, time::{Timeout, timeout}}; +use tokio::{io::{AsyncRead, AsyncWrite, ReadBuf}, time::{Timeout, timeout}}; -use crate::{Error, Operator, Packet, Session, fs::Attrs}; +use crate::{Error, Operator, Packet, Receiver, Session, fs::Attrs}; pub struct File { session: Arc, @@ -10,10 +10,10 @@ pub struct File { closed: bool, cursor: u64, - close_rx: Option>>>, - read_rx: Option>>, - write_rx: Option<(oneshot::Receiver>, usize)>, - flush_rx: Option>>>, + close_rx: Option>, + read_rx: Option, + write_rx: Option<(Receiver, usize)>, + flush_rx: Option>, } impl Unpin for File {} diff --git a/yazi-sftp/src/lib.rs b/yazi-sftp/src/lib.rs index 2b6d05c0..ea980de9 100644 --- a/yazi-sftp/src/lib.rs +++ b/yazi-sftp/src/lib.rs @@ -9,6 +9,7 @@ mod macros; mod operator; mod packet; mod path; +mod receiver; mod ser; mod session; @@ -18,5 +19,6 @@ pub(crate) use id::*; pub use operator::*; pub use packet::*; pub use path::*; +pub use receiver::*; pub(crate) use ser::*; pub use session::*; diff --git a/yazi-sftp/src/operator.rs b/yazi-sftp/src/operator.rs index b28cb181..143e1963 100644 --- a/yazi-sftp/src/operator.rs +++ b/yazi-sftp/src/operator.rs @@ -1,15 +1,14 @@ use std::{ops::Deref, sync::Arc}; use russh::{ChannelStream, client::Msg}; -use tokio::sync::oneshot; use typed_path::UnixPathBuf; -use crate::{AsSftpPath, Error, Packet, Session, SftpPath, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; +use crate::{AsSftpPath, Error, Receiver, Session, SftpPath, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; pub struct Operator(Arc); impl Deref for Operator { - type Target = Session; + type Target = Arc; fn deref(&self) -> &Self::Target { &self.0 } } @@ -36,25 +35,15 @@ impl Operator { Ok(File::new(&self.0, handle.handle)) } - pub fn close(&self, handle: &str) -> Result>, Error> { + pub fn close(&self, handle: &str) -> Result { self.send_sync(requests::Close::new(handle)) } - pub fn read( - &self, - handle: &str, - offset: u64, - len: u32, - ) -> Result>, Error> { + pub fn read(&self, handle: &str, offset: u64, len: u32) -> Result { self.send_sync(requests::Read::new(handle, offset, len)) } - pub fn write( - &self, - handle: &str, - offset: u64, - data: &[u8], - ) -> Result>, Error> { + pub fn write(&self, handle: &str, offset: u64, data: &[u8]) -> Result { self.send_sync(requests::Write::new(handle, offset, data)) } @@ -183,7 +172,7 @@ impl Operator { status.into() } - pub fn fsync(&self, handle: &str) -> Result>, Error> { + pub fn fsync(&self, handle: &str) -> Result { if self.extensions.lock().get("fsync@openssh.com").is_none_or(|s| s != "1") { return Err(Error::Unsupported); } diff --git a/yazi-sftp/src/receiver.rs b/yazi-sftp/src/receiver.rs new file mode 100644 index 00000000..c0304586 --- /dev/null +++ b/yazi-sftp/src/receiver.rs @@ -0,0 +1,47 @@ +use std::{pin::Pin, sync::Arc, task::Poll}; + +use tokio::sync::oneshot; + +use crate::{Packet, Session}; + +pub struct Receiver { + rx: oneshot::Receiver>, + received: bool, + + session: Arc, + id: u32, +} + +impl Drop for Receiver { + fn drop(&mut self) { + if !self.received { + self.session.callback.lock().remove(&self.id); + } + } +} + +impl Receiver { + pub(crate) fn new( + session: &Arc, + id: u32, + rx: oneshot::Receiver>, + ) -> Self { + Self { rx, received: false, session: session.clone(), id } + } +} + +impl Future for Receiver { + type Output = Result, oneshot::error::RecvError>; + + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let me = self.get_mut(); + match Pin::new(&mut me.rx).poll(cx) { + Poll::Ready(Ok(packet)) => { + me.received = true; + Poll::Ready(Ok(packet)) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/yazi-sftp/src/responses/status.rs b/yazi-sftp/src/responses/status.rs index fc864c89..137267df 100644 --- a/yazi-sftp/src/responses/status.rs +++ b/yazi-sftp/src/responses/status.rs @@ -27,6 +27,15 @@ impl Status { pub fn is_ok(&self) -> bool { self.code == StatusCode::Ok } pub fn is_eof(&self) -> bool { self.code == StatusCode::Eof } + + pub(crate) fn connection_lost(id: u32) -> Self { + Self { + id, + code: StatusCode::ConnectionLost, + message: "connection lost".to_owned(), + language: "en".to_owned(), + } + } } #[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] diff --git a/yazi-sftp/src/session.rs b/yazi-sftp/src/session.rs index 1f1c1c44..0857bc2c 100644 --- a/yazi-sftp/src/session.rs +++ b/yazi-sftp/src/session.rs @@ -1,16 +1,16 @@ -use std::{any::TypeId, collections::HashMap, io::{self, ErrorKind}, sync::Arc}; +use std::{any::TypeId, collections::HashMap, io::{self, ErrorKind}, sync::Arc, time::Duration}; use parking_lot::Mutex; use russh::{ChannelStream, client::Msg}; use serde::Serialize; -use tokio::{io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, select, sync::{mpsc, oneshot}}; +use tokio::{io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, select, sync::{mpsc, oneshot}, time::timeout}; -use crate::{Error, Id, Packet, responses}; +use crate::{Error, Id, Packet, Receiver, responses}; pub struct Session { tx: mpsc::UnboundedSender>, id: Id, - callback: Mutex>>>, + pub(super) callback: Mutex>>>, pub(super) extensions: Mutex>, } @@ -43,6 +43,7 @@ impl Session { } } + let me_ = me.clone(); let (mut reader, mut writer) = tokio::io::split(stream); tokio::spawn(async move { while let Some(data) = rx.recv().await { @@ -51,6 +52,9 @@ impl Session { { rx.close(); writer.shutdown().await.ok(); + for (id, cb) in me_.callback.lock().drain() { + cb.send(responses::Status::connection_lost(id).into()).ok(); + } break; } } @@ -84,12 +88,12 @@ impl Session { me } - pub async fn send<'a, I, O>(&self, input: I) -> Result + pub async fn send<'a, I, O>(self: &Arc, input: I) -> Result where I: Into> + Serialize, O: TryFrom, Error = Error> + 'static, { - match self.send_sync(input)?.await? { + match timeout(Duration::from_secs(30), self.send_sync(input)?).await?? { Packet::Status(status) if TypeId::of::() != TypeId::of::() => { Err(Error::Status(status)) } @@ -97,7 +101,7 @@ impl Session { } } - pub fn send_sync<'a, I>(&self, input: I) -> Result>, Error> + pub fn send_sync<'a, I>(self: &Arc, input: I) -> Result where I: Into> + Serialize, { @@ -106,12 +110,13 @@ impl Session { request = request.with_id(self.id.next()); } + let id = request.id(); let (tx, rx) = oneshot::channel(); - self.callback.lock().insert(request.id(), tx); - self.tx.send(crate::to_bytes(request)?)?; - Ok(rx) + self.callback.lock().insert(id, tx); + self.tx.send(crate::to_bytes(request)?)?; + Ok(Receiver::new(self, id, rx)) } - pub fn is_closed(&self) -> bool { self.tx.is_closed() } + pub fn is_closed(self: &Arc) -> bool { self.tx.is_closed() } } From 76196aab70e04065d43bee9ee41eeb1b547995df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sun, 30 Nov 2025 12:08:59 +0800 Subject: [PATCH 18/53] refactor!: return `Path` instead of `Url` from `File.link_to` (#3385) --- CHANGELOG.md | 3 ++- Cargo.lock | 4 ++-- yazi-actor/src/mgr/enter.rs | 2 +- yazi-actor/src/mgr/escape.rs | 2 +- yazi-actor/src/mgr/follow.rs | 4 ++-- yazi-actor/src/mgr/refresh.rs | 7 +++++-- yazi-actor/src/mgr/search.rs | 2 +- yazi-actor/src/mgr/stash.rs | 4 ++-- yazi-actor/src/mgr/tab_create.rs | 2 +- yazi-binding/src/macros.rs | 2 +- yazi-core/src/tab/preview.rs | 2 +- yazi-fs/src/file.rs | 19 +------------------ yazi-parser/src/mgr/cd.rs | 1 + yazi-plugin/preset/components/tasks.lua | 8 ++++++-- yazi-shared/src/url/buf.rs | 24 +----------------------- yazi-shared/src/url/cow.rs | 5 ----- yazi-shared/src/url/like.rs | 8 ++++++++ yazi-shared/src/url/url.rs | 14 +++++++++++++- yazi-vfs/src/files.rs | 2 +- 19 files changed, 50 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ed6fd5..41a7587b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Rename `name` to `url` for open, fetchers, spotters, preloaders, previewers, filetype, and `globs` icon rules to support virtual file system ([#3034]) - Rename `mime` fetcher to `mime.local`, and introduce `mime.dir` fetcher to support folder MIME types ([#3222]) - Remove `$0` parameter in opener rules to make the `open` command work under empty directories ([#3226]) -- Return `Path` instead of `Url` from `Url:strip_prefix()` to enforce type safety ([#3361]) +- Return `Path` instead of `Url` from `Url:strip_prefix()` and `File.link_to` to enforce type safety ([#3361], [#3385]) - Use `body` instead of the term `content` in confirmations ([#2921]) - Use `u16` instead of `u32` as the type of `max_width` and `max_height` options to avoid memory exhaustion ([#3313]) - Implement `__pairs` metamethod instead of `__index` for the callback argument of the `@yank` DDS event ([#2997]) @@ -1543,3 +1543,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3360]: https://github.com/sxyazi/yazi/pull/3360 [#3364]: https://github.com/sxyazi/yazi/pull/3364 [#3369]: https://github.com/sxyazi/yazi/pull/3369 +[#3385]: https://github.com/sxyazi/yazi/pull/3385 diff --git a/Cargo.lock b/Cargo.lock index d3a753f9..3267b6f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1764,9 +1764,9 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" dependencies = [ "darling", "indoc", diff --git a/yazi-actor/src/mgr/enter.rs b/yazi-actor/src/mgr/enter.rs index 4ec6ef85..abae6034 100644 --- a/yazi-actor/src/mgr/enter.rs +++ b/yazi-actor/src/mgr/enter.rs @@ -1,7 +1,7 @@ use anyhow::Result; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::CdSource}; -use yazi_shared::data::Data; +use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; diff --git a/yazi-actor/src/mgr/escape.rs b/yazi-actor/src/mgr/escape.rs index aee89094..ed4bbde0 100644 --- a/yazi-actor/src/mgr/escape.rs +++ b/yazi-actor/src/mgr/escape.rs @@ -2,7 +2,7 @@ use anyhow::{Result, bail}; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::{VoidOpt, mgr::EscapeOpt}; use yazi_proxy::AppProxy; -use yazi_shared::data::Data; +use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; diff --git a/yazi-actor/src/mgr/follow.rs b/yazi-actor/src/mgr/follow.rs index c79a3bf9..b0a699dc 100644 --- a/yazi-actor/src/mgr/follow.rs +++ b/yazi-actor/src/mgr/follow.rs @@ -1,7 +1,7 @@ use anyhow::Result; use yazi_fs::path::clean_url; use yazi_macro::{act, succ}; -use yazi_parser::VoidOpt; +use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; @@ -18,6 +18,6 @@ impl Actor for Follow { let Some(link_to) = &file.link_to else { succ!() }; let Some(parent) = file.url.parent() else { succ!() }; let Ok(joined) = parent.try_join(link_to) else { succ!() }; - act!(mgr:reveal, cx, clean_url(joined)) + act!(mgr:reveal, cx, (clean_url(joined), CdSource::Follow)) } } diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index a81599ae..7ca8ff14 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -57,8 +57,11 @@ impl Refresh { } } - let futs: Vec<_> = - folders.iter().filter(|&f| f.url.is_internal()).map(|&f| go(f.url.clone(), f.cha)).collect(); + let futs: Vec<_> = folders + .iter() + .filter(|&f| f.url.is_absolute() && f.url.is_internal()) + .map(|&f| go(f.url.clone(), f.cha)) + .collect(); if !futs.is_empty() { tokio::spawn(futures::future::join_all(futs)); diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index b559f3e4..eac83e7c 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -9,7 +9,7 @@ use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::{CdSource, SearchOpt, SearchOptVia}}; use yazi_plugin::external; use yazi_proxy::{InputProxy, MgrProxy}; -use yazi_shared::data::Data; +use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; diff --git a/yazi-actor/src/mgr/stash.rs b/yazi-actor/src/mgr/stash.rs index eb1bd502..33f7a3a9 100644 --- a/yazi-actor/src/mgr/stash.rs +++ b/yazi-actor/src/mgr/stash.rs @@ -2,7 +2,7 @@ use anyhow::Result; use yazi_dds::spark::SparkKind; use yazi_macro::succ; use yazi_parser::mgr::StashOpt; -use yazi_shared::{Source, data::Data, url::AsUrl}; +use yazi_shared::{Source, data::Data, url::{AsUrl, UrlLike}}; use crate::{Actor, Ctx}; @@ -14,7 +14,7 @@ impl Actor for Stash { const NAME: &str = "stash"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { - if opt.target.is_regular() { + if opt.target.is_internal() { cx.tab_mut().backstack.push(opt.target.as_url()); } diff --git a/yazi-actor/src/mgr/tab_create.rs b/yazi-actor/src/mgr/tab_create.rs index 2461f6e2..bc9263e3 100644 --- a/yazi-actor/src/mgr/tab_create.rs +++ b/yazi-actor/src/mgr/tab_create.rs @@ -3,7 +3,7 @@ use yazi_core::tab::Tab; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::{CdSource, TabCreateOpt}; use yazi_proxy::AppProxy; -use yazi_shared::data::Data; +use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index 9e0bbcb3..e9ff87f2 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -151,7 +151,7 @@ macro_rules! impl_file_fields { ($fields:ident) => { $crate::cached_field!($fields, cha, |_, me| Ok($crate::Cha(me.cha))); $crate::cached_field!($fields, url, |_, me| Ok($crate::Url::new(me.url_owned()))); - $crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to_url().map($crate::Url::new))); + $crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to.as_ref().map($crate::Path::new))); $crate::cached_field!($fields, name, |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index 3cfd7ec5..cc7143cb 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -9,7 +9,7 @@ use yazi_fs::{File, Files, FilesOp, cha::Cha}; use yazi_macro::render; use yazi_parser::mgr::PreviewLock; use yazi_plugin::{external::Highlighter, isolate}; -use yazi_shared::{pool::Symbol, url::UrlBuf}; +use yazi_shared::{pool::Symbol, url::{UrlBuf, UrlLike}}; use yazi_vfs::{VfsFiles, VfsFilesOp}; #[derive(Default)] diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index 399ca4c0..dbf067b4 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -1,6 +1,6 @@ use std::{hash::{Hash, Hasher}, ops::Deref, path::Path}; -use yazi_shared::{loc::Loc, path::{PathBufDyn, PathDyn, PathLike}, strand::Strand, url::{Url, UrlBuf, UrlLike}}; +use yazi_shared::{path::{PathBufDyn, PathDyn}, strand::Strand, url::{UrlBuf, UrlLike}}; use crate::cha::{Cha, ChaType}; @@ -47,23 +47,6 @@ impl File { #[inline] pub fn stem(&self) -> Option> { self.url.stem() } - - pub fn link_to_url(&self) -> Option> { - let to = self.link_to.as_ref()?; - let kind = self.url.kind(); - Some(match &self.url { - UrlBuf::Regular(_) => Url::Regular(Loc::bare(to.as_os().ok()?)), - UrlBuf::Search { domain, .. } => { - Url::Search { loc: Loc::saturated(to.as_os().ok()?, kind), domain } - } - UrlBuf::Archive { domain, .. } => { - Url::Archive { loc: Loc::saturated(to.as_os().ok()?, kind), domain } - } - UrlBuf::Sftp { domain, .. } => { - Url::Sftp { loc: Loc::saturated(to.as_unix().ok()?, kind), domain } - } - }) - } } impl Hash for File { diff --git a/yazi-parser/src/mgr/cd.rs b/yazi-parser/src/mgr/cd.rs index ee83f606..73752618 100644 --- a/yazi-parser/src/mgr/cd.rs +++ b/yazi-parser/src/mgr/cd.rs @@ -55,6 +55,7 @@ pub enum CdSource { Reveal, Enter, Leave, + Follow, Forward, Back, Escape, diff --git a/yazi-plugin/preset/components/tasks.lua b/yazi-plugin/preset/components/tasks.lua index 1976b72e..9ee4f185 100644 --- a/yazi-plugin/preset/components/tasks.lua +++ b/yazi-plugin/preset/components/tasks.lua @@ -63,9 +63,13 @@ end function Tasks:icon(snap) if snap.prog.kind == "FilePaste" then - return " " + return snap.name:find("Copy ", 1, true) == 1 and " " or " " elseif snap.prog.kind == "FileDelete" then return " " + elseif snap.prog.kind == "FileDownload" then + return " " + elseif snap.prog.kind == "FileUpload" then + return " " else return " " end @@ -73,7 +77,7 @@ end function Tasks:progress_redraw(snap, y) local kind = snap.prog.kind - if kind == "FilePaste" or kind == "FileDelete" then + if kind == "FilePaste" or kind == "FileDelete" or kind == "FileDownload" or kind == "FileUpload" then local percent if snap.success then percent = "Cleaning…" diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index b21688bc..3acfca56 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt::{Debug, Formatter}, hash::{Hash, Hasher}, path::{Pat use anyhow::Result; use serde::{Deserialize, Serialize}; -use crate::{loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::{Scheme, SchemeKind}, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlLike}}; +use crate::{loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::Scheme, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlLike}}; #[derive(Clone, Eq)] pub enum UrlBuf { @@ -171,10 +171,6 @@ impl UrlBuf { } impl UrlBuf { - // --- Regular - #[inline] - pub fn is_regular(&self) -> bool { self.as_url().is_regular() } - #[inline] pub fn to_regular(&self) -> Result { Ok(self.as_url().as_regular()?.into()) } @@ -183,10 +179,6 @@ impl UrlBuf { Ok(Self::Regular(self.into_loc().into_os()?.into())) } - // --- Search - #[inline] - pub fn is_search(&self) -> bool { self.kind() == SchemeKind::Search } - #[inline] pub fn to_search(&self, domain: impl AsRef) -> Result { Ok(Self::Search { @@ -202,20 +194,6 @@ impl UrlBuf { domain: Pool::::intern(domain), }) } - - // --- Archive - #[inline] - pub fn is_archive(&self) -> bool { self.kind() == SchemeKind::Archive } - - // --- Internal - #[inline] - pub fn is_internal(&self) -> bool { - match self.kind() { - SchemeKind::Regular | SchemeKind::Sftp => true, - SchemeKind::Search => !self.uri().is_empty(), - SchemeKind::Archive => false, - } - } } impl Debug for UrlBuf { diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index 2abf6bf6..7d9a0c5a 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -249,11 +249,6 @@ impl<'a> UrlCow<'a> { pub fn to_owned(&self) -> UrlBuf { self.as_url().into() } } -impl UrlCow<'_> { - #[inline] - pub fn is_regular(&self) -> bool { self.as_url().is_regular() } -} - impl Serialize for UrlCow<'_> { fn serialize(&self, serializer: S) -> Result where diff --git a/yazi-shared/src/url/like.rs b/yazi-shared/src/url/like.rs index 46c3544f..d9c0574c 100644 --- a/yazi-shared/src/url/like.rs +++ b/yazi-shared/src/url/like.rs @@ -28,6 +28,14 @@ where fn is_absolute(&self) -> bool { self.as_url().is_absolute() } + fn is_archive(&self) -> bool { self.as_url().is_archive() } + + fn is_internal(&self) -> bool { self.as_url().is_internal() } + + fn is_regular(&self) -> bool { self.as_url().is_regular() } + + fn is_search(&self) -> bool { self.as_url().is_search() } + fn kind(&self) -> SchemeKind { self.as_url().kind() } fn loc(&self) -> PathDyn<'_> { self.as_url().loc() } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index bd971a85..a815670a 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -112,6 +112,18 @@ impl<'a> Url<'a> { #[inline] pub fn is_absolute(self) -> bool { self.loc().is_absolute() } + #[inline] + pub fn is_archive(self) -> bool { matches!(self, Self::Archive { .. }) } + + #[inline] + pub fn is_internal(self) -> bool { + match self { + Self::Regular(_) | Self::Sftp { .. } => true, + Self::Search { .. } => !self.uri().is_empty(), + Self::Archive { .. } => false, + } + } + #[inline] pub fn is_regular(self) -> bool { matches!(self, Self::Regular(_)) } @@ -119,7 +131,7 @@ impl<'a> Url<'a> { pub fn is_search(self) -> bool { matches!(self, Self::Search { .. }) } #[inline] - pub fn kind(&self) -> SchemeKind { + pub fn kind(self) -> SchemeKind { match self { Self::Regular(_) => SchemeKind::Regular, Self::Search { .. } => SchemeKind::Search, diff --git a/yazi-vfs/src/files.rs b/yazi-vfs/src/files.rs index 6da53277..ed57afdf 100644 --- a/yazi-vfs/src/files.rs +++ b/yazi-vfs/src/files.rs @@ -38,7 +38,7 @@ impl VfsFiles for Files { async fn from_dir_bulk(dir: &UrlBuf) -> std::io::Result> { let mut it = provider::read_dir(dir).await?; - let mut entries = Vec::with_capacity(5000); + let mut entries = Vec::new(); while let Ok(Some(entry)) = it.next().await { entries.push(entry); } From 0fb652d2ecaeb3d9af7e0c94a4a6a59b4e7171c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sun, 30 Nov 2025 14:44:17 +0800 Subject: [PATCH 19/53] feat: new `null` plugin as a general-purpose error handler (#3387) --- yazi-binding/src/error.rs | 22 ++- yazi-config/preset/yazi-default.toml | 4 + yazi-fs/src/error/error.rs | 10 ++ yazi-fs/src/error/serde.rs | 187 +++++++++++----------- yazi-plugin/preset/plugins/mime-local.lua | 95 ++++++----- yazi-plugin/preset/plugins/null.lua | 22 +++ yazi-plugin/src/loader/loader.rs | 1 + 7 files changed, 211 insertions(+), 130 deletions(-) create mode 100644 yazi-plugin/preset/plugins/null.lua diff --git a/yazi-binding/src/error.rs b/yazi-binding/src/error.rs index eef599a3..e53698ae 100644 --- a/yazi-binding/src/error.rs +++ b/yazi-binding/src/error.rs @@ -14,9 +14,20 @@ pub enum Error { impl Error { pub fn install(lua: &Lua) -> mlua::Result<()> { - let new = lua.create_function(|_, msg: String| Ok(Self::custom(msg)))?; + let custom = lua.create_function(|_, msg: String| Ok(Self::custom(msg)))?; - lua.globals().raw_set("Error", lua.create_table_from([("custom", new)])?) + let fs = lua.create_function(|_, value: Value| { + Ok(Self::Fs(match value { + Value::Table(t) => yazi_fs::error::Error::custom( + &t.raw_get::("kind")?.to_str()?, + t.raw_get("code")?, + &t.raw_get::("message")?.to_str()?, + )?, + _ => Err("expected a table".into_lua_err())?, + })) + })?; + + lua.globals().raw_set("Error", lua.create_table_from([("custom", custom), ("fs", fs)])?) } pub fn custom(msg: impl Into) -> Self { Self::Custom(msg.into()) } @@ -60,6 +71,13 @@ impl UserData for Error { _ => None, }) }); + fields.add_field_method_get("kind", |_, me| { + Ok(match me { + Self::Io(e) => Some(yazi_fs::error::Error::from(e.kind()).kind_str()), + Self::Fs(e) => Some(e.kind_str()), + _ => None, + }) + }); } fn add_methods>(methods: &mut M) { diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index 038efea9..b74651cd 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -114,6 +114,8 @@ spotters = [ { mime = "video/*", run = "video" }, # Virtual file system { mime = "vfs/*", run = "vfs" }, + # Error + { mime = "null/*", run = "null" }, # Fallback { url = "*", run = "file" }, ] @@ -160,6 +162,8 @@ previewers = [ { mime = "inode/empty", run = "empty" }, # Virtual file system { mime = "vfs/*", run = "vfs" }, + # Error + { mime = "null/*", run = "null" }, # Fallback { url = "*", run = "file" }, ] diff --git a/yazi-fs/src/error/error.rs b/yazi-fs/src/error/error.rs index 161636c4..a017ae54 100644 --- a/yazi-fs/src/error/error.rs +++ b/yazi-fs/src/error/error.rs @@ -1,5 +1,9 @@ use std::{fmt, io, sync::Arc}; +use anyhow::Result; + +use crate::error::{kind_from_str, kind_to_str}; + #[derive(Clone, Debug, Eq, PartialEq)] pub enum Error { Kind(io::ErrorKind), @@ -38,6 +42,10 @@ impl fmt::Display for Error { } impl Error { + pub fn custom(kind: &str, code: Option, message: &str) -> Result { + Ok(Self::Custom { kind: kind_from_str(kind)?, code, message: message.into() }) + } + pub fn kind(&self) -> io::ErrorKind { match self { Self::Kind(kind) => *kind, @@ -46,6 +54,8 @@ impl Error { } } + pub fn kind_str(&self) -> &'static str { kind_to_str(self.kind()) } + pub fn raw_os_error(&self) -> Option { match self { Self::Kind(_) => None, diff --git a/yazi-fs/src/error/serde.rs b/yazi-fs/src/error/serde.rs index a5b9a289..1bc6b4a6 100644 --- a/yazi-fs/src/error/serde.rs +++ b/yazi-fs/src/error/serde.rs @@ -1,103 +1,104 @@ use std::io; +use anyhow::{Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::Error; -fn kind_to_str(kind: io::ErrorKind) -> &'static str { +pub(super) fn kind_to_str(kind: io::ErrorKind) -> &'static str { use std::io::ErrorKind as K; match kind { - K::NotFound => "not_found", - K::PermissionDenied => "permission_denied", - K::ConnectionRefused => "connection_refused", - K::ConnectionReset => "connection_reset", - K::HostUnreachable => "host_unreachable", - K::NetworkUnreachable => "network_unreachable", - K::ConnectionAborted => "connection_aborted", - K::NotConnected => "not_connected", - K::AddrInUse => "addr_in_use", - K::AddrNotAvailable => "addr_not_available", - K::NetworkDown => "network_down", - K::BrokenPipe => "broken_pipe", - K::AlreadyExists => "already_exists", - K::WouldBlock => "would_block", - K::NotADirectory => "not_a_directory", - K::IsADirectory => "is_a_directory", - K::DirectoryNotEmpty => "directory_not_empty", - K::ReadOnlyFilesystem => "read_only_filesystem", - // K::FilesystemLoop => "filesystem_loop", - K::StaleNetworkFileHandle => "stale_network_file_handle", - K::InvalidInput => "invalid_input", - K::InvalidData => "invalid_data", - K::TimedOut => "timed_out", - K::WriteZero => "write_zero", - K::StorageFull => "storage_full", - K::NotSeekable => "not_seekable", - K::QuotaExceeded => "quota_exceeded", - K::FileTooLarge => "file_too_large", - K::ResourceBusy => "resource_busy", - K::ExecutableFileBusy => "executable_file_busy", - K::Deadlock => "deadlock", - K::CrossesDevices => "crosses_devices", - K::TooManyLinks => "too_many_links", - K::InvalidFilename => "invalid_filename", - K::ArgumentListTooLong => "argument_list_too_long", - K::Interrupted => "interrupted", - K::Unsupported => "unsupported", - K::UnexpectedEof => "unexpected_eof", - K::OutOfMemory => "out_of_memory", - // K::InProgress => "in_progress", - K::Other => "other", - _ => "other", + K::NotFound => "NotFound", + K::PermissionDenied => "PermissionDenied", + K::ConnectionRefused => "ConnectionRefused", + K::ConnectionReset => "ConnectionReset", + K::HostUnreachable => "HostUnreachable", + K::NetworkUnreachable => "NetworkUnreachable", + K::ConnectionAborted => "ConnectionAborted", + K::NotConnected => "NotConnected", + K::AddrInUse => "AddrInUse", + K::AddrNotAvailable => "AddrNotAvailable", + K::NetworkDown => "NetworkDown", + K::BrokenPipe => "BrokenPipe", + K::AlreadyExists => "AlreadyExists", + K::WouldBlock => "WouldBlock", + K::NotADirectory => "NotADirectory", + K::IsADirectory => "IsADirectory", + K::DirectoryNotEmpty => "DirectoryNotEmpty", + K::ReadOnlyFilesystem => "ReadOnlyFilesystem", + // K::FilesystemLoop => "FilesystemLoop", + K::StaleNetworkFileHandle => "StaleNetworkFileHandle", + K::InvalidInput => "InvalidInput", + K::InvalidData => "InvalidData", + K::TimedOut => "TimedOut", + K::WriteZero => "WriteZero", + K::StorageFull => "StorageFull", + K::NotSeekable => "NotSeekable", + K::QuotaExceeded => "QuotaExceeded", + K::FileTooLarge => "FileTooLarge", + K::ResourceBusy => "ResourceBusy", + K::ExecutableFileBusy => "ExecutableFileBusy", + K::Deadlock => "Deadlock", + K::CrossesDevices => "CrossesDevices", + K::TooManyLinks => "TooManyLinks", + K::InvalidFilename => "InvalidFilename", + K::ArgumentListTooLong => "ArgumentListTooLong", + K::Interrupted => "Interrupted", + K::Unsupported => "Unsupported", + K::UnexpectedEof => "UnexpectedEof", + K::OutOfMemory => "OutOfMemory", + // K::InProgress => "InProgress", + K::Other => "Other", + _ => "Other", } } -fn kind_from_str(s: &str) -> io::ErrorKind { +pub(super) fn kind_from_str(s: &str) -> Result { use std::io::ErrorKind as K; - match s { - "not_found" => K::NotFound, - "permission_denied" => K::PermissionDenied, - "connection_refused" => K::ConnectionRefused, - "connection_reset" => K::ConnectionReset, - "host_unreachable" => K::HostUnreachable, - "network_unreachable" => K::NetworkUnreachable, - "connection_aborted" => K::ConnectionAborted, - "not_connected" => K::NotConnected, - "addr_in_use" => K::AddrInUse, - "addr_not_available" => K::AddrNotAvailable, - "network_down" => K::NetworkDown, - "broken_pipe" => K::BrokenPipe, - "already_exists" => K::AlreadyExists, - "would_block" => K::WouldBlock, - "not_a_directory" => K::NotADirectory, - "is_a_directory" => K::IsADirectory, - "directory_not_empty" => K::DirectoryNotEmpty, - "read_only_filesystem" => K::ReadOnlyFilesystem, - // "filesystem_loop" => K::FilesystemLoop, - "stale_network_file_handle" => K::StaleNetworkFileHandle, - "invalid_input" => K::InvalidInput, - "invalid_data" => K::InvalidData, - "timed_out" => K::TimedOut, - "write_zero" => K::WriteZero, - "storage_full" => K::StorageFull, - "not_seekable" => K::NotSeekable, - "quota_exceeded" => K::QuotaExceeded, - "file_too_large" => K::FileTooLarge, - "resource_busy" => K::ResourceBusy, - "executable_file_busy" => K::ExecutableFileBusy, - "deadlock" => K::Deadlock, - "crosses_devices" => K::CrossesDevices, - "too_many_links" => K::TooManyLinks, - "invalid_filename" => K::InvalidFilename, - "argument_list_too_long" => K::ArgumentListTooLong, - "interrupted" => K::Interrupted, - "unsupported" => K::Unsupported, - "unexpected_eof" => K::UnexpectedEof, - "out_of_memory" => K::OutOfMemory, - // "in_progress" => K::InProgress, - "other" => K::Other, - _ => K::Other, - } + Ok(match s { + "NotFound" => K::NotFound, + "PermissionDenied" => K::PermissionDenied, + "ConnectionRefused" => K::ConnectionRefused, + "ConnectionReset" => K::ConnectionReset, + "HostUnreachable" => K::HostUnreachable, + "NetworkUnreachable" => K::NetworkUnreachable, + "ConnectionAborted" => K::ConnectionAborted, + "NotConnected" => K::NotConnected, + "AddrInUse" => K::AddrInUse, + "AddrNotAvailable" => K::AddrNotAvailable, + "NetworkDown" => K::NetworkDown, + "BrokenPipe" => K::BrokenPipe, + "AlreadyExists" => K::AlreadyExists, + "WouldBlock" => K::WouldBlock, + "NotADirectory" => K::NotADirectory, + "IsADirectory" => K::IsADirectory, + "DirectoryNotEmpty" => K::DirectoryNotEmpty, + "ReadOnlyFilesystem" => K::ReadOnlyFilesystem, + // "FilesystemLoop" => K::FilesystemLoop, + "StaleNetworkFileHandle" => K::StaleNetworkFileHandle, + "InvalidInput" => K::InvalidInput, + "InvalidData" => K::InvalidData, + "TimedOut" => K::TimedOut, + "WriteZero" => K::WriteZero, + "StorageFull" => K::StorageFull, + "NotSeekable" => K::NotSeekable, + "QuotaExceeded" => K::QuotaExceeded, + "FileTooLarge" => K::FileTooLarge, + "ResourceBusy" => K::ResourceBusy, + "ExecutableFileBusy" => K::ExecutableFileBusy, + "Deadlock" => K::Deadlock, + "CrossesDevices" => K::CrossesDevices, + "TooManyLinks" => K::TooManyLinks, + "InvalidFilename" => K::InvalidFilename, + "ArgumentListTooLong" => K::ArgumentListTooLong, + "Interrupted" => K::Interrupted, + "Unsupported" => K::Unsupported, + "UnexpectedEof" => K::UnexpectedEof, + "OutOfMemory" => K::OutOfMemory, + // "InProgress" => K::InProgress, + "Other" => K::Other, + _ => bail!("unknown error kind: {s}"), + }) } impl Serialize for Error { @@ -138,15 +139,19 @@ impl<'de> Deserialize<'de> for Error { let shadow = Shadow::deserialize(deserializer)?; Ok(match shadow { - Shadow::Kind { kind } => Self::Kind(kind_from_str(&kind)), + Shadow::Kind { kind } => Self::Kind(kind_from_str(&kind).map_err(serde::de::Error::custom)?), Shadow::Raw { code } => Self::Raw(code), Shadow::Dyn { kind, code, message } => { if !message.is_empty() { - Self::Custom { kind: kind_from_str(&kind), code, message: message.into() } + Self::Custom { + kind: kind_from_str(&kind).map_err(serde::de::Error::custom)?, + code, + message: message.into(), + } } else if let Some(code) = code { Self::Raw(code) } else { - Self::Kind(kind_from_str(&kind)) + Self::Kind(kind_from_str(&kind).map_err(serde::de::Error::custom)?) } } }) diff --git a/yazi-plugin/preset/plugins/mime-local.lua b/yazi-plugin/preset/plugins/mime-local.lua index fb50e2d2..1e2cfdcb 100644 --- a/yazi-plugin/preset/plugins/mime-local.lua +++ b/yazi-plugin/preset/plugins/mime-local.lua @@ -3,41 +3,6 @@ local TYPE_PATS = { "text", "image", "video", "application", "audio", "font", "i local M = {} -local function match_mimetype(line) - for _, pat in ipairs(TYPE_PATS) do - local typ, sub = line:match(string.format("(%s/)([+-.a-zA-Z0-9]+)%%s+$", pat)) - if not sub then - elseif line:find(typ .. sub, 1, true) == 1 then - return typ:gsub("^x%-", "", 1) .. sub:gsub("^x%-", "", 1):gsub("^vnd%.", "", 1) - else - return nil, true - end - end -end - -local function spawn_file1(paths) - local bin = os.getenv("YAZI_FILE_ONE") or "file" - local windows = ya.target_family() == "windows" - - local cmd = Command(bin):arg({ "-bL", "--mime-type" }):stdout(Command.PIPED) - if windows then - cmd:arg({ "-f", "-" }):stdin(Command.PIPED) - else - cmd:arg("--"):arg(paths) - end - - local child, err = cmd:spawn() - if not child then - return nil, Err("Failed to start `%s`, error: %s", bin, err) - elseif windows then - child:write_all(table.concat(paths, "\n")) - child:flush() - ya.drop(child:take_stdin()) - end - - return child -end - function M:fetch(job) local urls, paths = {}, {} for i, file in ipairs(job.files) do @@ -48,8 +13,9 @@ function M:fetch(job) end end - local child, err = spawn_file1(paths) + local child, err = M.spawn_file1(paths) if not child then + M.placeholder(err, urls, paths) return true, err end @@ -74,7 +40,7 @@ function M:fetch(job) break end - match, ignore = match_mimetype(line) + match, ignore = M.match_mimetype(line) if match then updates[urls[i] or paths[i]], state[i], i = match, true, i + 1 flush(false) @@ -88,4 +54,59 @@ function M:fetch(job) return state end +function M.match_mimetype(line) + for _, pat in ipairs(TYPE_PATS) do + local typ, sub = line:match(string.format("(%s/)([+-.a-zA-Z0-9]+)%%s+$", pat)) + if not sub then + elseif line:find(typ .. sub, 1, true) == 1 then + return typ:gsub("^x%-", "", 1) .. sub:gsub("^x%-", "", 1):gsub("^vnd%.", "", 1) + else + return nil, true + end + end +end + +function M.file1_bin() return os.getenv("YAZI_FILE_ONE") or "file" end + +function M.spawn_file1(paths) + local bin = M.file1_bin() + local windows = ya.target_family() == "windows" + + local cmd = Command(bin):arg({ "-bL", "--mime-type" }):stdout(Command.PIPED) + if windows then + cmd:arg({ "-f", "-" }):stdin(Command.PIPED) + else + cmd:arg("--"):arg(paths) + end + + local child, err = cmd:spawn() + if not child then + local e = Error.fs { + kind = err.kind or "Other", + code = err.code, + message = string.format("Failed to start `%s`, error: %s", bin, err), + } + return nil, e + elseif windows then + child:write_all(table.concat(paths, "\n")) + child:flush() + ya.drop(child:take_stdin()) + end + + return child +end + +function M.placeholder(err, urls, paths) + if err.kind ~= "NotFound" then + return + end + + local updates = {} + for i = 1, #paths do + updates[urls[i] or paths[i]] = "null/file1-not-found" + end + + ya.emit("update_mimes", { updates = updates }) +end + return M diff --git a/yazi-plugin/preset/plugins/null.lua b/yazi-plugin/preset/plugins/null.lua new file mode 100644 index 00000000..279c9038 --- /dev/null +++ b/yazi-plugin/preset/plugins/null.lua @@ -0,0 +1,22 @@ +local M = {} + +function M:peek(job) + local err + if job.mime == "null/file1-not-found" then + err = string.format( + "Cannot find `%s` to detect the file's MIME type. Make sure it's installed and restart Yazi", + require("mime.local").file1_bin() + ) + else + err = "Unknown error occurred while detecting MIME type" + end + + local line = ui.Line(err):reverse() + ya.preview_widget(job, ui.Text(line):area(job.area):wrap(ui.Wrap.YES)) +end + +function M:seek() end + +function M:spot(job) require("file"):spot(job) end + +return M diff --git a/yazi-plugin/src/loader/loader.rs b/yazi-plugin/src/loader/loader.rs index d5fff9bd..4185718f 100644 --- a/yazi-plugin/src/loader/loader.rs +++ b/yazi-plugin/src/loader/loader.rs @@ -43,6 +43,7 @@ impl Default for Loader { ("mime.local".to_owned(), preset!("plugins/mime-local").into()), ("mime.remote".to_owned(), preset!("plugins/mime-remote").into()), ("noop".to_owned(), preset!("plugins/noop").into()), + ("null".to_owned(), preset!("plugins/null").into()), ("pdf".to_owned(), preset!("plugins/pdf").into()), ("session".to_owned(), preset!("plugins/session").into()), ("svg".to_owned(), preset!("plugins/svg").into()), From d0adb9c015943a861bbae695231fdd44fc722d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 1 Dec 2025 10:51:27 +0800 Subject: [PATCH 20/53] feat: new `ind-sort` and `key-sort` DDS events to change sorting in Lua (#3391) --- .github/workflows/cachix.yml | 3 ++ .github/workflows/check.yml | 3 ++ .github/workflows/draft.yml | 3 ++ .github/workflows/publish.yml | 3 ++ .github/workflows/test.yml | 3 ++ CHANGELOG.md | 6 +++- yazi-actor/src/core/preflight.rs | 17 ++++------- yazi-actor/src/mgr/cd.rs | 2 +- yazi-actor/src/mgr/sort.rs | 11 +++++++- yazi-actor/src/mgr/stash.rs | 2 +- yazi-actor/src/mgr/update_files.rs | 2 +- yazi-dds/src/macros.rs | 7 ++--- yazi-dds/src/spark/kind.rs | 14 ++++++++-- yazi-dds/src/spark/spark.rs | 45 ++++++++++++++++++++++++++++-- yazi-parser/src/mgr/sort.rs | 9 +++--- 15 files changed, 101 insertions(+), 29 deletions(-) diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 1bde031f..193cbf3d 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -4,6 +4,9 @@ on: push: branches: [main] +permissions: + contents: read + jobs: publish: name: Publish Flake diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 43466367..3d363578 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -10,6 +10,9 @@ env: SCCACHE_GHA_ENABLED: true RUSTC_WRAPPER: sccache +permissions: + contents: read + jobs: clippy: runs-on: ubuntu-latest diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml index f95b4562..4987245c 100644 --- a/.github/workflows/draft.yml +++ b/.github/workflows/draft.yml @@ -9,6 +9,9 @@ on: env: SCCACHE_GHA_ENABLED: true +permissions: + contents: read + jobs: build-unix: strategy: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f7776fd3..61fe3938 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: winget: runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 17ed52e8..5072fb74 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,9 @@ env: RUSTC_WRAPPER: sccache CARGO_TERM_COLOR: always +permissions: + contents: read + jobs: test: strategy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 41a7587b..0f1dfc73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,9 +33,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Support invalid UTF-8 paths throughout the codebase ([#2884], [#2889], [#2890], [#2895], [#3023], [#3290], [#3369]) - Allow upgrading only specific packages with `ya pkg` ([#2841]) - Respect the user's `image_filter` setting in the preset ImageMagick previewer ([#3286]) +- New `ind-sort` and `key-sort` DDS events to change sorting in Lua ([#3391]) - Allow custom mouse click behavior for individual files ([#2925]) - Display newlines in input as spaces to improve readability ([#2932]) -- Fill in error messages if preview fails ([#2917]) +- Fill in error messages if preview fails ([#2917], [#3383], [#3387]) - Search view shares file selection and yank state ([#2855]) - Offload mimetype fetching on opening files to the task scheduler ([#3141]) - Increase terminal response timeout to better tolerate slow SSH network environments ([#2843]) @@ -1543,4 +1544,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3360]: https://github.com/sxyazi/yazi/pull/3360 [#3364]: https://github.com/sxyazi/yazi/pull/3364 [#3369]: https://github.com/sxyazi/yazi/pull/3369 +[#3383]: https://github.com/sxyazi/yazi/pull/3383 [#3385]: https://github.com/sxyazi/yazi/pull/3385 +[#3387]: https://github.com/sxyazi/yazi/pull/3387 +[#3391]: https://github.com/sxyazi/yazi/pull/3391 diff --git a/yazi-actor/src/core/preflight.rs b/yazi-actor/src/core/preflight.rs index fbd2aac3..1c46a51c 100644 --- a/yazi-actor/src/core/preflight.rs +++ b/yazi-actor/src/core/preflight.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use anyhow::Result; use mlua::{ErrorContext, ExternalError, IntoLua, Value}; use yazi_binding::runtime_mut; @@ -8,12 +6,10 @@ use yazi_plugin::LUA; use crate::{Ctx, lives::Lives}; -pub struct Preflight<'a> { - _lifetime: PhantomData<&'a ()>, -} +pub struct Preflight; -impl<'a> Preflight<'a> { - pub fn act(cx: &mut Ctx, opt: (SparkKind, Spark<'a>)) -> Result> { +impl Preflight { + pub fn act<'a>(cx: &mut Ctx, opt: (SparkKind, Spark<'a>)) -> Result> { let kind = opt.0; let Some(handlers) = LOCAL.read().get(kind.as_ref()).filter(|&m| !m.is_empty()).cloned() else { return Ok(opt.1); @@ -27,10 +23,9 @@ impl<'a> Preflight<'a> { runtime_mut!(LUA)?.pop(); match result { - Ok(Value::Nil) => Err( - format!("Cancelled by `{kind}` event handler in `{id}` plugin on preflight") - .into_lua_err(), - )?, + Ok(Value::Nil) => { + Err(format!("`{kind}` event cancelled by `{id}` plugin on preflight").into_lua_err())? + } Ok(v) => body = v, Err(e) => Err( format!("Failed to run `{kind}` event handler in `{id}` plugin: {e}").into_lua_err(), diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 5025bc70..72a9433f 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -50,7 +50,7 @@ impl Actor for Cd { err!(Pubsub::pub_after_cd(tab.id, tab.cwd())); act!(mgr:displace, cx)?; act!(mgr:hidden, cx)?; - act!(mgr:sort, cx)?; + act!(mgr:sort, cx).ok(); act!(mgr:hover, cx)?; act!(mgr:refresh, cx)?; act!(mgr:stash, cx, opt).ok(); diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index d36604d0..7ad3b322 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -1,9 +1,10 @@ use anyhow::Result; use yazi_core::tab::Folder; +use yazi_dds::spark::SparkKind; use yazi_fs::{FilesSorter, FolderStage}; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::mgr::SortOpt; -use yazi_shared::data::Data; +use yazi_shared::{Source, data::Data}; use crate::{Actor, Ctx}; @@ -56,4 +57,12 @@ impl Actor for Sort { succ!(); } + + fn hook(cx: &Ctx, _: &Self::Options) -> Option { + match cx.source() { + Source::Ind => Some(SparkKind::IndSort), + Source::Key => Some(SparkKind::KeySort), + _ => None, + } + } } diff --git a/yazi-actor/src/mgr/stash.rs b/yazi-actor/src/mgr/stash.rs index 33f7a3a9..afcc8597 100644 --- a/yazi-actor/src/mgr/stash.rs +++ b/yazi-actor/src/mgr/stash.rs @@ -23,8 +23,8 @@ impl Actor for Stash { fn hook(cx: &Ctx, _opt: &Self::Options) -> Option { match cx.source() { - Source::Relay => Some(SparkKind::RelayStash), Source::Ind => Some(SparkKind::IndStash), + Source::Relay => Some(SparkKind::RelayStash), _ => None, } } diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index af0cd4a5..08e8d225 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -26,7 +26,7 @@ impl Actor for UpdateFiles { render!(cx.mgr.yanked.catchup_revision(false)); act!(mgr:hidden, cx)?; - act!(mgr:sort, cx)?; + act!(mgr:sort, cx).ok(); if revision != cx.current().files.revision { act!(mgr:hover, cx)?; diff --git a/yazi-dds/src/macros.rs b/yazi-dds/src/macros.rs index 97ec0a31..7cd44a8f 100644 --- a/yazi-dds/src/macros.rs +++ b/yazi-dds/src/macros.rs @@ -6,10 +6,9 @@ macro_rules! spark { } }; ($layer:ident : $name:ident, $body:expr) => { - $crate::spark::Spark::Void(yazi_parser::VoidOpt) - // paste::paste! { - // $crate::body::Body::[]($body) - // } + paste::paste! { + $crate::spark::Spark::[<$layer:camel $name:camel>]($body.into()) + } }; } diff --git a/yazi-dds/src/spark/kind.rs b/yazi-dds/src/spark/kind.rs index 3cefc874..bea38725 100644 --- a/yazi-dds/src/spark/kind.rs +++ b/yazi-dds/src/spark/kind.rs @@ -2,17 +2,27 @@ use std::fmt::Display; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SparkKind { + // Sort + KeySort, + IndSort, + // Stash IndStash, - KeyQuit, RelayStash, + // Quit + KeyQuit, } impl AsRef for SparkKind { fn as_ref(&self) -> &str { match self { + // Sort + Self::KeySort => "key-sort", + Self::IndSort => "ind-sort", + // Stash Self::IndStash => "ind-stash", - Self::KeyQuit => "key-quit", Self::RelayStash => "relay-stash", + // Quit + Self::KeyQuit => "key-quit", } } } diff --git a/yazi-dds/src/spark/spark.rs b/yazi-dds/src/spark/spark.rs index 1eaf6e24..8aad493a 100644 --- a/yazi-dds/src/spark/spark.rs +++ b/yazi-dds/src/spark/spark.rs @@ -76,15 +76,20 @@ pub enum Spark<'a> { Yank(yazi_parser::mgr::YankOpt), // Cmp + CmpArrow(yazi_parser::ArrowOpt), CmpClose(yazi_parser::cmp::CloseOpt), CmpShow(yazi_parser::cmp::ShowOpt), CmpTrigger(yazi_parser::cmp::TriggerOpt), // Confirm + ConfirmArrow(yazi_parser::ArrowOpt), ConfirmClose(yazi_parser::confirm::CloseOpt), ConfirmShow(Box), // Help + HelpArrow(yazi_parser::ArrowOpt), + HelpEscape(yazi_parser::VoidOpt), + HelpFilter(yazi_parser::VoidOpt), HelpToggle(yazi_parser::help::ToggleOpt), // Input @@ -93,6 +98,7 @@ pub enum Spark<'a> { InputClose(yazi_parser::input::CloseOpt), InputComplete(yazi_parser::input::CompleteOpt), InputDelete(yazi_parser::input::DeleteOpt), + InputEscape(yazi_parser::VoidOpt), InputForward(yazi_parser::input::ForwardOpt), InputInsert(yazi_parser::input::InsertOpt), InputKill(yazi_parser::input::KillOpt), @@ -104,14 +110,24 @@ pub enum Spark<'a> { NotifyTick(yazi_parser::notify::TickOpt), // Pick + PickArrow(yazi_parser::ArrowOpt), PickClose(yazi_parser::pick::CloseOpt), PickShow(yazi_parser::pick::ShowOpt), // Spot + SpotArrow(yazi_parser::ArrowOpt), + SpotClose(yazi_parser::VoidOpt), SpotCopy(yazi_parser::spot::CopyOpt), + SpotSwipe(yazi_parser::ArrowOpt), // Tasks + TasksArrow(yazi_parser::ArrowOpt), + TasksCancel(yazi_parser::VoidOpt), + TasksClose(yazi_parser::VoidOpt), + TasksInspect(yazi_parser::VoidOpt), + TasksOpenShellCompat(yazi_parser::tasks::ProcessOpenOpt), TasksProcessOpen(yazi_parser::tasks::ProcessOpenOpt), + TasksShow(yazi_parser::VoidOpt), TasksUpdateSucceed(yazi_parser::tasks::UpdateSucceedOpt), // Which @@ -121,10 +137,17 @@ pub enum Spark<'a> { impl<'a> Spark<'a> { pub fn from_lua(lua: &Lua, kind: SparkKind, value: Value) -> mlua::Result { + use SparkKind::*; + Ok(match kind { - SparkKind::IndStash => Self::Stash(<_>::from_lua(value, lua)?), - SparkKind::KeyQuit => Self::Quit(<_>::from_lua(value, lua)?), - SparkKind::RelayStash => Self::Stash(<_>::from_lua(value, lua)?), + // Sort + KeySort => Self::Sort(<_>::from_lua(value, lua)?), + IndSort => Self::Sort(<_>::from_lua(value, lua)?), + // Stash + IndStash => Self::Stash(<_>::from_lua(value, lua)?), + RelayStash => Self::Stash(<_>::from_lua(value, lua)?), + // Quit + KeyQuit => Self::Quit(<_>::from_lua(value, lua)?), }) } } @@ -204,15 +227,20 @@ impl<'a> IntoLua for Spark<'a> { Self::Yank(b) => b.into_lua(lua), // Cmp + Self::CmpArrow(b) => b.into_lua(lua), Self::CmpClose(b) => b.into_lua(lua), Self::CmpShow(b) => b.into_lua(lua), Self::CmpTrigger(b) => b.into_lua(lua), // Confirm + Self::ConfirmArrow(b) => b.into_lua(lua), Self::ConfirmClose(b) => b.into_lua(lua), Self::ConfirmShow(b) => b.into_lua(lua), // Help + Self::HelpArrow(b) => b.into_lua(lua), + Self::HelpEscape(b) => b.into_lua(lua), + Self::HelpFilter(b) => b.into_lua(lua), Self::HelpToggle(b) => b.into_lua(lua), // Input @@ -221,6 +249,7 @@ impl<'a> IntoLua for Spark<'a> { Self::InputClose(b) => b.into_lua(lua), Self::InputComplete(b) => b.into_lua(lua), Self::InputDelete(b) => b.into_lua(lua), + Self::InputEscape(b) => b.into_lua(lua), Self::InputForward(b) => b.into_lua(lua), Self::InputInsert(b) => b.into_lua(lua), Self::InputKill(b) => b.into_lua(lua), @@ -232,14 +261,24 @@ impl<'a> IntoLua for Spark<'a> { Self::NotifyTick(b) => b.into_lua(lua), // Pick + Self::PickArrow(b) => b.into_lua(lua), Self::PickClose(b) => b.into_lua(lua), Self::PickShow(b) => b.into_lua(lua), // Spot + Self::SpotArrow(b) => b.into_lua(lua), + Self::SpotClose(b) => b.into_lua(lua), Self::SpotCopy(b) => b.into_lua(lua), + Self::SpotSwipe(b) => b.into_lua(lua), // Tasks + Self::TasksArrow(b) => b.into_lua(lua), + Self::TasksCancel(b) => b.into_lua(lua), + Self::TasksClose(b) => b.into_lua(lua), + Self::TasksInspect(b) => b.into_lua(lua), + Self::TasksOpenShellCompat(b) => b.into_lua(lua), Self::TasksProcessOpen(b) => b.into_lua(lua), + Self::TasksShow(b) => b.into_lua(lua), Self::TasksUpdateSucceed(b) => b.into_lua(lua), // Which diff --git a/yazi-parser/src/mgr/sort.rs b/yazi-parser/src/mgr/sort.rs index 0e411696..2bccb47f 100644 --- a/yazi-parser/src/mgr/sort.rs +++ b/yazi-parser/src/mgr/sort.rs @@ -1,8 +1,9 @@ -use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value}; +use serde::{Deserialize, Serialize}; use yazi_fs::SortBy; use yazi_shared::event::CmdCow; -#[derive(Debug, Default)] +#[derive(Debug, Default, Deserialize, Serialize)] pub struct SortOpt { pub by: Option, pub reverse: Option, @@ -26,9 +27,9 @@ impl TryFrom for SortOpt { } impl FromLua for SortOpt { - fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { lua.from_value(value) } } impl IntoLua for SortOpt { - fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } + fn into_lua(self, lua: &Lua) -> mlua::Result { lua.to_value(&self) } } From d910104c00fa2cc3d248ce6fea4a87b02b0c60e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 1 Dec 2025 11:11:00 +0800 Subject: [PATCH 21/53] fix: errors out if the target package directory doesn't exist previously (#3392) --- yazi-cli/src/package/dependency.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yazi-cli/src/package/dependency.rs b/yazi-cli/src/package/dependency.rs index eb3eb146..ccb14696 100644 --- a/yazi-cli/src/package/dependency.rs +++ b/yazi-cli/src/package/dependency.rs @@ -4,6 +4,7 @@ use anyhow::{Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use twox_hash::XxHash3_128; use yazi_fs::Xdg; +use yazi_macro::ok_or_not_found; use yazi_shared::BytesExt; #[derive(Clone, Default)] @@ -62,7 +63,7 @@ impl Dependency { } pub(super) async fn plugin_files(dir: &Path) -> std::io::Result> { - let mut it = tokio::fs::read_dir(dir).await?; + let mut it = ok_or_not_found!(tokio::fs::read_dir(dir).await, return Ok(vec![])); let mut files: Vec = ["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect(); while let Some(entry) = it.next_entry().await? { From 44e244b9d6d4427d74b58a2a3bc971dc923e12db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 1 Dec 2025 22:39:36 +0800 Subject: [PATCH 22/53] perf: concurrent chunked upload and download of a single file over SFTP (#3393) --- yazi-fs/src/provider/local/copier.rs | 99 ++++++++++++++++++++ yazi-fs/src/provider/local/local.rs | 55 +++-------- yazi-fs/src/provider/local/mod.rs | 2 +- yazi-fs/src/provider/traits.rs | 15 ++- yazi-scheduler/src/file/file.rs | 11 ++- yazi-sftp/src/fs/file.rs | 99 ++++++++++++++++++-- yazi-shared/src/path/path.rs | 7 ++ yazi-vfs/src/fns.rs | 69 +------------- yazi-vfs/src/provider/copier.rs | 133 +++++++++++++++++++++++++++ yazi-vfs/src/provider/mod.rs | 2 +- yazi-vfs/src/provider/provider.rs | 42 ++++++--- yazi-vfs/src/provider/providers.rs | 12 +++ yazi-vfs/src/provider/rw_file.rs | 40 +++++++- yazi-vfs/src/provider/sftp/sftp.rs | 21 ++++- 14 files changed, 464 insertions(+), 143 deletions(-) create mode 100644 yazi-fs/src/provider/local/copier.rs create mode 100644 yazi-vfs/src/provider/copier.rs diff --git a/yazi-fs/src/provider/local/copier.rs b/yazi-fs/src/provider/local/copier.rs new file mode 100644 index 00000000..bab205af --- /dev/null +++ b/yazi-fs/src/provider/local/copier.rs @@ -0,0 +1,99 @@ +use std::{io, path::PathBuf}; + +use tokio::{select, sync::{mpsc, oneshot}}; + +use crate::provider::Attrs; + +pub(super) async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::Result { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + use std::os::unix::fs::OpenOptionsExt; + + tokio::task::spawn_blocking(move || { + let mut opts = std::fs::OpenOptions::new(); + if let Some(mode) = attrs.mode { + opts.mode(mode.bits() as _); + } + + let mut reader = std::fs::File::open(from)?; + let mut writer = opts.write(true).create(true).truncate(true).open(to)?; + let written = std::io::copy(&mut reader, &mut writer)?; + + if let Some(mode) = attrs.mode { + writer.set_permissions(mode.into()).ok(); + } + writer.set_times(attrs.into()).ok(); + + Ok(written) + }) + .await? + } + + #[cfg(not(any(target_os = "linux", target_os = "android")))] + { + tokio::task::spawn_blocking(move || { + let written = std::fs::copy(from, &to)?; + + if let Ok(file) = std::fs::File::options().write(true).open(to) { + file.set_times(attrs.into()).ok(); + } + + Ok(written) + }) + .await? + } +} + +pub(super) fn copy_with_progress_impl( + from: PathBuf, + to: PathBuf, + attrs: Attrs, +) -> mpsc::Receiver> { + let (prog_tx, prog_rx) = mpsc::channel(10); + let (done_tx, mut done_rx) = oneshot::channel(); + + tokio::spawn({ + let to = to.to_owned(); + async move { + done_tx.send(copy_impl(from, to, attrs).await).ok(); + } + }); + + tokio::spawn({ + let prog_tx = prog_tx.clone(); + async move { + let mut last = 0; + let mut done = None; + loop { + select! { + res = &mut done_rx => done = Some(res.unwrap()), + _ = prog_tx.closed() => break, + _ = tokio::time::sleep(std::time::Duration::from_secs(3)) => {}, + } + + match done { + Some(Ok(len)) => { + if len > last { + prog_tx.send(Ok(len - last)).await.ok(); + } + prog_tx.send(Ok(0)).await.ok(); + break; + } + Some(Err(e)) => { + prog_tx.send(Err(e)).await.ok(); + break; + } + None => {} + } + + let len = tokio::fs::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0); + if len > last { + prog_tx.send(Ok(len - last)).await.ok(); + last = len; + } + } + } + }); + + prog_rx +} diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index 8c51fed0..a3e25b17 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -1,5 +1,6 @@ -use std::{io, path::{Path, PathBuf}, sync::Arc}; +use std::{io, path::Path, sync::Arc}; +use tokio::sync::mpsc; use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow}}; use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Provider}}; @@ -35,7 +36,17 @@ impl<'a> Provider for Local<'a> { { let to = to.as_path().to_os_owned()?; let from = self.path.to_owned(); - Self::copy_impl(from, to, attrs).await + super::copy_impl(from, to, attrs).await + } + + fn copy_with_progress(&self, to: P, attrs: A) -> io::Result>> + where + P: AsPath, + A: Into, + { + let to = to.as_path().to_os_owned()?; + let from = self.path.to_owned(); + Ok(super::copy_with_progress_impl(from, to, attrs.into())) } #[inline] @@ -204,46 +215,6 @@ impl<'a> Provider for Local<'a> { } impl<'a> Local<'a> { - async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::Result { - #[cfg(any(target_os = "linux", target_os = "android"))] - { - use std::os::unix::fs::OpenOptionsExt; - - tokio::task::spawn_blocking(move || { - let mut opts = std::fs::OpenOptions::new(); - if let Some(mode) = attrs.mode { - opts.mode(mode.bits() as _); - } - - let mut reader = std::fs::File::open(from)?; - let mut writer = opts.write(true).create(true).truncate(true).open(to)?; - let written = std::io::copy(&mut reader, &mut writer)?; - - if let Some(mode) = attrs.mode { - writer.set_permissions(mode.into()).ok(); - } - writer.set_times(attrs.into()).ok(); - - Ok(written) - }) - .await? - } - - #[cfg(not(any(target_os = "linux", target_os = "android")))] - { - tokio::task::spawn_blocking(move || { - let written = std::fs::copy(from, &to)?; - - if let Ok(file) = std::fs::File::options().write(true).open(to) { - file.set_times(attrs.into()).ok(); - } - - Ok(written) - }) - .await? - } - } - #[inline] pub async fn read(&self) -> io::Result> { tokio::fs::read(self.path).await } diff --git a/yazi-fs/src/provider/local/mod.rs b/yazi-fs/src/provider/local/mod.rs index 2589b059..e69473f2 100644 --- a/yazi-fs/src/provider/local/mod.rs +++ b/yazi-fs/src/provider/local/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(calculator casefold dir_entry gate identical local read_dir); +yazi_macro::mod_flat!(calculator casefold copier dir_entry gate identical local read_dir); diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs index 3a92b04d..cc7c43f4 100644 --- a/yazi-fs/src/provider/traits.rs +++ b/yazi-fs/src/provider/traits.rs @@ -1,13 +1,13 @@ use std::io; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; +use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::StrandCow, url::{AsUrl, Url, UrlBuf}}; use crate::{cha::{Cha, ChaType}, provider::Attrs}; pub trait Provider: Sized { - type File: AsyncRead + AsyncWrite + Unpin; + type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin; type Gate: FileBuilder; type ReadDir: DirReader + 'static; type UrlCow; @@ -23,6 +23,15 @@ pub trait Provider: Sized { where P: AsPath; + fn copy_with_progress( + &self, + to: P, + attrs: A, + ) -> io::Result>> + where + P: AsPath, + A: Into; + fn create(&self) -> impl Future> { async move { self.gate().write(true).create(true).truncate(true).open(self.url()).await } } @@ -207,7 +216,7 @@ pub trait FileHolder { // --- FileBuilder pub trait FileBuilder: Sized + Default { - type File: AsyncRead + AsyncWrite + Unpin; + type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin; fn append(&mut self, append: bool) -> &mut Self; diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index d1a17619..075749cb 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -7,7 +7,7 @@ use yazi_config::YAZI; use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{skip_url, url_relative_to}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::PathCow, timestamp_us, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; -use yazi_vfs::{VfsCha, copy_with_progress, maybe_exists, provider::{self, DirEntry}, unique_name}; +use yazi_vfs::{VfsCha, maybe_exists, provider::{self, DirEntry}, unique_name}; use super::{FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; use crate::{LOW, NORMAL, TaskIn, TaskOp, TaskOps, file::{FileInDownload, FileInUpload, FileInUploadDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutPaste, FileOutPasteDo, FileOutTrash, FileOutUpload, FileOutUploadDo}}; @@ -96,7 +96,7 @@ impl File { pub(crate) async fn paste_do(&self, mut task: FileInPaste) -> Result<(), FileOutPasteDo> { ok_or_not_found!(provider::remove_file(&task.to).await); - let mut it = copy_with_progress(&task.from, &task.to, task.cha.unwrap()); + let mut it = provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await?; while let Some(res) = it.recv().await { match res { @@ -350,7 +350,7 @@ impl File { let cache = task.url.cache().context("Cannot determine cache path")?; let cache_tmp = Self::tmp(&cache).await.context("Cannot determine temporary download cache")?; - let mut it = copy_with_progress(&task.url, &cache_tmp, cha); + let mut it = provider::copy_with_progress(&task.url, &cache_tmp, cha).await?; while let Some(res) = it.recv().await { match res { Ok(0) => { @@ -456,12 +456,13 @@ impl File { } let tmp = Self::tmp(&task.url).await.context("Cannot determine temporary upload path")?; - let mut it = copy_with_progress(&task.cache, &tmp, Attrs { + let mut it = provider::copy_with_progress(&task.cache, &tmp, Attrs { mode: Some(task.cha.mode), atime: None, btime: None, mtime: None, - }); + }) + .await?; while let Some(res) = it.recv().await { match res { diff --git a/yazi-sftp/src/fs/file.rs b/yazi-sftp/src/fs/file.rs index 71b53c77..dd075b86 100644 --- a/yazi-sftp/src/fs/file.rs +++ b/yazi-sftp/src/fs/file.rs @@ -1,21 +1,28 @@ -use std::{io, pin::Pin, sync::Arc, task::{Context, Poll, ready}, time::Duration}; +use std::{io::{self, SeekFrom}, pin::Pin, sync::Arc, task::{Context, Poll, ready}, time::Duration}; -use tokio::{io::{AsyncRead, AsyncWrite, ReadBuf}, time::{Timeout, timeout}}; +use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}, time::{Timeout, timeout}}; -use crate::{Error, Operator, Packet, Receiver, Session, fs::Attrs}; +use crate::{Error, Operator, Packet, Receiver, Session, fs::Attrs, requests}; pub struct File { session: Arc, - handle: String, - closed: bool, - cursor: u64, + handle: String, + cursor: u64, + closed: bool, + close_rx: Option>, read_rx: Option, + seek_rx: Option, write_rx: Option<(Receiver, usize)>, flush_rx: Option>, } +enum SeekState { + NonBlocking(u64), + Blocking(i64, Timeout), +} + impl Unpin for File {} impl Drop for File { @@ -30,12 +37,14 @@ impl File { pub(crate) fn new(session: &Arc, handle: impl Into) -> Self { Self { session: session.clone(), - handle: handle.into(), - closed: false, - cursor: 0, + handle: handle.into(), + closed: false, + cursor: 0, + close_rx: None, read_rx: None, + seek_rx: None, write_rx: None, flush_rx: None, } @@ -81,6 +90,78 @@ impl AsyncRead for File { } } +impl AsyncSeek for File { + fn start_seek(mut self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { + if self.seek_rx.is_some() { + return Err(io::Error::other( + "other file operation is pending, call poll_complete before start_seek", + )); + } + + self.seek_rx = Some(match position { + SeekFrom::Start(n) => SeekState::NonBlocking(n), + SeekFrom::Current(n) => self + .cursor + .checked_add_signed(n) + .map(SeekState::NonBlocking) + .ok_or_else(|| io::Error::other("seeking to a negative or overflowed position"))?, + SeekFrom::End(n) => SeekState::Blocking( + n, + timeout( + Duration::from_secs(10), + self.session.send_sync(requests::Fstat::new(&self.handle))?, + ), + ), + }); + + Ok(()) + } + + fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let me = unsafe { self.get_unchecked_mut() }; + + let Some(state) = &mut me.seek_rx else { + return Poll::Ready(Ok(me.cursor)); + }; + + fn imp(cx: &mut Context<'_>, state: &mut SeekState) -> Poll> { + use Poll::Ready; + let (n, rx) = match state { + SeekState::NonBlocking(n) => return Ready(Ok(*n)), + SeekState::Blocking(n, rx) => (n, rx), + }; + + let Ok(result) = ready!(unsafe { Pin::new_unchecked(rx) }.poll(cx)) else { + return Ready(Err(Error::Timeout.into())); + }; + + let packet = match result { + Ok(Packet::Attrs(packet)) => packet, + Ok(_) => return Ready(Err(Error::Packet("not an Attrs").into())), + Err(e) => return Ready(Err(Error::from(e).into())), + }; + + let Some(size) = packet.attrs.size else { + return Ready(Err(io::Error::other("could not get file size for seeking from end"))); + }; + + Ready( + size + .checked_add_signed(*n) + .ok_or_else(|| io::Error::other("seeking to a negative or overflowed position")), + ) + } + + let result = ready!(imp(cx, state)); + if let Ok(n) = result { + me.cursor = n; + } + + me.seek_rx = None; + Poll::Ready(result) + } +} + impl AsyncWrite for File { fn poll_write( self: Pin<&mut Self>, diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index 0d33535f..8c1cce7e 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -182,6 +182,13 @@ impl<'p> PathDyn<'p> { pub fn to_string_lossy(self) -> Cow<'p, str> { String::from_utf8_lossy(self.encoded_bytes()) } + pub fn to_unix_owned(self) -> Result { + match self { + Self::Os(_) => Err(PathDynError::AsUnix), + Self::Unix(p) => Ok(p.to_owned()), + } + } + pub fn try_ends_with(self, child: T) -> Result where T: AsStrand, diff --git a/yazi-vfs/src/fns.rs b/yazi-vfs/src/fns.rs index a1f3cb6c..d83bd2ae 100644 --- a/yazi-vfs/src/fns.rs +++ b/yazi-vfs/src/fns.rs @@ -1,9 +1,7 @@ -use std::io; +use std::io::{self}; -use tokio::{select, sync::{mpsc, oneshot}}; -use yazi_fs::provider::Attrs; use yazi_macro::ok_or_not_found; -use yazi_shared::{strand::{StrandBuf, StrandLike}, url::{AsUrl, Url, UrlBuf, UrlLike}}; +use yazi_shared::{strand::{StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}}; use crate::provider; @@ -65,66 +63,3 @@ async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result { Ok(url) } - -pub fn copy_with_progress( - from: U, - to: V, - attrs: A, -) -> mpsc::Receiver> -where - U: AsUrl, - V: AsUrl, - A: Into, -{ - _copy_with_progress(from.as_url(), to.as_url(), attrs.into()) -} - -fn _copy_with_progress(from: Url, to: Url, attrs: Attrs) -> mpsc::Receiver> { - let (prog_tx, prog_rx) = mpsc::channel(1); - let (done_tx, mut done_rx) = oneshot::channel(); - - tokio::spawn({ - let (from, to) = (from.to_owned(), to.to_owned()); - async move { - done_tx.send(provider::copy(from, to, attrs).await).ok(); - } - }); - - tokio::spawn({ - let (prog_tx, to) = (prog_tx.clone(), to.to_owned()); - async move { - let mut last = 0; - let mut done = None; - loop { - select! { - res = &mut done_rx => done = Some(res.unwrap()), - _ = prog_tx.closed() => break, - _ = tokio::time::sleep(std::time::Duration::from_secs(3)) => {}, - } - - match done { - Some(Ok(len)) => { - if len > last { - prog_tx.send(Ok(len - last)).await.ok(); - } - prog_tx.send(Ok(0)).await.ok(); - break; - } - Some(Err(e)) => { - prog_tx.send(Err(e)).await.ok(); - break; - } - None => {} - } - - let len = provider::symlink_metadata(&to).await.map(|m| m.len).unwrap_or(0); - if len > last { - prog_tx.send(Ok(len - last)).await.ok(); - last = len; - } - } - } - }); - - prog_rx -} diff --git a/yazi-vfs/src/provider/copier.rs b/yazi-vfs/src/provider/copier.rs new file mode 100644 index 00000000..610006bf --- /dev/null +++ b/yazi-vfs/src/provider/copier.rs @@ -0,0 +1,133 @@ +use std::{io::{self, SeekFrom}, sync::{Arc, atomic::{AtomicU64, Ordering}}}; + +use futures::{StreamExt, TryStreamExt}; +use tokio::{io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufReader, BufWriter}, select, sync::{mpsc, oneshot}}; +use yazi_fs::provider::{Attrs, FileBuilder}; +use yazi_shared::url::{Url, UrlBuf}; + +use crate::provider::{self, Gate}; + +pub(super) async fn copy_impl(from: Url<'_>, to: Url<'_>, attrs: Attrs) -> io::Result { + let src = provider::open(from).await?; + let dist = provider::create(to).await?; + + let mut reader = BufReader::with_capacity(524288, src); + let mut writer = BufWriter::with_capacity(524288, dist); + let written = tokio::io::copy(&mut reader, &mut writer).await?; + + writer.flush().await?; + writer.get_ref().set_attrs(attrs).await.ok(); + writer.shutdown().await.ok(); + Ok(written) +} + +pub(super) fn copy_with_progress_impl( + from: UrlBuf, + to: UrlBuf, + attrs: Attrs, +) -> mpsc::Receiver> { + let acc = Arc::new(AtomicU64::new(0)); + let (from, to) = (Arc::new(from), Arc::new(to)); + let (prog_tx, prog_rx) = mpsc::channel(10); + let (done_tx, mut done_rx) = oneshot::channel(); + + let (acc_, prog_tx_) = (acc.clone(), prog_tx.clone()); + tokio::spawn(async move { + let (cha, mut src) = { + let f = provider::open(&*from).await?; + (f.metadata().await?, Some(f)) + }; + + let mut dist = { + let f = provider::create(&*to).await?; + f.set_len(cha.len).await?; + Some(f) + }; + + let chunks = (cha.len + 10485760 - 1) / 10485760; + let result = futures::stream::iter(0..chunks) + .map(|i| { + let acc_ = acc_.clone(); + let (from, to) = (from.clone(), to.clone()); + let (src, dist) = (src.take(), dist.take()); + async move { + let offset = i * 10485760; + let take = cha.len.saturating_sub(offset).min(10485760); + + let mut src = BufReader::with_capacity(524288, match src { + Some(f) => f, + None => provider::open(&*from).await?, + }); + let mut dist = BufWriter::with_capacity(524288, match dist { + Some(f) => f, + None => Gate::default().write(true).open(&*to).await?, + }); + + src.seek(SeekFrom::Start(offset)).await?; + dist.seek(SeekFrom::Start(offset)).await?; + + let mut src = src.take(take); + let mut buf = vec![0u8; 65536]; + let mut copied = 0u64; + loop { + let n = src.read(&mut buf).await?; + if n == 0 { + break; + } + + dist.write_all(&buf[..n]).await?; + copied += n as u64; + acc_.fetch_add(n as u64, Ordering::SeqCst); + } + + dist.flush().await?; + if i == chunks - 1 { + dist.get_ref().set_attrs(attrs).await.ok(); + } + dist.shutdown().await.ok(); + + if copied == take { + Ok(()) + } else { + Err(io::Error::other(format!( + "short copy for chunk {i}: copied {copied} bytes, expected {take}" + ))) + } + } + }) + .buffer_unordered(3) + .try_for_each(|_| async { Ok(()) }) + .await; + + let n = acc_.swap(0, Ordering::SeqCst); + if n > 0 { + prog_tx_.send(Ok(n)).await.ok(); + } + + if let Err(e) = result { + prog_tx_.send(Err(e)).await.ok(); + } else { + prog_tx_.send(Ok(0)).await.ok(); + } + + done_tx.send(()).ok(); + Ok::<_, io::Error>(()) + }); + + tokio::spawn(async move { + loop { + select! { + _ = &mut done_rx => break, + _ = prog_tx.closed() => break, + _ = tokio::time::sleep(std::time::Duration::from_secs(3)) => {}, + } + + let n = acc.swap(0, Ordering::SeqCst); + if n > 0 { + prog_tx.send(Ok(n)).await.ok(); + } + } + }); + + prog_rx +} diff --git a/yazi-vfs/src/provider/mod.rs b/yazi-vfs/src/provider/mod.rs index 913efe06..375515cc 100644 --- a/yazi-vfs/src/provider/mod.rs +++ b/yazi-vfs/src/provider/mod.rs @@ -1,5 +1,5 @@ yazi_macro::mod_pub!(sftp); -yazi_macro::mod_flat!(calculator dir_entry gate provider providers read_dir rw_file); +yazi_macro::mod_flat!(calculator copier dir_entry gate provider providers read_dir rw_file); pub(super) fn init() { sftp::init(); } diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/provider/provider.rs index b60e1547..d151dea0 100644 --- a/yazi-vfs/src/provider/provider.rs +++ b/yazi-vfs/src/provider/provider.rs @@ -1,6 +1,6 @@ use std::io; -use tokio::io::{AsyncWriteExt, BufReader, BufWriter}; +use tokio::sync::mpsc; use yazi_fs::{cha::Cha, provider::{Attrs, Provider, local::Local}}; use yazi_shared::{path::{AsPath, PathBufDyn}, url::{AsUrl, UrlBuf, UrlCow}}; @@ -51,18 +51,29 @@ where (false, false) if from.scheme().covariant(to.scheme()) => { Providers::new(from).await?.copy(to.loc(), attrs).await } + (true, false) | (false, true) | (false, false) => super::copy_impl(from, to, attrs).await, + } +} + +pub async fn copy_with_progress( + from: U, + to: V, + attrs: A, +) -> io::Result>> +where + U: AsUrl, + V: AsUrl, + A: Into, +{ + let (from, to) = (from.as_url(), to.as_url()); + + match (from.kind().is_local(), to.kind().is_local()) { + (true, true) => Local::new(from).await?.copy_with_progress(to.loc(), attrs), + (false, false) if from.scheme().covariant(to.scheme()) => { + Providers::new(from).await?.copy_with_progress(to.loc(), attrs) + } (true, false) | (false, true) | (false, false) => { - let src = Providers::new(from).await?.open().await?; - let dist = Providers::new(to).await?.create().await?; - - let mut reader = BufReader::with_capacity(524288, src); - let mut writer = BufWriter::with_capacity(524288, dist); - let written = tokio::io::copy(&mut reader, &mut writer).await?; - - writer.flush().await?; - writer.get_ref().set_attrs(attrs).await.ok(); - writer.shutdown().await.ok(); - Ok(written) + Ok(super::copy_with_progress_impl(from.to_owned(), to.to_owned(), attrs.into())) } } } @@ -128,6 +139,13 @@ where identical(a, b).await.unwrap_or(false) } +pub async fn open(url: U) -> io::Result +where + U: AsUrl, +{ + Providers::new(url.as_url()).await?.open().await +} + pub async fn read_dir(url: U) -> io::Result where U: AsUrl, diff --git a/yazi-vfs/src/provider/providers.rs b/yazi-vfs/src/provider/providers.rs index d0ec7f90..59e4b00e 100644 --- a/yazi-vfs/src/provider/providers.rs +++ b/yazi-vfs/src/provider/providers.rs @@ -1,5 +1,6 @@ use std::io; +use tokio::sync::mpsc; use yazi_fs::{cha::Cha, provider::{Attrs, Provider}}; use yazi_shared::{path::{AsPath, PathBufDyn}, url::{Url, UrlBuf, UrlCow}}; @@ -47,6 +48,17 @@ impl<'a> Provider for Providers<'a> { } } + fn copy_with_progress(&self, to: P, attrs: A) -> io::Result>> + where + P: AsPath, + A: Into, + { + match self { + Self::Local(p) => p.copy_with_progress(to, attrs), + Self::Sftp(p) => p.copy_with_progress(to, attrs), + } + } + async fn create(&self) -> io::Result { Ok(match self { Self::Local(p) => p.create().await?.into(), diff --git a/yazi-vfs/src/provider/rw_file.rs b/yazi-vfs/src/provider/rw_file.rs index 8f8cb1df..47fce3c7 100644 --- a/yazi-vfs/src/provider/rw_file.rs +++ b/yazi-vfs/src/provider/rw_file.rs @@ -1,6 +1,6 @@ use std::{io, pin::Pin}; -use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; use yazi_fs::provider::Attrs; pub enum RwFile { @@ -17,6 +17,14 @@ impl From for RwFile { } impl RwFile { + // FIXME: path + pub async fn metadata(&self) -> io::Result { + Ok(match self { + Self::Tokio(f) => yazi_fs::cha::Cha::new("// FIXME", f.metadata().await?), + Self::Sftp(f) => super::sftp::Cha::try_from(("// FIXME".as_bytes(), &f.fstat().await?))?.0, + }) + } + pub async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> { match self { Self::Tokio(f) => { @@ -37,6 +45,15 @@ impl RwFile { Ok(()) } + + pub async fn set_len(&self, size: u64) -> io::Result<()> { + Ok(match self { + Self::Tokio(f) => f.set_len(size).await?, + Self::Sftp(f) => { + f.fsetstat(&yazi_sftp::fs::Attrs { size: Some(size), ..Default::default() }).await? + } + }) + } } impl AsyncRead for RwFile { @@ -53,6 +70,27 @@ impl AsyncRead for RwFile { } } +impl AsyncSeek for RwFile { + #[inline] + fn start_seek(mut self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { + match &mut *self { + Self::Tokio(f) => Pin::new(f).start_seek(position), + Self::Sftp(f) => Pin::new(f).start_seek(position), + } + } + + #[inline] + fn poll_complete( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut *self { + Self::Tokio(f) => Pin::new(f).poll_complete(cx), + Self::Sftp(f) => Pin::new(f).poll_complete(cx), + } + } +} + impl AsyncWrite for RwFile { #[inline] fn poll_write( diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 9cc6143b..2be85fdc 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -1,10 +1,10 @@ use std::{io, sync::Arc}; -use tokio::io::{AsyncWriteExt, BufReader, BufWriter}; +use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; use yazi_config::vfs::{ProviderSftp, Vfs}; use yazi_fs::provider::{DirReader, FileHolder, Provider}; use yazi_sftp::fs::{Attrs, Flags}; -use yazi_shared::{path::{AsPath, PathBufDyn}, pool::InternStr, url::{Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow, UrlLike}}; use super::Cha; use crate::provider::sftp::Conn; @@ -88,6 +88,23 @@ impl<'a> Provider for Sftp<'a> { Ok(written) } + fn copy_with_progress(&self, to: P, attrs: A) -> io::Result>> + where + P: AsPath, + A: Into, + { + let to = UrlBuf::Sftp { + loc: LocBuf::::saturated( + to.as_path().to_unix_owned()?, + SchemeKind::Sftp, + ), + domain: self.name.intern(), + }; + let from = self.url.to_owned(); + + Ok(crate::provider::copy_with_progress_impl(from, to, attrs.into())) + } + async fn create_dir(&self) -> io::Result<()> { Ok(self.op().await?.mkdir(self.path, Attrs::default()).await?) } From 878b74acb71eac73d52bebc8a196bd8804f7574c Mon Sep 17 00:00:00 2001 From: rezky_nightky <130107241+oxyzenQ@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:38:09 +0700 Subject: [PATCH 23/53] docs: little typo in CHANGELOG (#3394) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1dfc73..968ccde6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -780,7 +780,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Show keywords when in search mode ([#152]) - Tab switch wraparound ([#160]) - Highlight matched keywords in find mode ([#211]) -- Customable main UI border styles ([#278]) +- Customizable main UI border styles ([#278]) - `` key notion ([#209]) - Use of environment variables in `cd` paths ([#241]) - Nix Flakes package ([#205]) From c7739c5941ee93f6a3ffa55718fd98cec41da7cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Tue, 2 Dec 2025 19:33:27 +0800 Subject: [PATCH 24/53] feat: remote file management (#3396) --- CHANGELOG.md | 5 ++++- yazi-actor/src/cmp/trigger.rs | 5 +++-- yazi-actor/src/mgr/displace.rs | 27 +++++++++++++---------- yazi-adapter/src/adapter.rs | 2 +- yazi-adapter/src/dimension.rs | 2 +- yazi-adapter/src/emulator.rs | 30 +++++++++++++++----------- yazi-adapter/src/lib.rs | 15 +++++++------ yazi-adapter/src/unknown.rs | 4 +--- yazi-boot/src/actions/debug.rs | 23 ++++++++++++++++++-- yazi-config/src/vfs/provider.rs | 2 +- yazi-fs/src/provider/local/casefold.rs | 4 ++-- yazi-parser/src/mgr/displace_do.rs | 2 +- yazi-parser/src/mgr/update_files.rs | 4 ++++ yazi-plugin/preset/plugins/extract.lua | 2 +- yazi-plugin/preset/plugins/folder.lua | 7 +++--- yazi-plugin/src/runtime/term.rs | 2 +- yazi-vfs/src/provider/copier.rs | 23 +++++++++++--------- yazi-vfs/src/provider/sftp/conn.rs | 6 ++++-- yazi-watcher/src/local/local.rs | 2 +- 19 files changed, 105 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 968ccde6..1da97b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Added -- Remote file management ([#3166], [#3170], [#3172], [#3198], [#3201], [#3243], [#3264], [#3268]) +- Remote file management ([#3396]) - Virtual file system ([#3034], [#3035], [#3094], [#3108], [#3187], [#3203]) - Shell formatting ([#3232]) - Multi-entry support for plugin system ([#3154]) @@ -83,6 +83,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Make preload tasks discardable ([#2875]) - Reduce file change event frequency ([#2820]) +- Upload and download of a single file over SFTP in chunks concurrently ([#3393]) - Do not listen for file changes in inactive tabs ([#2958]) - Switch to a higher-performance hash algorithm ([#3083]) - Sequence-based rendering merge strategy ([#2861]) @@ -1548,3 +1549,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3385]: https://github.com/sxyazi/yazi/pull/3385 [#3387]: https://github.com/sxyazi/yazi/pull/3387 [#3391]: https://github.com/sxyazi/yazi/pull/3391 +[#3393]: https://github.com/sxyazi/yazi/pull/3393 +[#3396]: https://github.com/sxyazi/yazi/pull/3396 diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index c2f36b73..c92ef137 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -5,7 +5,7 @@ use yazi_fs::{CWD, path::expand_url, provider::{DirReader, FileHolder}}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_proxy::CmpProxy; -use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{PathBufDyn, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::StrandLike, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{PathBufDyn, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::provider; use crate::{Actor, Ctx}; @@ -69,7 +69,8 @@ impl Trigger { fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> { let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?; - if scheme.is_local() && path == "~" { + tracing::debug!(?scheme, ?path); + if scheme.is_local() && path.as_strand() == "~" { return None; // We don't autocomplete a `~`, but `~/` } diff --git a/yazi-actor/src/mgr/displace.rs b/yazi-actor/src/mgr/displace.rs index a23f1f68..3fa0d6ea 100644 --- a/yazi-actor/src/mgr/displace.rs +++ b/yazi-actor/src/mgr/displace.rs @@ -1,4 +1,5 @@ use anyhow::{Result, bail}; +use yazi_fs::FilesOp; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::{CdSource, DisplaceDoOpt}}; use yazi_proxy::MgrProxy; @@ -22,11 +23,10 @@ impl Actor for Displace { let tab = cx.tab().id; let from = cx.cwd().to_owned(); tokio::spawn(async move { - if let Ok(to) = provider::absolute(&from).await - && to.is_owned() - { - MgrProxy::displace_do(tab, DisplaceDoOpt { to: to.into(), from }); - } + MgrProxy::displace_do(tab, DisplaceDoOpt { + to: provider::absolute(&from).await.map(|u| u.into_owned()), + from, + }); }); succ!(); @@ -42,18 +42,23 @@ impl Actor for DisplaceDo { const NAME: &str = "displace_do"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { - if !opt.to.is_absolute() { - bail!("Target URL must be absolute"); - } - if cx.cwd() != opt.from { succ!() + } + + let to = match opt.to { + Ok(url) => url, + Err(e) => return act!(mgr:update_files, cx, FilesOp::IOErr(opt.from, e.into())), + }; + + if !to.is_absolute() { + bail!("Target URL must be absolute"); } else if let Some(hovered) = cx.hovered() - && let Ok(url) = opt.to.try_join(hovered.urn()) + && let Ok(url) = to.try_join(hovered.urn()) { act!(mgr:reveal, cx, (url, CdSource::Displace)) } else { - act!(mgr:cd, cx, (opt.to, CdSource::Displace)) + act!(mgr:cd, cx, (to, CdSource::Displace)) } } } diff --git a/yazi-adapter/src/adapter.rs b/yazi-adapter/src/adapter.rs index 8a8f091b..e10b7db7 100644 --- a/yazi-adapter/src/adapter.rs +++ b/yazi-adapter/src/adapter.rs @@ -84,7 +84,7 @@ impl Adapter { } impl Adapter { - pub fn matches(emulator: Emulator) -> Self { + pub fn matches(emulator: &Emulator) -> Self { let mut protocols = emulator.adapters().to_owned(); if env_exists("ZELLIJ_SESSION_NAME") { protocols.retain(|p| *p == Self::Sixel); diff --git a/yazi-adapter/src/dimension.rs b/yazi-adapter/src/dimension.rs index 9a81e320..dcbcb6fd 100644 --- a/yazi-adapter/src/dimension.rs +++ b/yazi-adapter/src/dimension.rs @@ -50,7 +50,7 @@ impl Dimension { } pub fn cell_size() -> Option<(f64, f64)> { - let emu = EMULATOR.get(); + let emu = &*EMULATOR; Some(if emu.force_16t { (emu.csi_16t.0 as f64, emu.csi_16t.1 as f64) } else if let Some(r) = Self::available().ratio() { diff --git a/yazi-adapter/src/emulator.rs b/yazi-adapter/src/emulator.rs index ef77d842..4d192917 100644 --- a/yazi-adapter/src/emulator.rs +++ b/yazi-adapter/src/emulator.rs @@ -10,16 +10,25 @@ use yazi_term::tty::{Handle, TTY}; use crate::{Adapter, Brand, Dimension, Mux, TMUX, Unknown}; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct Emulator { pub kind: Either, + pub version: String, pub light: bool, pub csi_16t: (u16, u16), pub force_16t: bool, } impl Default for Emulator { - fn default() -> Self { Self::unknown() } + fn default() -> Self { + Self { + kind: Either::Right(Unknown::default()), + version: String::new(), + light: false, + csi_16t: (0, 0), + force_16t: false, + } + } } impl Emulator { @@ -60,22 +69,14 @@ impl Emulator { let csi_16t = Self::csi_16t(&resp).unwrap_or_default(); Ok(Self { kind, + version: Self::csi_gt_q(&resp).unwrap_or_default(), light: Self::light_bg(&resp).unwrap_or_default(), csi_16t, force_16t: Self::force_16t(csi_16t), }) } - pub const fn unknown() -> Self { - Self { - kind: Either::Right(Unknown::default()), - light: false, - csi_16t: (0, 0), - force_16t: false, - } - } - - pub fn adapters(self) -> &'static [Adapter] { + pub fn adapters(&self) -> &'static [Adapter] { match self.kind { Either::Left(brand) => brand.adapters(), Either::Right(unknown) => unknown.adapters(), @@ -182,6 +183,11 @@ impl Emulator { Some((w.parse().ok()?, h.parse().ok()?)) } + fn csi_gt_q(resp: &str) -> Option { + let (_, s) = resp.split_once("\x1bP>|")?; + Some(s[..s.find("\x1b\\")?].to_owned()) + } + fn light_bg(resp: &str) -> Result { match resp.split_once("]11;rgb:") { Some((_, s)) if s.len() >= 14 => { diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index cf37ad5f..29d87ad3 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -2,9 +2,9 @@ yazi_macro::mod_pub!(drivers); yazi_macro::mod_flat!(adapter brand dimension emulator image info mux unknown); -use yazi_shared::{SyncCell, in_wsl}; +use yazi_shared::{RoCell, SyncCell, in_wsl}; -pub static EMULATOR: SyncCell = SyncCell::new(Emulator::unknown()); +pub static EMULATOR: RoCell = RoCell::new(); pub static ADAPTOR: SyncCell = SyncCell::new(Adapter::Chafa); // Image state @@ -24,8 +24,8 @@ pub fn init() -> anyhow::Result<()> { WSL.set(in_wsl()); // Emulator detection - EMULATOR.set(Emulator::detect().unwrap_or_default()); - TMUX.set(EMULATOR.get().kind.is_left_and(|&b| b == Brand::Tmux)); + let mut emulator = Emulator::detect().unwrap_or_default(); + TMUX.set(emulator.kind.is_left_and(|&b| b == Brand::Tmux)); // Tmux support if TMUX.get() { @@ -33,12 +33,13 @@ pub fn init() -> anyhow::Result<()> { START.set("\x1bPtmux;\x1b\x1b"); CLOSE.set("\x1b\\"); Mux::tmux_passthrough(); - EMULATOR.set(Emulator::detect().unwrap_or_default()); + emulator = Emulator::detect().unwrap_or_default(); } - yazi_config::init_flavor(EMULATOR.get().light)?; + EMULATOR.init(emulator); + yazi_config::init_flavor(EMULATOR.light)?; - ADAPTOR.set(Adapter::matches(EMULATOR.get())); + ADAPTOR.set(Adapter::matches(&EMULATOR)); ADAPTOR.get().start(); Ok(()) } diff --git a/yazi-adapter/src/unknown.rs b/yazi-adapter/src/unknown.rs index fc1f447a..74f5f8fd 100644 --- a/yazi-adapter/src/unknown.rs +++ b/yazi-adapter/src/unknown.rs @@ -1,14 +1,12 @@ use crate::Adapter; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub struct Unknown { pub kgp: bool, pub sixel: bool, } impl Unknown { - pub(super) const fn default() -> Self { Self { kgp: false, sixel: false } } - pub(super) fn adapters(self) -> &'static [Adapter] { use Adapter as A; diff --git a/yazi-boot/src/actions/debug.rs b/yazi-boot/src/actions/debug.rs index b64302f8..b77ff130 100644 --- a/yazi-boot/src/actions/debug.rs +++ b/yazi-boot/src/actions/debug.rs @@ -2,7 +2,8 @@ use std::{env, ffi::OsStr, fmt::Write, path::Path}; use regex::Regex; use yazi_adapter::Mux; -use yazi_config::YAZI; +use yazi_config::{THEME, YAZI}; +use yazi_fs::Xdg; use yazi_shared::timestamp_us; use super::Actions; @@ -19,12 +20,20 @@ impl Actions { writeln!(s, "\nYa")?; writeln!(s, " Version: {}", Self::process_output("ya", "--version"))?; + writeln!(s, "\nConfig")?; + writeln!(s, " Yazi : {}", Self::config_state("yazi"))?; + writeln!(s, " Keymap : {}", Self::config_state("keymap"))?; + writeln!(s, " Theme : {}", Self::config_state("theme"))?; + writeln!(s, " VFS : {}", Self::config_state("vfs"))?; + writeln!(s, " Package : {}", Self::config_state("package"))?; + writeln!(s, " Dark/light flavor: {:?} / {:?}", THEME.flavor.dark, THEME.flavor.light)?; + writeln!(s, "\nEmulator")?; writeln!(s, " TERM : {:?}", env::var_os("TERM"))?; writeln!(s, " TERM_PROGRAM : {:?}", env::var_os("TERM_PROGRAM"))?; writeln!(s, " TERM_PROGRAM_VERSION: {:?}", env::var_os("TERM_PROGRAM_VERSION"))?; writeln!(s, " Brand.from_env : {:?}", yazi_adapter::Brand::from_env())?; - writeln!(s, " Emulator.detect : {:?}", yazi_adapter::EMULATOR)?; + writeln!(s, " Emulator.detect : {:?}", &*yazi_adapter::EMULATOR)?; writeln!(s, "\nAdapter")?; writeln!(s, " Adapter.matches : {:?}", yazi_adapter::ADAPTOR)?; @@ -115,6 +124,16 @@ impl Actions { Ok(s) } + fn config_state(name: &str) -> String { + let p = Xdg::config_dir().join(format!("{name}.toml")); + match std::fs::read_to_string(&p) { + Ok(s) if s.is_empty() => format!("{} (empty)", p.display()), + Ok(s) if s.trim().is_empty() => format!("{} (whitespaces)", p.display()), + Ok(s) => format!("{} ({} chars)", p.display(), s.chars().count()), + Err(e) => format!("{} ({e})", p.display()), + } + } + fn process_output(name: impl AsRef, arg: impl AsRef) -> String { match std::process::Command::new(&name).arg(arg).output() { Ok(out) if out.status.success() => { diff --git a/yazi-config/src/vfs/provider.rs b/yazi-config/src/vfs/provider.rs index 5ae75cdc..544e73ac 100644 --- a/yazi-config/src/vfs/provider.rs +++ b/yazi-config/src/vfs/provider.rs @@ -28,7 +28,7 @@ impl Provider { } // --- SFTP -#[derive(Deserialize, Hash, Serialize, Eq, PartialEq)] +#[derive(Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct ProviderSftp { pub host: String, pub user: String, diff --git a/yazi-fs/src/provider/local/casefold.rs b/yazi-fs/src/provider/local/casefold.rs index c34e1c17..dc576e34 100644 --- a/yazi-fs/src/provider/local/casefold.rs +++ b/yazi-fs/src/provider/local/casefold.rs @@ -1,8 +1,8 @@ use std::{io, path::{Path, PathBuf}}; -pub async fn must_case_match(path: impl AsRef) -> bool { +pub async fn match_name_case(path: impl AsRef) -> bool { let path = path.as_ref(); - casefold(path).await.is_ok_and(|p| p == path) + casefold(path).await.is_ok_and(|p| p.file_name() == path.file_name()) } pub(super) async fn casefold(path: impl AsRef) -> io::Result { diff --git a/yazi-parser/src/mgr/displace_do.rs b/yazi-parser/src/mgr/displace_do.rs index 49d25409..56716945 100644 --- a/yazi-parser/src/mgr/displace_do.rs +++ b/yazi-parser/src/mgr/displace_do.rs @@ -4,7 +4,7 @@ use yazi_shared::{event::CmdCow, url::UrlBuf}; #[derive(Debug)] pub struct DisplaceDoOpt { - pub to: UrlBuf, + pub to: std::io::Result, pub from: UrlBuf, } diff --git a/yazi-parser/src/mgr/update_files.rs b/yazi-parser/src/mgr/update_files.rs index 899c15c4..0402f841 100644 --- a/yazi-parser/src/mgr/update_files.rs +++ b/yazi-parser/src/mgr/update_files.rs @@ -20,6 +20,10 @@ impl TryFrom for UpdateFilesOpt { } } +impl From for UpdateFilesOpt { + fn from(op: FilesOp) -> Self { Self { op } } +} + impl FromLua for UpdateFilesOpt { fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } } diff --git a/yazi-plugin/preset/plugins/extract.lua b/yazi-plugin/preset/plugins/extract.lua index 0c6ae959..d271f3cd 100644 --- a/yazi-plugin/preset/plugins/extract.lua +++ b/yazi-plugin/preset/plugins/extract.lua @@ -66,7 +66,7 @@ function M:try_with(from, pwd, to) if not output then fail("7zip failed to output when extracting '%s', error: %s", from, err) elseif output.status.code ~= 0 then - fail("7zip exited when extracting '%s', error code %s", from, output.status.code) + fail("7zip exited with error code %s when extracting '%s':\n%s", output.status.code, from, output.stderr) end end diff --git a/yazi-plugin/preset/plugins/folder.lua b/yazi-plugin/preset/plugins/folder.lua index 91ecf720..46284a72 100644 --- a/yazi-plugin/preset/plugins/folder.lua +++ b/yazi-plugin/preset/plugins/folder.lua @@ -50,6 +50,7 @@ end function M:spot(job) self.size, self.last = 0, 0 + self:spot_multi(job, false) local url = job.file.url local it = fs.calc_size(url) @@ -69,15 +70,15 @@ function M:spot(job) self:spot_multi(job, true) end -function M:spot_multi(job, force) +function M:spot_multi(job, comp) local now = ya.time() - if not force and now < self.last + 0.1 then + if not comp and now < self.last + 0.1 then return end local rows = { ui.Row({ "Folder" }):style(ui.Style():fg("green")), - ui.Row { " Size:", ya.readable_size(self.size) .. (force and "" or " (?)") }, + ui.Row { " Size:", ya.readable_size(self.size) .. (comp and "" or " (?)") }, ui.Row {}, } diff --git a/yazi-plugin/src/runtime/term.rs b/yazi-plugin/src/runtime/term.rs index 6b8c9713..a4ba61b4 100644 --- a/yazi-plugin/src/runtime/term.rs +++ b/yazi-plugin/src/runtime/term.rs @@ -5,7 +5,7 @@ use yazi_binding::{Composer, ComposerGet, ComposerSet}; pub(super) fn term() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { match key { - b"light" => EMULATOR.get().light.into_lua(lua), + b"light" => EMULATOR.light.into_lua(lua), b"cell_size" => cell_size(lua)?.into_lua(lua), _ => Ok(Value::Nil), } diff --git a/yazi-vfs/src/provider/copier.rs b/yazi-vfs/src/provider/copier.rs index 610006bf..805346a0 100644 --- a/yazi-vfs/src/provider/copier.rs +++ b/yazi-vfs/src/provider/copier.rs @@ -45,7 +45,7 @@ pub(super) fn copy_with_progress_impl( }; let chunks = (cha.len + 10485760 - 1) / 10485760; - let result = futures::stream::iter(0..chunks) + let mut result = futures::stream::iter(0..chunks) .map(|i| { let acc_ = acc_.clone(); let (from, to) = (from.clone(), to.clone()); @@ -79,24 +79,22 @@ pub(super) fn copy_with_progress_impl( copied += n as u64; acc_.fetch_add(n as u64, Ordering::SeqCst); } - dist.flush().await?; - if i == chunks - 1 { - dist.get_ref().set_attrs(attrs).await.ok(); - } - dist.shutdown().await.ok(); - if copied == take { - Ok(()) - } else { + if copied != take { Err(io::Error::other(format!( "short copy for chunk {i}: copied {copied} bytes, expected {take}" ))) + } else if i == chunks - 1 { + Ok(Some(dist.into_inner())) + } else { + dist.shutdown().await.ok(); + Ok(None) } } }) .buffer_unordered(3) - .try_for_each(|_| async { Ok(()) }) + .try_fold(None, |first, file| async { Ok(first.or(file)) }) .await; let n = acc_.swap(0, Ordering::SeqCst); @@ -104,6 +102,11 @@ pub(super) fn copy_with_progress_impl( prog_tx_.send(Ok(n)).await.ok(); } + if let Ok(Some(file)) = &mut result { + file.set_attrs(attrs).await.ok(); + file.shutdown().await.ok(); + } + if let Err(e) = result { prog_tx_.send(Err(e)).await.ok(); } else { diff --git a/yazi-vfs/src/provider/sftp/conn.rs b/yazi-vfs/src/provider/sftp/conn.rs index 14761226..e3d2672e 100644 --- a/yazi-vfs/src/provider/sftp/conn.rs +++ b/yazi-vfs/src/provider/sftp/conn.rs @@ -164,9 +164,11 @@ impl Conn { russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; for key in keys { - match session.authenticate_publickey_with(&self.config.user, key, None, &mut agent).await { + let hash_alg = session.best_supported_rsa_hash().await?.flatten(); + match session.authenticate_publickey_with(&self.config.user, key, hash_alg, &mut agent).await + { Ok(result) if result.success() => return Ok(session), - Ok(_) => {} + Ok(result) => tracing::debug!("Identity agent authentication failed: {result:?}"), Err(e) => tracing::error!("Identity agent authentication error: {e}"), } } diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index 2c317c3c..53ca671e 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -75,7 +75,7 @@ impl Local { }; if let Some(p) = file.url.as_local() - && !provider::local::must_case_match(p).await + && !provider::local::match_name_case(p).await { ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); continue; From c6e03e91d2d3cd45b4728d864e37fb89e5dd526d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 3 Dec 2025 17:06:42 +0800 Subject: [PATCH 25/53] feat: remote filesystem support for interactive `cd` (#3398) --- yazi-actor/src/cmp/trigger.rs | 17 +++++++++-- yazi-actor/src/mgr/cd.rs | 15 +++++----- yazi-actor/src/mgr/displace.rs | 42 +++----------------------- yazi-actor/src/mgr/displace_do.rs | 36 ++++++++++++++++++++++ yazi-actor/src/mgr/mod.rs | 1 + yazi-actor/src/mgr/search.rs | 6 ++-- yazi-actor/src/mgr/stash.rs | 2 +- yazi-config/src/popup/options.rs | 5 ++-- yazi-plugin/src/external/fd.rs | 13 ++------ yazi-plugin/src/external/rg.rs | 12 +++----- yazi-plugin/src/external/rga.rs | 12 +++----- yazi-proxy/src/cmp.rs | 4 +-- yazi-proxy/src/mgr.rs | 8 ++--- yazi-shared/src/scheme/cow.rs | 22 ++++++++++---- yazi-shared/src/scheme/ref.rs | 48 ++++++++++++++++-------------- yazi-shared/src/scheme/scheme.rs | 3 ++ yazi-vfs/src/provider/copier.rs | 8 ++--- yazi-vfs/src/provider/sftp/sftp.rs | 11 +++++-- yazi-watcher/src/local/linked.rs | 2 +- yazi-watcher/src/watcher.rs | 4 +-- 20 files changed, 148 insertions(+), 123 deletions(-) create mode 100644 yazi-actor/src/mgr/displace_do.rs diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index c92ef137..07f192ff 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -68,8 +68,8 @@ impl Actor for Trigger { impl Trigger { fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> { let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?; + let scheme = scheme.zeroed(); - tracing::debug!(?scheme, ?path); if scheme.is_local() && path.as_strand() == "~" { return None; // We don't autocomplete a `~`, but `~/` } @@ -86,7 +86,11 @@ impl Trigger { (UrlCow::try_from((scheme, root)).ok()?.into_owned(), c.into()) } Some((p, c)) => (expand_url(UrlCow::try_from((scheme, p)).ok()?), c.into()), - None => (CWD.load().as_ref().clone(), path.into()), + None if CWD.load().scheme().covariant(&scheme) => (CWD.load().as_ref().clone(), path.into()), + None => { + let empty = PathDyn::with_str(scheme.kind(), ""); + (UrlCow::try_from((scheme, empty)).ok()?.into_owned(), path.into()) + } }) } } @@ -108,6 +112,7 @@ mod tests { #[cfg(unix)] #[test] fn test_split() { + yazi_shared::init_tests(); yazi_fs::init(); compare("", "", ""); compare(" ", "", " "); @@ -124,6 +129,12 @@ mod tests { compare("//foo/", "/foo/", ""); compare("/foo/bar", "/foo/", "bar"); compare("///foo/bar", "/foo/", "bar"); + + CWD.set(&"sftp://test/".parse::().unwrap(), || {}); + compare("sftp://test/a", "sftp://test/", "a"); + compare("sftp://test//a", "sftp://test:0//", "a"); + compare("sftp://test2/a", "sftp://test2/", "a"); + compare("sftp://test2//a", "sftp://test2:0//", "a"); } #[cfg(windows)] @@ -131,9 +142,11 @@ mod tests { fn test_split() { yazi_fs::init(); compare("foo", "", "foo"); + compare(r"foo\", r"foo\", ""); compare(r"foo\bar", r"foo\", "bar"); compare(r"foo\bar\", r"foo\bar\", ""); + compare(r"C:\", r"C:\", ""); compare(r"C:\foo", r"C:\", "foo"); compare(r"C:\foo\", r"C:\foo\", ""); diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 72a9433f..0f855dbc 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -9,8 +9,8 @@ use yazi_fs::{File, FilesOp, path::expand_url}; use yazi_macro::{act, err, render, succ}; use yazi_parser::mgr::CdOpt; use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; -use yazi_shared::{Debounce, data::Data, errors::InputError, url::{UrlBuf, UrlLike}}; -use yazi_vfs::VfsFile; +use yazi_shared::{Debounce, data::Data, errors::InputError, url::{AsUrl, UrlBuf, UrlLike}}; +use yazi_vfs::{VfsFile, provider}; use crate::{Actor, Ctx}; @@ -24,7 +24,7 @@ impl Actor for Cd { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; if opt.interactive { - return Self::cd_interactive(); + return Self::cd_interactive(cx); } let tab = cx.tab_mut(); @@ -59,8 +59,8 @@ impl Actor for Cd { } impl Cd { - fn cd_interactive() -> Result { - let input = InputProxy::show(InputCfg::cd()); + fn cd_interactive(cx: &Ctx) -> Result { + let input = InputProxy::show(InputCfg::cd(cx.cwd().as_url())); tokio::spawn(async move { let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50)); @@ -70,6 +70,7 @@ impl Cd { match result { Ok(s) => { let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return }; + let Ok(url) = provider::absolute(&url).await else { return }; let Ok(file) = File::new(&url).await else { return }; if file.is_dir() { @@ -79,10 +80,10 @@ impl Cd { if let Some(p) = url.parent() { FilesOp::Upserting(p.into(), [(url.urn().into(), file)].into()).emit(); } - MgrProxy::reveal(&url); + MgrProxy::reveal(url); } Err(InputError::Completed(before, ticket)) => { - CmpProxy::trigger(&before, ticket); + CmpProxy::trigger(before, ticket); } _ => break, } diff --git a/yazi-actor/src/mgr/displace.rs b/yazi-actor/src/mgr/displace.rs index 3fa0d6ea..ef039798 100644 --- a/yazi-actor/src/mgr/displace.rs +++ b/yazi-actor/src/mgr/displace.rs @@ -1,7 +1,6 @@ -use anyhow::{Result, bail}; -use yazi_fs::FilesOp; -use yazi_macro::{act, succ}; -use yazi_parser::{VoidOpt, mgr::{CdSource, DisplaceDoOpt}}; +use anyhow::Result; +use yazi_macro::succ; +use yazi_parser::{VoidOpt, mgr::DisplaceDoOpt}; use yazi_proxy::MgrProxy; use yazi_shared::{data::Data, url::UrlLike}; use yazi_vfs::provider; @@ -23,42 +22,9 @@ impl Actor for Displace { let tab = cx.tab().id; let from = cx.cwd().to_owned(); tokio::spawn(async move { - MgrProxy::displace_do(tab, DisplaceDoOpt { - to: provider::absolute(&from).await.map(|u| u.into_owned()), - from, - }); + MgrProxy::displace_do(tab, DisplaceDoOpt { to: provider::canonicalize(&from).await, from }); }); succ!(); } } - -// --- Do -pub struct DisplaceDo; - -impl Actor for DisplaceDo { - type Options = DisplaceDoOpt; - - const NAME: &str = "displace_do"; - - fn act(cx: &mut Ctx, opt: Self::Options) -> Result { - if cx.cwd() != opt.from { - succ!() - } - - let to = match opt.to { - Ok(url) => url, - Err(e) => return act!(mgr:update_files, cx, FilesOp::IOErr(opt.from, e.into())), - }; - - if !to.is_absolute() { - bail!("Target URL must be absolute"); - } else if let Some(hovered) = cx.hovered() - && let Ok(url) = to.try_join(hovered.urn()) - { - act!(mgr:reveal, cx, (url, CdSource::Displace)) - } else { - act!(mgr:cd, cx, (to, CdSource::Displace)) - } - } -} diff --git a/yazi-actor/src/mgr/displace_do.rs b/yazi-actor/src/mgr/displace_do.rs new file mode 100644 index 00000000..aa707ed2 --- /dev/null +++ b/yazi-actor/src/mgr/displace_do.rs @@ -0,0 +1,36 @@ +use anyhow::{Result, bail}; +use yazi_fs::FilesOp; +use yazi_macro::{act, succ}; +use yazi_parser::mgr::{CdSource, DisplaceDoOpt}; +use yazi_shared::{data::Data, url::UrlLike}; + +use crate::{Actor, Ctx}; + +pub struct DisplaceDo; + +impl Actor for DisplaceDo { + type Options = DisplaceDoOpt; + + const NAME: &str = "displace_do"; + + fn act(cx: &mut Ctx, opt: Self::Options) -> Result { + if cx.cwd() != opt.from { + succ!() + } + + let to = match opt.to { + Ok(url) => url, + Err(e) => return act!(mgr:update_files, cx, FilesOp::IOErr(opt.from, e.into())), + }; + + if !to.is_absolute() { + bail!("Target URL must be absolute"); + } else if let Some(hovered) = cx.hovered() + && let Ok(url) = to.try_join(hovered.urn()) + { + act!(mgr:reveal, cx, (url, CdSource::Displace)) + } else { + act!(mgr:cd, cx, (to, CdSource::Displace)) + } + } +} diff --git a/yazi-actor/src/mgr/mod.rs b/yazi-actor/src/mgr/mod.rs index 68c0a218..b6bf2d4e 100644 --- a/yazi-actor/src/mgr/mod.rs +++ b/yazi-actor/src/mgr/mod.rs @@ -7,6 +7,7 @@ yazi_macro::mod_flat!( copy create displace + displace_do download enter escape diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index eac83e7c..af526351 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -8,7 +8,7 @@ use yazi_fs::{FilesOp, cha::Cha}; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::{CdSource, SearchOpt, SearchOptVia}}; use yazi_plugin::external; -use yazi_proxy::{InputProxy, MgrProxy}; +use yazi_proxy::{AppProxy, InputProxy, MgrProxy}; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; @@ -52,8 +52,10 @@ impl Actor for SearchDo { handle.abort(); } - let cwd = tab.cwd().to_search(&opt.subject)?; let hidden = tab.pref.show_hidden; + let Ok(cwd) = tab.cwd().to_search(&opt.subject) else { + succ!(AppProxy::notify_warn("Search", "Only local filesystem searches are supported")); + }; tab.search = Some(tokio::spawn(async move { let rx = match opt.via { diff --git a/yazi-actor/src/mgr/stash.rs b/yazi-actor/src/mgr/stash.rs index afcc8597..2dede4a5 100644 --- a/yazi-actor/src/mgr/stash.rs +++ b/yazi-actor/src/mgr/stash.rs @@ -14,7 +14,7 @@ impl Actor for Stash { const NAME: &str = "stash"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { - if opt.target.is_internal() { + if opt.target.is_absolute() && opt.target.is_internal() { cx.tab_mut().backstack.push(opt.target.as_url()); } diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs index 17dcf238..55a41be1 100644 --- a/yazi-config/src/popup/options.rs +++ b/yazi-config/src/popup/options.rs @@ -1,5 +1,5 @@ use ratatui::{text::{Line, Text}, widgets::{Paragraph, Wrap}}; -use yazi_shared::{strand::ToStrand, url::UrlBuf}; +use yazi_shared::{scheme::Encode as EncodeScheme, strand::ToStrand, url::{Url, UrlBuf}}; use super::{Offset, Position}; use crate::YAZI; @@ -31,9 +31,10 @@ pub struct ConfirmCfg { } impl InputCfg { - pub fn cd() -> Self { + pub fn cd(cwd: Url) -> Self { Self { title: YAZI.input.cd_title.clone(), + value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() }, position: Position::new(YAZI.input.cd_origin, YAZI.input.cd_offset), completion: true, ..Default::default() diff --git a/yazi-plugin/src/external/fd.rs b/yazi-plugin/src/external/fd.rs index 2f9585dc..5b0545a5 100644 --- a/yazi-plugin/src/external/fd.rs +++ b/yazi-plugin/src/external/fd.rs @@ -2,8 +2,8 @@ use std::process::Stdio; use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::{Child, Command}, sync::mpsc::{self, UnboundedReceiver}}; -use yazi_fs::File; -use yazi_shared::url::{UrlBuf, UrlLike}; +use yazi_fs::{File, FsUrl}; +use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; use yazi_vfs::VfsFile; pub struct FdOpt { @@ -34,16 +34,9 @@ pub fn fd(opt: FdOpt) -> Result> { } fn spawn(program: &str, opt: &FdOpt) -> std::io::Result { - let Some(path) = opt.cwd.as_local() else { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "fd can only search local filesystem", - )); - }; - Command::new(program) .arg("--base-directory") - .arg(path) + .arg(&*opt.cwd.as_url().unified_path()) .arg("--regex") .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(&opt.args) diff --git a/yazi-plugin/src/external/rg.rs b/yazi-plugin/src/external/rg.rs index 4f3c6452..65c72431 100644 --- a/yazi-plugin/src/external/rg.rs +++ b/yazi-plugin/src/external/rg.rs @@ -1,9 +1,9 @@ use std::process::Stdio; -use anyhow::{Result, bail}; +use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}}; -use yazi_fs::File; -use yazi_shared::url::{UrlBuf, UrlLike}; +use yazi_fs::{File, FsUrl}; +use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; use yazi_vfs::VfsFile; pub struct RgOpt { @@ -14,16 +14,12 @@ pub struct RgOpt { } pub fn rg(opt: RgOpt) -> Result> { - let Some(path) = opt.cwd.as_local() else { - bail!("rg can only search local filesystem"); - }; - let mut child = Command::new("rg") .args(["--color=never", "--files-with-matches", "--smart-case"]) .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(opt.args) .arg(opt.subject) - .arg(path) + .arg(&*opt.cwd.as_url().unified_path()) .kill_on_drop(true) .stdout(Stdio::piped()) .stderr(Stdio::null()) diff --git a/yazi-plugin/src/external/rga.rs b/yazi-plugin/src/external/rga.rs index d038eae8..a41a5acb 100644 --- a/yazi-plugin/src/external/rga.rs +++ b/yazi-plugin/src/external/rga.rs @@ -1,9 +1,9 @@ use std::process::Stdio; -use anyhow::{Result, bail}; +use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}}; -use yazi_fs::File; -use yazi_shared::url::{UrlBuf, UrlLike}; +use yazi_fs::{File, FsUrl}; +use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; use yazi_vfs::VfsFile; pub struct RgaOpt { @@ -14,16 +14,12 @@ pub struct RgaOpt { } pub fn rga(opt: RgaOpt) -> Result> { - let Some(path) = opt.cwd.as_local() else { - bail!("rga can only search local filesystem"); - }; - let mut child = Command::new("rga") .args(["--color=never", "--files-with-matches", "--smart-case"]) .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(opt.args) .arg(opt.subject) - .arg(path) + .arg(&*opt.cwd.as_url().unified_path()) .kill_on_drop(true) .stdout(Stdio::piped()) .stderr(Stdio::null()) diff --git a/yazi-proxy/src/cmp.rs b/yazi-proxy/src/cmp.rs index 2597244e..f77a166e 100644 --- a/yazi-proxy/src/cmp.rs +++ b/yazi-proxy/src/cmp.rs @@ -9,7 +9,7 @@ impl CmpProxy { emit!(Call(relay!(cmp:show).with_any("opt", opt))); } - pub fn trigger(word: &str, ticket: Id) { - emit!(Call(relay!(cmp:trigger, [word]).with("ticket", ticket))); + pub fn trigger(word: impl Into, ticket: Id) { + emit!(Call(relay!(cmp:trigger, [word.into()]).with("ticket", ticket))); } } diff --git a/yazi-proxy/src/mgr.rs b/yazi-proxy/src/mgr.rs index 37826c26..757b0c6b 100644 --- a/yazi-proxy/src/mgr.rs +++ b/yazi-proxy/src/mgr.rs @@ -11,8 +11,8 @@ impl MgrProxy { emit!(Call(relay!(mgr:arrow, [step.into()]))); } - pub fn cd(target: &UrlBuf) { - emit!(Call(relay!(mgr:cd, [target]).with("raw", true))); + pub fn cd(target: impl Into) { + emit!(Call(relay!(mgr:cd, [target.into()]).with("raw", true))); } pub fn displace_do(tab: Id, opt: DisplaceDoOpt) { @@ -41,8 +41,8 @@ impl MgrProxy { )); } - pub fn reveal(target: &UrlBuf) { - emit!(Call(relay!(mgr:reveal, [target]).with("raw", true).with("no-dummy", true))); + pub fn reveal(target: impl Into) { + emit!(Call(relay!(mgr:reveal, [target.into()]).with("raw", true).with("no-dummy", true))); } pub fn search_do(opt: SearchOpt) { diff --git a/yazi-shared/src/scheme/cow.rs b/yazi-shared/src/scheme/cow.rs index 7c92c371..1ec93849 100644 --- a/yazi-shared/src/scheme/cow.rs +++ b/yazi-shared/src/scheme/cow.rs @@ -168,6 +168,14 @@ impl<'a> SchemeCow<'a> { } impl<'a> SchemeCow<'a> { + #[inline] + pub fn into_domain(self) -> Option> { + Some(match self { + SchemeCow::Borrowed(s) => s.domain()?.into(), + SchemeCow::Owned(s) => s.into_domain()?.into(), + }) + } + #[inline] pub fn into_owned(self) -> Scheme { match self { @@ -176,13 +184,15 @@ impl<'a> SchemeCow<'a> { } } - #[inline] - pub fn into_domain(self) -> Option> { - Some(match self { - SchemeCow::Borrowed(s) => s.domain()?.into(), - SchemeCow::Owned(s) => s.into_domain()?.into(), - }) + pub fn with_ports(self, uri: usize, urn: usize) -> Self { + match self { + Self::Borrowed(s) => s.with_ports(uri, urn).into(), + Self::Owned(s) => s.with_ports(uri, urn).into(), + } } + + #[inline] + pub fn zeroed(self) -> Self { self.with_ports(0, 0) } } #[cfg(test)] diff --git a/yazi-shared/src/scheme/ref.rs b/yazi-shared/src/scheme/ref.rs index 1468305e..a6a69beb 100644 --- a/yazi-shared/src/scheme/ref.rs +++ b/yazi-shared/src/scheme/ref.rs @@ -43,13 +43,9 @@ impl From> for Scheme { impl<'a> SchemeRef<'a> { #[inline] - pub const fn kind(self) -> SchemeKind { - match self { - Self::Regular { .. } => SchemeKind::Regular, - Self::Search { .. } => SchemeKind::Search, - Self::Archive { .. } => SchemeKind::Archive, - Self::Sftp { .. } => SchemeKind::Sftp, - } + pub fn covariant(self, other: impl AsScheme) -> bool { + let other = other.as_scheme(); + if self.is_virtual() || other.is_virtual() { self == other } else { true } } #[inline] @@ -62,6 +58,16 @@ impl<'a> SchemeRef<'a> { } } + #[inline] + pub const fn kind(self) -> SchemeKind { + match self { + Self::Regular { .. } => SchemeKind::Regular, + Self::Search { .. } => SchemeKind::Search, + Self::Archive { .. } => SchemeKind::Archive, + Self::Sftp { .. } => SchemeKind::Sftp, + } + } + #[inline] pub const fn ports(self) -> (usize, usize) { match self { @@ -72,22 +78,6 @@ impl<'a> SchemeRef<'a> { } } - #[inline] - pub const fn zeroed(self) -> Self { - match self { - Self::Regular { .. } => Self::Regular { uri: 0, urn: 0 }, - Self::Search { domain, .. } => Self::Search { domain, uri: 0, urn: 0 }, - Self::Archive { domain, .. } => Self::Archive { domain, uri: 0, urn: 0 }, - Self::Sftp { domain, .. } => Self::Sftp { domain, uri: 0, urn: 0 }, - } - } - - #[inline] - pub fn covariant(self, other: impl AsScheme) -> bool { - let other = other.as_scheme(); - if self.is_virtual() || other.is_virtual() { self == other } else { true } - } - pub fn to_owned(self) -> Scheme { match self { Self::Regular { uri, urn } => Scheme::Regular { uri, urn }, @@ -96,4 +86,16 @@ impl<'a> SchemeRef<'a> { Self::Sftp { domain, uri, urn } => Scheme::Sftp { domain: domain.intern(), uri, urn }, } } + + pub const fn with_ports(self, uri: usize, urn: usize) -> Self { + match self { + Self::Regular { .. } => Self::Regular { uri, urn }, + Self::Search { domain, .. } => Self::Search { domain, uri, urn }, + Self::Archive { domain, .. } => Self::Archive { domain, uri, urn }, + Self::Sftp { domain, .. } => Self::Sftp { domain, uri, urn }, + } + } + + #[inline] + pub const fn zeroed(self) -> Self { self.with_ports(0, 0) } } diff --git a/yazi-shared/src/scheme/scheme.rs b/yazi-shared/src/scheme/scheme.rs index 71d6a84f..1d5961f3 100644 --- a/yazi-shared/src/scheme/scheme.rs +++ b/yazi-shared/src/scheme/scheme.rs @@ -38,4 +38,7 @@ impl Scheme { Self::Sftp { domain, .. } => Self::Sftp { domain, uri, urn }, } } + + #[inline] + pub fn zeroed(self) -> Self { self.with_ports(0, 0) } } diff --git a/yazi-vfs/src/provider/copier.rs b/yazi-vfs/src/provider/copier.rs index 805346a0..c07b0946 100644 --- a/yazi-vfs/src/provider/copier.rs +++ b/yazi-vfs/src/provider/copier.rs @@ -44,15 +44,15 @@ pub(super) fn copy_with_progress_impl( Some(f) }; - let chunks = (cha.len + 10485760 - 1) / 10485760; + let chunks = (cha.len + 5242880 - 1) / 5242880; let mut result = futures::stream::iter(0..chunks) .map(|i| { let acc_ = acc_.clone(); let (from, to) = (from.clone(), to.clone()); let (src, dist) = (src.take(), dist.take()); async move { - let offset = i * 10485760; - let take = cha.len.saturating_sub(offset).min(10485760); + let offset = i * 5242880; + let take = cha.len.saturating_sub(offset).min(5242880); let mut src = BufReader::with_capacity(524288, match src { Some(f) => f, @@ -93,7 +93,7 @@ pub(super) fn copy_with_progress_impl( } } }) - .buffer_unordered(3) + .buffer_unordered(4) .try_fold(None, |first, file| async { Ok(first.or(file)) }) .await; diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 2be85fdc..87c9928f 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -2,7 +2,7 @@ use std::{io, sync::Arc}; use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; use yazi_config::vfs::{ProviderSftp, Vfs}; -use yazi_fs::provider::{DirReader, FileHolder, Provider}; +use yazi_fs::{CWD, provider::{DirReader, FileHolder, Provider}}; use yazi_sftp::fs::{Attrs, Flags}; use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow, UrlLike}}; @@ -26,7 +26,14 @@ impl<'a> Provider for Sftp<'a> { type UrlCow = UrlCow<'a>; async fn absolute(&self) -> io::Result { - Ok(if self.url.is_absolute() { self.url.into() } else { self.canonicalize().await?.into() }) + let cwd = CWD.load(); + Ok(if self.url.is_absolute() { + self.url.into() + } else if cwd.scheme().covariant(self.url.scheme()) { + cwd.try_join(self.path)?.into() + } else { + self.canonicalize().await?.into() + }) } async fn canonicalize(&self) -> io::Result { diff --git a/yazi-watcher/src/local/linked.rs b/yazi-watcher/src/local/linked.rs index 44a6c4de..41d5b4c1 100644 --- a/yazi-watcher/src/local/linked.rs +++ b/yazi-watcher/src/local/linked.rs @@ -59,7 +59,7 @@ impl Linked { Ok(to) if to != *from => _ = linked.write().entry_ref(from).insert(to), Ok(_) => _ = linked.write().remove(from), Err(e) if e.kind() == std::io::ErrorKind::NotFound => _ = linked.write().remove(from), - Err(_) => {} + Err(e) => tracing::error!("Failed to canonicalize watched path {from:?}: {e:?}"), } } }) diff --git a/yazi-watcher/src/watcher.rs b/yazi-watcher/src/watcher.rs index de7fa1ee..a8ce1dcd 100644 --- a/yazi-watcher/src/watcher.rs +++ b/yazi-watcher/src/watcher.rs @@ -56,9 +56,7 @@ impl Watcher { if !to_unwatch.is_empty() || !to_watch.is_empty() { backend = Self::sync(backend, to_unwatch, to_watch).await; - if !rx.has_changed().unwrap_or(false) { - backend = backend.sync().await; - } + backend = backend.sync().await; } if rx.changed().await.is_err() { From dccc9b2c2a2317fa2d7a73c44fd99bb0dacd6360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Thu, 4 Dec 2025 16:13:39 +0800 Subject: [PATCH 26/53] fix: escape control characters in filenames (#3400) --- Cargo.lock | 267 ++++++++++++++--------- Cargo.toml | 4 +- yazi-actor/src/cmp/trigger.rs | 41 ++-- yazi-actor/src/mgr/cd.rs | 3 +- yazi-fs/src/path/clean.rs | 29 ++- yazi-plugin/preset/components/entity.lua | 10 +- yazi-plugin/preset/components/status.lua | 2 +- yazi-plugin/src/elements/elements.rs | 61 ++++-- yazi-plugin/src/external/highlighter.rs | 14 +- yazi-shared/src/chars.rs | 50 +++-- yazi-shared/src/loc/able.rs | 30 ++- yazi-shared/src/loc/loc.rs | 39 +++- yazi-shared/src/path/cow.rs | 4 + yazi-shared/src/strand/conversion.rs | 4 + yazi-shared/src/strand/strand.rs | 2 +- yazi-shared/src/translit/traits.rs | 2 +- 16 files changed, 374 insertions(+), 188 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3267b6f7..4303d578 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -283,7 +283,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -300,9 +300,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bcrypt-pbkdf" @@ -391,7 +391,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -400,7 +400,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -548,7 +548,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -694,9 +694,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -709,9 +709,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-models" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94950e87ea550d6d68f1993f3e7bebc8cb7235157bff84337d46195c3aa0b3f0" +checksum = "0940496e5c83c54f3b753d5317daec82e8edac71c33aaa1f666d76f518de2444" dependencies = [ "hax-lib", "pastey", @@ -837,7 +837,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "subtle", "zeroize", @@ -849,7 +849,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array", + "generic-array 0.14.7", "typenum", ] @@ -1015,22 +1015,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ "convert_case", "proc-macro2", "quote", + "rustc_version", "syn", ] @@ -1131,7 +1132,7 @@ dependencies = [ "crypto-bigint", "digest", "ff", - "generic-array", + "generic-array 0.14.7", "group", "hkdf", "pem-rfc7468", @@ -1445,6 +1446,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-array" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" +dependencies = [ + "generic-array 0.14.7", + "rustversion", + "typenum", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1482,9 +1494,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f954a9e9159ec994f73a30a12b96a702dde78f5547bcb561174597924f7d4162" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" dependencies = [ "color_quant", "weezl", @@ -1759,7 +1771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1901,15 +1913,15 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libcrux-intrinsics" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3b41dcbc21a5fb7efbbb5af7405b2e79c4bfe443924e90b13afc0080318d31" +checksum = "bc9ee7ef66569dd7516454fe26de4e401c0c62073929803486b96744594b9632" dependencies = [ "core-models", "hax-lib", @@ -1917,16 +1929,18 @@ dependencies = [ [[package]] name = "libcrux-ml-kem" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d368d3e8d6a74e277178d54921eca112a1e6b7837d7d8bc555091acb5d817f5" +checksum = "4bb6a88086bf11bd2ec90926c749c4a427f2e59841437dbdede8cde8a96334ab" dependencies = [ "hax-lib", "libcrux-intrinsics", "libcrux-platform", "libcrux-secrets", "libcrux-sha3", + "libcrux-traits", "rand 0.9.2", + "tls_codec", ] [[package]] @@ -1940,22 +1954,33 @@ dependencies = [ [[package]] name = "libcrux-secrets" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332737e629fe6ba7547f5c0f90559eac865d5dbecf98138ffae8f16ab8cbe33f" +checksum = "6e4dbbf6bc9f2bc0f20dc3bea3e5c99adff3bdccf6d2a40488963da69e2ec307" dependencies = [ "hax-lib", ] [[package]] name = "libcrux-sha3" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d95de4257eafdfaf3bffecadb615219b0ca920c553722b3646d32dde76c797" +checksum = "2400bec764d1c75b8a496d5747cffe32f1fb864a12577f0aca2f55a92021c962" dependencies = [ "hax-lib", "libcrux-intrinsics", "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9adfd58e79d860f6b9e40e35127bfae9e5bd3ade33201d1347459011a2add034" +dependencies = [ + "libcrux-secrets", + "rand 0.9.2", ] [[package]] @@ -2013,9 +2038,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loop9" @@ -2501,9 +2526,9 @@ dependencies = [ [[package]] name = "pageant" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb28bd89a207e5cad59072ac4b364b08459d05f90ccfbcdaa920a95857d94430" +checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595" dependencies = [ "byteorder", "bytes", @@ -2511,9 +2536,11 @@ dependencies = [ "futures", "log", "rand 0.8.5", + "sha2", "thiserror 1.0.69", "tokio", - "windows 0.59.0", + "windows 0.62.2", + "windows-strings", ] [[package]] @@ -2566,7 +2593,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -3139,12 +3166,11 @@ dependencies = [ [[package]] name = "russh" -version = "0.54.6" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ca8e9091b72afdc9979bddcd1988ad898fa7bc0b85c8da8c154c29d44319eb" +checksum = "82b4d036bb45d7bbe99dbfef4ec60eaeb614708d22ff107124272f8ef6b54548" dependencies = [ "aes", - "base64ct", "bitflags 2.10.0", "block-padding", "byteorder", @@ -3161,7 +3187,7 @@ dependencies = [ "elliptic-curve", "enum_dispatch", "futures", - "generic-array", + "generic-array 1.3.5", "getrandom 0.2.16", "hex-literal", "hmac", @@ -3172,7 +3198,6 @@ dependencies = [ "log", "md5", "num-bigint", - "once_cell", "p256", "p384", "p521", @@ -3336,7 +3361,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", - "generic-array", + "generic-array 0.14.7", "pkcs8", "subtle", "zeroize", @@ -3772,6 +3797,27 @@ dependencies = [ "time-core", ] +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.48.0" @@ -4046,9 +4092,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", @@ -4290,12 +4336,23 @@ dependencies = [ [[package]] name = "windows" -version = "0.59.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-core 0.59.0", - "windows-targets 0.53.5", + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", ] [[package]] @@ -4310,19 +4367,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" -dependencies = [ - "windows-implement 0.59.0", - "windows-interface 0.59.3", - "windows-result 0.3.4", - "windows-strings 0.3.1", - "windows-targets 0.53.5", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -4331,9 +4375,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement 0.60.2", "windows-interface 0.59.3", - "windows-link 0.2.1", + "windows-link", "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", ] [[package]] @@ -4347,17 +4402,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-implement" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -4391,18 +4435,22 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -4412,31 +4460,13 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4445,7 +4475,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4481,7 +4511,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4506,7 +4536,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -4517,6 +4547,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5176,18 +5215,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -5199,6 +5238,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index 5ab7628e..e0d1044b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ futures = "0.3.31" globset = "0.4.18" hashbrown = { version = "0.16.1", features = [ "serde" ] } indexmap = { version = "2.12.1", features = [ "serde" ] } -libc = "0.2.177" +libc = "0.2.178" lru = "0.16.2" mlua = { version = "0.11.5", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } objc2 = "0.6.3" @@ -44,7 +44,7 @@ percent-encoding = "2.3.2" rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] } ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] } regex = "1.12.2" -russh = { version = "0.54.6", default-features = false, features = [ "ring", "rsa" ] } +russh = { version = "0.55.0", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.145" diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index 07f192ff..91e18739 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -1,11 +1,11 @@ -use std::{mem, path::MAIN_SEPARATOR_STR}; +use std::mem; use anyhow::Result; -use yazi_fs::{CWD, path::expand_url, provider::{DirReader, FileHolder}}; +use yazi_fs::{CWD, path::clean_url, provider::{DirReader, FileHolder}}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_proxy::CmpProxy; -use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{PathBufDyn, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{AsPath, PathBufDyn, PathCow, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::provider; use crate::{Actor, Ctx}; @@ -74,24 +74,25 @@ impl Trigger { return None; // We don't autocomplete a `~`, but `~/` } + let cwd = CWD.load(); + let abs = if !path.is_absolute() && cwd.scheme().covariant(&scheme) { + cwd.loc().try_join(&path).ok()?.into() + } else { + PathCow::from(&path) + }; + let sep = if cfg!(windows) { AnyAsciiChar::new(b"/\\").unwrap() } else { AnyAsciiChar::new(b"/").unwrap() }; - Some(match path.rsplit_pred(sep) { - Some((p, c)) if p.is_empty() => { - let root = PathDyn::with_str(scheme.kind(), MAIN_SEPARATOR_STR); - (UrlCow::try_from((scheme, root)).ok()?.into_owned(), c.into()) - } - Some((p, c)) => (expand_url(UrlCow::try_from((scheme, p)).ok()?), c.into()), - None if CWD.load().scheme().covariant(&scheme) => (CWD.load().as_ref().clone(), path.into()), - None => { - let empty = PathDyn::with_str(scheme.kind(), ""); - (UrlCow::try_from((scheme, empty)).ok()?.into_owned(), path.into()) - } - }) + let child = path.rsplit_pred(sep).map_or(path.as_path(), |(_, c)| c); + let parent = + PathDyn::with(scheme.kind(), abs.encoded_bytes().strip_suffix(child.encoded_bytes())?) + .ok()?; + + Some((clean_url(UrlCow::try_from((scheme, parent)).ok()?), child.into())) } } @@ -130,11 +131,11 @@ mod tests { compare("/foo/bar", "/foo/", "bar"); compare("///foo/bar", "/foo/", "bar"); - CWD.set(&"sftp://test/".parse::().unwrap(), || {}); - compare("sftp://test/a", "sftp://test/", "a"); - compare("sftp://test//a", "sftp://test:0//", "a"); - compare("sftp://test2/a", "sftp://test2/", "a"); - compare("sftp://test2//a", "sftp://test2:0//", "a"); + CWD.set(&"sftp://test".parse::().unwrap(), || {}); + compare("sftp://test/a", "sftp://test/.", "a"); + compare("sftp://test//a", "sftp://test//", "a"); + compare("sftp://test2/a", "sftp://test2/.", "a"); + compare("sftp://test2//a", "sftp://test2//", "a"); } #[cfg(windows)] diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 0f855dbc..1e0a6287 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -5,7 +5,7 @@ use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; use yazi_dds::Pubsub; -use yazi_fs::{File, FilesOp, path::expand_url}; +use yazi_fs::{File, FilesOp, path::{clean_url, expand_url}}; use yazi_macro::{act, err, render, succ}; use yazi_parser::mgr::CdOpt; use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; @@ -71,6 +71,7 @@ impl Cd { Ok(s) => { let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return }; let Ok(url) = provider::absolute(&url).await else { return }; + let url = clean_url(url); let Ok(file) = File::new(&url).await else { return }; if file.is_dir() { diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs index a5b9bc29..7fdac319 100644 --- a/yazi-fs/src/path/clean.rs +++ b/yazi-fs/src/path/clean.rs @@ -21,7 +21,7 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi macro_rules! push { ($i:ident, $c:ident) => {{ - out.push($c); + out.push(($i, $c)); if $i >= base { uri_count += 1; } @@ -31,12 +31,25 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi }}; } + macro_rules! pop { + () => {{ + if let Some((i, _)) = out.pop() { + if i >= base { + uri_count -= 1; + } + if i >= trail { + urn_count -= 1; + } + } + }}; + } + for (i, c) in path.components().enumerate() { match c { CurDir => {} - ParentDir => match out.last() { + ParentDir => match out.last().map(|(_, c)| c) { Some(RootDir) => {} - Some(Normal(_)) => _ = out.pop(), + Some(Normal(_)) => pop!(), None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c), }, c => push!(i, c), @@ -47,7 +60,8 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi let path = if out.is_empty() { PathBufDyn::with_str(kind, ".") } else { - PathBufDyn::from_components(kind, out).expect("components with same kind") + PathBufDyn::from_components(kind, out.into_iter().map(|(_, c)| c)) + .expect("components with same kind") }; (path, uri_count, urn_count) @@ -70,10 +84,11 @@ mod tests { ("archive://:3:2//../../tmp/test.zip/foo/bar", "archive://:3:2//tmp/test.zip/foo/bar"), ("archive://:3:2//tmp/../../test.zip/foo/bar", "archive://:3:2//test.zip/foo/bar"), ("archive://:4:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"), - ("archive://:5:2//tmp/test.zip/../../foo/bar", "archive://:3:2//foo/bar"), - ("archive://:4:4//tmp/test.zip/foo/bar/../../", "archive://:1:1//tmp/test.zip"), - ("archive://:5:4//tmp/test.zip/foo/bar/../../", "archive://:2:1//tmp/test.zip"), + ("archive://:5:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"), + ("archive://:4:4//tmp/test.zip/foo/bar/../../", "archive:////tmp/test.zip"), + ("archive://:5:4//tmp/test.zip/foo/bar/../../", "archive://:1//tmp/test.zip"), ("archive://:4:4//tmp/test.zip/foo/bar/../../../", "archive:////tmp"), + ("sftp://test//root/.config/yazi/../../Downloads", "sftp://test//root/Downloads"), ]; for (input, expected) in cases { diff --git a/yazi-plugin/preset/components/entity.lua b/yazi-plugin/preset/components/entity.lua index 7a287128..3f416737 100644 --- a/yazi-plugin/preset/components/entity.lua +++ b/yazi-plugin/preset/components/entity.lua @@ -31,22 +31,22 @@ function Entity:prefix() end function Entity:highlights() - local name = self._file.name:gsub("\r", "?", 1) + local name, p = self._file.name, ui.printable local highlights = self._file:highlights() if not highlights or #highlights == 0 then - return name + return p(name) end local spans, last = {}, 0 for _, h in ipairs(highlights) do if h[1] > last then - spans[#spans + 1] = name:sub(last + 1, h[1]) + spans[#spans + 1] = p(name:sub(last + 1, h[1])) end - spans[#spans + 1] = ui.Span(name:sub(h[1] + 1, h[2])):style(th.mgr.find_keyword) + spans[#spans + 1] = ui.Span(p(name:sub(h[1] + 1, h[2]))):style(th.mgr.find_keyword) last = h[2] end if last < #name then - spans[#spans + 1] = name:sub(last + 1) + spans[#spans + 1] = p(name:sub(last + 1)) end return ui.Line(spans) end diff --git a/yazi-plugin/preset/components/status.lua b/yazi-plugin/preset/components/status.lua index 45249606..347495f7 100644 --- a/yazi-plugin/preset/components/status.lua +++ b/yazi-plugin/preset/components/status.lua @@ -64,7 +64,7 @@ function Status:name() return "" end - return " " .. h.name:gsub("\r", "?", 1) + return " " .. ui.printable(h.name) end function Status:perm() diff --git a/yazi-plugin/src/elements/elements.rs b/yazi-plugin/src/elements/elements.rs index 6196f4d0..f17b0876 100644 --- a/yazi-plugin/src/elements/elements.rs +++ b/yazi-plugin/src/elements/elements.rs @@ -1,19 +1,23 @@ +use std::borrow::Cow; + use mlua::{AnyUserData, ExternalError, IntoLua, Lua, ObjectLike, Table, Value}; use tracing::error; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use yazi_binding::{Composer, ComposerGet, ComposerSet, Permit, PermitRef, elements::{Line, Rect, Span}}; use yazi_config::LAYOUT; use yazi_proxy::{AppProxy, HIDER}; +use yazi_shared::replace_to_printable; pub fn compose() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { match key { b"area" => area(lua)?, b"hide" => hide(lua)?, - b"width" => width(lua)?, + b"printable" => printable(lua)?, b"redraw" => redraw(lua)?, b"render" => render(lua)?, b"truncate" => truncate(lua)?, + b"width" => width(lua)?, _ => return Ok(Value::Nil), } .into_lua(lua) @@ -54,28 +58,12 @@ pub(super) fn hide(lua: &Lua) -> mlua::Result { f.into_lua(lua) } -pub(super) fn width(lua: &Lua) -> mlua::Result { - let f = lua.create_function(|_, v: Value| match v { - Value::String(s) => { - let (mut acc, b) = (0, s.as_bytes()); - for c in b.utf8_chunks() { - acc += c.valid().width(); - if !c.invalid().is_empty() { - acc += 1; - } - } - Ok(acc) - } - Value::UserData(ud) => { - if let Ok(line) = ud.borrow::() { - Ok(line.width()) - } else if let Ok(span) = ud.borrow::() { - Ok(span.width()) - } else { - Err("expected a string, Line, or Span".into_lua_err())? - } - } - _ => Err("expected a string, Line, or Span".into_lua_err())?, +pub(super) fn printable(lua: &Lua) -> mlua::Result { + let f = lua.create_function(|lua, s: mlua::String| { + Ok(match replace_to_printable(&*s.as_bytes(), false, 1, true) { + Cow::Borrowed(_) => s, + Cow::Owned(new) => lua.create_string(&new)?, + }) })?; f.into_lua(lua) @@ -176,6 +164,33 @@ pub(super) fn truncate(lua: &Lua) -> mlua::Result { f.into_lua(lua) } +pub(super) fn width(lua: &Lua) -> mlua::Result { + let f = lua.create_function(|_, v: Value| match v { + Value::String(s) => { + let (mut acc, b) = (0, s.as_bytes()); + for c in b.utf8_chunks() { + acc += c.valid().width(); + if !c.invalid().is_empty() { + acc += 1; + } + } + Ok(acc) + } + Value::UserData(ud) => { + if let Ok(line) = ud.borrow::() { + Ok(line.width()) + } else if let Ok(span) = ud.borrow::() { + Ok(span.width()) + } else { + Err("expected a string, Line, or Span".into_lua_err())? + } + } + _ => Err("expected a string, Line, or Span".into_lua_err())?, + })?; + + f.into_lua(lua) +} + #[cfg(test)] mod tests { use mlua::{Lua, chunk}; diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index b352a055..e79cbbec 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -6,7 +6,7 @@ use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Th use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader}; use yazi_config::{THEME, YAZI, preview::PreviewWrap}; use yazi_fs::provider::{Provider, local::Local}; -use yazi_shared::{Ids, errors::PeekError, replace_to_printable}; +use yazi_shared::{Ids, errors::PeekError, push_printable_char}; static INCR: Ids = Ids::new(); static SYNTECT: OnceLock<(Theme, SyntaxSet)> = OnceLock::new(); @@ -95,7 +95,7 @@ impl Highlighter { } Ok(if plain { - Text::from(replace_to_printable(&after, YAZI.preview.tab_size)) + Text::from(Self::merge_highlight_lines(&after, YAZI.preview.tab_size)) } else { Self::highlight_with(before, after, syntax.unwrap()).await? }) @@ -203,6 +203,16 @@ impl Highlighter { *b = b'\n'; } } + + fn merge_highlight_lines(s: &[String], tab_size: u8) -> String { + let mut buf = Vec::new(); + buf.reserve_exact(s.iter().map(|s| s.len()).sum::() | 15); + + for &b in s.iter().flat_map(|s| s.as_bytes()) { + push_printable_char(&mut buf, b, true, tab_size, false); + } + unsafe { String::from_utf8_unchecked(buf) } + } } impl Highlighter { diff --git a/yazi-shared/src/chars.rs b/yazi-shared/src/chars.rs index 590067b6..40bfe8f9 100644 --- a/yazi-shared/src/chars.rs +++ b/yazi-shared/src/chars.rs @@ -94,26 +94,48 @@ pub fn replace_vec_cow<'a>(v: &'a [u8], from: &[u8], to: &[u8]) -> Cow<'a, [u8]> Cow::Owned(out) } -pub fn replace_to_printable(s: &[String], tab_size: u8) -> String { - let mut buf = Vec::new(); - buf.try_reserve_exact(s.iter().map(|s| s.len()).sum::() | 15).unwrap_or_else(|_| panic!()); +pub fn replace_to_printable(b: &[u8], lf: bool, tab_size: u8, replacement: bool) -> Cow<'_, [u8]> { + // Fast path to skip over printable chars at the beginning of the string + let printable_len = b.iter().take_while(|&&c| !c.is_ascii_control()).count(); + if printable_len >= b.len() { + return Cow::Borrowed(b); + } - for &b in s.iter().flat_map(|s| s.as_bytes()) { - match b { - b'\n' => buf.push(b'\n'), - b'\t' => { - buf.extend((0..tab_size).map(|_| b' ')); - } - b'\0'..=b'\x1F' => { + let (printable, rest) = b.split_at(printable_len); + + let mut out = Vec::new(); + out.reserve_exact(b.len() | 15); + out.extend_from_slice(printable); + + for &c in rest { + push_printable_char(&mut out, c, lf, tab_size, replacement); + } + Cow::Owned(out) +} + +#[inline] +pub fn push_printable_char(buf: &mut Vec, c: u8, lf: bool, tab_size: u8, replacement: bool) { + match c { + b'\n' if lf => buf.push(b'\n'), + b'\t' => { + buf.extend((0..tab_size).map(|_| b' ')); + } + b'\0'..=b'\x1F' => { + if replacement { + buf.extend_from_slice(&[0xef, 0xbf, 0xbd]); + } else { buf.push(b'^'); - buf.push(b + b'@'); + buf.push(c + b'@'); } - 0x7f => { + } + 0x7f => { + if replacement { + buf.extend_from_slice(&[0xef, 0xbf, 0xbd]); + } else { buf.push(b'^'); buf.push(b'?'); } - _ => buf.push(b), } + _ => buf.push(c), } - unsafe { String::from_utf8_unchecked(buf) } } diff --git a/yazi-shared/src/loc/able.rs b/yazi-shared/src/loc/able.rs index f25b9a3e..a87e17a1 100644 --- a/yazi-shared/src/loc/able.rs +++ b/yazi-shared/src/loc/able.rs @@ -107,7 +107,22 @@ impl<'p> LocAbleImpl<'p> for &'p std::path::Path { where T: AsStrandView<'a, Self::Strand<'a>>, { - self.strip_prefix(base.as_strand_view()).ok() + use std::path::is_separator; + + let p = self.strip_prefix(base.as_strand_view()).ok()?; + let mut b = p.as_encoded_bytes(); + + if b.last().is_none_or(|&c| !is_separator(c as char)) || p.parent().is_none() { + return Some(p); + } + + while let [head @ .., last] = b + && is_separator(*last as char) + { + b = head; + } + + Some(unsafe { Self::from_encoded_bytes_unchecked(b) }) } fn to_path_buf(self) -> Self::Owned { self.to_path_buf() } @@ -139,7 +154,18 @@ impl<'p> LocAbleImpl<'p> for &'p typed_path::UnixPath { where T: AsStrandView<'a, Self::Strand<'a>>, { - self.strip_prefix(base.as_strand_view()).ok() + let p = self.strip_prefix(base.as_strand_view()).ok()?; + let mut b = p.as_bytes(); + + if b.last().is_none_or(|&c| c != b'/') || p.parent().is_none() { + return Some(p); + } + + while let [head @ .., b'/'] = b { + b = head; + } + + Some(typed_path::UnixPath::new(b)) } fn to_path_buf(self) -> Self::Owned { self.to_path_buf() } diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 59492801..d377b396 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -85,8 +85,8 @@ where { let path = path.as_path_view(); let Some(name) = path.file_name() else { - let uri = path.strip_prefix(P::empty()).unwrap().len(); - return Self { inner: path, uri, urn: 0, _phantom: PhantomData }; + let p = path.strip_prefix(P::empty()).unwrap(); + return Self { inner: p, uri: p.len(), urn: 0, _phantom: PhantomData }; }; let name_len = name.len(); @@ -244,3 +244,38 @@ where loc } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_with() -> Result<()> { + let cases = [ + // Relative paths + ("tmp/test.zip/foo/bar", 3, 2, "test.zip/foo/bar", "foo/bar"), + ("tmp/test.zip/foo/bar/", 3, 2, "test.zip/foo/bar", "foo/bar"), + // Absolute paths + ("/tmp/test.zip/foo/bar", 3, 2, "test.zip/foo/bar", "foo/bar"), + ("/tmp/test.zip/foo/bar/", 3, 2, "test.zip/foo/bar", "foo/bar"), + // Relative path with parent components + ("tmp/test.zip/foo/bar/../..", 5, 4, "test.zip/foo/bar/../..", "foo/bar/../.."), + ("tmp/test.zip/foo/bar/../../", 5, 4, "test.zip/foo/bar/../..", "foo/bar/../.."), + // Absolute path with parent components + ("/tmp/test.zip/foo/bar/../..", 5, 4, "test.zip/foo/bar/../..", "foo/bar/../.."), + ("/tmp/test.zip/foo/bar/../../", 5, 4, "test.zip/foo/bar/../..", "foo/bar/../.."), + ]; + + for (path, uri, urn, expect_uri, expect_urn) in cases { + let loc = Loc::with(std::path::Path::new(path), uri, urn)?; + assert_eq!(loc.uri().to_str().unwrap(), expect_uri); + assert_eq!(loc.urn().to_str().unwrap(), expect_urn); + + let loc = Loc::with(typed_path::UnixPath::new(path), uri, urn)?; + assert_eq!(loc.uri().to_str().unwrap(), expect_uri); + assert_eq!(loc.urn().to_str().unwrap(), expect_urn); + } + + Ok(()) + } +} diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs index 49b7ef4f..fe3f1073 100644 --- a/yazi-shared/src/path/cow.rs +++ b/yazi-shared/src/path/cow.rs @@ -19,6 +19,10 @@ impl From for PathCow<'_> { fn from(value: PathBufDyn) -> Self { Self::Owned(value) } } +impl<'a> From<&'a PathCow<'_>> for PathCow<'a> { + fn from(value: &'a PathCow<'_>) -> Self { Self::Borrowed(value.as_path()) } +} + impl From> for PathBufDyn { fn from(value: PathCow<'_>) -> Self { value.into_owned() } } diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs index 846f139c..8e46e012 100644 --- a/yazi-shared/src/strand/conversion.rs +++ b/yazi-shared/src/strand/conversion.rs @@ -98,6 +98,10 @@ impl AsStrand for PathCow<'_> { } } +impl AsStrand for &PathCow<'_> { + fn as_strand(&self) -> Strand<'_> { (**self).as_strand() } +} + impl AsStrand for Strand<'_> { fn as_strand(&self) -> Strand<'_> { *self } } diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs index a1cc5711..daddd691 100644 --- a/yazi-shared/src/strand/strand.rs +++ b/yazi-shared/src/strand/strand.rs @@ -109,7 +109,7 @@ impl<'a> Strand<'a> { let (skip, rest) = bytes.split_at(skip_len); let mut out = Vec::new(); - out.try_reserve_exact(bytes.len()).unwrap_or_else(|_| panic!()); + out.reserve_exact(bytes.len()); out.extend(skip); for &b in rest { diff --git a/yazi-shared/src/translit/traits.rs b/yazi-shared/src/translit/traits.rs index 590746f9..71d01824 100644 --- a/yazi-shared/src/translit/traits.rs +++ b/yazi-shared/src/translit/traits.rs @@ -19,7 +19,7 @@ impl Transliterator for &[u8] { // but instead of `+ 16` uses `| 15` to stay in the smallest allocation bucket // for short strings let mut out = String::new(); - out.try_reserve_exact(self.len() | 15).unwrap_or_else(|_| panic!()); + out.reserve_exact(self.len() | 15); out.push_str(unsafe { str::from_utf8_unchecked(ascii) }); for c in String::from_utf8_lossy(rest).chars() { From 205b7fe11fdc5780e65779bb1e0350f3efc1c0a8 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Fri, 5 Dec 2025 15:04:39 +0800 Subject: [PATCH 27/53] refactor: rename the "provider" term in VFS config to "service" (#3403) --- Cargo.lock | 8 +- yazi-config/src/vfs/mod.rs | 2 +- .../src/vfs/{provider.rs => service.rs} | 16 ++-- yazi-config/src/vfs/vfs.rs | 18 ++--- yazi-fs/src/path/path.rs | 56 -------------- yazi-fs/src/path/relative.rs | 77 ++++++++++++++----- yazi-plugin/preset/plugins/fzf.lua | 5 +- yazi-plugin/preset/plugins/mime-remote.lua | 4 +- yazi-scheduler/src/file/file.rs | 26 +++---- yazi-shared/src/path/cow.rs | 4 + yazi-vfs/src/provider/provider.rs | 19 ++--- yazi-vfs/src/provider/sftp/conn.rs | 4 +- yazi-vfs/src/provider/sftp/gate.rs | 4 +- yazi-vfs/src/provider/sftp/mod.rs | 2 +- yazi-vfs/src/provider/sftp/sftp.rs | 6 +- 15 files changed, 119 insertions(+), 132 deletions(-) rename yazi-config/src/vfs/{provider.rs => service.rs} (83%) diff --git a/Cargo.lock b/Cargo.lock index 4303d578..58543723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1305,9 +1305,9 @@ checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "30f4148e3c9b7dbe0cc7e842ad5a61b28f9025f201d78149383e778a08bc9215" dependencies = [ "crc32fast", "miniz_oxide", @@ -2136,9 +2136,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", diff --git a/yazi-config/src/vfs/mod.rs b/yazi-config/src/vfs/mod.rs index b27c77fc..e52b3881 100644 --- a/yazi-config/src/vfs/mod.rs +++ b/yazi-config/src/vfs/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(provider vfs); +yazi_macro::mod_flat!(service vfs); diff --git a/yazi-config/src/vfs/provider.rs b/yazi-config/src/vfs/service.rs similarity index 83% rename from yazi-config/src/vfs/provider.rs rename to yazi-config/src/vfs/service.rs index 544e73ac..1a6f5126 100644 --- a/yazi-config/src/vfs/provider.rs +++ b/yazi-config/src/vfs/service.rs @@ -5,21 +5,21 @@ use yazi_fs::path::expand_url; #[derive(Deserialize, Serialize)] #[serde(tag = "type", rename_all = "kebab-case")] -pub enum Provider { - Sftp(ProviderSftp), +pub enum Service { + Sftp(ServiceSftp), } -impl TryFrom<&'static Provider> for &'static ProviderSftp { +impl TryFrom<&'static Service> for &'static ServiceSftp { type Error = &'static str; - fn try_from(value: &'static Provider) -> Result { + fn try_from(value: &'static Service) -> Result { match value { - Provider::Sftp(p) => Ok(p), + Service::Sftp(p) => Ok(p), } } } -impl Provider { +impl Service { pub(super) fn reshape(&mut self) -> io::Result<()> { match self { Self::Sftp(p) => p.reshape(), @@ -29,7 +29,7 @@ impl Provider { // --- SFTP #[derive(Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct ProviderSftp { +pub struct ServiceSftp { pub host: String, pub user: String, pub port: u16, @@ -41,7 +41,7 @@ pub struct ProviderSftp { pub identity_agent: PathBuf, } -impl ProviderSftp { +impl ServiceSftp { fn reshape(&mut self) -> io::Result<()> { if !self.key_file.as_os_str().is_empty() { self.key_file = expand_url(&self.key_file) diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs index 3459ad82..c5039ac8 100644 --- a/yazi-config/src/vfs/vfs.rs +++ b/yazi-config/src/vfs/vfs.rs @@ -6,11 +6,11 @@ use tokio::sync::OnceCell; use yazi_fs::Xdg; use yazi_macro::ok_or_not_found; -use super::Provider; +use super::Service; #[derive(Deserialize, Serialize)] pub struct Vfs { - pub providers: HashMap, + pub services: HashMap, } impl Vfs { @@ -27,16 +27,16 @@ impl Vfs { LOADED.get_or_try_init(init).await } - pub async fn provider<'a, P>(name: &str) -> io::Result<(&'a str, P)> + pub async fn service<'a, P>(name: &str) -> io::Result<(&'a str, P)> where - P: TryFrom<&'a Provider, Error = &'static str>, + P: TryFrom<&'a Service, Error = &'static str>, { - let Some((key, value)) = Self::load().await?.providers.get_key_value(name) else { - return Err(io::Error::other(format!("No such VFS provider: {name}"))); + let Some((key, value)) = Self::load().await?.services.get_key_value(name) else { + return Err(io::Error::other(format!("No such VFS service: {name}"))); }; match value.try_into() { Ok(p) => Ok((key.as_str(), p)), - Err(e) => Err(io::Error::other(format!("VFS provider `{key}` has wrong type: {e}"))), + Err(e) => Err(io::Error::other(format!("VFS service `{key}` has wrong type: {e}"))), } } @@ -48,14 +48,14 @@ impl Vfs { } fn reshape(mut self) -> io::Result { - for (name, provider) in &mut self.providers { + for (name, service) in &mut self.services { if name.is_empty() || name.len() > 20 { Err(io::Error::other(format!("VFS name `{name}` must be between 1 and 20 characters")))?; } else if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { Err(io::Error::other(format!("VFS name `{name}` must be in kebab-case")))?; } - provider.reshape()?; + service.reshape()?; } Ok(self) diff --git a/yazi-fs/src/path/path.rs b/yazi-fs/src/path/path.rs index f59eb489..c1b46fcd 100644 --- a/yazi-fs/src/path/path.rs +++ b/yazi-fs/src/path/path.rs @@ -9,59 +9,3 @@ pub fn skip_url(url: &UrlBuf, n: usize) -> StrandCow<'_> { } it.strand() } - -#[cfg(test)] -mod tests { - use yazi_shared::url::{AsUrl, UrlCow}; - - use crate::path::url_relative_to; - - #[test] - fn test_url_relative_to() { - yazi_shared::init_tests(); - - #[cfg(unix)] - let cases = [ - // Same urls - ("", "", "."), - (".", ".", "."), - ("/a", "/a", "."), - ("regular:///", "/", "."), - ("regular://", "regular://", "."), - ("regular://", "search://kw/", "search://kw/."), - ("regular:///b", "search://kw//b", "search://kw/."), - // Relative urls - ("foo", "bar", "../bar"), - // Absolute urls - ("/a/b/c", "/a/b", ".."), - ("/a/b", "/a/b/c", "c"), - ("/a/b/d", "/a/b/c", "../c"), - ("/a/b/c", "/a", "../.."), - ("/a/b/b", "/a/a/b", "../../a/b"), - ("regular:///a/b", "regular:///a/b/c", "c"), - ("/a/b/c/", "search://kw//a/d/", "search://kw/../../d"), - ("search://kw//a/b/c", "search://kw//a/b", "search://kw/.."), - // Different schemes - ("", "sftp://test/", "sftp://test/"), - ("a", "sftp://test/", "sftp://test/"), - ("a", "sftp://test/b", "sftp://test/b"), - ("/a", "sftp://test//b", "sftp://test//b"), - ("sftp://test//a/b", "sftp://test//a/d", "sftp://test:0:0/../d"), - ]; - - #[cfg(windows)] - let cases = [ - (r"C:\a\b\c", r"C:\a\b", r".."), - (r"C:\a\b", r"C:\a\b\c", "c"), - (r"C:\a\b\d", r"C:\a\b\c", r"..\c"), - (r"C:\a\b\c", r"C:\a", r"..\.."), - (r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b"), - ]; - - for (from, to, expected) in cases { - let from: UrlCow = from.try_into().unwrap(); - let to: UrlCow = to.try_into().unwrap(); - assert_eq!(format!("{:?}", url_relative_to(from, to).unwrap().as_url()), expected); - } - } -} diff --git a/yazi-fs/src/path/relative.rs b/yazi-fs/src/path/relative.rs index 172ec187..30a2b8e4 100644 --- a/yazi-fs/src/path/relative.rs +++ b/yazi-fs/src/path/relative.rs @@ -1,39 +1,35 @@ use anyhow::{Result, bail}; -use yazi_shared::{path::PathBufDyn, url::{UrlCow, UrlLike}}; +use yazi_shared::path::{PathBufDyn, PathCow, PathDyn, PathLike}; -pub fn url_relative_to<'a, 'b, U, V>(from: U, to: V) -> Result> +pub fn path_relative_to<'a, 'b, P, Q>(from: P, to: Q) -> Result> where - U: Into>, - V: Into>, + P: Into>, + Q: Into>, { - url_relative_to_(from.into(), to.into()) + path_relative_to_impl(from.into(), to.into()) } -fn url_relative_to_<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> { - use yazi_shared::url::Component::*; +fn path_relative_to_impl<'a>(from: PathCow<'_>, to: PathCow<'a>) -> Result> { + use yazi_shared::path::Component::*; if from.is_absolute() != to.is_absolute() { return if to.is_absolute() { Ok(to) } else { - bail!("Urls must be both absolute or both relative: {from:?} and {to:?}"); + bail!("Paths must be both absolute or both relative: {from:?} and {to:?}"); }; } - if from.covariant(&to) { - return UrlCow::try_from(( - to.scheme().zeroed().to_owned(), - PathBufDyn::with_str(to.kind(), "."), - )); + if from == to { + return Ok(PathDyn::with_str(to.kind(), ".").into()); } let (mut f_it, mut t_it) = (from.components(), to.components()); let (f_head, t_head) = loop { match (f_it.next(), t_it.next()) { - (Some(Scheme(a)), Some(Scheme(b))) if a.covariant(b) => {} (Some(RootDir), Some(RootDir)) => {} (Some(Prefix(a)), Some(Prefix(b))) if a == b => {} - (Some(Scheme(_) | Prefix(_) | RootDir), _) | (_, Some(Scheme(_) | Prefix(_) | RootDir)) => { + (Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => { return Ok(to); } (None, None) => break (None, None), @@ -45,8 +41,51 @@ fn url_relative_to_<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir); let rest = t_head.into_iter().chain(t_it); - let iter = dots.chain(rest).map(|c| c.downgrade().expect("path component from dot or normal")); - let buf = PathBufDyn::from_components(to.kind(), iter)?; - - UrlCow::try_from((to.scheme().zeroed().to_owned(), buf)) + let buf = PathBufDyn::from_components(to.kind(), dots.chain(rest))?; + Ok(buf.into()) +} + +#[cfg(test)] +mod tests { + use yazi_shared::path::PathDyn; + + use super::*; + + #[test] + fn test_path_relative_to() { + yazi_shared::init_tests(); + + #[cfg(unix)] + let cases = [ + // Same paths + ("", "", "."), + (".", ".", "."), + ("/", "/", "."), + ("/a", "/a", "."), + // Relative paths + ("foo", "bar", "../bar"), + // Absolute paths + ("/a/b", "/a/b/c", "c"), + ("/a/b/c", "/a/b", ".."), + ("/a/b/d", "/a/b/c", "../c"), + ("/a/b/c", "/a", "../.."), + ("/a/b/c/", "/a/d/", "../../d"), + ("/a/b/b", "/a/a/b", "../../a/b"), + ]; + + #[cfg(windows)] + let cases = [ + (r"C:\a\b", r"C:\a\b\c", "c"), + (r"C:\a\b\c", r"C:\a\b", r".."), + (r"C:\a\b\d", r"C:\a\b\c", r"..\c"), + (r"C:\a\b\c", r"C:\a", r"..\.."), + (r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b"), + ]; + + for (from, to, expected) in cases { + let from = PathDyn::Os(from.as_ref()); + let to = PathDyn::Os(to.as_ref()); + assert_eq!(path_relative_to(from, to).unwrap().to_str().unwrap(), expected); + } + } } diff --git a/yazi-plugin/preset/plugins/fzf.lua b/yazi-plugin/preset/plugins/fzf.lua index 3de191bb..ca239c43 100644 --- a/yazi-plugin/preset/plugins/fzf.lua +++ b/yazi-plugin/preset/plugins/fzf.lua @@ -11,9 +11,12 @@ end) function M:entry() ya.emit("escape", { visual = true }) - local _permit = ui.hide() local cwd, selected = state() + if cwd.scheme.is_virtual then + return ya.notify { title = "Fzf", content = "Not supported under virtual filesystems", timeout = 5, level = "warn" } + end + local _permit = ui.hide() local output, err = M.run_with(cwd, selected) if not output then return ya.notify { title = "Fzf", content = tostring(err), timeout = 5, level = "error" } diff --git a/yazi-plugin/preset/plugins/mime-remote.lua b/yazi-plugin/preset/plugins/mime-remote.lua index 581e675d..d936fe5c 100644 --- a/yazi-plugin/preset/plugins/mime-remote.lua +++ b/yazi-plugin/preset/plugins/mime-remote.lua @@ -17,7 +17,9 @@ end function M:fetch(job) local updates, unknown, state = {}, {}, {} for i, file in ipairs(job.files) do - if not file.cache then + if file.cha.is_dummy then + -- Skip dummy files + elseif not file.cache then unknown[#unknown + 1] = file elseif not fs.cha(file.cache) then updates[file.url], state[i] = "vfs/absent", true diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 075749cb..da141ab2 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,10 +1,10 @@ -use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}}; +use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}, time::Duration}; use anyhow::{Context, Result, anyhow}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::YAZI; -use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{skip_url, url_relative_to}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; +use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::PathCow, timestamp_us, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::{VfsCha, maybe_exists, provider::{self, DirEntry}, unique_name}; @@ -132,27 +132,27 @@ impl File { } pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> { - let src: PathCow = if task.resolve { + if !task.from.scheme().covariant(task.to.scheme()) { + Err(anyhow!("Source and target must be on the same filesystem: {task:?}"))? + } + + let mut src: PathCow = if task.resolve { ok_or_not_found!( provider::read_link(&task.from).await, return Ok(self.ops.out(task.id, FileOutLink::Succ)) ) .into() - } else if task.from.scheme().covariant(task.to.scheme()) { - task.from.loc().into() } else { - Err(anyhow!("Source and target must be on the same filesystem: {task:?}"))? + task.from.loc().into() }; - let src = UrlCow::try_from((task.from.scheme(), src))?; - let src = if task.relative { - url_relative_to(provider::canonicalize(task.to.parent().unwrap()).await?, &src)? - } else { - src - }; + if task.relative { + let canon = provider::canonicalize(task.to.parent().unwrap()).await?; + src = path_relative_to(canon.loc(), src)?; + } ok_or_not_found!(provider::remove_file(&task.to).await); - provider::symlink(&src, &task.to, async || { + provider::symlink(task.to, src, async || { Ok(match task.cha { Some(cha) => cha.is_dir(), None => Self::cha(&task.from, task.resolve, None).await?.is_dir(), diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs index fe3f1073..dc8a0a32 100644 --- a/yazi-shared/src/path/cow.rs +++ b/yazi-shared/src/path/cow.rs @@ -27,6 +27,10 @@ impl From> for PathBufDyn { fn from(value: PathCow<'_>) -> Self { value.into_owned() } } +impl PartialEq for PathCow<'_> { + fn eq(&self, other: &Self) -> bool { self.as_path() == other.as_path() } +} + impl PartialEq<&str> for PathCow<'_> { fn eq(&self, other: &&str) -> bool { match self { diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/provider/provider.rs index d151dea0..4a93a644 100644 --- a/yazi-vfs/src/provider/provider.rs +++ b/yazi-vfs/src/provider/provider.rs @@ -201,32 +201,27 @@ where } } -pub async fn symlink(original: U, link: V, is_dir: F) -> io::Result<()> +pub async fn symlink(link: U, original: P, is_dir: F) -> io::Result<()> where U: AsUrl, - V: AsUrl, + P: AsPath, F: AsyncFnOnce() -> io::Result, { - let (original, link) = (original.as_url(), link.as_url()); - if original.scheme().covariant(link.scheme()) { - Providers::new(link).await?.symlink(original.loc(), is_dir).await - } else { - Err(io::Error::from(io::ErrorKind::CrossesDevices)) - } + Providers::new(link.as_url()).await?.symlink(original, is_dir).await } -pub async fn symlink_dir(original: P, link: U) -> io::Result<()> +pub async fn symlink_dir(link: U, original: P) -> io::Result<()> where - P: AsPath, U: AsUrl, + P: AsPath, { Providers::new(link.as_url()).await?.symlink_dir(original).await } -pub async fn symlink_file(original: P, link: U) -> io::Result<()> +pub async fn symlink_file(link: U, original: P) -> io::Result<()> where - P: AsPath, U: AsUrl, + P: AsPath, { Providers::new(link.as_url()).await?.symlink_file(original).await } diff --git a/yazi-vfs/src/provider/sftp/conn.rs b/yazi-vfs/src/provider/sftp/conn.rs index e3d2672e..251b41ec 100644 --- a/yazi-vfs/src/provider/sftp/conn.rs +++ b/yazi-vfs/src/provider/sftp/conn.rs @@ -1,13 +1,13 @@ use std::{io, sync::Arc}; use russh::keys::PrivateKeyWithHashAlg; -use yazi_config::vfs::ProviderSftp; +use yazi_config::vfs::ServiceSftp; use yazi_fs::provider::local::Local; #[derive(Clone, Copy)] pub(super) struct Conn { pub(super) name: &'static str, - pub(super) config: &'static ProviderSftp, + pub(super) config: &'static ServiceSftp, } impl russh::client::Handler for Conn { diff --git a/yazi-vfs/src/provider/sftp/gate.rs b/yazi-vfs/src/provider/sftp/gate.rs index 4585b531..16c4048a 100644 --- a/yazi-vfs/src/provider/sftp/gate.rs +++ b/yazi-vfs/src/provider/sftp/gate.rs @@ -1,6 +1,6 @@ use std::io; -use yazi_config::vfs::{ProviderSftp, Vfs}; +use yazi_config::vfs::{ServiceSftp, Vfs}; use yazi_fs::provider::{Attrs, FileBuilder}; use yazi_sftp::fs::Flags; use yazi_shared::url::{AsUrl, Url}; @@ -64,7 +64,7 @@ impl FileBuilder for Gate { { let url = url.as_url(); let (path, (name, config)) = match url { - Url::Sftp { loc, domain } => (*loc, Vfs::provider::<&ProviderSftp>(domain).await?), + Url::Sftp { loc, domain } => (*loc, Vfs::service::<&ServiceSftp>(domain).await?), _ => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not an SFTP URL: {url:?}")))?, }; diff --git a/yazi-vfs/src/provider/sftp/mod.rs b/yazi-vfs/src/provider/sftp/mod.rs index 2d9809ae..cef2f5ab 100644 --- a/yazi-vfs/src/provider/sftp/mod.rs +++ b/yazi-vfs/src/provider/sftp/mod.rs @@ -3,7 +3,7 @@ yazi_macro::mod_flat!(conn gate metadata read_dir sftp); static CONN: yazi_shared::RoCell< parking_lot::Mutex< hashbrown::HashMap< - &'static yazi_config::vfs::ProviderSftp, + &'static yazi_config::vfs::ServiceSftp, &'static deadpool::managed::Pool, >, >, diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 87c9928f..6fdc063a 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -1,7 +1,7 @@ use std::{io, sync::Arc}; use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; -use yazi_config::vfs::{ProviderSftp, Vfs}; +use yazi_config::vfs::{ServiceSftp, Vfs}; use yazi_fs::{CWD, provider::{DirReader, FileHolder, Provider}}; use yazi_sftp::fs::{Attrs, Flags}; use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow, UrlLike}}; @@ -15,7 +15,7 @@ pub struct Sftp<'a> { path: &'a typed_path::UnixPath, name: &'static str, - config: &'static ProviderSftp, + config: &'static ServiceSftp, } impl<'a> Provider for Sftp<'a> { @@ -136,7 +136,7 @@ impl<'a> Provider for Sftp<'a> { Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url:?}"))) } Url::Sftp { loc, domain } => { - let (name, config) = Vfs::provider::<&ProviderSftp>(domain).await?; + let (name, config) = Vfs::service::<&ServiceSftp>(domain).await?; Ok(Self::Me { url, path: loc.as_inner(), name, config }) } } From c569263a5084f627ae70f983c271464b42890426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Fri, 5 Dec 2025 23:22:47 +0800 Subject: [PATCH 28/53] fix: always follow symlinks when pasting remote files (#3406) --- yazi-fs/src/path/relative.rs | 4 +-- yazi-fs/src/provider/local/local.rs | 20 +++++++-------- yazi-fs/src/provider/traits.rs | 14 +++++------ yazi-scheduler/src/file/file.rs | 24 ++++++++++++------ yazi-scheduler/src/scheduler.rs | 33 ++++++++---------------- yazi-sftp/src/fs/read_dir.rs | 15 ++++++++--- yazi-sftp/src/session.rs | 26 +++++++++++++------ yazi-vfs/src/provider/copier.rs | 39 ++++++++++++++++++----------- yazi-vfs/src/provider/provider.rs | 14 +++++------ yazi-vfs/src/provider/providers.rs | 14 +++++------ yazi-vfs/src/provider/sftp/sftp.rs | 10 ++++---- 11 files changed, 120 insertions(+), 93 deletions(-) diff --git a/yazi-fs/src/path/relative.rs b/yazi-fs/src/path/relative.rs index 30a2b8e4..7b0808e2 100644 --- a/yazi-fs/src/path/relative.rs +++ b/yazi-fs/src/path/relative.rs @@ -21,7 +21,7 @@ fn path_relative_to_impl<'a>(from: PathCow<'_>, to: PathCow<'a>) -> Result(from: PathCow<'_>, to: PathCow<'a>) -> Result Provider for Local<'a> { } #[inline] - async fn symlink(&self, original: P, _is_dir: F) -> io::Result<()> + async fn symlink(&self, original: S, _is_dir: F) -> io::Result<()> where - P: AsPath, + S: AsStrand, F: AsyncFnOnce() -> io::Result, { #[cfg(unix)] { - let original = original.as_path().as_os()?; + let original = original.as_strand().as_os()?; tokio::fs::symlink(original, self.path).await } #[cfg(windows)] @@ -139,11 +139,11 @@ impl<'a> Provider for Local<'a> { } #[inline] - async fn symlink_dir

(&self, original: P) -> io::Result<()> + async fn symlink_dir(&self, original: S) -> io::Result<()> where - P: AsPath, + S: AsStrand, { - let original = original.as_path().as_os()?; + let original = original.as_strand().as_os()?; #[cfg(unix)] { @@ -156,11 +156,11 @@ impl<'a> Provider for Local<'a> { } #[inline] - async fn symlink_file

(&self, original: P) -> io::Result<()> + async fn symlink_file(&self, original: S) -> io::Result<()> where - P: AsPath, + S: AsStrand, { - let original = original.as_path().as_os()?; + let original = original.as_strand().as_os()?; #[cfg(unix)] { diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs index cc7c43f4..cfe2db55 100644 --- a/yazi-fs/src/provider/traits.rs +++ b/yazi-fs/src/provider/traits.rs @@ -2,7 +2,7 @@ use std::io; use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use yazi_macro::ok_or_not_found; -use yazi_shared::{path::{AsPath, PathBufDyn}, strand::StrandCow, url::{AsUrl, Url, UrlBuf}}; +use yazi_shared::{path::{AsPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; use crate::{cha::{Cha, ChaType}, provider::Attrs}; @@ -155,21 +155,21 @@ pub trait Provider: Sized { where P: AsPath; - fn symlink(&self, original: P, _is_dir: F) -> impl Future> + fn symlink(&self, original: S, _is_dir: F) -> impl Future> where - P: AsPath, + S: AsStrand, F: AsyncFnOnce() -> io::Result; - fn symlink_dir

(&self, original: P) -> impl Future> + fn symlink_dir(&self, original: S) -> impl Future> where - P: AsPath, + S: AsStrand, { self.symlink(original, async || Ok(true)) } - fn symlink_file

(&self, original: P) -> impl Future> + fn symlink_file(&self, original: S) -> impl Future> where - P: AsPath, + S: AsStrand, { self.symlink(original, async || Ok(false)) } diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index da141ab2..496e54c8 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,4 +1,4 @@ -use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}, time::Duration}; +use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}}; use anyhow::{Context, Result, anyhow}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; @@ -26,7 +26,10 @@ impl File { } pub(crate) async fn paste(&self, mut task: FileInPaste) -> Result<(), FileOutPaste> { - if task.cut && ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok() { + if task.cut + && !task.follow + && ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok() + { return Ok(self.ops.out(task.id, FileOutPaste::Succ)); } @@ -108,8 +111,17 @@ impl File { } Ok(n) => self.ops.out(task.id, FileOutPasteDo::Adv(n)), Err(e) if e.kind() == NotFound => { - warn!("Paste task partially done: {task:?}"); - break; + let Ok(cha) = Self::cha(&task.from, task.follow, None).await else { + warn!("Paste task partially done: {task:?}"); + break; + }; + + if cha.is_orphan() || (cha.is_link() && !task.follow) { + task.cha = Some(cha); + return Ok(self.queue(task.into_link(), NORMAL)); + } + + Err(e)? } // Operation not permitted (os error 1) // Attribute not found (os error 93) @@ -132,10 +144,6 @@ impl File { } pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> { - if !task.from.scheme().covariant(task.to.scheme()) { - Err(anyhow!("Source and target must be on the same filesystem: {task:?}"))? - } - let mut src: PathCow = if task.resolve { ok_or_not_found!( provider::read_link(&task.from).await, diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 2c72a50b..f14547ec 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -79,13 +79,7 @@ impl Scheduler { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Cut {} to {}", from.display(), to.display())); - let Ok(prefixed) = to.try_starts_with(&from) else { - return self - .ops - .out(id, FileOutPaste::Fail("Path being cut has a different encoding".to_owned())); - }; - - if prefixed && !to.covariant(&from) { + if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) { return self.ops.out(id, FileOutPaste::Fail("Cannot cut directory into itself".to_owned())); } @@ -103,11 +97,12 @@ impl Scheduler { }); let file = self.file.clone(); + let follow = !from.scheme().covariant(to.scheme()); self.send_micro(id, LOW, async move { if !force { to = unique_name(to, must_be_dir(&from)).await?; } - file.paste(FileInPaste { id, from, to, cha: None, cut: true, follow: false, retry: 0 }).await + file.paste(FileInPaste { id, from, to, cha: None, cut: true, follow, retry: 0 }).await }); } @@ -118,17 +113,12 @@ impl Scheduler { to.display() )); - let Ok(prefixed) = to.try_starts_with(&from) else { - return self - .ops - .out(id, FileOutPaste::Fail("Path being copied has a different encoding".to_owned())); - }; - - if prefixed && !to.covariant(&from) { + if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) { return self.ops.out(id, FileOutPaste::Fail("Cannot copy directory into itself".to_owned())); } let file = self.file.clone(); + let follow = follow || !from.scheme().covariant(to.scheme()); self.send_micro(id, LOW, async move { if !force { to = unique_name(to, must_be_dir(&from)).await?; @@ -160,14 +150,13 @@ impl Scheduler { to.display() )); - let Ok(prefixed) = to.try_starts_with(&from) else { - return self.ops.out( - id, - FileOutHardlink::Fail("Path being hardlinked has a different encoding".to_owned()), - ); - }; + if !from.scheme().covariant(to.scheme()) { + return self + .ops + .out(id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned())); + } - if prefixed && !to.covariant(&from) { + if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) { return self .ops .out(id, FileOutHardlink::Fail("Cannot hardlink directory into itself".to_owned())); diff --git a/yazi-sftp/src/fs/read_dir.rs b/yazi-sftp/src/fs/read_dir.rs index 22e0a354..ddc2f0d3 100644 --- a/yazi-sftp/src/fs/read_dir.rs +++ b/yazi-sftp/src/fs/read_dir.rs @@ -1,6 +1,6 @@ -use std::{mem, sync::Arc}; +use std::{mem, sync::Arc, time::Duration}; -use crate::{Error, Session, SftpPath, fs::DirEntry, requests, responses}; +use crate::{Error, Operator, Session, SftpPath, fs::DirEntry, requests, responses}; pub struct ReadDir { session: Arc, @@ -12,6 +12,10 @@ pub struct ReadDir { done: bool, } +impl Drop for ReadDir { + fn drop(&mut self) { Operator::from(&self.session).close(&self.handle).ok(); } +} + impl ReadDir { pub(crate) fn new(session: &Arc, dir: SftpPath, handle: String) -> Self { Self { @@ -49,7 +53,12 @@ impl ReadDir { return Ok(()); } - self.name = match self.session.send(requests::ReadDir::new(&self.handle)).await { + let result = self + .session + .send_with_timeout(requests::ReadDir::new(&self.handle), Duration::from_mins(5)) + .await; + + self.name = match result { Ok(resp) => resp, Err(Error::Status(status)) if status.is_eof() => { self.done = true; diff --git a/yazi-sftp/src/session.rs b/yazi-sftp/src/session.rs index 0857bc2c..1ad674b5 100644 --- a/yazi-sftp/src/session.rs +++ b/yazi-sftp/src/session.rs @@ -3,7 +3,7 @@ use std::{any::TypeId, collections::HashMap, io::{self, ErrorKind}, sync::Arc, t use parking_lot::Mutex; use russh::{ChannelStream, client::Msg}; use serde::Serialize; -use tokio::{io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, select, sync::{mpsc, oneshot}, time::timeout}; +use tokio::{io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, select, sync::{mpsc, oneshot}}; use crate::{Error, Id, Packet, Receiver, responses}; @@ -93,12 +93,7 @@ impl Session { I: Into> + Serialize, O: TryFrom, Error = Error> + 'static, { - match timeout(Duration::from_secs(30), self.send_sync(input)?).await?? { - Packet::Status(status) if TypeId::of::() != TypeId::of::() => { - Err(Error::Status(status)) - } - response => response.try_into(), - } + self.send_with_timeout(input, Duration::from_secs(45)).await } pub fn send_sync<'a, I>(self: &Arc, input: I) -> Result @@ -118,5 +113,22 @@ impl Session { Ok(Receiver::new(self, id, rx)) } + pub async fn send_with_timeout<'a, I, O>( + self: &Arc, + input: I, + timeout: Duration, + ) -> Result + where + I: Into> + Serialize, + O: TryFrom, Error = Error> + 'static, + { + match tokio::time::timeout(timeout, self.send_sync(input)?).await?? { + Packet::Status(status) if TypeId::of::() != TypeId::of::() => { + Err(Error::Status(status)) + } + response => response.try_into(), + } + } + pub fn is_closed(self: &Arc) -> bool { self.tx.is_closed() } } diff --git a/yazi-vfs/src/provider/copier.rs b/yazi-vfs/src/provider/copier.rs index c07b0946..211a1bf1 100644 --- a/yazi-vfs/src/provider/copier.rs +++ b/yazi-vfs/src/provider/copier.rs @@ -7,12 +7,15 @@ use yazi_shared::url::{Url, UrlBuf}; use crate::provider::{self, Gate}; +const BUF_SIZE: usize = 512 * 1024; +const PER_CHUNK: u64 = 8 * 1024 * 1024; + pub(super) async fn copy_impl(from: Url<'_>, to: Url<'_>, attrs: Attrs) -> io::Result { let src = provider::open(from).await?; let dist = provider::create(to).await?; - let mut reader = BufReader::with_capacity(524288, src); - let mut writer = BufWriter::with_capacity(524288, dist); + let mut reader = BufReader::with_capacity(BUF_SIZE, src); + let mut writer = BufWriter::with_capacity(BUF_SIZE, dist); let written = tokio::io::copy(&mut reader, &mut writer).await?; writer.flush().await?; @@ -33,32 +36,39 @@ pub(super) fn copy_with_progress_impl( let (acc_, prog_tx_) = (acc.clone(), prog_tx.clone()); tokio::spawn(async move { - let (cha, mut src) = { - let f = provider::open(&*from).await?; - (f.metadata().await?, Some(f)) + let init = async { + let src = provider::open(&*from).await?; + let cha = src.metadata().await?; + + let dist = provider::create(&*to).await?; + dist.set_len(cha.len).await?; + Ok((cha, Some(src), Some(dist))) }; - let mut dist = { - let f = provider::create(&*to).await?; - f.set_len(cha.len).await?; - Some(f) + let (cha, mut src, mut dist) = match init.await { + Ok(r) => r, + Err(e) => { + prog_tx_.send(Err(e)).await.ok(); + done_tx.send(()).ok(); + return; + } }; - let chunks = (cha.len + 5242880 - 1) / 5242880; + let chunks = (cha.len + PER_CHUNK - 1) / PER_CHUNK; let mut result = futures::stream::iter(0..chunks) .map(|i| { let acc_ = acc_.clone(); let (from, to) = (from.clone(), to.clone()); let (src, dist) = (src.take(), dist.take()); async move { - let offset = i * 5242880; - let take = cha.len.saturating_sub(offset).min(5242880); + let offset = i * PER_CHUNK; + let take = cha.len.saturating_sub(offset).min(PER_CHUNK); - let mut src = BufReader::with_capacity(524288, match src { + let mut src = BufReader::with_capacity(BUF_SIZE, match src { Some(f) => f, None => provider::open(&*from).await?, }); - let mut dist = BufWriter::with_capacity(524288, match dist { + let mut dist = BufWriter::with_capacity(BUF_SIZE, match dist { Some(f) => f, None => Gate::default().write(true).open(&*to).await?, }); @@ -114,7 +124,6 @@ pub(super) fn copy_with_progress_impl( } done_tx.send(()).ok(); - Ok::<_, io::Error>(()) }); tokio::spawn(async move { diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/provider/provider.rs index 4a93a644..218f9ca3 100644 --- a/yazi-vfs/src/provider/provider.rs +++ b/yazi-vfs/src/provider/provider.rs @@ -2,7 +2,7 @@ use std::io; use tokio::sync::mpsc; use yazi_fs::{cha::Cha, provider::{Attrs, Provider, local::Local}}; -use yazi_shared::{path::{AsPath, PathBufDyn}, url::{AsUrl, UrlBuf, UrlCow}}; +use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, UrlBuf, UrlCow}}; use super::{Providers, ReadDir, RwFile}; @@ -201,27 +201,27 @@ where } } -pub async fn symlink(link: U, original: P, is_dir: F) -> io::Result<()> +pub async fn symlink(link: U, original: S, is_dir: F) -> io::Result<()> where U: AsUrl, - P: AsPath, + S: AsStrand, F: AsyncFnOnce() -> io::Result, { Providers::new(link.as_url()).await?.symlink(original, is_dir).await } -pub async fn symlink_dir(link: U, original: P) -> io::Result<()> +pub async fn symlink_dir(link: U, original: S) -> io::Result<()> where U: AsUrl, - P: AsPath, + S: AsStrand, { Providers::new(link.as_url()).await?.symlink_dir(original).await } -pub async fn symlink_file(link: U, original: P) -> io::Result<()> +pub async fn symlink_file(link: U, original: S) -> io::Result<()> where U: AsUrl, - P: AsPath, + S: AsStrand, { Providers::new(link.as_url()).await?.symlink_file(original).await } diff --git a/yazi-vfs/src/provider/providers.rs b/yazi-vfs/src/provider/providers.rs index 59e4b00e..e66b8356 100644 --- a/yazi-vfs/src/provider/providers.rs +++ b/yazi-vfs/src/provider/providers.rs @@ -2,7 +2,7 @@ use std::io; use tokio::sync::mpsc; use yazi_fs::{cha::Cha, provider::{Attrs, Provider}}; -use yazi_shared::{path::{AsPath, PathBufDyn}, url::{Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] pub(super) enum Providers<'a> { @@ -161,9 +161,9 @@ impl<'a> Provider for Providers<'a> { } } - async fn symlink(&self, original: P, is_dir: F) -> io::Result<()> + async fn symlink(&self, original: S, is_dir: F) -> io::Result<()> where - P: AsPath, + S: AsStrand, F: AsyncFnOnce() -> io::Result, { match self { @@ -172,9 +172,9 @@ impl<'a> Provider for Providers<'a> { } } - async fn symlink_dir

(&self, original: P) -> io::Result<()> + async fn symlink_dir(&self, original: S) -> io::Result<()> where - P: AsPath, + S: AsStrand, { match self { Self::Local(p) => p.symlink_dir(original).await, @@ -182,9 +182,9 @@ impl<'a> Provider for Providers<'a> { } } - async fn symlink_file

(&self, original: P) -> io::Result<()> + async fn symlink_file(&self, original: S) -> io::Result<()> where - P: AsPath, + S: AsStrand, { match self { Self::Local(p) => p.symlink_file(original).await, diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 6fdc063a..00e1797d 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -4,7 +4,7 @@ use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; use yazi_config::vfs::{ServiceSftp, Vfs}; use yazi_fs::{CWD, provider::{DirReader, FileHolder, Provider}}; use yazi_sftp::fs::{Attrs, Flags}; -use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; use super::Cha; use crate::provider::sftp::Conn; @@ -175,14 +175,14 @@ impl<'a> Provider for Sftp<'a> { Ok(()) } - async fn symlink(&self, original: P, _is_dir: F) -> io::Result<()> + async fn symlink(&self, original: S, _is_dir: F) -> io::Result<()> where - P: AsPath, + S: AsStrand, F: AsyncFnOnce() -> io::Result, { - let original = original.as_path().as_unix()?; + let original = original.as_strand().encoded_bytes(); - Ok(self.op().await?.symlink(&original, self.path).await?) + Ok(self.op().await?.symlink(original, self.path).await?) } async fn symlink_metadata(&self) -> io::Result { From d2e9e72684805886950111506488bdc43ccf5658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sun, 7 Dec 2025 01:28:45 +0800 Subject: [PATCH 29/53] refactor: pull directory traversal out into a function (#3411) --- yazi-plugin/preset/components/tasks.lua | 14 +- yazi-scheduler/src/file/file.rs | 456 +++++++++--------------- yazi-scheduler/src/file/in.rs | 71 ++-- yazi-scheduler/src/file/mod.rs | 2 +- yazi-scheduler/src/file/out.rs | 103 +++++- yazi-scheduler/src/file/progress.rs | 60 +++- yazi-scheduler/src/file/traverse.rs | 208 +++++++++++ yazi-scheduler/src/in.rs | 14 +- yazi-scheduler/src/out.rs | 16 +- yazi-scheduler/src/progress.rs | 28 +- yazi-scheduler/src/scheduler.rs | 27 +- yazi-vfs/src/provider/sftp/sftp.rs | 6 +- 12 files changed, 630 insertions(+), 375 deletions(-) create mode 100644 yazi-scheduler/src/file/traverse.rs diff --git a/yazi-plugin/preset/components/tasks.lua b/yazi-plugin/preset/components/tasks.lua index 9ee4f185..11b5ec4e 100644 --- a/yazi-plugin/preset/components/tasks.lua +++ b/yazi-plugin/preset/components/tasks.lua @@ -62,8 +62,10 @@ function Tasks:redraw() end function Tasks:icon(snap) - if snap.prog.kind == "FilePaste" then - return snap.name:find("Copy ", 1, true) == 1 and " " or " " + if snap.prog.kind == "FileCopy" then + return " " + elseif snap.prog.kind == "FileCut" then + return " " elseif snap.prog.kind == "FileDelete" then return " " elseif snap.prog.kind == "FileDownload" then @@ -77,7 +79,13 @@ end function Tasks:progress_redraw(snap, y) local kind = snap.prog.kind - if kind == "FilePaste" or kind == "FileDelete" or kind == "FileDownload" or kind == "FileUpload" then + if + kind == "FileCopy" + or kind == "FileCut" + or kind == "FileDelete" + or kind == "FileDownload" + or kind == "FileUpload" + then local percent if snap.success then percent = "Cleaning…" diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 496e54c8..9b7f8df8 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,16 +1,16 @@ -use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}}; +use std::hash::{BuildHasher, Hash, Hasher}; use anyhow::{Context, Result, anyhow}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::YAZI; -use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; +use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::path_relative_to, provider::{Attrs, FileHolder, Provider, local::Local}}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::PathCow, timestamp_us, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::{VfsCha, maybe_exists, provider::{self, DirEntry}, unique_name}; -use super::{FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; -use crate::{LOW, NORMAL, TaskIn, TaskOp, TaskOps, file::{FileInDownload, FileInUpload, FileInUploadDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutPaste, FileOutPasteDo, FileOutTrash, FileOutUpload, FileOutUploadDo}}; +use super::{FileInCopy, FileInDelete, FileInHardlink, FileInLink, FileInTrash}; +use crate::{LOW, NORMAL, TaskIn, TaskOp, TaskOps, file::{FileInCut, FileInDownload, FileInUpload, FileOutCopy, FileOutCopyDo, FileOutCut, FileOutCutDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutTrash, FileOutUpload, FileOutUploadDo}}; pub(crate) struct File { ops: TaskOps, @@ -25,103 +25,44 @@ impl File { Self { ops: tx.into(), r#macro: r#macro.clone() } } - pub(crate) async fn paste(&self, mut task: FileInPaste) -> Result<(), FileOutPaste> { - if task.cut - && !task.follow - && ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok() - { - return Ok(self.ops.out(task.id, FileOutPaste::Succ)); - } + pub(crate) async fn copy(&self, task: FileInCopy) -> Result<(), FileOutCopy> { + let id = task.id; - if task.cha.is_none() { - task.cha = Some(Self::cha(&task.from, task.follow, None).await?); - } - - let cha = task.cha.unwrap(); - if !cha.is_dir() { - let id = task.id; - if cha.is_orphan() || (cha.is_link() && !task.follow) { - self.ops.out(id, FileOutPaste::New(0)); - self.queue(task.into_link(), NORMAL); - } else { - self.ops.out(id, FileOutPaste::New(cha.len)); - self.queue(task, LOW); - } - return Ok(self.ops.out(id, FileOutPaste::Succ)); - } - - macro_rules! continue_unless_ok { - ($result:expr) => { - match $result { - Ok(v) => v, - Err(e) => { - self.ops.out(task.id, FileOutPaste::Deform(e.to_string())); - continue; - } - } - }; - } - - let root = &task.to; - let skip = task.from.components().count(); - let mut dirs = VecDeque::from([task.from.clone()]); - - while let Some(src) = dirs.pop_front() { - let dest = continue_unless_ok!(root.try_join(skip_url(&src, skip))); - continue_unless_ok!(match provider::create_dir(&dest).await { - Err(e) if e.kind() != AlreadyExists => Err(e), + super::traverse::( + task, + async |dir| match provider::create_dir(dir).await { + Err(e) if e.kind() != AlreadyExists => Err(e)?, _ => Ok(()), - }); - - let mut it = continue_unless_ok!(provider::read_dir(&src).await); - while let Ok(Some(entry)) = it.next().await { - let from = entry.url(); - let cha = continue_unless_ok!(Self::cha(&from, task.follow, Some(entry)).await); - - if cha.is_dir() { - dirs.push_back(from); - continue; - } - - let to = continue_unless_ok!(dest.try_join(from.name().unwrap())); - if cha.is_orphan() || (cha.is_link() && !task.follow) { - self.ops.out(task.id, FileOutPaste::New(0)); - self.queue(task.spawn(from, to, cha).into_link(), NORMAL); + }, + async |task, cha| { + Ok(if cha.is_orphan() || (cha.is_link() && !task.follow) { + self.ops.out(id, FileOutCopy::New(0)); + self.queue(task.into_link(), NORMAL); } else { - self.ops.out(task.id, FileOutPaste::New(cha.len)); - self.queue(task.spawn(from, to, cha), LOW); - } - } - } + self.ops.out(id, FileOutCopy::New(cha.len)); + self.queue(task, LOW); + }) + }, + |err| { + self.ops.out(id, FileOutCopy::Deform(err)); + }, + ) + .await?; - Ok(self.ops.out(task.id, FileOutPaste::Succ)) + Ok(self.ops.out(id, FileOutCopy::Succ)) } - pub(crate) async fn paste_do(&self, mut task: FileInPaste) -> Result<(), FileOutPasteDo> { - ok_or_not_found!(provider::remove_file(&task.to).await); + pub(crate) async fn copy_do(&self, mut task: FileInCopy) -> Result<(), FileOutCopyDo> { + ok_or_not_found!(provider::remove_file(&task.to).await); // FIXME: integrate into copy_with_progress let mut it = provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await?; while let Some(res) = it.recv().await { match res { - Ok(0) => { - if task.cut { - provider::remove_file(&task.from).await.ok(); - } - break; - } - Ok(n) => self.ops.out(task.id, FileOutPasteDo::Adv(n)), + Ok(0) => break, + Ok(n) => self.ops.out(task.id, FileOutCopyDo::Adv(n)), Err(e) if e.kind() == NotFound => { - let Ok(cha) = Self::cha(&task.from, task.follow, None).await else { - warn!("Paste task partially done: {task:?}"); - break; - }; - - if cha.is_orphan() || (cha.is_link() && !task.follow) { - task.cha = Some(cha); - return Ok(self.queue(task.into_link(), NORMAL)); - } - - Err(e)? + warn!("Copy task partially done: {task:?}"); + break; } // Operation not permitted (os error 1) // Attribute not found (os error 93) @@ -130,13 +71,75 @@ impl File { && matches!(e.raw_os_error(), Some(1) | Some(93)) => { task.retry += 1; - self.ops.out(task.id, FileOutPasteDo::Log(format!("Retrying due to error: {e}"))); + self.ops.out(task.id, FileOutCopyDo::Log(format!("Retrying due to error: {e}"))); return Ok(self.queue(task, LOW)); } Err(e) => Err(e)?, } } - Ok(self.ops.out(task.id, FileOutPasteDo::Succ)) + Ok(self.ops.out(task.id, FileOutCopyDo::Succ)) + } + + pub(crate) async fn cut(&self, task: FileInCut) -> Result<(), FileOutCut> { + let id = task.id; + + if !task.follow && ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok() { + return Ok(self.ops.out(id, FileOutCut::Succ)); + } + + super::traverse::( + task, + async |dir| match provider::create_dir(dir).await { + Err(e) if e.kind() != AlreadyExists => Err(e)?, + _ => Ok(()), + }, + async |task, cha| { + Ok(if cha.is_orphan() || (cha.is_link() && !task.follow) { + self.ops.out(id, FileOutCut::New(0)); + self.queue(task.into_link(), NORMAL); + } else { + self.ops.out(id, FileOutCut::New(cha.len)); + self.queue(task, LOW); + }) + }, + |err| { + self.ops.out(id, FileOutCut::Deform(err)); + }, + ) + .await?; + + Ok(self.ops.out(id, FileOutCut::Succ)) + } + + pub(crate) async fn cut_do(&self, mut task: FileInCut) -> Result<(), FileOutCutDo> { + ok_or_not_found!(provider::remove_file(&task.to).await); // FIXME: integrate into copy_with_progress + let mut it = provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await?; + + while let Some(res) = it.recv().await { + match res { + Ok(0) => { + provider::remove_file(&task.from).await.ok(); + break; + } + Ok(n) => self.ops.out(task.id, FileOutCutDo::Adv(n)), + Err(e) if e.kind() == NotFound => { + warn!("Cut task partially done: {task:?}"); + break; + } + // Operation not permitted (os error 1) + // Attribute not found (os error 93) + Err(e) + if task.retry < YAZI.tasks.bizarre_retry + && matches!(e.raw_os_error(), Some(1) | Some(93)) => + { + task.retry += 1; + self.ops.out(task.id, FileOutCutDo::Log(format!("Retrying due to error: {e}"))); + return Ok(self.queue(task, LOW)); + } + Err(e) => Err(e)?, + } + } + Ok(self.ops.out(task.id, FileOutCutDo::Succ)) } pub(crate) fn link(&self, task: FileInLink) -> Result<(), FileOutLink> { @@ -174,60 +177,26 @@ impl File { Ok(self.ops.out(task.id, FileOutLink::Succ)) } - pub(crate) async fn hardlink(&self, mut task: FileInHardlink) -> Result<(), FileOutHardlink> { - if task.cha.is_none() { - task.cha = Some(Self::cha(&task.from, task.follow, None).await?); - } + pub(crate) async fn hardlink(&self, task: FileInHardlink) -> Result<(), FileOutHardlink> { + let id = task.id; - let cha = task.cha.unwrap(); - if !cha.is_dir() { - let id = task.id; - self.ops.out(id, FileOutHardlink::New); - self.queue(task, NORMAL); - self.ops.out(id, FileOutHardlink::Succ); - return Ok(()); - } - - macro_rules! continue_unless_ok { - ($result:expr) => { - match $result { - Ok(v) => v, - Err(e) => { - self.ops.out(task.id, FileOutHardlink::Deform(e.to_string())); - continue; - } - } - }; - } - - let root = &task.to; - let skip = task.from.components().count(); - let mut dirs = VecDeque::from([task.from.clone()]); - - while let Some(src) = dirs.pop_front() { - let dest = continue_unless_ok!(root.try_join(skip_url(&src, skip))); - continue_unless_ok!(match provider::create_dir(&dest).await { - Err(e) if e.kind() != AlreadyExists => Err(e), + super::traverse::( + task, + async |dir| match provider::create_dir(dir).await { + Err(e) if e.kind() != AlreadyExists => Err(e)?, _ => Ok(()), - }); + }, + async |task, _cha| { + self.ops.out(id, FileOutHardlink::New); + Ok(self.queue(task, NORMAL)) + }, + |err| { + self.ops.out(id, FileOutHardlink::Deform(err)); + }, + ) + .await?; - let mut it = continue_unless_ok!(provider::read_dir(&src).await); - while let Ok(Some(entry)) = it.next().await { - let from = entry.url(); - let cha = continue_unless_ok!(Self::cha(&from, task.follow, Some(entry)).await); - - if cha.is_dir() { - dirs.push_back(from); - continue; - } - - let to = continue_unless_ok!(dest.try_join(from.name().unwrap())); - self.ops.out(task.id, FileOutHardlink::New); - self.queue(task.spawn(from, to, cha), NORMAL); - } - } - - Ok(self.ops.out(task.id, FileOutHardlink::Succ)) + Ok(self.ops.out(id, FileOutHardlink::Succ)) } pub(crate) async fn hardlink_do(&self, task: FileInHardlink) -> Result<(), FileOutHardlinkDo> { @@ -245,37 +214,21 @@ impl File { Ok(self.ops.out(task.id, FileOutHardlinkDo::Succ)) } - pub(crate) async fn delete(&self, mut task: FileInDelete) -> Result<(), FileOutDelete> { - let cha = provider::symlink_metadata(&task.target).await?; - if !cha.is_dir() { - let id = task.id; - task.length = cha.len; - self.ops.out(id, FileOutDelete::New(cha.len)); - self.queue(task, NORMAL); - self.ops.out(id, FileOutDelete::Succ); - return Ok(()); - } + pub(crate) async fn delete(&self, task: FileInDelete) -> Result<(), FileOutDelete> { + let id = task.id; - let mut dirs = VecDeque::from([task.target]); - while let Some(target) = dirs.pop_front() { - let Ok(mut it) = provider::read_dir(&target).await else { continue }; + super::traverse::( + task, + async |_dir| Ok(()), + async |task, cha| { + self.ops.out(id, FileOutDelete::New(cha.len)); + Ok(self.queue(task, NORMAL)) + }, + |_err| {}, + ) + .await?; - while let Ok(Some(entry)) = it.next().await { - let Ok(cha) = entry.metadata().await else { continue }; - - if cha.is_dir() { - dirs.push_front(entry.url()); - continue; - } - - task.target = entry.url(); - task.length = cha.len; - self.ops.out(task.id, FileOutDelete::New(cha.len)); - self.queue(task.clone(), NORMAL); - } - } - - Ok(self.ops.out(task.id, FileOutDelete::Succ)) + Ok(self.ops.out(id, FileOutDelete::Succ)) } pub(crate) async fn delete_do(&self, task: FileInDelete) -> Result<(), FileOutDeleteDo> { @@ -285,7 +238,7 @@ impl File { Err(_) if !maybe_exists(&task.target).await => {} Err(e) => Err(e)?, } - Ok(self.ops.out(task.id, FileOutDeleteDo::Succ(task.length))) + Ok(self.ops.out(task.id, FileOutDeleteDo::Succ(task.cha.unwrap().len))) } pub(crate) fn trash(&self, task: FileInTrash) -> Result<(), FileOutTrash> { @@ -297,56 +250,31 @@ impl File { Ok(self.ops.out(task.id, FileOutTrash::Succ)) } - pub(crate) async fn download(&self, mut task: FileInDownload) -> Result<(), FileOutDownload> { - if task.cha.is_none() { - task.cha = Some(Self::cha(&task.url, true, None).await?); - } + pub(crate) async fn download(&self, task: FileInDownload) -> Result<(), FileOutDownload> { + let id = task.id; - let cha = task.cha.unwrap(); - if cha.is_orphan() { - Err(io::Error::new(NotFound, "Source of symlink doesn't exist"))?; - } - - if !cha.is_dir() { - let id = task.id; - self.ops.out(id, FileOutDownload::New(cha.len)); - self.queue(task, LOW); - return Ok(self.ops.out(id, FileOutDownload::Succ)); - } - - macro_rules! continue_unless_ok { - ($result:expr) => { - match $result { - Ok(v) => v, - Err(e) => { - self.ops.out(task.id, FileOutDownload::Deform(e.to_string())); - continue; - } - } - }; - } - - let mut dirs = VecDeque::from([task.url.clone()]); - while let Some(src) = dirs.pop_front() { - let mut it = continue_unless_ok!(provider::read_dir(&src).await); - tokio::task::spawn_blocking(move || _ = Cwd::ensure(src.as_url())).await.ok(); - - while let Ok(Some(entry)) = it.next().await { - let from = entry.url(); - let cha = continue_unless_ok!(Self::cha(&from, true, Some(entry)).await); - - if cha.is_orphan() { - continue_unless_ok!(Err("Source of symlink doesn't exist")); - } else if cha.is_dir() { - dirs.push_back(from); + super::traverse::( + task, + async |dir| { + let dir = dir.to_owned(); + tokio::task::spawn_blocking(move || _ = Cwd::ensure(dir.as_url())).await.ok(); + Ok(()) + }, + async |task, cha| { + Ok(if cha.is_orphan() { + Err(io::Error::new(NotFound, "Source of symlink doesn't exist"))?; } else { - self.ops.out(task.id, FileOutDownload::New(cha.len)); - self.queue(task.spawn(from, cha), LOW); - } - } - } + self.ops.out(id, FileOutDownload::New(cha.len)); + self.queue(task, LOW) + }) + }, + |err| { + self.ops.out(id, FileOutDownload::Deform(err)); + }, + ) + .await?; - Ok(self.ops.out(task.id, FileOutDownload::Succ)) + Ok(self.ops.out(id, FileOutDownload::Succ)) } pub(crate) async fn download_do( @@ -393,79 +321,47 @@ impl File { } pub(crate) async fn upload(&self, task: FileInUpload) -> Result<(), FileOutUpload> { - let cha = Self::cha(&task.url, true, None).await?; - if cha.is_orphan() { - Err(anyhow!("Source of symlink doesn't exist"))?; - } + let id = task.id; - if !cha.is_dir() { - let cache = task.url.cache().context("Cannot determine cache path")?; - match Self::cha(&cache, true, None).await { - Ok(c) if c.mtime == cha.mtime => {} - Ok(c) => { - self.ops.out(task.id, FileOutUpload::New(c.len)); - self.queue(FileInUploadDo { id: task.id, url: task.url, cha, cache }, LOW); - } - Err(e) if e.kind() == NotFound => {} - Err(e) => Err(e)?, - }; - return Ok(self.ops.out(task.id, FileOutUpload::Succ)); - } + super::traverse::( + task, + async |_dir| Ok(()), + async |task, cha| { + let cache = task.cache.as_ref().context("Cannot determine cache path")?; - macro_rules! continue_unless_ok { - ($result:expr) => { - match $result { - Ok(v) => v, - Err(e) => { - self.ops.out(task.id, FileOutUpload::Deform(e.to_string())); - continue; + Ok(match Self::cha(cache, true, None).await { + Ok(c) if c.mtime == cha.mtime => {} + Ok(c) => { + self.ops.out(id, FileOutUpload::New(c.len)); + self.queue(task, LOW); } - } - }; - } + Err(e) if e.kind() == NotFound => {} + Err(e) => Err(e)?, + }) + }, + |err| { + self.ops.out(id, FileOutUpload::Deform(err)); + }, + ) + .await?; - let mut dirs = VecDeque::from([task.url]); - while let Some(src) = dirs.pop_front() { - let mut it = continue_unless_ok!(provider::read_dir(&src).await); - while let Ok(Some(entry)) = it.next().await { - let from = entry.url(); - let cha = continue_unless_ok!(Self::cha(&from, true, Some(entry)).await); - - if cha.is_orphan() { - continue_unless_ok!(Err("Source of symlink doesn't exist")); - } else if cha.is_dir() { - dirs.push_back(from); - continue; - } - - let cache = continue_unless_ok!(from.cache().ok_or("Cannot determine cache path")); - let cache_cha = continue_unless_ok!(match Self::cha(&cache, true, None).await { - Ok(c) if c.mtime == cha.mtime => continue, - Ok(c) => Ok(c), - Err(e) if e.kind() == NotFound => continue, - Err(e) => Err(e), - }); - - self.ops.out(task.id, FileOutUpload::New(cache_cha.len)); - self.queue(FileInUploadDo { id: task.id, url: from, cha, cache }, LOW); - } - } - - Ok(self.ops.out(task.id, FileOutUpload::Succ)) + Ok(self.ops.out(id, FileOutUpload::Succ)) } - pub(crate) async fn upload_do(&self, task: FileInUploadDo) -> Result<(), FileOutUploadDo> { + pub(crate) async fn upload_do(&self, task: FileInUpload) -> Result<(), FileOutUploadDo> { + let cha = task.cha.unwrap(); + let cache = task.cache.context("Cannot determine cache path")?; let lock = task.url.cache_lock().context("Cannot determine cache lock")?; let hash = Local::regular(&lock).read_to_string().await.context("Cannot read cache lock")?; let hash = u128::from_str_radix(&hash, 16).context("Cannot parse hash from lock")?; - if hash != task.cha.hash_u128() { + if hash != cha.hash_u128() { Err(anyhow!("Remote file has changed since last download"))?; } let tmp = Self::tmp(&task.url).await.context("Cannot determine temporary upload path")?; - let mut it = provider::copy_with_progress(&task.cache, &tmp, Attrs { - mode: Some(task.cha.mode), + let mut it = provider::copy_with_progress(&cache, &tmp, Attrs { + mode: Some(cha.mode), atime: None, btime: None, mtime: None, @@ -495,7 +391,7 @@ impl File { Ok(self.ops.out(task.id, FileOutUploadDo::Succ)) } - async fn cha(url: U, follow: bool, entry: Option) -> io::Result + pub(super) async fn cha(url: U, follow: bool, entry: Option) -> io::Result where U: AsUrl, { diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index f56b07c3..0a447094 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -3,31 +3,18 @@ use std::path::PathBuf; use yazi_fs::cha::Cha; use yazi_shared::{Id, url::UrlBuf}; -// --- Paste +// --- Copy #[derive(Clone, Debug)] -pub(crate) struct FileInPaste { +pub(crate) struct FileInCopy { pub(crate) id: Id, pub(crate) from: UrlBuf, pub(crate) to: UrlBuf, pub(crate) cha: Option, - pub(crate) cut: bool, pub(crate) follow: bool, pub(crate) retry: u8, } -impl FileInPaste { - pub(super) fn spawn(&self, from: UrlBuf, to: UrlBuf, cha: Cha) -> Self { - Self { - id: self.id, - from, - to, - cha: Some(cha), - cut: self.cut, - follow: self.follow, - retry: self.retry, - } - } - +impl FileInCopy { pub(super) fn into_link(self) -> FileInLink { FileInLink { id: self.id, @@ -36,7 +23,32 @@ impl FileInPaste { cha: self.cha, resolve: true, relative: false, - delete: self.cut, + delete: false, + } + } +} + +// --- Cut +#[derive(Clone, Debug)] +pub(crate) struct FileInCut { + pub(crate) id: Id, + pub(crate) from: UrlBuf, + pub(crate) to: UrlBuf, + pub(crate) cha: Option, + pub(crate) follow: bool, + pub(crate) retry: u8, +} + +impl FileInCut { + pub(super) fn into_link(self) -> FileInLink { + FileInLink { + id: self.id, + from: self.from, + to: self.to, + cha: self.cha, + resolve: true, + relative: false, + delete: true, } } } @@ -63,18 +75,12 @@ pub(crate) struct FileInHardlink { pub(crate) follow: bool, } -impl FileInHardlink { - pub(super) fn spawn(&self, from: UrlBuf, to: UrlBuf, cha: Cha) -> Self { - Self { id: self.id, from, to, cha: Some(cha), follow: self.follow } - } -} - // --- Delete #[derive(Clone, Debug)] pub(crate) struct FileInDelete { pub(crate) id: Id, pub(crate) target: UrlBuf, - pub(crate) length: u64, + pub(crate) cha: Option, } // --- Trash @@ -93,24 +99,11 @@ pub(crate) struct FileInDownload { pub(crate) retry: u8, } -impl FileInDownload { - pub(super) fn spawn(&self, url: UrlBuf, cha: Cha) -> Self { - Self { id: self.id, url, cha: Some(cha), retry: self.retry } - } -} - // --- Upload #[derive(Clone, Debug)] pub(crate) struct FileInUpload { - pub(crate) id: Id, - pub(crate) url: UrlBuf, -} - -// --- UploadDo -#[derive(Clone, Debug)] -pub(crate) struct FileInUploadDo { pub(crate) id: Id, pub(crate) url: UrlBuf, - pub(crate) cha: Cha, - pub(crate) cache: PathBuf, + pub(crate) cha: Option, + pub(crate) cache: Option, } diff --git a/yazi-scheduler/src/file/mod.rs b/yazi-scheduler/src/file/mod.rs index 9ae8d171..2a1a30ce 100644 --- a/yazi-scheduler/src/file/mod.rs +++ b/yazi-scheduler/src/file/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(file out progress r#in); +yazi_macro::mod_flat!(file out progress r#in traverse); diff --git a/yazi-scheduler/src/file/out.rs b/yazi-scheduler/src/file/out.rs index 5994103c..6e0cdb1d 100644 --- a/yazi-scheduler/src/file/out.rs +++ b/yazi-scheduler/src/file/out.rs @@ -1,8 +1,79 @@ use crate::{Task, TaskProg}; -// --- Paste +// --- Copy #[derive(Debug)] -pub(crate) enum FileOutPaste { +pub(crate) enum FileOutCopy { + New(u64), + Deform(String), + Succ, + Fail(String), +} + +impl From for FileOutCopy { + fn from(value: std::io::Error) -> Self { Self::Fail(format!("{value:?}")) } +} + +impl FileOutCopy { + pub(crate) fn reduce(self, task: &mut Task) { + let TaskProg::FileCopy(prog) = &mut task.prog else { return }; + match self { + Self::New(bytes) => { + prog.total_files += 1; + prog.total_bytes += bytes; + } + Self::Deform(reason) => { + prog.total_files += 1; + prog.failed_files += 1; + task.log(reason); + } + Self::Succ => { + prog.collected = Some(true); + } + Self::Fail(reason) => { + prog.collected = Some(false); + task.log(reason); + } + } + } +} + +// --- CopyDo +#[derive(Debug)] +pub(crate) enum FileOutCopyDo { + Adv(u64), + Log(String), + Succ, + Fail(String), +} + +impl From for FileOutCopyDo { + fn from(value: std::io::Error) -> Self { Self::Fail(format!("{value:?}")) } +} + +impl FileOutCopyDo { + pub(crate) fn reduce(self, task: &mut Task) { + let TaskProg::FileCopy(prog) = &mut task.prog else { return }; + match self { + Self::Adv(size) => { + prog.processed_bytes += size; + } + Self::Log(line) => { + task.log(line); + } + Self::Succ => { + prog.success_files += 1; + } + Self::Fail(reason) => { + prog.failed_files += 1; + task.log(reason); + } + } + } +} + +// --- Cut +#[derive(Debug)] +pub(crate) enum FileOutCut { New(u64), Deform(String), Succ, @@ -10,13 +81,13 @@ pub(crate) enum FileOutPaste { Clean, } -impl From for FileOutPaste { +impl From for FileOutCut { fn from(value: std::io::Error) -> Self { Self::Fail(format!("{value:?}")) } } -impl FileOutPaste { +impl FileOutCut { pub(crate) fn reduce(self, task: &mut Task) { - let TaskProg::FilePaste(prog) = &mut task.prog else { return }; + let TaskProg::FileCut(prog) = &mut task.prog else { return }; match self { Self::New(bytes) => { prog.total_files += 1; @@ -41,22 +112,22 @@ impl FileOutPaste { } } -// --- PasteDo +// --- CutDo #[derive(Debug)] -pub(crate) enum FileOutPasteDo { +pub(crate) enum FileOutCutDo { Adv(u64), Log(String), Succ, Fail(String), } -impl From for FileOutPasteDo { +impl From for FileOutCutDo { fn from(value: std::io::Error) -> Self { Self::Fail(format!("{value:?}")) } } -impl FileOutPasteDo { +impl FileOutCutDo { pub(crate) fn reduce(self, task: &mut Task) { - let TaskProg::FilePaste(prog) = &mut task.prog else { return }; + let TaskProg::FileCut(prog) = &mut task.prog else { return }; match self { Self::Adv(size) => { prog.processed_bytes += size; @@ -102,7 +173,17 @@ impl FileOutLink { task.log(reason); } } - } else if let TaskProg::FilePaste(prog) = &mut task.prog { + } else if let TaskProg::FileCopy(prog) = &mut task.prog { + match self { + Self::Succ => { + prog.success_files += 1; + } + Self::Fail(reason) => { + prog.failed_files += 1; + task.log(reason); + } + } + } else if let TaskProg::FileCut(prog) = &mut task.prog { match self { Self::Succ => { prog.success_files += 1; diff --git a/yazi-scheduler/src/file/progress.rs b/yazi-scheduler/src/file/progress.rs index 2238d922..709ece50 100644 --- a/yazi-scheduler/src/file/progress.rs +++ b/yazi-scheduler/src/file/progress.rs @@ -1,20 +1,19 @@ use serde::Serialize; use yazi_parser::app::TaskSummary; -// --- Paste +// --- Copy #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)] -pub struct FileProgPaste { +pub struct FileProgCopy { pub total_files: u32, pub success_files: u32, pub failed_files: u32, pub total_bytes: u64, pub processed_bytes: u64, pub collected: Option, - pub cleaned: bool, } -impl From for TaskSummary { - fn from(value: FileProgPaste) -> Self { +impl From for TaskSummary { + fn from(value: FileProgCopy) -> Self { Self { total: value.total_files, success: value.success_files, @@ -24,7 +23,56 @@ impl From for TaskSummary { } } -impl FileProgPaste { +impl FileProgCopy { + pub fn running(self) -> bool { + self.collected.is_none() || self.success_files + self.failed_files != self.total_files + } + + pub fn success(self) -> bool { + self.collected == Some(true) && self.success_files == self.total_files + } + + pub fn failed(self) -> bool { self.collected == Some(false) } + + pub fn cleaned(self) -> bool { false } + + pub fn percent(self) -> Option { + Some(if self.success() { + 100.0 + } else if self.failed() { + 0.0 + } else if self.total_bytes != 0 { + 99.99f32.min(self.processed_bytes as f32 / self.total_bytes as f32 * 100.0) + } else { + 99.99 + }) + } +} + +// --- Cut +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)] +pub struct FileProgCut { + pub total_files: u32, + pub success_files: u32, + pub failed_files: u32, + pub total_bytes: u64, + pub processed_bytes: u64, + pub collected: Option, + pub cleaned: bool, +} + +impl From for TaskSummary { + fn from(value: FileProgCut) -> Self { + Self { + total: value.total_files, + success: value.success_files, + failed: value.failed_files, + percent: value.percent().map(Into::into), + } + } +} + +impl FileProgCut { pub fn running(self) -> bool { self.collected.is_none() || self.success_files + self.failed_files != self.total_files } diff --git a/yazi-scheduler/src/file/traverse.rs b/yazi-scheduler/src/file/traverse.rs new file mode 100644 index 00000000..0336da0e --- /dev/null +++ b/yazi-scheduler/src/file/traverse.rs @@ -0,0 +1,208 @@ +use std::{collections::VecDeque, fmt::Debug, io}; + +use yazi_fs::{FsUrl, cha::Cha, path::skip_url, provider::{DirReader, FileHolder}}; +use yazi_shared::{strand::StrandLike, url::{AsUrl, Url, UrlBuf, UrlLike}}; +use yazi_vfs::provider::{self}; + +use crate::file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInUpload}; + +trait Traverse { + fn cha(&mut self) -> &mut Option; + + fn follow(&self) -> bool; + + fn from(&self) -> Url<'_>; + + async fn init(&mut self) -> io::Result { + if self.cha().is_none() { + *self.cha() = Some(super::File::cha(self.from(), self.follow(), None).await?) + } + Ok(self.cha().unwrap()) + } + + fn spawn(&self, from: UrlBuf, to: Option, cha: Cha) -> Self; + + fn to(&self) -> Option>; +} + +impl Traverse for FileInCopy { + fn cha(&mut self) -> &mut Option { &mut self.cha } + + fn follow(&self) -> bool { self.follow } + + fn from(&self) -> Url<'_> { self.from.as_url() } + + fn spawn(&self, from: UrlBuf, to: Option, cha: Cha) -> Self { + Self { + id: self.id, + from, + to: to.unwrap(), + cha: Some(cha), + follow: self.follow, + retry: self.retry, + } + } + + fn to(&self) -> Option> { Some(self.to.as_url()) } +} + +impl Traverse for FileInCut { + fn cha(&mut self) -> &mut Option { &mut self.cha } + + fn follow(&self) -> bool { self.follow } + + fn from(&self) -> Url<'_> { self.from.as_url() } + + fn spawn(&self, from: UrlBuf, to: Option, cha: Cha) -> Self { + Self { + id: self.id, + from, + to: to.unwrap(), + cha: Some(cha), + follow: self.follow, + retry: self.retry, + } + } + + fn to(&self) -> Option> { Some(self.to.as_url()) } +} + +impl Traverse for FileInHardlink { + fn cha(&mut self) -> &mut Option { &mut self.cha } + + fn follow(&self) -> bool { self.follow } + + fn from(&self) -> Url<'_> { self.from.as_url() } + + fn spawn(&self, from: UrlBuf, to: Option, cha: Cha) -> Self { + Self { id: self.id, from, to: to.unwrap(), cha: Some(cha), follow: self.follow } + } + + fn to(&self) -> Option> { Some(self.to.as_url()) } +} + +impl Traverse for FileInDelete { + fn cha(&mut self) -> &mut Option { &mut self.cha } + + fn follow(&self) -> bool { false } + + fn from(&self) -> Url<'_> { self.target.as_url() } + + fn spawn(&self, from: UrlBuf, _to: Option, cha: Cha) -> Self { + Self { id: self.id, target: from, cha: Some(cha) } + } + + fn to(&self) -> Option> { None } +} + +impl Traverse for FileInDownload { + fn cha(&mut self) -> &mut Option { &mut self.cha } + + fn follow(&self) -> bool { true } + + fn from(&self) -> Url<'_> { self.url.as_url() } + + fn spawn(&self, from: UrlBuf, _to: Option, cha: Cha) -> Self { + Self { id: self.id, url: from, cha: Some(cha), retry: self.retry } + } + + fn to(&self) -> Option> { None } +} + +impl Traverse for FileInUpload { + fn cha(&mut self) -> &mut Option { &mut self.cha } + + fn follow(&self) -> bool { true } + + fn from(&self) -> Url<'_> { self.url.as_url() } + + async fn init(&mut self) -> io::Result { + if self.cha.is_none() { + self.cha = Some(super::File::cha(self.from(), self.follow(), None).await?) + } + if self.cache.is_none() { + self.cache = self.url.cache(); + } + Ok(self.cha.unwrap()) + } + + fn spawn(&self, from: UrlBuf, _to: Option, cha: Cha) -> Self { + Self { id: self.id, cha: Some(cha), cache: from.cache(), url: from } + } + + fn to(&self) -> Option> { None } +} + +#[allow(private_bounds)] +pub(super) async fn traverse( + mut task: T, + on_dir: D, + on_file: FC, + on_error: E, +) -> Result<(), R> +where + R: Debug + From, + T: Traverse, + D: AsyncFn(Url) -> Result<(), R>, + FC: Fn(T, Cha) -> FR, + FR: Future>, + E: Fn(String), +{ + let cha = task.init().await?; + if !cha.is_dir() { + return on_file(task, cha).await; + } + + let root = task.to(); + let skip = task.from().components().count(); + let mut dirs = VecDeque::from([task.from().to_owned()]); + + macro_rules! err { + ($result:expr, $($args:tt)*) => { + match $result { + Ok(v) => v, + Err(e) => { + on_error(format!("{}: {e:?}", format_args!($($args)*))); + continue; + } + } + }; + } + + while let Some(src) = dirs.pop_front() { + let mut it = err!(provider::read_dir(&src).await, "Cannot read directory {src:?}"); + + let dest = if let Some(root) = root { + let s = skip_url(&src, skip); + err!(root.try_join(&s), "Cannot join {root:?} with {}", s.display()) + } else { + src + }; + + () = err!(on_dir(dest.as_url()).await, "Cannot process directory {dest:?}"); + + while let Ok(Some(entry)) = it.next().await { + let from = entry.url(); + let cha = err!( + super::File::cha(&from, task.follow(), Some(entry)).await, + "Cannot get metadata for {from:?}" + ); + + if cha.is_dir() { + dirs.push_back(from); + continue; + } + + let to = if root.is_some() { + let name = from.name().unwrap(); + Some(err!(dest.try_join(name), "Cannot join {dest:?} with {}", name.display())) + } else { + None + }; + + err!(on_file(task.spawn(from, to, cha), cha).await, "Cannot process file"); + } + } + + Ok(()) +} diff --git a/yazi-scheduler/src/in.rs b/yazi-scheduler/src/in.rs index 3ba780cc..d008d1c8 100644 --- a/yazi-scheduler/src/in.rs +++ b/yazi-scheduler/src/in.rs @@ -1,17 +1,18 @@ use yazi_shared::Id; -use crate::{file::{FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInPaste, FileInTrash, FileInUploadDo}, impl_from_in, plugin::PluginInEntry, prework::{PreworkInFetch, PreworkInLoad, PreworkInSize}}; +use crate::{file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload}, impl_from_in, plugin::PluginInEntry, prework::{PreworkInFetch, PreworkInLoad, PreworkInSize}}; #[derive(Debug)] pub(crate) enum TaskIn { // File - FilePaste(FileInPaste), + FileCopy(FileInCopy), + FileCut(FileInCut), FileLink(FileInLink), FileHardlink(FileInHardlink), FileDelete(FileInDelete), FileTrash(FileInTrash), FileDownload(FileInDownload), - FileUploadDo(FileInUploadDo), + FileUpload(FileInUpload), // Plugin PluginEntry(PluginInEntry), // Prework @@ -22,7 +23,7 @@ pub(crate) enum TaskIn { impl_from_in! { // File - FilePaste(FileInPaste), FileLink(FileInLink), FileHardlink(FileInHardlink), FileDelete(FileInDelete), FileTrash(FileInTrash), FileDownload(FileInDownload), FileUploadDo(FileInUploadDo), + FileCopy(FileInCopy), FileCut(FileInCut), FileLink(FileInLink), FileHardlink(FileInHardlink), FileDelete(FileInDelete), FileTrash(FileInTrash), FileDownload(FileInDownload), FileUpload(FileInUpload), // Plugin PluginEntry(PluginInEntry), // Prework @@ -33,13 +34,14 @@ impl TaskIn { pub fn id(&self) -> Id { match self { // File - Self::FilePaste(r#in) => r#in.id, + Self::FileCopy(r#in) => r#in.id, + Self::FileCut(r#in) => r#in.id, Self::FileLink(r#in) => r#in.id, Self::FileHardlink(r#in) => r#in.id, Self::FileDelete(r#in) => r#in.id, Self::FileTrash(r#in) => r#in.id, Self::FileDownload(r#in) => r#in.id, - Self::FileUploadDo(r#in) => r#in.id, + Self::FileUpload(r#in) => r#in.id, // Plugin Self::PluginEntry(r#in) => r#in.id, // Prework diff --git a/yazi-scheduler/src/out.rs b/yazi-scheduler/src/out.rs index cd4b687f..b428a7e3 100644 --- a/yazi-scheduler/src/out.rs +++ b/yazi-scheduler/src/out.rs @@ -1,10 +1,12 @@ -use crate::{Task, file::{FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutPaste, FileOutPasteDo, FileOutTrash, FileOutUpload, FileOutUploadDo}, impl_from_out, plugin::PluginOutEntry, prework::{PreworkOutFetch, PreworkOutLoad, PreworkOutSize}, process::{ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}}; +use crate::{Task, file::{FileOutCopy, FileOutCopyDo, FileOutCut, FileOutCutDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutTrash, FileOutUpload, FileOutUploadDo}, impl_from_out, plugin::PluginOutEntry, prework::{PreworkOutFetch, PreworkOutLoad, PreworkOutSize}, process::{ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}}; #[derive(Debug)] pub(super) enum TaskOut { // File - FilePaste(FileOutPaste), - FilePasteDo(FileOutPasteDo), + FileCopy(FileOutCopy), + FileCopyDo(FileOutCopyDo), + FileCut(FileOutCut), + FileCutDo(FileOutCutDo), FileLink(FileOutLink), FileHardlink(FileOutHardlink), FileHardlinkDo(FileOutHardlinkDo), @@ -32,7 +34,7 @@ pub(super) enum TaskOut { impl_from_out! { // File - FilePaste(FileOutPaste), FilePasteDo(FileOutPasteDo), FileLink(FileOutLink), FileHardlink(FileOutHardlink), FileHardlinkDo(FileOutHardlinkDo), FileDelete(FileOutDelete), FileDeleteDo(FileOutDeleteDo), FileTrash(FileOutTrash), FileDownload(FileOutDownload), FileDownloadDo(FileOutDownloadDo), FileUpload(FileOutUpload), FileUploadDo(FileOutUploadDo), + FileCopy(FileOutCopy), FileCopyDo(FileOutCopyDo), FileCut(FileOutCut), FileCutDo(FileOutCutDo), FileLink(FileOutLink), FileHardlink(FileOutHardlink), FileHardlinkDo(FileOutHardlinkDo), FileDelete(FileOutDelete), FileDeleteDo(FileOutDeleteDo), FileTrash(FileOutTrash), FileDownload(FileOutDownload), FileDownloadDo(FileOutDownloadDo), FileUpload(FileOutUpload), FileUploadDo(FileOutUploadDo), // Plugin PluginEntry(PluginOutEntry), // Prework @@ -45,8 +47,10 @@ impl TaskOut { pub(crate) fn reduce(self, task: &mut Task) { match self { // File - Self::FilePaste(out) => out.reduce(task), - Self::FilePasteDo(out) => out.reduce(task), + Self::FileCopy(out) => out.reduce(task), + Self::FileCopyDo(out) => out.reduce(task), + Self::FileCut(out) => out.reduce(task), + Self::FileCutDo(out) => out.reduce(task), Self::FileLink(out) => out.reduce(task), Self::FileHardlink(out) => out.reduce(task), Self::FileHardlinkDo(out) => out.reduce(task), diff --git a/yazi-scheduler/src/progress.rs b/yazi-scheduler/src/progress.rs index 10abc5fc..05ade88f 100644 --- a/yazi-scheduler/src/progress.rs +++ b/yazi-scheduler/src/progress.rs @@ -1,13 +1,14 @@ use serde::Serialize; use yazi_parser::app::TaskSummary; -use crate::{file::{FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgPaste, FileProgTrash, FileProgUpload}, impl_from_prog, plugin::PluginProgEntry, prework::{PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; +use crate::{file::{FileProgCopy, FileProgCut, FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgTrash, FileProgUpload}, impl_from_prog, plugin::PluginProgEntry, prework::{PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(tag = "kind")] pub enum TaskProg { // File - FilePaste(FileProgPaste), + FileCopy(FileProgCopy), + FileCut(FileProgCut), FileLink(FileProgLink), FileHardlink(FileProgHardlink), FileDelete(FileProgDelete), @@ -28,7 +29,7 @@ pub enum TaskProg { impl_from_prog! { // File - FilePaste(FileProgPaste), FileLink(FileProgLink), FileHardlink(FileProgHardlink), FileDelete(FileProgDelete), FileTrash(FileProgTrash), FileDownload(FileProgDownload), FileUpload(FileProgUpload), + FileCopy(FileProgCopy), FileCut(FileProgCut), FileLink(FileProgLink), FileHardlink(FileProgHardlink), FileDelete(FileProgDelete), FileTrash(FileProgTrash), FileDownload(FileProgDownload), FileUpload(FileProgUpload), // Plugin PluginEntry(PluginProgEntry), // Prework @@ -41,7 +42,8 @@ impl From for TaskSummary { fn from(value: TaskProg) -> Self { match value { // File - TaskProg::FilePaste(p) => p.into(), + TaskProg::FileCopy(p) => p.into(), + TaskProg::FileCut(p) => p.into(), TaskProg::FileLink(p) => p.into(), TaskProg::FileHardlink(p) => p.into(), TaskProg::FileDelete(p) => p.into(), @@ -66,7 +68,8 @@ impl TaskProg { pub fn running(self) -> bool { match self { // File - Self::FilePaste(p) => p.running(), + Self::FileCopy(p) => p.running(), + Self::FileCut(p) => p.running(), Self::FileLink(p) => p.running(), Self::FileHardlink(p) => p.running(), Self::FileDelete(p) => p.running(), @@ -89,7 +92,8 @@ impl TaskProg { pub fn success(self) -> bool { match self { // File - Self::FilePaste(p) => p.success(), + Self::FileCopy(p) => p.success(), + Self::FileCut(p) => p.success(), Self::FileLink(p) => p.success(), Self::FileHardlink(p) => p.success(), Self::FileDelete(p) => p.success(), @@ -112,7 +116,8 @@ impl TaskProg { pub fn failed(self) -> bool { match self { // File - Self::FilePaste(p) => p.failed(), + Self::FileCopy(p) => p.failed(), + Self::FileCut(p) => p.failed(), Self::FileLink(p) => p.failed(), Self::FileHardlink(p) => p.failed(), Self::FileDelete(p) => p.failed(), @@ -135,7 +140,8 @@ impl TaskProg { pub fn cleaned(self) -> bool { match self { // File - Self::FilePaste(p) => p.cleaned(), + Self::FileCopy(p) => p.cleaned(), + Self::FileCut(p) => p.cleaned(), Self::FileLink(p) => p.cleaned(), Self::FileHardlink(p) => p.cleaned(), Self::FileDelete(p) => p.cleaned(), @@ -158,7 +164,8 @@ impl TaskProg { pub fn percent(self) -> Option { match self { // File - Self::FilePaste(p) => p.percent(), + Self::FileCopy(p) => p.percent(), + Self::FileCut(p) => p.percent(), Self::FileLink(p) => p.percent(), Self::FileHardlink(p) => p.percent(), Self::FileDelete(p) => p.percent(), @@ -181,7 +188,8 @@ impl TaskProg { pub(crate) fn is_user(self) -> bool { match self { // File - Self::FilePaste(_) => true, + Self::FileCopy(_) => true, + Self::FileCut(_) => true, Self::FileLink(_) => true, Self::FileHardlink(_) => true, Self::FileDelete(_) => true, diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index f14547ec..464b90a0 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -12,7 +12,7 @@ use yazi_shared::{Id, Throttle, url::{UrlBuf, UrlLike}}; use yazi_vfs::{must_be_dir, provider, unique_name}; use super::{Ongoing, TaskOp}; -use crate::{HIGH, LOW, NORMAL, TaskIn, TaskOps, TaskOut, file::{File, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInPaste, FileInTrash, FileInUpload, FileOutDelete, FileOutDownload, FileOutHardlink, FileOutPaste, FileOutUpload, FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgPaste, FileProgTrash, FileProgUpload}, plugin::{Plugin, PluginInEntry, PluginProgEntry}, prework::{Prework, PreworkInFetch, PreworkInLoad, PreworkInSize, PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{Process, ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessOutBg, ProcessOutBlock, ProcessOutOrphan, ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; +use crate::{HIGH, LOW, NORMAL, TaskIn, TaskOps, TaskOut, file::{File, FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDelete, FileOutDownload, FileOutHardlink, FileOutUpload, FileProgCopy, FileProgCut, FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgTrash, FileProgUpload}, plugin::{Plugin, PluginInEntry, PluginProgEntry}, prework::{Prework, PreworkInFetch, PreworkInLoad, PreworkInSize, PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{Process, ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessOutBg, ProcessOutBlock, ProcessOutOrphan, ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; pub struct Scheduler { file: Arc, @@ -77,10 +77,10 @@ impl Scheduler { pub fn file_cut(&self, from: UrlBuf, mut to: UrlBuf, force: bool) { let mut ongoing = self.ongoing.lock(); - let id = ongoing.add::(format!("Cut {} to {}", from.display(), to.display())); + let id = ongoing.add::(format!("Cut {} to {}", from.display(), to.display())); if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) { - return self.ops.out(id, FileOutPaste::Fail("Cannot cut directory into itself".to_owned())); + return self.ops.out(id, FileOutCut::Fail("Cannot cut directory into itself".to_owned())); } ongoing.hooks.add_async(id, { @@ -92,7 +92,7 @@ impl Scheduler { provider::remove_dir_clean(&from).await.ok(); Pump::push_move(from, to); } - ops.out(id, FileOutPaste::Clean); + ops.out(id, FileOutCut::Clean); } }); @@ -102,19 +102,19 @@ impl Scheduler { if !force { to = unique_name(to, must_be_dir(&from)).await?; } - file.paste(FileInPaste { id, from, to, cha: None, cut: true, follow, retry: 0 }).await + file.cut(FileInCut { id, from, to, cha: None, follow, retry: 0 }).await }); } pub fn file_copy(&self, from: UrlBuf, mut to: UrlBuf, force: bool, follow: bool) { - let id = self.ongoing.lock().add::(format!( + let id = self.ongoing.lock().add::(format!( "Copy {} to {}", from.display(), to.display() )); if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) { - return self.ops.out(id, FileOutPaste::Fail("Cannot copy directory into itself".to_owned())); + return self.ops.out(id, FileOutCopy::Fail("Cannot copy directory into itself".to_owned())); } let file = self.file.clone(); @@ -123,7 +123,7 @@ impl Scheduler { if !force { to = unique_name(to, must_be_dir(&from)).await?; } - file.paste(FileInPaste { id, from, to, cha: None, cut: false, follow, retry: 0 }).await + file.copy(FileInCopy { id, from, to, cha: None, follow, retry: 0 }).await }); } @@ -193,7 +193,7 @@ impl Scheduler { self.send_micro( id, LOW, - async move { file.delete(FileInDelete { id, target, length: 0 }).await }, + async move { file.delete(FileInDelete { id, target, cha: None }).await }, ); } @@ -242,7 +242,9 @@ impl Scheduler { }; let file = self.file.clone(); - self.send_micro(id, LOW, async move { file.upload(FileInUpload { id, url }).await }); + self.send_micro(id, LOW, async move { + file.upload(FileInUpload { id, url, cha: None, cache: None }).await + }); } pub fn plugin_micro(&self, opt: PluginOpt) { @@ -411,13 +413,14 @@ impl Scheduler { let result: Result<_, TaskOut> = match r#in { // File - TaskIn::FilePaste(r#in) => file.paste_do(r#in).await.map_err(Into::into), + TaskIn::FileCopy(r#in) => file.copy_do(r#in).await.map_err(Into::into), + TaskIn::FileCut(r#in) => file.cut_do(r#in).await.map_err(Into::into), TaskIn::FileLink(r#in) => file.link_do(r#in).await.map_err(Into::into), TaskIn::FileHardlink(r#in) => file.hardlink_do(r#in).await.map_err(Into::into), TaskIn::FileDelete(r#in) => file.delete_do(r#in).await.map_err(Into::into), TaskIn::FileTrash(r#in) => file.trash_do(r#in).await.map_err(Into::into), TaskIn::FileDownload(r#in) => file.download_do(r#in).await.map_err(Into::into), - TaskIn::FileUploadDo(r#in) => file.upload_do(r#in).await.map_err(Into::into), + TaskIn::FileUpload(r#in) => file.upload_do(r#in).await.map_err(Into::into), // Plugin TaskIn::PluginEntry(r#in) => plugin.macro_do(r#in).await.map_err(Into::into), // Prework diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 00e1797d..db44f0a5 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -167,7 +167,11 @@ impl<'a> Provider for Sftp<'a> { match op.rename_posix(self.path, &to).await { Ok(()) => {} Err(yazi_sftp::Error::Unsupported) => { - op.remove(&to).await?; + match op.remove(&to).await.map_err(io::Error::from) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::NotFound => {} + Err(e) => Err(e)?, + } op.rename(self.path, &to).await?; } Err(e) => Err(e)?, From 9ae8b90d2328139622e6e1285889310adb57b81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sun, 7 Dec 2025 18:00:49 +0800 Subject: [PATCH 30/53] feat: prioritize symbolic links when cutting remote files (#3412) --- yazi-fs/src/provider/capabilities.rs | 4 +++ yazi-fs/src/provider/local/local.rs | 5 +++- yazi-fs/src/provider/mod.rs | 2 +- yazi-fs/src/provider/traits.rs | 4 ++- yazi-scheduler/src/file/file.rs | 41 ++++++++++++++++++++++------ yazi-scheduler/src/file/in.rs | 23 +++++++++++++--- yazi-scheduler/src/file/traverse.rs | 5 ++-- yazi-scheduler/src/scheduler.rs | 2 +- yazi-vfs/src/provider/provider.rs | 9 +++++- yazi-vfs/src/provider/providers.rs | 9 +++++- yazi-vfs/src/provider/sftp/sftp.rs | 4 ++- 11 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 yazi-fs/src/provider/capabilities.rs diff --git a/yazi-fs/src/provider/capabilities.rs b/yazi-fs/src/provider/capabilities.rs new file mode 100644 index 00000000..323bbf95 --- /dev/null +++ b/yazi-fs/src/provider/capabilities.rs @@ -0,0 +1,4 @@ +#[derive(Clone, Copy, Debug)] +pub struct Capabilities { + pub symlink: bool, +} diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index 53ce13a1..9c9d1951 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -3,7 +3,7 @@ use std::{io, path::Path, sync::Arc}; use tokio::sync::mpsc; use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; -use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Provider}}; +use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Capabilities, Provider}}; #[derive(Clone)] pub struct Local<'a> { @@ -25,6 +25,9 @@ impl<'a> Provider for Local<'a> { tokio::fs::canonicalize(self.path).await.map(Into::into) } + #[inline] + fn capabilities(&self) -> Capabilities { Capabilities { symlink: true } } + async fn casefold(&self) -> io::Result { super::casefold(self.path).await.map(Into::into) } diff --git a/yazi-fs/src/provider/mod.rs b/yazi-fs/src/provider/mod.rs index 3297b1c6..1a96a542 100644 --- a/yazi-fs/src/provider/mod.rs +++ b/yazi-fs/src/provider/mod.rs @@ -1,3 +1,3 @@ yazi_macro::mod_pub!(local); -yazi_macro::mod_flat!(attrs traits); +yazi_macro::mod_flat!(attrs capabilities traits); diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs index cfe2db55..9cb591c4 100644 --- a/yazi-fs/src/provider/traits.rs +++ b/yazi-fs/src/provider/traits.rs @@ -4,7 +4,7 @@ use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; -use crate::{cha::{Cha, ChaType}, provider::Attrs}; +use crate::{cha::{Cha, ChaType}, provider::{Attrs, Capabilities}}; pub trait Provider: Sized { type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin; @@ -17,6 +17,8 @@ pub trait Provider: Sized { fn canonicalize(&self) -> impl Future>; + fn capabilities(&self) -> Capabilities; + fn casefold(&self) -> impl Future>; fn copy

(&self, to: P, attrs: Attrs) -> impl Future> diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 9b7f8df8..8144eea5 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -53,7 +53,7 @@ impl File { } pub(crate) async fn copy_do(&self, mut task: FileInCopy) -> Result<(), FileOutCopyDo> { - ok_or_not_found!(provider::remove_file(&task.to).await); // FIXME: integrate into copy_with_progress + ok_or_not_found!(provider::remove_file(&task.to).await); let mut it = provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await?; while let Some(res) = it.recv().await { @@ -87,20 +87,30 @@ impl File { return Ok(self.ops.out(id, FileOutCut::Succ)); } + let (mut links, mut files) = (vec![], vec![]); + let reorder = task.follow && provider::capabilities(&task.from).await?.symlink; + super::traverse::( task, async |dir| match provider::create_dir(dir).await { Err(e) if e.kind() != AlreadyExists => Err(e)?, _ => Ok(()), }, - async |task, cha| { - Ok(if cha.is_orphan() || (cha.is_link() && !task.follow) { - self.ops.out(id, FileOutCut::New(0)); + |task, cha| { + let nofollow = cha.is_orphan() || (cha.is_link() && !task.follow); + self.ops.out(id, FileOutCut::New(if nofollow { 0 } else { cha.len })); + + if nofollow { self.queue(task.into_link(), NORMAL); } else { - self.ops.out(id, FileOutCut::New(cha.len)); - self.queue(task, LOW); - }) + match (cha.is_link(), reorder) { + (_, false) => self.queue(task, LOW), + (true, true) => links.push(task), + (false, true) => files.push(task), + } + }; + + async { Ok(()) } }, |err| { self.ops.out(id, FileOutCut::Deform(err)); @@ -108,11 +118,26 @@ impl File { ) .await?; + if !links.is_empty() { + let len = links.len(); + let (tx, mut rx) = mpsc::channel(len); + for task in links { + self.queue(task.with_drop(&tx), LOW); + } + for _ in 0..len { + rx.recv().await; + } + } + + for task in files { + self.queue(task, LOW); + } + Ok(self.ops.out(id, FileOutCut::Succ)) } pub(crate) async fn cut_do(&self, mut task: FileInCut) -> Result<(), FileOutCutDo> { - ok_or_not_found!(provider::remove_file(&task.to).await); // FIXME: integrate into copy_with_progress + ok_or_not_found!(provider::remove_file(&task.to).await); let mut it = provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await?; while let Some(res) = it.recv().await { diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 0a447094..91483054 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -1,5 +1,6 @@ -use std::path::PathBuf; +use std::{mem, path::PathBuf}; +use tokio::sync::mpsc; use yazi_fs::cha::Cha; use yazi_shared::{Id, url::UrlBuf}; @@ -37,20 +38,34 @@ pub(crate) struct FileInCut { pub(crate) cha: Option, pub(crate) follow: bool, pub(crate) retry: u8, + pub(crate) drop: Option>, +} + +impl Drop for FileInCut { + fn drop(&mut self) { + if let Some(tx) = self.drop.take() { + tx.try_send(()).ok(); + } + } } impl FileInCut { - pub(super) fn into_link(self) -> FileInLink { + pub(super) fn into_link(mut self) -> FileInLink { FileInLink { id: self.id, - from: self.from, - to: self.to, + from: mem::take(&mut self.from), + to: mem::take(&mut self.to), cha: self.cha, resolve: true, relative: false, delete: true, } } + + pub(super) fn with_drop(mut self, drop: &mpsc::Sender<()>) -> Self { + self.drop = Some(drop.clone()); + self + } } // --- Link diff --git a/yazi-scheduler/src/file/traverse.rs b/yazi-scheduler/src/file/traverse.rs index 0336da0e..bd30db80 100644 --- a/yazi-scheduler/src/file/traverse.rs +++ b/yazi-scheduler/src/file/traverse.rs @@ -61,6 +61,7 @@ impl Traverse for FileInCut { cha: Some(cha), follow: self.follow, retry: self.retry, + drop: self.drop.clone(), } } @@ -137,14 +138,14 @@ impl Traverse for FileInUpload { pub(super) async fn traverse( mut task: T, on_dir: D, - on_file: FC, + mut on_file: FC, on_error: E, ) -> Result<(), R> where R: Debug + From, T: Traverse, D: AsyncFn(Url) -> Result<(), R>, - FC: Fn(T, Cha) -> FR, + FC: FnMut(T, Cha) -> FR, FR: Future>, E: Fn(String), { diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 464b90a0..1d489bc4 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -102,7 +102,7 @@ impl Scheduler { if !force { to = unique_name(to, must_be_dir(&from)).await?; } - file.cut(FileInCut { id, from, to, cha: None, follow, retry: 0 }).await + file.cut(FileInCut { id, from, to, cha: None, follow, retry: 0, drop: None }).await }); } diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/provider/provider.rs index 218f9ca3..ca2d6d9e 100644 --- a/yazi-vfs/src/provider/provider.rs +++ b/yazi-vfs/src/provider/provider.rs @@ -1,7 +1,7 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::Cha, provider::{Attrs, Provider, local::Local}}; +use yazi_fs::{cha::Cha, provider::{Attrs, Capabilities, Provider, local::Local}}; use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, UrlBuf, UrlCow}}; use super::{Providers, ReadDir, RwFile}; @@ -32,6 +32,13 @@ where Providers::new(url.as_url()).await?.canonicalize().await } +pub async fn capabilities(url: U) -> io::Result +where + U: AsUrl, +{ + Ok(Providers::new(url.as_url()).await?.capabilities()) +} + pub async fn casefold(url: U) -> io::Result where U: AsUrl, diff --git a/yazi-vfs/src/provider/providers.rs b/yazi-vfs/src/provider/providers.rs index e66b8356..7a6f8243 100644 --- a/yazi-vfs/src/provider/providers.rs +++ b/yazi-vfs/src/provider/providers.rs @@ -1,7 +1,7 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::Cha, provider::{Attrs, Provider}}; +use yazi_fs::{cha::Cha, provider::{Attrs, Capabilities, Provider}}; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] @@ -31,6 +31,13 @@ impl<'a> Provider for Providers<'a> { } } + fn capabilities(&self) -> Capabilities { + match self { + Self::Local(p) => p.capabilities(), + Self::Sftp(p) => p.capabilities(), + } + } + async fn casefold(&self) -> io::Result { match self { Self::Local(p) => p.casefold().await, diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index db44f0a5..7078076d 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -2,7 +2,7 @@ use std::{io, sync::Arc}; use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; use yazi_config::vfs::{ServiceSftp, Vfs}; -use yazi_fs::{CWD, provider::{DirReader, FileHolder, Provider}}; +use yazi_fs::{CWD, provider::{Capabilities, DirReader, FileHolder, Provider}}; use yazi_sftp::fs::{Attrs, Flags}; use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; @@ -43,6 +43,8 @@ impl<'a> Provider for Sftp<'a> { }) } + fn capabilities(&self) -> Capabilities { Capabilities { symlink: true } } + async fn casefold(&self) -> io::Result { let Some((parent, name)) = self.url.parent().zip(self.url.name()) else { return Ok(self.url.to_owned()); From 396f634bb85d20b31971f4fb77fd938101bce494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 8 Dec 2025 16:24:19 +0800 Subject: [PATCH 31/53] fix: add preset VFS config file to avoid TOML parsing errors (#3414) --- yazi-config/preset/vfs-default.toml | 1 + yazi-config/src/preset.rs | 6 ++- yazi-config/src/vfs/mod.rs | 2 +- yazi-config/src/vfs/services.rs | 42 +++++++++++++++ yazi-config/src/vfs/vfs.rs | 35 +++++------- yazi-scheduler/src/file/file.rs | 8 ++- yazi-scheduler/src/file/in.rs | 6 +-- yazi-shared/src/loc/buf.rs | 12 ++--- yazi-shared/src/loc/loc.rs | 4 +- yazi-shared/src/scheme/cow.rs | 11 ++-- yazi-shared/src/scheme/encode.rs | 2 +- yazi-shared/src/url/cow.rs | 84 +++++++++++++++++++++++++++++ yazi-shared/src/url/url.rs | 23 ++++++-- 13 files changed, 184 insertions(+), 52 deletions(-) create mode 100644 yazi-config/preset/vfs-default.toml create mode 100644 yazi-config/src/vfs/services.rs diff --git a/yazi-config/preset/vfs-default.toml b/yazi-config/preset/vfs-default.toml new file mode 100644 index 00000000..48be1dab --- /dev/null +++ b/yazi-config/preset/vfs-default.toml @@ -0,0 +1 @@ +[services] diff --git a/yazi-config/src/preset.rs b/yazi-config/src/preset.rs index 7c7762ef..7030e5f4 100644 --- a/yazi-config/src/preset.rs +++ b/yazi-config/src/preset.rs @@ -1,4 +1,4 @@ -use crate::{Yazi, keymap::Keymap, theme::Theme}; +use crate::{Yazi, keymap::Keymap, theme::Theme, vfs::Vfs}; pub(crate) struct Preset; @@ -19,6 +19,10 @@ impl Preset { }) } + pub(super) fn vfs() -> Result { + toml::from_str(&yazi_macro::config_preset!("vfs")) + } + #[inline] pub(crate) fn mix(a: A, b: B, c: C) -> impl Iterator where diff --git a/yazi-config/src/vfs/mod.rs b/yazi-config/src/vfs/mod.rs index e52b3881..363908c7 100644 --- a/yazi-config/src/vfs/mod.rs +++ b/yazi-config/src/vfs/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(service vfs); +yazi_macro::mod_flat!(service services vfs); diff --git a/yazi-config/src/vfs/services.rs b/yazi-config/src/vfs/services.rs new file mode 100644 index 00000000..0e8814a2 --- /dev/null +++ b/yazi-config/src/vfs/services.rs @@ -0,0 +1,42 @@ +use std::ops::Deref; + +use anyhow::{Result, bail}; +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; + +use crate::vfs::Service; + +#[derive(Deserialize, Serialize)] +pub struct Services(HashMap); + +impl Deref for Services { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl Services { + pub(super) fn reshape(mut self) -> Result { + for (name, service) in &mut self.0 { + if name.is_empty() || name.len() > 20 { + bail!("VFS name `{name}` must be between 1 and 20 characters"); + } else if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { + bail!("VFS name `{name}` must be in kebab-case"); + } + + service.reshape()?; + } + + Ok(self) + } + + pub(super) fn deserialize_over<'de, D>(mut self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map: HashMap = HashMap::deserialize(deserializer)?; + self.0.extend(map); + + Ok(self) + } +} diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs index c5039ac8..b035889f 100644 --- a/yazi-config/src/vfs/vfs.rs +++ b/yazi-config/src/vfs/vfs.rs @@ -1,16 +1,17 @@ use std::io; -use hashbrown::HashMap; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use tokio::sync::OnceCell; -use yazi_fs::Xdg; -use yazi_macro::ok_or_not_found; +use yazi_codegen::DeserializeOver1; +use yazi_fs::{Xdg, ok_or_not_found}; use super::Service; +use crate::{Preset, vfs::Services}; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, DeserializeOver1)] pub struct Vfs { - pub services: HashMap, + pub services: Services, } impl Vfs { @@ -19,9 +20,10 @@ impl Vfs { async fn init() -> io::Result { tokio::task::spawn_blocking(|| { - toml::from_str::(&Vfs::read()?).map_err(io::Error::other)?.reshape() + Preset::vfs()?.deserialize_over(toml::Deserializer::parse(&Vfs::read()?)?)?.reshape() }) .await? + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } LOADED.get_or_try_init(init).await @@ -40,24 +42,11 @@ impl Vfs { } } - fn read() -> io::Result { + fn read() -> Result { let p = Xdg::config_dir().join("vfs.toml"); - Ok(ok_or_not_found!(std::fs::read_to_string(&p).map_err(|e| { - std::io::Error::new(e.kind(), format!("Failed to read VFS config {p:?}: {e}")) - }))) + ok_or_not_found(std::fs::read_to_string(&p)) + .with_context(|| format!("Failed to read config {p:?}")) } - fn reshape(mut self) -> io::Result { - for (name, service) in &mut self.services { - if name.is_empty() || name.len() > 20 { - Err(io::Error::other(format!("VFS name `{name}` must be between 1 and 20 characters")))?; - } else if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { - Err(io::Error::other(format!("VFS name `{name}` must be in kebab-case")))?; - } - - service.reshape()?; - } - - Ok(self) - } + fn reshape(self) -> Result { Ok(Self { services: self.services.reshape()? }) } } diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 8144eea5..8976a40a 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -119,14 +119,12 @@ impl File { .await?; if !links.is_empty() { - let len = links.len(); - let (tx, mut rx) = mpsc::channel(len); + let (tx, mut rx) = mpsc::channel(1); for task in links { self.queue(task.with_drop(&tx), LOW); } - for _ in 0..len { - rx.recv().await; - } + drop(tx); + while rx.recv().await.is_some() {} } for task in files { diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 91483054..bee53877 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -42,11 +42,7 @@ pub(crate) struct FileInCut { } impl Drop for FileInCut { - fn drop(&mut self) { - if let Some(tx) = self.drop.take() { - tx.try_send(()).ok(); - } - } + fn drop(&mut self) { _ = self.drop.take(); } } impl FileInCut { diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index 115ef646..59db3862 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -266,24 +266,24 @@ mod tests { #[test] fn test_new() { let loc: LocBuf = Path::new("/").into(); - assert_eq!(loc.uri().as_os_str(), OsStr::new("/")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("")); assert_eq!(loc.urn().as_os_str(), OsStr::new("")); assert_eq!(loc.file_name(), None); - assert_eq!(loc.base().as_os_str(), OsStr::new("")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: LocBuf = Path::new("/root").into(); - assert_eq!(loc.uri().as_os_str(), OsStr::new("/root")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("root")); assert_eq!(loc.urn().as_os_str(), OsStr::new("root")); assert_eq!(loc.file_name().unwrap(), OsStr::new("root")); - assert_eq!(loc.base().as_os_str(), OsStr::new("")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: LocBuf = Path::new("/root/code/foo/").into(); - assert_eq!(loc.uri().as_os_str(), OsStr::new("/root/code/foo")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo")); assert_eq!(loc.file_name().unwrap(), OsStr::new("foo")); - assert_eq!(loc.base().as_os_str(), OsStr::new("")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); } diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index d377b396..a1824ee8 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -86,7 +86,7 @@ where let path = path.as_path_view(); let Some(name) = path.file_name() else { let p = path.strip_prefix(P::empty()).unwrap(); - return Self { inner: p, uri: p.len(), urn: 0, _phantom: PhantomData }; + return Self { inner: p, uri: 0, urn: 0, _phantom: PhantomData }; }; let name_len = name.len(); @@ -97,7 +97,7 @@ where let bytes = &path.as_encoded_bytes()[..prefix_len + name_len]; Self { inner: unsafe { P::from_encoded_bytes_unchecked(bytes) }, - uri: bytes.len(), + uri: name_len, urn: name_len, _phantom: PhantomData, } diff --git a/yazi-shared/src/scheme/cow.rs b/yazi-shared/src/scheme/cow.rs index 1ec93849..30a4eea0 100644 --- a/yazi-shared/src/scheme/cow.rs +++ b/yazi-shared/src/scheme/cow.rs @@ -146,7 +146,7 @@ impl<'a> SchemeCow<'a> { Ok(match kind { SchemeKind::Regular => { ensure!(uri.is_none() && urn.is_none(), "Regular scheme cannot have ports"); - (path.components().count(), path.name().is_some() as usize) + (path.name().is_some() as usize, path.name().is_some() as usize) } SchemeKind::Search => { let (uri, urn) = (uri.unwrap_or(0), urn.unwrap_or(0)); @@ -155,7 +155,7 @@ impl<'a> SchemeCow<'a> { } SchemeKind::Archive => (uri.unwrap_or(0), urn.unwrap_or(0)), SchemeKind::Sftp => { - let uri = uri.unwrap_or_else(|| path.components().count()); + let uri = uri.unwrap_or(path.name().is_some() as usize); let urn = urn.unwrap_or(path.name().is_some() as usize); (uri, urn) } @@ -163,7 +163,12 @@ impl<'a> SchemeCow<'a> { } pub fn retrieve_ports(url: Url) -> (usize, usize) { - (url.uri().components().count(), url.urn().components().count()) + match url { + Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize), + Url::Search { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), + Url::Archive { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), + Url::Sftp { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), + } } } diff --git a/yazi-shared/src/scheme/encode.rs b/yazi-shared/src/scheme/encode.rs index f191d2d5..c51e28ea 100644 --- a/yazi-shared/src/scheme/encode.rs +++ b/yazi-shared/src/scheme/encode.rs @@ -39,7 +39,7 @@ impl<'a> Encode<'a> { SchemeKind::Regular => Ok(()), SchemeKind::Search | SchemeKind::Archive => w!(0, 0), SchemeKind::Sftp => { - w!(self.0.0.loc().components().count(), self.0.0.loc().name().is_some() as usize) + w!(self.0.0.loc().name().is_some() as usize, self.0.0.loc().name().is_some() as usize) } } } diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index 7d9a0c5a..d6bb9569 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -266,3 +266,87 @@ impl<'de> Deserialize<'de> for UrlCow<'_> { UrlBuf::deserialize(deserializer).map(UrlCow::from) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::url::UrlLike; + + #[test] + fn test_parse() -> Result<()> { + struct Case { + url: &'static str, + urn: &'static str, + uri: &'static str, + trail: &'static str, + base: &'static str, + } + + let cases = [ + // Regular + Case { + url: "/root/music/rock/song.mp3", + urn: "song.mp3", + uri: "song.mp3", + trail: "/root/music/rock/", + base: "/root/music/rock/", + }, + // Search portal + Case { + url: "search://keyword//root/Documents/reports", + urn: "", + uri: "", + trail: "search://keyword//root/Documents/reports", + base: "search://keyword//root/Documents/reports", + }, + // Search item + Case { + url: "search://keyword:2:2//root/Documents/reports/2023/summary.docx", + urn: "2023/summary.docx", + uri: "2023/summary.docx", + trail: "search://keyword//root/Documents/reports/", + base: "search://keyword//root/Documents/reports/", + }, + // Archive portal + Case { + url: "archive://domain//root/Downloads/images.zip", + urn: "", + uri: "", + trail: "archive://domain//root/Downloads/images.zip", + base: "archive://domain//root/Downloads/images.zip", + }, + // Archive item + Case { + url: "archive://domain:2:1//root/Downloads/images.zip/2025/city.jpg", + urn: "city.jpg", + uri: "2025/city.jpg", + trail: "archive://domain:1:1//root/Downloads/images.zip/2025/", + base: "archive://domain//root/Downloads/images.zip/", + }, + // SFTP + Case { + url: "sftp://my-server//root/docs/report.pdf", + urn: "report.pdf", + uri: "report.pdf", + trail: "sftp://my-server//root/docs/", + base: "sftp://my-server//root/docs/", + }, + ]; + + for case in cases { + let url = UrlCow::try_from(case.url)?; + assert_eq!(url.urn().to_str()?, case.urn); + assert_eq!(url.uri().to_str()?, case.uri); + assert_eq!( + format!("{:?}", url.trail()), + format!("{:?}", UrlCow::try_from(case.trail)?.as_url()) + ); + assert_eq!( + format!("{:?}", url.base()), + format!("{:?}", UrlCow::try_from(case.base)?.as_url()) + ); + } + + Ok(()) + } +} diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index a815670a..de27bbe5 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -61,8 +61,8 @@ impl<'a> Url<'a> { pub fn base(self) -> Self { match self { Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())), - Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.base()), domain }, - Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.base()), domain }, + Self::Search { loc, domain } => Self::Search { loc: Loc::zeroed(loc.base()), domain }, + Self::Archive { loc, domain } => Self::Archive { loc: Loc::zeroed(loc.base()), domain }, Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.base()), domain }, } } @@ -222,12 +222,25 @@ impl<'a> Url<'a> { #[inline] pub fn to_owned(self) -> UrlBuf { self.into() } - #[inline] pub fn trail(self) -> Self { + let uri = self.uri(); match self { Self::Regular(loc) => Self::Regular(Loc::bare(loc.trail())), - Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.trail()), domain }, - Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.trail()), domain }, + + Self::Search { loc, domain } if uri.is_empty() => { + Self::Search { loc: Loc::zeroed(loc.trail()), domain } + } + Self::Search { loc, domain } => { + Self::Search { loc: Loc::new(loc.trail(), loc.base(), loc.base()), domain } + } + + Self::Archive { loc, domain } if uri.is_empty() => { + Self::Archive { loc: Loc::zeroed(loc.trail()), domain } + } + Self::Archive { loc, domain } => { + Self::Archive { loc: Loc::new(loc.trail(), loc.base(), loc.base()), domain } + } + Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.trail()), domain }, } } From 55ef88b7262ef5c2fa2eede4dbbbc9bd8e8c226c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Tue, 9 Dec 2025 22:25:01 +0800 Subject: [PATCH 32/53] feat!: rounded corners for indicator bar (#3419) --- CHANGELOG.md | 3 + Cargo.lock | 26 ++--- Cargo.toml | 2 +- yazi-actor/src/lives/core.rs | 2 +- yazi-actor/src/lives/file.rs | 5 +- yazi-actor/src/lives/task.rs | 3 +- yazi-actor/src/lives/tasks.rs | 3 +- yazi-binding/src/elements/rect.rs | 2 +- yazi-binding/src/macros.rs | 109 +++++++++++++++----- yazi-binding/src/style.rs | 7 +- yazi-binding/src/utils.rs | 5 +- yazi-config/preset/theme-dark.toml | 15 ++- yazi-config/preset/theme-light.toml | 15 ++- yazi-config/src/color.rs | 27 ----- yazi-config/src/lib.rs | 2 +- yazi-config/src/style.rs | 113 +++++++++++---------- yazi-config/src/theme/icon.rs | 3 +- yazi-config/src/theme/theme.rs | 49 +++++---- yazi-fm/src/app/commands/reflow.rs | 2 +- yazi-plugin/preset/components/entity.lua | 33 +++++- yazi-plugin/preset/components/linemode.lua | 19 +++- yazi-plugin/preset/components/parent.lua | 15 +-- yazi-plugin/src/elements/elements.rs | 4 +- yazi-plugin/src/fs/fs.rs | 8 +- yazi-plugin/src/runtime/runtime.rs | 7 +- yazi-plugin/src/theme/theme.rs | 42 +++++++- yazi-plugin/src/utils/app.rs | 2 +- yazi-plugin/src/utils/json.rs | 3 +- 28 files changed, 340 insertions(+), 186 deletions(-) delete mode 100644 yazi-config/src/color.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da97b9d..b8c51214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Improve the UX of the pick and input components ([#2906], [#2935]) - Show progress of each task in task manager ([#3121], [#3131], [#3134]) - New `overall` option to set the overall background color ([#3317]) +- Rounded corners for indicator bar ([#3419]) - New `bulk_rename` command always renames files with the editor ([#2984]) - `key-*` DDS events to allow changing or canceling user key events ([#3005], [#3037]) - New `--bg` specifying image background color in the preset SVG and ImageMagick previewers ([#3189]) @@ -45,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Rename `name` to `url` for open, fetchers, spotters, preloaders, previewers, filetype, and `globs` icon rules to support virtual file system ([#3034]) - Rename `mime` fetcher to `mime.local`, and introduce `mime.dir` fetcher to support folder MIME types ([#3222]) +- Reclassify `hovered` and `preview_hovered` under `[mgr]` of `theme.toml` into `[indicator]` as `current` and `preview`, respectively ([#3419]) - Remove `$0` parameter in opener rules to make the `open` command work under empty directories ([#3226]) - Return `Path` instead of `Url` from `Url:strip_prefix()` and `File.link_to` to enforce type safety ([#3361], [#3385]) - Use `body` instead of the term `content` in confirmations ([#2921]) @@ -1551,3 +1553,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3391]: https://github.com/sxyazi/yazi/pull/3391 [#3393]: https://github.com/sxyazi/yazi/pull/3393 [#3396]: https://github.com/sxyazi/yazi/pull/3396 +[#3419]: https://github.com/sxyazi/yazi/pull/3419 diff --git a/Cargo.lock b/Cargo.lock index 58543723..77c36bf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,9 +300,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "bcrypt-pbkdf" @@ -505,9 +505,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.48" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -662,6 +662,7 @@ dependencies = [ "itoa", "rustversion", "ryu", + "serde", "static_assertions", ] @@ -1305,9 +1306,9 @@ checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" -version = "1.1.6" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30f4148e3c9b7dbe0cc7e842ad5a61b28f9025f201d78149383e778a08bc9215" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -2825,9 +2826,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3502d6155304a4173a5f2c34b52b7ed0dd085890326cb50fd625fdf39e86b3b" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -2969,6 +2970,7 @@ dependencies = [ "itertools 0.13.0", "lru 0.12.5", "paste", + "serde", "strum", "unicode-segmentation", "unicode-truncate", @@ -3526,9 +3528,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simd_helpers" @@ -5270,9 +5272,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e" +checksum = "f520eebad972262a1dde0ec455bce4f8b298b1e5154513de58c114c4c54303e8" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index e0d1044b..1988d1c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ parking_lot = "0.12.5" paste = "1.0.15" percent-encoding = "2.3.2" rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] } -ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] } +ratatui = { version = "0.29.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] } regex = "1.12.2" russh = { version = "0.55.0", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" diff --git a/yazi-actor/src/lives/core.rs b/yazi-actor/src/lives/core.rs index d7860c01..a0341054 100644 --- a/yazi-actor/src/lives/core.rs +++ b/yazi-actor/src/lives/core.rs @@ -50,7 +50,7 @@ impl UserData for Core { } }; } - Ok(match key.as_bytes().as_ref() { + Ok(match &*key.as_bytes() { b"active" => reuse!(active, super::Tab::make(me.active())), b"tabs" => reuse!(tabs, super::Tabs::make(&me.mgr.tabs)), b"tasks" => reuse!(tasks, super::Tasks::make(&me.tasks)), diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index d3c22917..c981e750 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -68,8 +68,11 @@ impl UserData for File { fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); fields.add_field_method_get("is_hovered", |_, me| Ok(me.idx == me.folder.cursor)); + fields.add_field_method_get("in_current", |_, me| { + Ok(&*me.folder as *const _ == &me.tab.current as *const _) + }); fields.add_field_method_get("in_preview", |_, me| { - Ok(me.tab.hovered().is_some_and(|f| f.url == me.folder.url)) + Ok(me.idx == me.folder.cursor && me.tab.hovered().is_some_and(|f| f.url == me.folder.url)) }); } diff --git a/yazi-actor/src/lives/task.rs b/yazi-actor/src/lives/task.rs index 9003c252..b646c3b3 100644 --- a/yazi-actor/src/lives/task.rs +++ b/yazi-actor/src/lives/task.rs @@ -1,8 +1,7 @@ use std::ops::Deref; use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value}; -use yazi_binding::cached_field; -use yazi_plugin::runtime::SER_OPT; +use yazi_binding::{SER_OPT, cached_field}; use super::{Lives, PtrCell}; diff --git a/yazi-actor/src/lives/tasks.rs b/yazi-actor/src/lives/tasks.rs index 9d264484..e39129cc 100644 --- a/yazi-actor/src/lives/tasks.rs +++ b/yazi-actor/src/lives/tasks.rs @@ -1,8 +1,7 @@ use std::ops::Deref; use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value}; -use yazi_binding::{cached_field, deprecate}; -use yazi_plugin::runtime::SER_OPT; +use yazi_binding::{SER_OPT, cached_field, deprecate}; use super::{Lives, PtrCell}; use crate::lives::TaskSnap; diff --git a/yazi-binding/src/elements/rect.rs b/yazi-binding/src/elements/rect.rs index c1f8262d..f54d9069 100644 --- a/yazi-binding/src/elements/rect.rs +++ b/yazi-binding/src/elements/rect.rs @@ -36,7 +36,7 @@ impl Rect { })?; let index = lua.create_function(move |lua, (_, key): (Table, mlua::String)| { - Ok(match key.as_bytes().as_ref() { + Ok(match &*key.as_bytes() { b"default" => { deprecate!(lua, "`ui.Rect.default` is deprecated, use `ui.Rect{{}}` instead, in your {}\nSee #2927 for more details: https://github.com/sxyazi/yazi/pull/2927"); Some(Self(Default::default())) diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index e9ff87f2..8ad662a7 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -82,65 +82,122 @@ macro_rules! impl_style_method { macro_rules! impl_style_shorthands { ($methods:ident, $($field:tt).+) => { $methods.add_function_mut("fg", |lua, (ud, value): (mlua::AnyUserData, mlua::Value)| { + use ratatui::style::Modifier; + + let me = &mut ud.borrow_mut::()?.$($field).+; match value { - mlua::Value::Nil => { - ud.borrow::()?.$($field).+.fg.map($crate::Color).into_lua(lua) - }, + mlua::Value::Boolean(true) if me.add_modifier.contains(Modifier::REVERSED) && !me.sub_modifier.contains(Modifier::REVERSED) => { + me.bg.map($crate::Color).into_lua(lua) + } + mlua::Value::Nil | mlua::Value::Boolean(_) => { + me.fg.map($crate::Color).into_lua(lua) + } _ => { - ud.borrow_mut::()?.$($field).+.fg = Some($crate::Color::try_from(value)?.0); + me.fg = Some($crate::Color::try_from(value)?.0); ud.into_lua(lua) } } }); $methods.add_function_mut("bg", |lua, (ud, value): (mlua::AnyUserData, mlua::Value)| { + use ratatui::style::Modifier; + + let me = &mut ud.borrow_mut::()?.$($field).+; match value { - mlua::Value::Nil => { - ud.borrow::()?.$($field).+.bg.map($crate::Color).into_lua(lua) + mlua::Value::Boolean(true) if me.add_modifier.contains(Modifier::REVERSED) && !me.sub_modifier.contains(Modifier::REVERSED) => { + me.fg.map($crate::Color).into_lua(lua) + } + mlua::Value::Nil | mlua::Value::Boolean(_) => { + me.bg.map($crate::Color).into_lua(lua) } _ => { - ud.borrow_mut::()?.$($field).+.bg = Some($crate::Color::try_from(value)?.0); + me.bg = Some($crate::Color::try_from(value)?.0); ud.into_lua(lua) } } }); - $methods.add_function_mut("bold", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier |= ratatui::style::Modifier::BOLD; + $methods.add_function_mut("bold", |_, (ud, remove): (mlua::AnyUserData, bool)| { + let me = &mut ud.borrow_mut::()?.$($field).+; + if remove { + *me = me.remove_modifier(ratatui::style::Modifier::BOLD); + } else { + *me = me.add_modifier(ratatui::style::Modifier::BOLD); + } Ok(ud) }); - $methods.add_function_mut("dim", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier |= ratatui::style::Modifier::DIM; + $methods.add_function_mut("dim", |_, (ud, remove): (mlua::AnyUserData, bool)| { + let me = &mut ud.borrow_mut::()?.$($field).+; + if remove { + *me = me.remove_modifier(ratatui::style::Modifier::DIM); + } else { + *me = me.add_modifier(ratatui::style::Modifier::DIM); + } Ok(ud) }); - $methods.add_function_mut("italic", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier |= ratatui::style::Modifier::ITALIC; + $methods.add_function_mut("italic", |_, (ud, remove): (mlua::AnyUserData, bool)| { + let me = &mut ud.borrow_mut::()?.$($field).+; + if remove { + *me = me.remove_modifier(ratatui::style::Modifier::ITALIC); + } else { + *me = me.add_modifier(ratatui::style::Modifier::ITALIC); + } Ok(ud) }); - $methods.add_function_mut("underline", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier |= ratatui::style::Modifier::UNDERLINED; + $methods.add_function_mut("underline", |_, (ud, remove): (mlua::AnyUserData, bool)| { + let me = &mut ud.borrow_mut::()?.$($field).+; + if remove { + *me = me.remove_modifier(ratatui::style::Modifier::UNDERLINED); + } else { + *me = me.add_modifier(ratatui::style::Modifier::UNDERLINED); + } Ok(ud) }); - $methods.add_function_mut("blink", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier |= ratatui::style::Modifier::SLOW_BLINK; + $methods.add_function_mut("blink", |_, (ud, remove): (mlua::AnyUserData, bool)| { + let me = &mut ud.borrow_mut::()?.$($field).+; + if remove { + *me = me.remove_modifier(ratatui::style::Modifier::SLOW_BLINK); + } else { + *me = me.add_modifier(ratatui::style::Modifier::SLOW_BLINK); + } Ok(ud) }); - $methods.add_function_mut("blink_rapid", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier |= ratatui::style::Modifier::RAPID_BLINK; + $methods.add_function_mut("blink_rapid", |_, (ud, remove): (mlua::AnyUserData, bool)| { + let me = &mut ud.borrow_mut::()?.$($field).+; + if remove { + *me = me.remove_modifier(ratatui::style::Modifier::RAPID_BLINK); + } else { + *me = me.add_modifier(ratatui::style::Modifier::RAPID_BLINK); + } Ok(ud) }); - $methods.add_function_mut("reverse", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier |= ratatui::style::Modifier::REVERSED; + $methods.add_function_mut("reverse", |_, (ud, remove): (mlua::AnyUserData, bool)| { + let me = &mut ud.borrow_mut::()?.$($field).+; + if remove { + *me = me.remove_modifier(ratatui::style::Modifier::REVERSED); + } else { + *me = me.add_modifier(ratatui::style::Modifier::REVERSED); + } Ok(ud) }); - $methods.add_function_mut("hidden", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier |= ratatui::style::Modifier::HIDDEN; + $methods.add_function_mut("hidden", |_, (ud, remove): (mlua::AnyUserData, bool)| { + let me = &mut ud.borrow_mut::()?.$($field).+; + if remove { + *me = me.remove_modifier(ratatui::style::Modifier::HIDDEN); + } else { + *me = me.add_modifier(ratatui::style::Modifier::HIDDEN); + } Ok(ud) }); - $methods.add_function_mut("crossed", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier |= ratatui::style::Modifier::CROSSED_OUT; + $methods.add_function_mut("crossed", |_, (ud, remove): (mlua::AnyUserData, bool)| { + let me = &mut ud.borrow_mut::()?.$($field).+; + if remove { + *me = me.remove_modifier(ratatui::style::Modifier::CROSSED_OUT); + } else { + *me = me.add_modifier(ratatui::style::Modifier::CROSSED_OUT); + } Ok(ud) }); $methods.add_function_mut("reset", |_, ud: mlua::AnyUserData| { - ud.borrow_mut::()?.$($field).+.add_modifier = ratatui::style::Modifier::empty(); + ud.borrow_mut::()?.$($field).+ = ratatui::style::Style::reset(); Ok(ud) }); }; diff --git a/yazi-binding/src/style.rs b/yazi-binding/src/style.rs index 3403619e..ddcaf181 100644 --- a/yazi-binding/src/style.rs +++ b/yazi-binding/src/style.rs @@ -1,4 +1,6 @@ -use mlua::{AnyUserData, ExternalError, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value}; +use mlua::{AnyUserData, ExternalError, IntoLua, Lua, LuaSerdeExt, MetaMethod, Table, UserData, UserDataMethods, Value}; + +use crate::SER_OPT; #[derive(Clone, Copy, Default)] pub struct Style(pub ratatui::style::Style); @@ -34,6 +36,9 @@ impl UserData for Style { fn add_methods>(methods: &mut M) { crate::impl_style_shorthands!(methods, 0); + methods + .add_method("raw", |lua, me, ()| lua.to_value_with(&yazi_config::Style::from(me.0), SER_OPT)); + methods.add_function_mut("patch", |_, (ud, value): (AnyUserData, Value)| { { let mut me = ud.borrow_mut::()?; diff --git a/yazi-binding/src/utils.rs b/yazi-binding/src/utils.rs index 52cada49..c48f3173 100644 --- a/yazi-binding/src/utils.rs +++ b/yazi-binding/src/utils.rs @@ -1,4 +1,7 @@ -use mlua::{IntoLua, Lua, Table, Value}; +use mlua::{IntoLua, Lua, SerializeOptions, Table, Value}; + +pub const SER_OPT: SerializeOptions = + SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false); pub fn get_metatable(lua: &Lua, value: impl IntoLua) -> mlua::Result { let (_, mt): (Value, Table) = unsafe { diff --git a/yazi-config/preset/theme-dark.toml b/yazi-config/preset/theme-dark.toml index d4bb9230..56f46344 100644 --- a/yazi-config/preset/theme-dark.toml +++ b/yazi-config/preset/theme-dark.toml @@ -29,10 +29,6 @@ overall = {} [mgr] cwd = { fg = "cyan" } -# Hovered -hovered = { reversed = true } -preview_hovered = { underline = true } - # Find find_keyword = { fg = "yellow", bold = true, italic = true, underline = true } find_position = { fg = "magenta", bg = "reset", bold = true, italic = true } @@ -91,6 +87,17 @@ unset_alt = { fg = "red", bg = "gray" } # : }}} +# : Indicator of hovered file {{{ + +[indicator] +parent = { reversed = true } +current = { reversed = true } +preview = { underline = true } +padding = { open = "", close = "" } + +# : }}} + + # : Status bar {{{ [status] diff --git a/yazi-config/preset/theme-light.toml b/yazi-config/preset/theme-light.toml index 8bd2c90e..d6aef72b 100644 --- a/yazi-config/preset/theme-light.toml +++ b/yazi-config/preset/theme-light.toml @@ -29,10 +29,6 @@ overall = {} [mgr] cwd = { fg = "cyan" } -# Hovered -hovered = { reversed = true } -preview_hovered = { underline = true } - # Find find_keyword = { fg = "yellow", bold = true, italic = true, underline = true } find_position = { fg = "magenta", bg = "reset", bold = true, italic = true } @@ -91,6 +87,17 @@ unset_alt = { fg = "red", bg = "gray" } # : }}} +# : Indicator of hovered file {{{ + +[indicator] +parent = { reversed = true } +current = { reversed = true } +preview = { underline = true } +padding = { open = "", close = "" } + +# : }}} + + # : Status bar {{{ [status] diff --git a/yazi-config/src/color.rs b/yazi-config/src/color.rs deleted file mode 100644 index bf0b63ba..00000000 --- a/yazi-config/src/color.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::{ops::Deref, str::FromStr}; - -use serde::Deserialize; - -#[derive(Clone, Copy, Debug, Default)] -pub struct Color(ratatui::style::Color); - -impl Deref for Color { - type Target = ratatui::style::Color; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl From for ratatui::style::Color { - fn from(value: Color) -> Self { value.0 } -} - -impl<'de> Deserialize<'de> for Color { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - ratatui::style::Color::from_str(&String::deserialize(deserializer)?) - .map_err(serde::de::Error::custom) - .map(Self) - } -} diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 11c4e37f..f08dd301 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -1,6 +1,6 @@ yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which vfs); -yazi_macro::mod_flat!(color icon layout pattern platform preset priority style yazi); +yazi_macro::mod_flat!(icon layout pattern platform preset priority style yazi); use std::io::{Read, Write}; diff --git a/yazi-config/src/style.rs b/yazi-config/src/style.rs index e2531bb0..d9d326fd 100644 --- a/yazi-config/src/style.rs +++ b/yazi-config/src/style.rs @@ -1,75 +1,86 @@ -use ratatui::style::Modifier; -use serde::Deserialize; +use ratatui::style::Color; +use serde::{Deserialize, Serialize}; -use crate::Color; - -#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)] pub struct Style { pub fg: Option, pub bg: Option, - #[serde(default)] - pub bold: bool, - #[serde(default)] - pub dim: bool, - #[serde(default)] - pub italic: bool, - #[serde(default)] - pub underline: bool, - #[serde(default)] - pub blink: bool, - #[serde(default)] - pub blink_rapid: bool, - #[serde(default)] - pub reversed: bool, - #[serde(default)] - pub hidden: bool, - #[serde(default)] - pub crossed: bool, + pub bold: Option, + pub dim: Option, + pub italic: Option, + pub underline: Option, + pub blink: Option, + pub blink_rapid: Option, + pub reversed: Option, + pub hidden: Option, + pub crossed: Option, } -impl From