feat: introduce the concept of Uri (#3049)

This commit is contained in:
三咲雅 misaki masa 2025-08-13 01:15:49 +08:00 committed by sxyazi
parent a9bfeeeaad
commit 1bfcbc1664
No known key found for this signature in database
35 changed files with 356 additions and 281 deletions

View file

@ -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/')

64
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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"}
{"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"]}

View file

@ -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:

View file

@ -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()),
})
}

View file

@ -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);
}

View file

@ -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(),
));
}
}

View file

@ -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" ] }

View file

@ -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())
}
}

View file

@ -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 }

View file

@ -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")?;

View file

@ -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)
}

View file

@ -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(

View file

@ -38,10 +38,10 @@ impl Preview {
pub fn go_folder(&mut self, file: File, dir: Option<Cha>, 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;
}

View file

@ -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(),
);
}
}

View file

@ -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() }

View file

@ -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>>) -> 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<Path>) -> PathBuf {
expand_url(Url::from(p.as_ref())).into_owned().loc.into_path()
}
pub fn expand_url<'a>(url: impl AsRef<Url>) -> 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();

View file

@ -46,11 +46,9 @@ async fn _unique_name(mut url: Url, append: bool) -> io::Result<Url> {
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);
}

View file

@ -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;
}

View file

@ -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<CmdCow> 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 }

View file

@ -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<CmdCow> 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") }

View file

@ -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<CmdCow> 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) }
}

View file

@ -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

View file

@ -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<Function> {
}
fn expand_url(lua: &Lua) -> mlua::Result<Function> {
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::<yazi_binding::Url>()?) {
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::<yazi_binding::Url>()?),
_ => Err("must be a string or a Url".into_lua_err())?,
}))
})
}

View file

@ -5,38 +5,32 @@ use yazi_shared::Id;
#[derive(Default)]
pub(super) struct Hooks {
inner: HashMap<Id, Hook>,
inner: HashMap<Id, Box<dyn Hook>>,
}
impl Hooks {
#[allow(dead_code)]
pub(super) fn add_sync<F>(&mut self, id: Id, f: F)
#[inline]
pub(super) fn add_async<F, Fut>(&mut self, id: Id, f: F)
where
F: FnOnce(bool) + Send + Sync + 'static,
F: FnOnce(bool) -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.inner.insert(id, Hook::Sync(Box::new(f)));
self.inner.insert(id, Box::new(f));
}
pub(super) fn add_async<F>(&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<BoxFuture<'static, ()>> {
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<Box<dyn Hook>> { self.inner.remove(&id) }
}
// --- Hook
pub(super) enum Hook {
Sync(Box<dyn FnOnce(bool) + Send>),
Async(Box<dyn (FnOnce(bool) -> BoxFuture<'static, ()>) + Send>),
pub trait Hook: Send {
fn call(self: Box<Self>, cancel: bool) -> BoxFuture<'static, ()>;
}
impl<F, Fut> Hook for F
where
F: FnOnce(bool) -> Fut + Send,
Fut: Future<Output = ()> + Send + 'static,
{
fn call(self: Box<Self>, cancel: bool) -> BoxFuture<'static, ()> { Box::pin((*self)(cancel)) }
}

View file

@ -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<BoxFuture<'static, ()>> {
pub fn try_remove(&mut self, id: Id, stage: TaskStage) -> Option<Box<dyn Hook>> {
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 => {}

View file

@ -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) => {

View file

@ -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())),
}
}
}

View file

@ -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<OsStr>) {
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<F: FnOnce(&mut PathBuf)>(&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(())
}
}

View file

@ -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);

View file

@ -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<usize>, Option<usize>)> {
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<usize>, Option<usize>)> {
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<usize>, Option<usize>)> {
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<usize>,
urn: Option<usize>,
path: &Path,
) -> Result<Option<(usize, usize)>> {
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<usize>, urn: Option<usize>) -> 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(())
}
}

View file

@ -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<T: AsRef<Path> + ?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<Component<'_>> { 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 }
}

View file

@ -197,6 +197,11 @@ impl Url {
Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual())
}
#[inline]
pub fn into_path(self) -> Option<PathBuf> {
Some(self.loc.into_path()).filter(|_| !self.scheme.is_virtual())
}
#[inline]
pub fn set_name(&mut self, name: impl AsRef<OsStr>) { 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

View file

@ -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<Component<'_>> { 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 {