From 1bfcbc1664af36f75e14bb453ab6bd7a40131457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 13 Aug 2025 01:15:49 +0800 Subject: [PATCH] feat: introduce the concept of `Uri` (#3049) --- .github/workflows/draft.yml | 4 +- Cargo.lock | 64 +++++++++--------- Cargo.toml | 8 +-- cspell.json | 2 +- snap/snapcraft.yaml | 6 +- yazi-actor/src/cmp/trigger.rs | 2 +- yazi-actor/src/mgr/cd.rs | 2 +- yazi-actor/src/tasks/open_with.rs | 2 +- yazi-boot/Cargo.toml | 2 +- yazi-boot/src/boot.rs | 8 +-- yazi-cli/Cargo.toml | 2 +- yazi-config/src/preview/preview.rs | 8 ++- yazi-config/src/theme/theme.rs | 12 ++-- yazi-core/src/mgr/watcher.rs | 18 ++--- yazi-core/src/tab/preview.rs | 4 +- yazi-core/src/tasks/process.rs | 2 +- yazi-fs/src/file.rs | 4 +- yazi-fs/src/path/expand.rs | 16 +---- yazi-fs/src/path/path.rs | 6 +- yazi-fs/src/xdg.rs | 6 +- yazi-parser/src/mgr/cd.rs | 8 +-- yazi-parser/src/mgr/reveal.rs | 8 +-- yazi-parser/src/mgr/tab_create.rs | 8 +-- yazi-plugin/preset/plugins/folder.lua | 4 +- yazi-plugin/src/fs/fs.rs | 17 ++--- yazi-scheduler/src/hooks.rs | 42 +++++------- yazi-scheduler/src/ongoing.rs | 9 ++- yazi-scheduler/src/scheduler.rs | 72 +++++++++----------- yazi-shared/src/url/encode.rs | 53 ++++++++++----- yazi-shared/src/url/loc.rs | 83 +++++++++++++++-------- yazi-shared/src/url/mod.rs | 2 +- yazi-shared/src/url/scheme.rs | 97 +++++++++++++++++++-------- yazi-shared/src/url/uri.rs | 27 ++++++++ yazi-shared/src/url/url.rs | 21 ++++-- yazi-shared/src/url/urn.rs | 8 +-- 35 files changed, 356 insertions(+), 281 deletions(-) create mode 100644 yazi-shared/src/url/uri.rs diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml index dbc7f44c..9703ca34 100644 --- a/.github/workflows/draft.yml +++ b/.github/workflows/draft.yml @@ -184,14 +184,14 @@ jobs: env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} run: | - parallel 'snapcraft push -v --release latest/candidate {}' ::: yazi-*.snap || true + parallel 'snapcraft upload -v --release latest/candidate {}' ::: yazi-*.snap || true - name: Push snap to edge channel if: ${{ !startsWith(github.ref, 'refs/tags/') }} env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} run: | - parallel 'snapcraft push -v --release latest/edge {}' ::: yazi-*.snap || true + parallel 'snapcraft upload -v --release latest/edge {}' ::: yazi-*.snap || true draft: if: startsWith(github.ref, 'refs/tags/') diff --git a/Cargo.lock b/Cargo.lock index 7a8c4073..5b7794d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "approx" @@ -319,9 +319,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder-lite" @@ -352,9 +352,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.31" +version = "1.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" dependencies = [ "jobserver", "libc", @@ -391,9 +391,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.42" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" +checksum = "1c1f056bae57e3e54c3375c41ff79619ddd13460a17d7438712bd0d83fda4ff8" dependencies = [ "clap_builder", "clap_derive", @@ -401,9 +401,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.42" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -413,9 +413,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.55" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a" +checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" dependencies = [ "clap", ] @@ -1072,9 +1072,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -1319,9 +1319,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libfuzzer-sys" @@ -1486,9 +1486,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de25fc513588ac1273aa8c6dc0fffee6d32c12f38dc75f5cdc74547121a107ef" +checksum = "ab2fea92b2adabd51808311b101551d6e3f8602b65e9fae51f7ad5b3d500f4cd" dependencies = [ "anyhow", "bstr", @@ -1507,9 +1507,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcdf7c9e260ca82aaa32ac11148941952b856bb8c69aa5a9e65962f21fcb8637" +checksum = "3d4dc9cfc5a7698899802e97480617d9726f7da78c910db989d4d0fd4991d900" dependencies = [ "cc", "cfg-if", @@ -1928,9 +1928,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" dependencies = [ "unicode-ident", ] @@ -2170,7 +2170,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -2272,9 +2272,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2452,9 +2452,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -2591,11 +2591,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.14", ] [[package]] @@ -2611,9 +2611,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9f761991..91ba220d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,10 +21,10 @@ lto = false [workspace.dependencies] ansi-to-tui = "7.0.0" -anyhow = "1.0.98" +anyhow = "1.0.99" base64 = "0.22.1" bitflags = { version = "2.9.1", features = [ "serde" ] } -clap = { version = "4.5.42", features = [ "derive" ] } +clap = { version = "4.5.44", features = [ "derive" ] } core-foundation-sys = "0.8.7" crossterm = { version = "0.29.0", features = [ "event-stream" ] } dirs = "6.0.0" @@ -32,9 +32,9 @@ foldhash = "0.1.5" futures = "0.3.31" globset = "0.4.16" indexmap = { version = "2.10.0", features = [ "serde" ] } -libc = "0.2.174" +libc = "0.2.175" lru = "0.16.0" -mlua = { version = "0.11.1", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } +mlua = { version = "0.11.2", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } objc = "0.2.7" ordered-float = { version = "5.0.0", features = [ "serde" ] } parking_lot = "0.12.4" diff --git a/cspell.json b/cspell.json index 6cfa834e..175105ee 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded"],"language":"en"} \ No newline at end of file +{"language":"en","flagWords":[],"version":"0.2","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"]} \ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 9018c5b7..78abbfe2 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -123,8 +123,10 @@ parts: # expected binary without much overhead, and works across platforms. magick: plugin: autotools - source: https://imagemagick.org/archive/ImageMagick.tar.gz - source-type: tar + source: https://github.com/ImageMagick/ImageMagick.git + source-type: git + source-tag: 7.1.2-0 + source-depth: 1 stage-packages: - libgomp1 autotools-configure-parameters: diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index 17f997b7..7d1f55f9 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -80,7 +80,7 @@ impl Trigger { Some(match path.as_os_str().rsplit_once(SEP) { Some((p, c)) if p.is_empty() => (Url { loc: MAIN_SEPARATOR_STR.into(), scheme }, c.into()), - Some((p, c)) => (expand_url(Url { loc: p.into(), scheme }).into_owned(), c.into()), + Some((p, c)) => (expand_url(Url { loc: p.into(), scheme }), c.into()), None => (CWD.load().as_ref().clone(), path.into()), }) } diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index b9ed52f2..6d4a3b34 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -78,7 +78,7 @@ impl Cd { Ok(s) => { let Ok(url) = Url::try_from(s).map(expand_url) else { return }; - let Ok(file) = File::new(url.as_ref().clone()).await else { return }; + let Ok(file) = File::new(url.clone()).await else { return }; if file.is_dir() { return MgrProxy::cd(&url); } diff --git a/yazi-actor/src/tasks/open_with.rs b/yazi-actor/src/tasks/open_with.rs index 08384c43..ee73c406 100644 --- a/yazi-actor/src/tasks/open_with.rs +++ b/yazi-actor/src/tasks/open_with.rs @@ -16,7 +16,7 @@ impl Actor for OpenWith { succ!(cx.tasks.process_from_opener( opt.cwd, opt.opener, - opt.targets.into_iter().map(|u| u.into_path().into_os_string()).collect(), + opt.targets.into_iter().map(|u| u.into_path2().into_os_string()).collect(), )); } } diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 38a4d5b1..407f117c 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -26,7 +26,7 @@ yazi-shared = { path = "../yazi-shared", version = "25.6.11" } # External dependencies clap = { workspace = true } -clap_complete = "4.5.55" +clap_complete = "4.5.57" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.5.8" vergen-gitcl = { version = "1.0.8", features = [ "build", "rustc" ] } diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index e0b852a1..8b20d9f5 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::HashSet, path::PathBuf}; +use std::{collections::HashSet, path::PathBuf}; use futures::executor::block_on; use serde::Serialize; @@ -25,15 +25,15 @@ impl Boot { return (vec![CWD.load().as_ref().clone()], vec![UrnBuf::default()]); } - async fn go<'a>(entry: Cow<'a, Url>) -> (Url, UrnBuf) { + async fn go<'a>(entry: Url) -> (Url, UrnBuf) { let Some((parent, child)) = entry.pair() else { - return (entry.into_owned(), UrnBuf::default()); + return (entry, UrnBuf::default()); }; if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) { (parent, child) } else { - (entry.into_owned(), UrnBuf::default()) + (entry, UrnBuf::default()) } } diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index cafe1aae..3bb9f46d 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -41,7 +41,7 @@ yazi-shared = { path = "../yazi-shared", version = "25.6.11" } # External build dependencies anyhow = { workspace = true } clap = { workspace = true } -clap_complete = "4.5.55" +clap_complete = "4.5.57" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.5.8" serde_json = { workspace = true } diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index 2061df6f..55f7247a 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -3,8 +3,8 @@ use std::{borrow::Cow, path::PathBuf}; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; use yazi_codegen::DeserializeOver2; -use yazi_fs::{Xdg, path::expand_path}; -use yazi_shared::{SStr, timestamp_us}; +use yazi_fs::{Xdg, path::expand_url}; +use yazi_shared::{SStr, timestamp_us, url::Url}; use super::PreviewWrap; @@ -53,8 +53,10 @@ impl Preview { self.cache_dir = if self.cache_dir.as_os_str().is_empty() { Xdg::cache_dir() + } else if let Some(p) = expand_url(Url::from(&self.cache_dir)).into_path() { + p } else { - expand_path(&self.cache_dir) + bail!("[preview].cache_dir must be a path within local filesystem."); }; std::fs::create_dir_all(&self.cache_dir).context("Failed to create cache directory")?; diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index 3566f5aa..b660ff02 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -1,9 +1,10 @@ use std::path::PathBuf; -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result, anyhow, bail}; use serde::Deserialize; use yazi_codegen::{DeserializeOver1, DeserializeOver2}; -use yazi_fs::{Xdg, ok_or_not_found, path::expand_path}; +use yazi_fs::{Xdg, ok_or_not_found, path::expand_url}; +use yazi_shared::url::Url; use super::{Filetype, Flavor, Icon}; use crate::Style; @@ -219,8 +220,11 @@ impl Theme { self.icon = self.icon.reshape()?; - self.mgr.syntect_theme = - self.flavor.syntect_path(light).unwrap_or_else(|| expand_path(&self.mgr.syntect_theme)); + self.mgr.syntect_theme = self + .flavor + .syntect_path(light) + .or_else(|| expand_url(Url::from(&self.mgr.syntect_theme)).into_path()) + .ok_or(anyhow!("[mgr].syntect_theme must be a path within local filesystem"))?; Ok(self) } diff --git a/yazi-core/src/mgr/watcher.rs b/yazi-core/src/mgr/watcher.rs index fb3ae06f..90155356 100644 --- a/yazi-core/src/mgr/watcher.rs +++ b/yazi-core/src/mgr/watcher.rs @@ -77,12 +77,6 @@ impl Watcher { // TODO: performance improvement pub fn trigger_dirs(&self, folders: &[&Folder]) { - let todo: Vec<_> = - folders.iter().filter(|&f| f.url.is_regular()).map(|&f| (f.url.to_owned(), f.cha)).collect(); - if todo.is_empty() { - return; - } - async fn go(cwd: Url, cha: Cha) { let Some(cha) = Files::assert_stale(&cwd, cha).await else { return }; @@ -92,9 +86,15 @@ impl Watcher { } } - tokio::spawn(async move { - futures::future::join_all(todo.into_iter().map(|(cwd, cha)| go(cwd, cha))).await; - }); + let futs: Vec<_> = folders + .iter() + .filter(|&f| f.url.scheme.is_builtin()) + .map(|&f| go(f.url.to_owned(), f.cha)) + .collect(); + + if !futs.is_empty() { + tokio::spawn(futures::future::join_all(futs)); + } } async fn fan_in( diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index c498ec4b..0b60ebf8 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -38,10 +38,10 @@ impl Preview { pub fn go_folder(&mut self, file: File, dir: Option, force: bool) { let same = self.same_file(&file, MIME_DIR); - let (wd, cha) = (file.url_owned(), file.cha); + let (wd, cha, builtin) = (file.url_owned(), file.cha, file.url.scheme.is_builtin()); self.go(file, Cow::Borrowed(MIME_DIR), force); - if same { + if same || !builtin { return; } diff --git a/yazi-core/src/tasks/process.rs b/yazi-core/src/tasks/process.rs index 7eeb3cc8..a79930f1 100644 --- a/yazi-core/src/tasks/process.rs +++ b/yazi-core/src/tasks/process.rs @@ -18,7 +18,7 @@ impl Tasks { self.process_from_opener( cwd.clone(), Cow::Borrowed(opener), - args.into_iter().map(|u| u.into_path().into_os_string()).collect(), + args.into_iter().map(|u| u.into_path2().into_os_string()).collect(), ); } } diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index f4afdefd..d11decc7 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -1,7 +1,7 @@ use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref}; use anyhow::Result; -use yazi_shared::url::{Url, Urn, UrnBuf}; +use yazi_shared::url::{Uri, Url, Urn, UrnBuf}; use crate::{cha::Cha, provider}; @@ -56,7 +56,7 @@ impl File { pub fn url_owned(&self) -> Url { self.url.to_owned() } #[inline] - pub fn uri(&self) -> &Urn { self.url.uri() } + pub fn uri(&self) -> &Uri { self.url.uri() } #[inline] pub fn urn(&self) -> &Urn { self.url.urn() } diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index cbf6f668..8e0421ef 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -2,22 +2,10 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}, path::{Path, PathBuf}}; use yazi_shared::url::{Loc, Url}; -use crate::CWD; +use crate::{CWD, path::clean_url}; #[inline] -pub fn expand_url<'a>(url: impl Into>) -> Cow<'a, Url> { - let cow = url.into(); - match expand_url_impl(&cow) { - Cow::Borrowed(_) => cow, - Cow::Owned(url) => url.into(), - } -} - -// FIXME: VFS -#[inline] -pub fn expand_path(p: impl AsRef) -> PathBuf { - expand_url(Url::from(p.as_ref())).into_owned().loc.into_path() -} +pub fn expand_url<'a>(url: impl AsRef) -> Url { clean_url(expand_url_impl(url.as_ref())) } fn expand_url_impl(url: &Url) -> Cow<'_, Url> { let (o_base, o_rest, o_urn) = url.loc.triple(); diff --git a/yazi-fs/src/path/path.rs b/yazi-fs/src/path/path.rs index b994e635..4e1b4a3e 100644 --- a/yazi-fs/src/path/path.rs +++ b/yazi-fs/src/path/path.rs @@ -46,11 +46,9 @@ async fn _unique_name(mut url: Url, append: bool) -> io::Result { if append { name.push(&dot_ext); - name.push("_"); - name.push(i.to_string()); + name.push(format!("_{i}")); } else { - name.push("_"); - name.push(i.to_string()); + name.push(format!("_{i}")); name.push(&dot_ext); } diff --git a/yazi-fs/src/xdg.rs b/yazi-fs/src/xdg.rs index e95541be..c6f11789 100644 --- a/yazi-fs/src/xdg.rs +++ b/yazi-fs/src/xdg.rs @@ -1,12 +1,12 @@ use std::{env, path::PathBuf}; -use crate::path::expand_path; - pub struct Xdg; impl Xdg { pub fn config_dir() -> PathBuf { - if let Some(p) = env::var_os("YAZI_CONFIG_HOME").map(expand_path).filter(|p| p.is_absolute()) { + if let Some(p) = env::var_os("YAZI_CONFIG_HOME").map(PathBuf::from) + && p.is_absolute() + { return p; } diff --git a/yazi-parser/src/mgr/cd.rs b/yazi-parser/src/mgr/cd.rs index 766e7f4c..d73d85bf 100644 --- a/yazi-parser/src/mgr/cd.rs +++ b/yazi-parser/src/mgr/cd.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use yazi_fs::path::expand_url; use yazi_shared::{event::CmdCow, url::Url}; @@ -15,10 +13,8 @@ impl From for CdOpt { fn from(mut c: CmdCow) -> Self { let mut target = c.take_first_url().unwrap_or_default(); - if !c.bool("raw") - && let Cow::Owned(u) = expand_url(&target) - { - target = u; + if !c.bool("raw") { + target = expand_url(&target); } Self { target, interactive: c.bool("interactive"), source: CdSource::Cd } diff --git a/yazi-parser/src/mgr/reveal.rs b/yazi-parser/src/mgr/reveal.rs index 7dd3dcb6..4bb15fa6 100644 --- a/yazi-parser/src/mgr/reveal.rs +++ b/yazi-parser/src/mgr/reveal.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use yazi_fs::path::expand_url; use yazi_shared::{event::CmdCow, url::Url}; @@ -17,10 +15,8 @@ impl From for RevealOpt { fn from(mut c: CmdCow) -> Self { let mut target = c.take_first_url().unwrap_or_default(); - if !c.bool("raw") - && let Cow::Owned(u) = expand_url(&target) - { - target = u; + if !c.bool("raw") { + target = expand_url(&target); } Self { target, source: CdSource::Reveal, no_dummy: c.bool("no-dummy") } diff --git a/yazi-parser/src/mgr/tab_create.rs b/yazi-parser/src/mgr/tab_create.rs index 92065266..3d23a6a3 100644 --- a/yazi-parser/src/mgr/tab_create.rs +++ b/yazi-parser/src/mgr/tab_create.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use yazi_boot::BOOT; use yazi_fs::path::expand_url; @@ -18,10 +16,8 @@ impl From for TabCreateOpt { let Some(mut wd) = c.take_first_url() else { return Self { wd: Some(BOOT.cwds[0].clone()) }; }; - if !c.bool("raw") - && let Cow::Owned(u) = expand_url(&wd) - { - wd = u; + if !c.bool("raw") { + wd = expand_url(&wd); } Self { wd: Some(wd) } } diff --git a/yazi-plugin/preset/plugins/folder.lua b/yazi-plugin/preset/plugins/folder.lua index 6f03ed29..fb3d2dcd 100644 --- a/yazi-plugin/preset/plugins/folder.lua +++ b/yazi-plugin/preset/plugins/folder.lua @@ -4,7 +4,9 @@ local M = {} function M:peek(job) local folder = cx.active.preview.folder - if not folder or folder.cwd ~= job.file.url then + if not folder then + return ya.preview_widget(job, ui.Line("Loading..."):area(job.area):align(ui.Align.CENTER)) + elseif folder.cwd ~= job.file.url then return end diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index fcc1a2ab..6c76bbec 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, str::FromStr}; +use std::str::FromStr; use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, Table, Value}; use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef}; @@ -154,16 +154,13 @@ fn calc_size(lua: &Lua) -> mlua::Result { } fn expand_url(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, value: Value| { + lua.create_function(|_, value: Value| { use yazi_fs::path::expand_url; - match &value { - Value::String(s) => Url::new(expand_url(Url::try_from(s.as_bytes().as_ref())?)).into_lua(lua), - Value::UserData(ud) => match expand_url(&*ud.borrow::()?) { - Cow::Borrowed(_) => Ok(value), - Cow::Owned(u) => Url::new(u).into_lua(lua), - }, - _ => Err("must be a string or a Url".into_lua_err()), - } + Ok(Url::new(match value { + Value::String(s) => expand_url(Url::try_from(s.as_bytes().as_ref())?), + Value::UserData(ud) => expand_url(&*ud.borrow::()?), + _ => Err("must be a string or a Url".into_lua_err())?, + })) }) } diff --git a/yazi-scheduler/src/hooks.rs b/yazi-scheduler/src/hooks.rs index 62ac9f31..b2f0840f 100644 --- a/yazi-scheduler/src/hooks.rs +++ b/yazi-scheduler/src/hooks.rs @@ -5,38 +5,32 @@ use yazi_shared::Id; #[derive(Default)] pub(super) struct Hooks { - inner: HashMap, + inner: HashMap>, } impl Hooks { - #[allow(dead_code)] - pub(super) fn add_sync(&mut self, id: Id, f: F) + #[inline] + pub(super) fn add_async(&mut self, id: Id, f: F) where - F: FnOnce(bool) + Send + Sync + 'static, + F: FnOnce(bool) -> Fut + Send + 'static, + Fut: Future + Send + 'static, { - self.inner.insert(id, Hook::Sync(Box::new(f))); + self.inner.insert(id, Box::new(f)); } - pub(super) fn add_async(&mut self, id: Id, f: F) - where - F: FnOnce(bool) -> BoxFuture<'static, ()> + Send + Sync + 'static, - { - self.inner.insert(id, Hook::Async(Box::new(f))); - } - - pub(super) fn run_or_pop(&mut self, id: Id, cancel: bool) -> Option> { - match self.inner.remove(&id)? { - Hook::Sync(f) => { - f(cancel); - None - } - Hook::Async(f) => Some(f(cancel)), - } - } + #[inline] + pub(super) fn pop(&mut self, id: Id) -> Option> { self.inner.remove(&id) } } // --- Hook -pub(super) enum Hook { - Sync(Box), - Async(Box BoxFuture<'static, ()>) + Send>), +pub trait Hook: Send { + fn call(self: Box, cancel: bool) -> BoxFuture<'static, ()>; +} + +impl Hook for F +where + F: FnOnce(bool) -> Fut + Send, + Fut: Future + Send + 'static, +{ + fn call(self: Box, cancel: bool) -> BoxFuture<'static, ()> { Box::pin((*self)(cancel)) } } diff --git a/yazi-scheduler/src/ongoing.rs b/yazi-scheduler/src/ongoing.rs index 05a61703..8705140a 100644 --- a/yazi-scheduler/src/ongoing.rs +++ b/yazi-scheduler/src/ongoing.rs @@ -1,12 +1,11 @@ use std::collections::HashMap; -use futures::future::BoxFuture; use yazi_config::YAZI; use yazi_parser::app::TasksProgress; use yazi_shared::{Id, Ids}; use super::{Task, TaskStage}; -use crate::{Hooks, TaskKind}; +use crate::{Hook, Hooks, TaskKind}; #[derive(Default)] pub struct Ongoing { @@ -56,7 +55,7 @@ impl Ongoing { #[inline] pub fn is_empty(&self) -> bool { self.len() == 0 } - pub fn try_remove(&mut self, id: Id, stage: TaskStage) -> Option> { + pub fn try_remove(&mut self, id: Id, stage: TaskStage) -> Option> { if let Some(task) = self.get_mut(id) { if stage > task.stage { task.stage = stage; @@ -68,8 +67,8 @@ impl Ongoing { if task.succ < task.total { return None; } - if let Some(fut) = self.hooks.run_or_pop(id, false) { - return Some(fut); + if let Some(hook) = self.hooks.pop(id) { + return Some(hook); } } TaskStage::Hooked => {} diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index c1b0404b..8993831a 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -59,8 +59,8 @@ impl Scheduler { pub fn cancel(&self, id: Id) -> bool { let mut ongoing = self.ongoing.lock(); - if let Some(fut) = ongoing.hooks.run_or_pop(id, true) { - self.micro.try_send(fut, HIGH).ok(); + if let Some(hook) = ongoing.hooks.pop(id) { + self.micro.try_send(hook.call(true), HIGH).ok(); return false; } @@ -86,15 +86,12 @@ impl Scheduler { let ongoing = self.ongoing.clone(); let (from, to) = (from.clone(), to.clone()); - move |canceled: bool| { - async move { - if !canceled { - remove_dir_clean(&from).await; - Pump::push_move(from, to); - } - ongoing.lock().try_remove(id, TaskStage::Hooked); + move |canceled: bool| async move { + if !canceled { + remove_dir_clean(&from).await; + Pump::push_move(from, to); } - .boxed() + ongoing.lock().try_remove(id, TaskStage::Hooked); } }); @@ -172,16 +169,13 @@ impl Scheduler { let target = target.clone(); let ongoing = self.ongoing.clone(); - move |canceled: bool| { - async move { - if !canceled { - provider::remove_dir_all(&target).await.ok(); - MgrProxy::update_tasks(&target); - Pump::push_delete(target); - } - ongoing.lock().try_remove(id, TaskStage::Hooked); + move |canceled: bool| async move { + if !canceled { + provider::remove_dir_all(&target).await.ok(); + MgrProxy::update_tasks(&target); + Pump::push_delete(target); } - .boxed() + ongoing.lock().try_remove(id, TaskStage::Hooked); } }); @@ -201,15 +195,12 @@ impl Scheduler { let target = target.clone(); let ongoing = self.ongoing.clone(); - move |canceled: bool| { - async move { - if !canceled { - MgrProxy::update_tasks(&target); - Pump::push_trash(target); - } - ongoing.lock().try_remove(id, TaskStage::Hooked); + move |canceled: bool| async move { + if !canceled { + MgrProxy::update_tasks(&target); + Pump::push_trash(target); } - .boxed() + ongoing.lock().try_remove(id, TaskStage::Hooked); } }); @@ -288,18 +279,15 @@ impl Scheduler { let id = ongoing.add(TaskKind::User, name); ongoing.hooks.add_async(id, { let ongoing = self.ongoing.clone(); - move |canceled: bool| { - async move { - if canceled { - cancel_tx.send(()).await.ok(); - cancel_tx.closed().await; - } - if let Some(tx) = done { - tx.send(()).ok(); - } - ongoing.lock().try_remove(id, TaskStage::Hooked); + move |canceled: bool| async move { + if canceled { + cancel_tx.send(()).await.ok(); + cancel_tx.closed().await; } - .boxed() + if let Some(tx) = done { + tx.send(()).ok(); + } + ongoing.lock().try_remove(id, TaskStage::Hooked); } }); @@ -388,14 +376,14 @@ impl Scheduler { task.processed += processed; } if succ > 0 - && let Some(fut) = ongoing.try_remove(id, TaskStage::Pending) + && let Some(hook) = ongoing.try_remove(id, TaskStage::Pending) { - micro.try_send(fut, LOW).ok(); + micro.try_send(hook.call(false), LOW).ok(); } } TaskProg::Succ(id) => { - if let Some(fut) = ongoing.lock().try_remove(id, TaskStage::Dispatched) { - micro.try_send(fut, LOW).ok(); + if let Some(hook) = ongoing.lock().try_remove(id, TaskStage::Dispatched) { + micro.try_send(hook.call(false), LOW).ok(); } } TaskProg::Fail(id, reason) => { diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index 1c1f63d3..ae8a1d36 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -1,4 +1,4 @@ -use std::fmt::{self, Display}; +use std::{fmt::{self, Display}, ops::Not}; use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; @@ -23,23 +23,36 @@ impl<'a> Encode<'a> { percent_encode(s.as_bytes(), SET) } - #[inline] - fn urn(loc: &'a Loc) -> impl Display { - struct D(usize, usize); + fn urn(&self) -> impl Display { + struct D<'a>(&'a Encode<'a>); - impl Display for D { + impl Display for D<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let (uri, urn) = (self.0, self.1); - match (uri != 0, urn != 0) { - (true, true) => write!(f, ":{uri}:{urn}"), - (true, false) => write!(f, ":{uri}"), - (false, true) => write!(f, "::{urn}"), - (false, false) => Ok(()), + macro_rules! w { + ($default_uri:expr, $default_urn:expr) => {{ + let uri = self.0.loc.uri().count(); + let urn = self.0.loc.urn().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.scheme { + Scheme::Regular => Ok(()), + Scheme::Search(_) | Scheme::Archive(_) => w!(0, 0), + Scheme::Sftp(_) => w!( + self.0.loc.as_os_str().is_empty().not() as usize, + self.0.loc.file_name().is_some() as usize + ), } } } - D(loc.uri().count(), loc.urn().count()) + D(self) } } @@ -47,9 +60,9 @@ impl Display for Encode<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.scheme { Scheme::Regular => write!(f, "regular://"), - Scheme::Search(d) => write!(f, "search://{}{}/", Self::domain(d), Self::urn(self.loc)), - Scheme::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), Self::urn(self.loc)), - Scheme::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), Self::urn(self.loc)), + Scheme::Search(d) => write!(f, "search://{}{}/", Self::domain(d), self.urn()), + Scheme::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), self.urn()), + Scheme::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), self.urn()), } } } @@ -64,6 +77,10 @@ impl<'a> From<&'a Url> for EncodeTilded<'a> { fn from(url: &'a Url) -> Self { Self { loc: &url.loc, scheme: &url.scheme } } } +impl<'a> From<&'a EncodeTilded<'a>> for Encode<'a> { + fn from(value: &'a EncodeTilded<'a>) -> Self { Self::new(value.loc, value.scheme) } +} + impl Display for EncodeTilded<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use Encode as E; @@ -71,9 +88,9 @@ impl Display for EncodeTilded<'_> { let loc = percent_encode(self.loc.as_os_str().as_encoded_bytes(), CONTROLS); match self.scheme { Scheme::Regular => write!(f, "regular~://{loc}"), - Scheme::Search(d) => write!(f, "search~://{}{}/{loc}", E::domain(d), E::urn(self.loc)), - Scheme::Archive(d) => write!(f, "archive~://{}{}/{loc}", E::domain(d), E::urn(self.loc)), - Scheme::Sftp(d) => write!(f, "sftp~://{}{}/{loc}", E::domain(d), E::urn(self.loc)), + Scheme::Search(d) => write!(f, "search~://{}{}/{loc}", E::domain(d), E::urn(&self.into())), + Scheme::Archive(d) => write!(f, "archive~://{}{}/{loc}", E::domain(d), E::urn(&self.into())), + Scheme::Sftp(d) => write!(f, "sftp~://{}{}/{loc}", E::domain(d), E::urn(&self.into())), } } } diff --git a/yazi-shared/src/url/loc.rs b/yazi-shared/src/url/loc.rs index e781ac22..c67e2d84 100644 --- a/yazi-shared/src/url/loc.rs +++ b/yazi-shared/src/url/loc.rs @@ -1,8 +1,8 @@ -use std::{borrow::Cow, cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; +use std::{borrow::Cow, cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, mem, ops::Deref, path::{Path, PathBuf}}; use anyhow::{Result, bail}; -use crate::url::{Urn, UrnBuf}; +use crate::url::{Uri, Urn, UrnBuf}; #[derive(Clone, Default)] pub struct Loc { @@ -143,8 +143,8 @@ impl Loc { } #[inline] - pub fn uri(&self) -> &Urn { - Urn::new(unsafe { + pub fn uri(&self) -> &Uri { + Uri::new(unsafe { OsStr::from_encoded_bytes_unchecked( self.bytes().get_unchecked(self.bytes().len() - self.uri..), ) @@ -167,19 +167,28 @@ impl Loc { pub fn name(&self) -> &OsStr { self.inner.file_name().unwrap_or(OsStr::new("")) } pub fn set_name(&mut self, name: impl AsRef) { - let (old, new) = (self.name(), name.as_ref()); - if old == new { + let old = self.bytes().len(); + self.mutate(|path| path.set_file_name(name)); + + let new = self.bytes().len(); + if new == old { return; } - if old.len() > new.len() { - let n = old.len() - new.len(); - (self.uri, self.urn) = (self.uri - n, self.urn - n); - } else { - let n = new.len() - old.len(); - (self.uri, self.urn) = (self.uri + n, self.urn + n); + if self.uri != 0 { + if new > old { + self.uri += new - old; + } else { + self.uri -= old - new; + } + } + if self.urn != 0 { + if new > old { + self.urn += new - old; + } else { + self.urn -= old - new; + } } - self.inner.set_file_name(new); } #[inline] @@ -240,11 +249,19 @@ impl Loc { #[inline] fn bytes(&self) -> &[u8] { self.inner.as_os_str().as_encoded_bytes() } + + #[inline] + fn mutate(&mut self, f: F) { + let mut inner = mem::take(&mut self.inner); + f(&mut inner); + self.inner = Self::from(inner).inner; + } } #[cfg(test)] mod tests { use super::*; + use crate::url::Url; #[test] fn test_new() { @@ -318,22 +335,34 @@ mod tests { #[test] fn test_set_name() -> Result<()> { - const S: char = std::path::MAIN_SEPARATOR; + let cases = [ + // Regular + ("/", "a", "regular:///a"), + ("/a/b", "c", "regular:///a/c"), + // Archive + ("archive:////", "a.zip", "archive:////a.zip"), + ("archive:////a.zip/b", "c", "archive:////a.zip/c"), + ("archive://:2//a.zip/b", "c", "archive://:2//a.zip/c"), + ("archive://:2:1//a.zip/b", "c", "archive://:2:1//a.zip/c"), + // Empty + ("/a", "", "regular:///"), + ("archive:////a.zip", "", "archive:////"), + ("archive:////a.zip/b", "", "archive:////a.zip"), + ("archive://:1:1//a.zip", "", "archive:////"), + ("archive://:2//a.zip/b", "", "archive://:1//a.zip"), + ("archive://:2:2//a.zip/b", "", "archive://:1:1//a.zip"), + ]; - let mut loc = Loc::with("/root/code/foo/".into(), 2, 1)?; - assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo")); - assert_eq!(loc.name(), OsStr::new("foo")); - assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); + for (input, name, expected) in cases { + let mut a: Url = input.parse()?; + let b: Url = expected.parse()?; + a.set_name(name); + assert_eq!( + (a.name(), format!("{a:?}").replace(r"\", "/")), + (b.name(), expected.replace(r"\", "/")) + ); + } - loc.set_name("bar.txt"); - assert_eq!(loc.uri().as_os_str(), OsString::from(format!("code{S}bar.txt"))); - assert_eq!(loc.name(), OsStr::new("bar.txt")); - assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); - - loc.set_name("baz"); - assert_eq!(loc.uri().as_os_str(), OsString::from(format!("code{S}baz"))); - assert_eq!(loc.name(), OsStr::new("baz")); - assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); Ok(()) } } diff --git a/yazi-shared/src/url/mod.rs b/yazi-shared/src/url/mod.rs index 62cf9b0f..edf2010b 100644 --- a/yazi-shared/src/url/mod.rs +++ b/yazi-shared/src/url/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(component cov display encode loc scheme url urn); +yazi_macro::mod_flat!(component cov display encode loc scheme uri url urn); diff --git a/yazi-shared/src/url/scheme.rs b/yazi-shared/src/url/scheme.rs index bc33eb27..9b5bba8f 100644 --- a/yazi-shared/src/url/scheme.rs +++ b/yazi-shared/src/url/scheme.rs @@ -1,6 +1,6 @@ -use std::borrow::Cow; +use std::{borrow::Cow, ops::Not, path::Path}; -use anyhow::{Result, bail}; +use anyhow::{Result, bail, ensure}; use percent_encoding::percent_decode; use crate::BytesExt; @@ -39,9 +39,9 @@ impl Scheme { pub(super) fn parse( bytes: &[u8], skip: &mut usize, - ) -> Result<(Self, bool, Option<(usize, usize)>)> { + ) -> Result<(Self, bool, Option, Option)> { let Some((mut protocol, rest)) = bytes.split_by_seq(b"://") else { - return Ok((Self::Regular, false, None)); + return Ok((Self::Regular, false, None, None)); }; // Advance to the beginning of the path @@ -53,24 +53,24 @@ impl Scheme { protocol = &protocol[..protocol.len() - 1]; } - let (scheme, port) = match protocol { - b"regular" => (Self::Regular, None), + let (scheme, uri, urn) = match protocol { + b"regular" => (Self::Regular, None, None), b"search" => { let (domain, uri, urn) = Self::decode_param(rest, skip)?; - (Self::Search(domain), Some((uri, urn))) + (Self::Search(domain), uri, urn) } b"archive" => { let (domain, uri, urn) = Self::decode_param(rest, skip)?; - (Self::Archive(domain), Some((uri, urn))) + (Self::Archive(domain), uri, urn) } b"sftp" => { let (domain, uri, urn) = Self::decode_param(rest, skip)?; - (Self::Sftp(domain), Some((uri, urn))) + (Self::Sftp(domain), uri, urn) } _ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)), }; - Ok((scheme, tilde, port)) + Ok((scheme, tilde, uri, urn)) } #[inline] @@ -89,6 +89,14 @@ impl Scheme { if self.is_virtual() || other.is_virtual() { self == other } else { true } } + #[inline] + pub fn is_builtin(&self) -> bool { + match self { + Self::Regular | Self::Search(_) | Self::Sftp(_) => true, + Self::Archive(_) => false, + } + } + #[inline] pub fn is_virtual(&self) -> bool { match self { @@ -97,12 +105,15 @@ impl Scheme { } } - fn decode_param(bytes: &[u8], skip: &mut usize) -> Result<(String, usize, usize)> { + fn decode_param( + bytes: &[u8], + skip: &mut usize, + ) -> Result<(String, Option, Option)> { let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count(); let slash = bytes.get(len).is_some_and(|&b| b == b'/'); *skip += len + slash as usize; - let (uri, urn) = Self::decode_port(&bytes[..len], &mut len)?; + let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?; let domain = match Cow::from(percent_decode(&bytes[..len])) { Cow::Borrowed(b) => str::from_utf8(b)?.to_owned(), Cow::Owned(b) => String::from_utf8(b)?, @@ -111,19 +122,47 @@ impl Scheme { Ok((domain, uri, urn)) } - fn decode_port(bytes: &[u8], skip: &mut usize) -> anyhow::Result<(usize, usize)> { - let Some(a_idx) = bytes.iter().rposition(|&b| b == b':') else { return Ok((0, 0)) }; + fn decode_ports(bytes: &[u8], skip: &mut usize) -> Result<(Option, Option)> { + let Some(a_idx) = bytes.iter().rposition(|&b| b == b':') else { return Ok((None, None)) }; let a_len = bytes.len() - a_idx; *skip -= a_len; - let a = if a_len == 1 { 0 } else { str::from_utf8(&bytes[a_idx + 1..])?.parse()? }; + let a = if a_len == 1 { None } else { Some(str::from_utf8(&bytes[a_idx + 1..])?.parse()?) }; - let Some(b_idx) = bytes[..a_idx].iter().rposition(|&b| b == b':') else { return Ok((a, 0)) }; + let Some(b_idx) = bytes[..a_idx].iter().rposition(|&b| b == b':') else { + return Ok((a, None)); + }; let b_len = bytes[..a_idx].len() - b_idx; *skip -= b_len; - let b = if b_len == 1 { 0 } else { str::from_utf8(&bytes[b_idx + 1..a_idx])?.parse()? }; + let b = + if b_len == 1 { None } else { Some(str::from_utf8(&bytes[b_idx + 1..a_idx])?.parse()?) }; Ok((b, a)) } + + pub(super) fn normalize_ports( + &self, + uri: Option, + urn: Option, + path: &Path, + ) -> Result> { + Ok(match self { + Scheme::Regular => { + ensure!(uri.is_none() && urn.is_none(), "Regular scheme cannot have ports"); + None + } + Scheme::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)) + } + Scheme::Archive(_) => Some((uri.unwrap_or(0), urn.unwrap_or(0))), + Scheme::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)) + } + }) + } } #[cfg(test)] @@ -132,27 +171,27 @@ mod tests { #[test] fn test_decode_port() -> Result<()> { - fn assert(s: &str, uri: usize, urn: usize, len: usize) -> Result<()> { + fn assert(s: &str, len: usize, uri: Option, urn: Option) -> Result<()> { let mut n = usize::MAX; - let port = Scheme::decode_port(s.as_bytes(), &mut n)?; - assert_eq!((port.0, port.1, usize::MAX - n), (uri, urn, len)); + let port = Scheme::decode_ports(s.as_bytes(), &mut n)?; + assert_eq!((usize::MAX - n, port.0, port.1), (len, uri, urn)); Ok(()) } // Zeros - assert("", 0, 0, 0)?; - assert(":", 0, 0, 1)?; - assert("::", 0, 0, 2)?; + assert("", 0, None, None)?; + assert(":", 1, None, None)?; + assert("::", 2, None, None)?; // URI - assert(":2", 2, 0, 2)?; - assert(":2:", 2, 0, 3)?; - assert(":22:", 22, 0, 4)?; + assert(":2", 2, Some(2), None)?; + assert(":2:", 3, Some(2), None)?; + assert(":22:", 4, Some(22), None)?; // URN - assert("::1", 0, 1, 3)?; - assert(":2:1", 2, 1, 4)?; - assert(":22:11", 22, 11, 6)?; + assert("::1", 3, None, Some(1))?; + assert(":2:1", 4, Some(2), Some(1))?; + assert(":22:11", 6, Some(22), Some(11))?; Ok(()) } } diff --git a/yazi-shared/src/url/uri.rs b/yazi-shared/src/url/uri.rs new file mode 100644 index 00000000..efe786c5 --- /dev/null +++ b/yazi-shared/src/url/uri.rs @@ -0,0 +1,27 @@ +use std::{ops::Deref, path::{Component, Path}}; + +#[derive(Debug, Eq, PartialEq, Hash)] +#[repr(transparent)] +pub struct Uri(Path); + +impl Uri { + #[inline] + pub fn new + ?Sized>(p: &T) -> &Self { + unsafe { &*(p.as_ref() as *const Path as *const Self) } + } + + #[inline] + pub fn count(&self) -> usize { self.0.components().count() } + + #[inline] + pub fn nth(&self, n: usize) -> Option> { self.0.components().nth(n) } + + #[inline] + pub fn is_empty(&self) -> bool { self.0.as_os_str().is_empty() } +} + +impl Deref for Uri { + type Target = Path; + + fn deref(&self) -> &Self::Target { &self.0 } +} diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 83d0c32c..da383700 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -197,6 +197,11 @@ impl Url { Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual()) } + #[inline] + pub fn into_path(self) -> Option { + Some(self.loc.into_path()).filter(|_| !self.scheme.is_virtual()) + } + #[inline] pub fn set_name(&mut self, name: impl AsRef) { self.loc.set_name(name); } @@ -214,7 +219,7 @@ impl Url { pub fn parse(bytes: &[u8]) -> Result<(Scheme, PathBuf, Option<(usize, usize)>)> { let mut skip = 0; - let (scheme, tilde, port) = Scheme::parse(bytes, &mut skip)?; + let (scheme, tilde, uri, urn) = Scheme::parse(bytes, &mut skip)?; let rest = if tilde { Cow::from(percent_decode(&bytes[skip..])).into_os_str()? @@ -227,7 +232,9 @@ impl Url { Cow::Owned(s) => PathBuf::from(s), }; - Ok((scheme, path, port)) + let ports = scheme.normalize_ports(uri, urn, &path)?; + + Ok((scheme, path, ports)) } } @@ -273,7 +280,7 @@ impl Url { // FIXME: remove #[inline] - pub fn into_path(self) -> PathBuf { self.loc.into_path() } + pub fn into_path2(self) -> PathBuf { self.loc.into_path() } } impl Debug for Url { @@ -323,8 +330,8 @@ mod tests { ("archive://:2:1//a/b.zip/c/d", "e/f", "archive://:4:1//a/b.zip/c/d/e/f"), ("archive://:2:2//a/b.zip/c/d", "e/f", "archive://:4:1//a/b.zip/c/d/e/f"), // SFTP - ("sftp://remote//a", "b/c", "sftp://remote:1:1//a/b/c"), - ("sftp://remote:1:1//a/b/c", "d/e", "sftp://remote:1:1//a/b/c/d/e"), + ("sftp://remote//a", "b/c", "sftp://remote//a/b/c"), + ("sftp://remote:1:1//a/b/c", "d/e", "sftp://remote//a/b/c/d/e"), // Relative ("search://kw", "b/c", "search://kw:2:2/b/c"), ("search://kw/", "b/c", "search://kw:2:2/b/c"), @@ -356,8 +363,8 @@ mod tests { ("archive://:1:1//a/b.zip/c", Some("archive:////a/b.zip")), ("archive:////a/b.zip", Some("regular:///a")), // SFTP - ("sftp://remote:1:1//a/b", Some("sftp://remote:1:1//a")), - ("sftp://remote:1:1//a", Some("sftp://remote:1//")), + ("sftp://remote:1:1//a/b", Some("sftp://remote//a")), + ("sftp://remote:1:1//a", Some("sftp://remote//")), ("sftp://remote:1//", None), ("sftp://remote//", None), // Relative diff --git a/yazi-shared/src/url/urn.rs b/yazi-shared/src/url/urn.rs index 303db99c..ebea5339 100644 --- a/yazi-shared/src/url/urn.rs +++ b/yazi-shared/src/url/urn.rs @@ -1,4 +1,4 @@ -use std::{borrow::{Borrow, Cow}, ffi::OsStr, ops::Deref, path::{Component, Path, PathBuf}}; +use std::{borrow::{Borrow, Cow}, ffi::OsStr, ops::Deref, path::{Path, PathBuf}}; use serde::Serialize; @@ -18,9 +18,6 @@ impl Urn { #[inline] pub fn count(&self) -> usize { self.0.components().count() } - #[inline] - pub fn nth(&self, n: usize) -> Option> { self.0.components().nth(n) } - #[inline] pub fn encoded_bytes(&self) -> &[u8] { self.0.as_os_str().as_encoded_bytes() } @@ -30,9 +27,6 @@ impl Urn { use std::os::unix::ffi::OsStrExt; self.name().is_some_and(|s| s.as_bytes().starts_with(b".")) } - - #[inline] - pub fn is_empty(&self) -> bool { self.0.as_os_str().is_empty() } } impl Deref for Urn {