fix: unify path expansion and absolutization behavior (#3438)

This commit is contained in:
三咲雅 misaki masa 2025-12-17 23:00:05 +08:00 committed by GitHub
parent 80b3b8465d
commit e9eb8b9155
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 453 additions and 230 deletions

View file

@ -59,7 +59,7 @@ jobs:
run: ./scripts/build.sh ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.target }}
path: |
@ -105,7 +105,7 @@ jobs:
Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip"
- name: Upload artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.target }}
path: yazi-${{ matrix.target }}.zip
@ -133,7 +133,7 @@ jobs:
run: ./scripts/build.sh ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.target }}
path: |
@ -155,7 +155,7 @@ jobs:
fetch-depth: 0
- name: Setup LXD
uses: canonical/setup-lxd@v0.1.3
uses: canonical/setup-lxd@v1
- name: Setup snapcraft
run: sudo snap install --classic snapcraft
@ -167,7 +167,7 @@ jobs:
run: mv yazi_*.snap yazi-${{ matrix.arch }}.snap
- name: Upload artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: snap-${{ matrix.arch }}
path: yazi-${{ matrix.arch }}.snap
@ -176,7 +176,7 @@ jobs:
runs-on: ubuntu-latest
needs: [build-snap]
steps:
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
pattern: snap-*
merge-multiple: true
@ -205,7 +205,7 @@ jobs:
runs-on: ubuntu-latest
needs: [build-unix, build-windows, build-musl, build-snap]
steps:
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
merge-multiple: true
@ -234,7 +234,7 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
merge-multiple: true

View file

@ -16,7 +16,7 @@ jobs:
pull-requests: write
discussions: write
steps:
- uses: dessant/lock-threads@v5
- uses: dessant/lock-threads@v6
with:
issue-inactive-days: "30"
issue-comment: >

View file

@ -11,7 +11,7 @@ jobs:
winget:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
merge-multiple: true

29
Cargo.lock generated
View file

@ -63,9 +63,9 @@ dependencies = [
[[package]]
name = "aligned"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923"
checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
dependencies = [
"as-slice",
]
@ -431,9 +431,9 @@ checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
[[package]]
name = "bumpalo"
version = "3.19.0"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "by_address"
@ -2002,9 +2002,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50"
dependencies = [
"bitflags 2.10.0",
"libc",
@ -2081,9 +2081,9 @@ dependencies = [
[[package]]
name = "luajit-src"
version = "210.6.4+e17ee83"
version = "210.6.5+7152e15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a0ceb2a395ffa403a863adcf365e82cc8d8338ac7f5f949b9df5ca3de251e1"
checksum = "29e64ac463f01a02ee793423f9b351369cf244c5ee8bb9e2729a75b2eb404181"
dependencies = [
"cc",
"which",
@ -2199,9 +2199,9 @@ dependencies = [
[[package]]
name = "moxcms"
version = "0.7.10"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
@ -4105,9 +4105,9 @@ dependencies = [
[[package]]
name = "uzers"
version = "0.12.1"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4df81ff504e7d82ad53e95ed1ad5b72103c11253f39238bcc0235b90768a97dd"
checksum = "0b8275fb1afee25b4111d2dc8b5c505dbbc4afd0b990cb96deb2d88bff8be18d"
dependencies = [
"libc",
"log",
@ -5008,6 +5008,7 @@ dependencies = [
"yazi-fs",
"yazi-macro",
"yazi-shared",
"yazi-vfs",
]
[[package]]
@ -5272,9 +5273,9 @@ dependencies = [
[[package]]
name = "zune-jpeg"
version = "0.5.5"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e"
checksum = "51d915729b0e7d5fe35c2f294c5dc10b30207cc637920e5b59077bfa3da63f28"
dependencies = [
"zune-core",
]

View file

@ -64,7 +64,7 @@ tracing = { version = "0.1.43", features = [ "max_level_debug", "rel
twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
typed-path = "0.12.0"
unicode-width = { version = "0.2.0", default-features = false }
uzers = "0.12.1"
uzers = "0.12.2"
[workspace.lints.clippy]
format_push_string = "warn"

View file

@ -8,27 +8,37 @@
"name": "validate-form",
"version": "1.0.0",
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/core": "^2.0.1",
"@actions/github": "^6.0.0"
}
},
"node_modules/@actions/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.1.tgz",
"integrity": "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==",
"license": "MIT",
"dependencies": {
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
"@actions/exec": "^2.0.0",
"@actions/http-client": "^3.0.0"
}
},
"node_modules/@actions/core/node_modules/@actions/http-client": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz",
"integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.28.5"
}
},
"node_modules/@actions/exec": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz",
"integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==",
"license": "MIT",
"dependencies": {
"@actions/io": "^1.0.1"
"@actions/io": "^2.0.0"
}
},
"node_modules/@actions/github": {
@ -57,9 +67,9 @@
}
},
"node_modules/@actions/io": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz",
"integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==",
"license": "MIT"
},
"node_modules/@fastify/busboy": {
@ -85,6 +95,7 @@
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz",
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",

View file

@ -6,7 +6,7 @@
},
"private": true,
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/core": "^2.0.1",
"@actions/github": "^6.0.0"
}
}

View file

@ -1,11 +1,11 @@
use std::mem;
use anyhow::Result;
use yazi_fs::{CWD, path::clean_url, provider::{DirReader, FileHolder}};
use yazi_fs::{path::clean_url, provider::{DirReader, FileHolder}};
use yazi_macro::{act, render, succ};
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
use yazi_proxy::CmpProxy;
use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{AsPath, PathBufDyn, PathCow, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}};
use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}};
use yazi_vfs::provider;
use crate::{Actor, Ctx};
@ -67,37 +67,35 @@ impl Actor for Trigger {
impl Trigger {
fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> {
let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?;
let scheme = scheme.zeroed();
if scheme.is_local() && path.as_strand() == "~" {
return None; // We don't autocomplete a `~`, but `~/`
}
let cwd = CWD.load();
let abs = if !path.is_absolute() && cwd.scheme().covariant(&scheme) {
cwd.loc().try_join(&path).ok()?.into()
} else {
PathCow::from(&path)
};
let sep = if cfg!(windows) {
AnyAsciiChar::new(b"/\\").unwrap()
} else {
AnyAsciiChar::new(b"/").unwrap()
};
let child = path.rsplit_pred(sep).map_or(path.as_path(), |(_, c)| c);
let parent =
PathDyn::with(scheme.kind(), abs.encoded_bytes().strip_suffix(child.encoded_bytes())?)
.ok()?;
let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?;
let scheme = scheme.zeroed();
Some((clean_url(UrlCow::try_from((scheme, parent)).ok()?), child.into()))
// We don't complete a `~`, but `~/`
if scheme.is_local() && path.as_strand() == "~" {
return None;
}
// Child
let child = path.rsplit_pred(sep).map_or(path.as_path(), |(_, c)| c).to_owned();
// Parent
let url = UrlCow::try_from((scheme.clone().zeroed(), path)).ok()?;
let abs = if let Some(u) = provider::try_absolute(&url) { u } else { url };
let parent = abs.loc().try_strip_suffix(&child).ok()?;
Some((clean_url(UrlCow::try_from((scheme, parent)).ok()?), child))
}
}
#[cfg(test)]
mod tests {
use yazi_fs::CWD;
use yazi_shared::url::UrlLike;
use super::*;

View file

@ -1,4 +1,4 @@
use std::ops::Deref;
use std::{ops::Deref, ptr};
use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods, Value};
use yazi_binding::{Style, cached_field};
@ -73,9 +73,7 @@ impl UserData for File {
fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1));
fields.add_field_method_get("is_hovered", |_, me| Ok(me.idx == me.folder.cursor));
fields.add_field_method_get("in_current", |_, me| {
Ok(&*me.folder as *const _ == &me.tab.current as *const _)
});
fields.add_field_method_get("in_current", |_, me| Ok(ptr::eq(&*me.folder, &me.tab.current)));
fields.add_field_method_get("in_preview", |_, me| {
Ok(me.idx == me.folder.cursor && me.tab.hovered().is_some_and(|f| f.url == me.folder.url))
});

View file

@ -25,7 +25,7 @@ impl Actor for TabCreate {
}
let mut tab = Tab::default();
let (cd, url) = if let Some(wd) = opt.wd {
let (cd, url) = if let Some(wd) = opt.url {
(true, wd.into_owned())
} else if let Some(h) = cx.hovered() {
tab.pref = cx.tab().pref.clone();

View file

@ -2,7 +2,7 @@ use std::path::PathBuf;
use futures::executor::block_on;
use hashbrown::HashSet;
use yazi_fs::{CWD, Xdg, path::expand_url};
use yazi_fs::{CWD, Xdg, path::clean_url};
use yazi_shared::{strand::StrandBuf, url::{UrlBuf, UrlLike}};
use yazi_vfs::provider;
@ -27,7 +27,7 @@ impl Boot {
}
async fn go(entry: &UrlBuf) -> (UrlBuf, StrandBuf) {
let mut entry = expand_url(entry);
let mut entry = clean_url(entry);
if let Ok(u) = provider::absolute(&entry).await
&& u.is_owned()

View file

@ -1,6 +1,6 @@
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which vfs);
yazi_macro::mod_flat!(icon layout pattern platform preset priority style yazi);
yazi_macro::mod_flat!(icon layout pattern platform preset priority style utils yazi);
use std::io::{Read, Write};

View file

@ -3,10 +3,11 @@ 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_url};
use yazi_fs::Xdg;
use yazi_shared::{SStr, timestamp_us};
use super::PreviewWrap;
use crate::normalize_path;
#[rustfmt::skip]
const TABS: &[&str] = &["", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "];
@ -50,10 +51,10 @@ impl Preview {
self.cache_dir = if self.cache_dir.as_os_str().is_empty() {
Xdg::cache_dir().to_owned()
} else if let Some(p) = expand_url(self.cache_dir).into_local() {
} else if let Some(p) = normalize_path(self.cache_dir) {
p
} else {
bail!("[preview].cache_dir must be a path within local filesystem.");
bail!("[preview].cache_dir must be either empty or an absolute path.");
};
std::fs::create_dir_all(&self.cache_dir)

View file

@ -25,8 +25,8 @@ impl From<ratatui::style::Style> for Style {
let all = value.add_modifier - sub;
Self {
fg: value.fg.map(Into::into),
bg: value.bg.map(Into::into),
fg: value.fg,
bg: value.bg,
bold: if all.contains(M::BOLD) { Some(true) } else if sub.contains(M::BOLD) { Some(false) } else { None },
dim: if all.contains(M::DIM) { Some(true) } else if sub.contains(M::DIM) { Some(false) } else { None },
italic: if all.contains(M::ITALIC) { Some(true) } else if sub.contains(M::ITALIC) { Some(false) } else { None },
@ -75,8 +75,8 @@ impl From<Style> for ratatui::style::Style {
}
Self {
fg: value.fg.map(Into::into),
bg: value.bg.map(Into::into),
fg: value.fg,
bg: value.bg,
underline_color: None,
add_modifier: add,
sub_modifier: sub,

View file

@ -3,11 +3,10 @@ use std::path::PathBuf;
use anyhow::{Context, Result, anyhow, bail};
use serde::Deserialize;
use yazi_codegen::{DeserializeOver1, DeserializeOver2};
use yazi_fs::{Xdg, ok_or_not_found, path::expand_url};
use yazi_shared::url::UrlBuf;
use yazi_fs::{Xdg, ok_or_not_found};
use super::{Filetype, Flavor, Icon};
use crate::Style;
use crate::{Style, normalize_path};
#[derive(Deserialize, DeserializeOver1)]
pub struct Theme {
@ -240,8 +239,8 @@ impl Theme {
self.mgr.syntect_theme = self
.flavor
.syntect_path(light)
.or_else(|| expand_url(UrlBuf::from(&self.mgr.syntect_theme)).into_local())
.ok_or(anyhow!("[mgr].syntect_theme must be a path within local filesystem"))?;
.or_else(|| normalize_path(self.mgr.syntect_theme))
.ok_or(anyhow!("[mgr].syntect_theme must be either empty or an absolute path."))?;
Ok(self)
}

12
yazi-config/src/utils.rs Normal file
View file

@ -0,0 +1,12 @@
use std::path::PathBuf;
use yazi_fs::path::{clean_url, expand_url};
use yazi_shared::url::UrlBuf;
pub(crate) fn normalize_path(path: PathBuf) -> Option<PathBuf> {
clean_url(yazi_fs::provider::local::try_absolute(expand_url(UrlBuf::from(path)))?)
.into_loc()
.into_os()
.ok()
.filter(|p| p.as_os_str().is_empty() || p.is_absolute())
}

View file

@ -1,7 +1,8 @@
use std::{io, path::PathBuf};
use std::{io, mem, path::PathBuf};
use serde::{Deserialize, Serialize};
use yazi_fs::path::expand_url;
use crate::normalize_path;
#[derive(Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
@ -44,9 +45,8 @@ pub struct ServiceSftp {
impl ServiceSftp {
fn reshape(&mut self) -> io::Result<()> {
if !self.key_file.as_os_str().is_empty() {
self.key_file = expand_url(&self.key_file)
.into_local()
.ok_or_else(|| io::Error::other("key_file must be a path within local filesystem"))?;
self.key_file = normalize_path(mem::take(&mut self.key_file))
.ok_or_else(|| io::Error::other("key_file must be either empty or an absolute path"))?;
}
self.identity_agent = if self.identity_agent.as_os_str().is_empty() {
@ -55,9 +55,9 @@ impl ServiceSftp {
.filter(|p| p.is_absolute())
.unwrap_or_default()
} else {
expand_url(&self.identity_agent)
.into_local()
.ok_or_else(|| io::Error::other("identity_agent must be a path within local filesystem"))?
normalize_path(mem::take(&mut self.identity_agent)).ok_or_else(|| {
io::Error::other("identity_agent must be either empty or an absolute path")
})?
};
Ok(())

View file

@ -23,7 +23,7 @@ impl Vfs {
Preset::vfs()?.deserialize_over(toml::Deserializer::parse(&Vfs::read()?)?)?.reshape()
})
.await?
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
.map_err(io::Error::other)
}
LOADED.get_or_try_init(init).await

View file

@ -17,8 +17,8 @@ impl Deref for File {
fn deref(&self) -> &Self::Target { &self.cha }
}
impl From<&File> for File {
fn from(value: &File) -> Self { value.clone() }
impl From<&Self> for File {
fn from(value: &Self) -> Self { value.clone() }
}
impl File {

View file

@ -8,7 +8,7 @@ pub fn clean_url<'a>(url: impl Into<UrlCow<'a>>) -> UrlBuf {
cow.trail().components().count() - 1,
);
let scheme = cow.scheme().to_owned().with_ports(uri, urn);
let scheme = cow.into_scheme().into_owned().with_ports(uri, urn);
(scheme, path).try_into().expect("UrlBuf from cleaned path")
}

View file

@ -1,20 +1,19 @@
use std::{borrow::Cow, path::PathBuf};
use std::borrow::Cow;
use yazi_shared::{loc::LocBuf, path::{PathBufDyn, PathCow, PathDyn, PathKind, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}, wtf8::FromWtf8Vec};
use crate::{CWD, path::clean_url};
use yazi_shared::{loc::LocBuf, path::{PathBufDyn, PathCow, PathKind, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}, wtf8::FromWtf8Vec};
#[inline]
pub fn expand_url<'a>(url: impl Into<UrlCow<'a>>) -> UrlBuf {
clean_url(expand_url_impl(url.into().as_url()))
}
pub fn expand_url<'a>(url: impl Into<UrlCow<'a>>) -> UrlCow<'a> { expand_url_impl(url.into()) }
fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> {
fn expand_url_impl(url: UrlCow) -> UrlCow {
let (o_base, o_rest, o_urn) = url.triple();
let n_base = expand_variables(o_base);
let n_rest = expand_variables(o_rest);
let n_urn = expand_variables(o_urn);
let n_base = expand_variables(o_base.into());
let n_rest = expand_variables(o_rest.into());
let n_urn = expand_variables(o_urn.into());
if n_base.is_borrowed() && n_rest.is_borrowed() && n_urn.is_borrowed() {
return url;
}
let rest_diff = n_rest.components().count() as isize - o_rest.components().count() as isize;
let urn_diff = n_urn.components().count() as isize - o_urn.components().count() as isize;
@ -28,7 +27,7 @@ fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> {
let uri = (uri_count + rest_diff + urn_diff) as usize;
let urn = (urn_count + urn_diff) as usize;
let expanded = match url {
match url.as_url() {
Url::Regular(_) => UrlBuf::Regular(
LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
),
@ -44,12 +43,11 @@ fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> {
loc: LocBuf::<typed_path::UnixPathBuf>::with(path.into_unix().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),
},
};
absolute_url(expanded)
}
.into()
}
fn expand_variables<'a>(p: PathDyn<'a>) -> PathCow<'a> {
fn expand_variables(p: PathCow) -> PathCow {
// ${HOME} or $HOME
#[cfg(unix)]
let re = regex::bytes::Regex::new(r"\$(?:\{([^}]+)\}|([a-zA-Z\d_]+))").unwrap();
@ -76,53 +74,6 @@ fn expand_variables<'a>(p: PathDyn<'a>) -> PathCow<'a> {
}
}
pub fn absolute_url<'a>(url: impl Into<UrlCow<'a>>) -> UrlCow<'a> { absolute_url_impl(url.into()) }
fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> {
if url.kind().is_virtual() {
return url;
}
let path = url.loc().as_os().expect("must be a local path");
let b = path.as_os_str().as_encoded_bytes();
let loc = if cfg!(windows) && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() {
LocBuf::<PathBuf>::with(
format!(r"{}:\", b[0].to_ascii_uppercase() as char).into(),
if url.has_base() { 0 } else { 2 },
if url.has_trail() { 0 } else { 2 },
)
.expect("Loc from drive letter")
} else if let Ok(rest) = path.strip_prefix("~/")
&& let Some(home) = dirs::home_dir()
&& home.is_absolute()
{
let add = home.components().count() - 1; // Home root ("~") has offset by the absolute root ("/")
LocBuf::<PathBuf>::with(
home.join(rest),
url.uri().components().count() + if url.has_base() { 0 } else { add },
url.urn().components().count() + if url.has_trail() { 0 } else { add },
)
.expect("Loc from home directory")
} else if !url.is_absolute() {
let cwd = CWD.path();
LocBuf::<PathBuf>::with(
cwd.join(path),
url.uri().components().count(),
url.urn().components().count(),
)
.expect("Loc from relative path")
} else {
return url;
};
match url.as_url() {
Url::Regular(_) => UrlBuf::Regular(loc).into(),
Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }.into(),
Url::Archive { .. } | Url::Sftp { .. } => unreachable!(),
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
@ -179,7 +130,7 @@ mod tests {
for (input, expected) in cases {
let u: UrlBuf = input.parse()?;
assert_eq!(format!("{:?}", expand_url(u)), expected);
assert_eq!(format!("{:?}", expand_url(u).as_url()), expected);
}
Ok(())

View file

@ -0,0 +1,56 @@
use std::path::PathBuf;
use yazi_shared::{loc::LocBuf, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}};
use crate::CWD;
pub fn try_absolute<'a, U>(url: U) -> Option<UrlCow<'a>>
where
U: Into<UrlCow<'a>>,
{
try_absolute_impl(url.into())
}
fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option<UrlCow<'a>> {
if url.kind().is_virtual() {
return None;
}
let path = url.loc().as_os().expect("must be a local path");
let b = path.as_os_str().as_encoded_bytes();
let loc = if cfg!(windows) && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() {
LocBuf::<PathBuf>::with(
format!(r"{}:\", b[0].to_ascii_uppercase() as char).into(),
if url.has_base() { 0 } else { 2 },
if url.has_trail() { 0 } else { 2 },
)
.expect("Loc from drive letter")
} else if let Ok(rest) = path.strip_prefix("~/")
&& let Some(home) = dirs::home_dir()
&& home.is_absolute()
{
let add = home.components().count() - 1; // Home root ("~") has offset by the absolute root ("/")
LocBuf::<PathBuf>::with(
home.join(rest),
url.uri().components().count() + if url.has_base() { 0 } else { add },
url.urn().components().count() + if url.has_trail() { 0 } else { add },
)
.expect("Loc from home directory")
} else if !url.is_absolute() {
LocBuf::<PathBuf>::with(
CWD.path().join(path),
url.uri().components().count(),
url.urn().components().count(),
)
.expect("Loc from relative path")
} else {
return Some(url);
};
Some(match url.as_url() {
Url::Regular(_) => UrlBuf::Regular(loc).into(),
Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }.into(),
Url::Archive { .. } | Url::Sftp { .. } => None?,
})
}

View file

@ -53,7 +53,7 @@ pub(super) fn copy_with_progress_impl(
let (done_tx, mut done_rx) = oneshot::channel();
tokio::spawn({
let to = to.to_owned();
let to = to.clone();
async move {
done_tx.send(copy_impl(from, to, attrs).await).ok();
}

View file

@ -3,7 +3,7 @@ use std::{io, path::Path, sync::Arc};
use tokio::sync::mpsc;
use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow}};
use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Capabilities, Provider}};
use crate::{cha::Cha, provider::{Attrs, Capabilities, Provider}};
#[derive(Clone)]
pub struct Local<'a> {
@ -18,7 +18,10 @@ impl<'a> Provider for Local<'a> {
type ReadDir = super::ReadDir;
type UrlCow = UrlCow<'a>;
async fn absolute(&self) -> io::Result<Self::UrlCow> { Ok(absolute_url(self.url)) }
async fn absolute(&self) -> io::Result<Self::UrlCow> {
super::try_absolute(self.url)
.ok_or_else(|| io::Error::other("Cannot get absolute path for local URL"))
}
#[inline]
async fn canonicalize(&self) -> io::Result<UrlBuf> {

View file

@ -1 +1 @@
yazi_macro::mod_flat!(calculator casefold copier dir_entry gate identical local read_dir);
yazi_macro::mod_flat!(absolute calculator casefold copier dir_entry gate identical local read_dir);

View file

@ -22,6 +22,7 @@ yazi-config = { path = "../yazi-config", version = "25.9.15" }
yazi-fs = { path = "../yazi-fs", version = "25.9.15" }
yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
# External dependencies
anyhow = { workspace = true }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(deprecate mouse notify plugin stop update_progress);
yazi_macro::mod_flat!(deprecate mouse notify plugin resume stop update_progress);

View file

@ -0,0 +1,10 @@
use tokio::sync::oneshot;
use yazi_shared::event::CmdCow;
pub struct ResumeOpt {
pub tx: Option<oneshot::Sender<()>>,
}
impl From<CmdCow> for ResumeOpt {
fn from(mut c: CmdCow) -> Self { Self { tx: c.take_any("tx") } }
}

View file

@ -1,11 +1,12 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use serde::{Deserialize, Serialize};
use yazi_fs::path::expand_url;
use yazi_shared::{event::CmdCow, url::{Url, UrlBuf, UrlCow}};
use yazi_fs::path::{clean_url, expand_url};
use yazi_shared::{event::CmdCow, url::{Url, UrlBuf}};
use yazi_vfs::provider;
#[derive(Debug)]
pub struct CdOpt {
pub target: UrlCow<'static>,
pub target: UrlBuf,
pub interactive: bool,
pub source: CdSource,
}
@ -18,19 +19,23 @@ impl From<CmdCow> for CdOpt {
target = expand_url(target).into();
}
Self { target, interactive: c.bool("interactive"), source: CdSource::Cd }
}
}
if let Some(u) = provider::try_absolute(&target)
&& u.is_owned()
{
target = u.into_static();
}
impl From<(UrlCow<'static>, CdSource)> for CdOpt {
fn from((target, source): (UrlCow<'static>, CdSource)) -> Self {
Self { target, interactive: false, source }
Self {
target: clean_url(target),
interactive: c.bool("interactive"),
source: CdSource::Cd,
}
}
}
impl From<(UrlBuf, CdSource)> for CdOpt {
fn from((target, source): (UrlBuf, CdSource)) -> Self {
Self::from((UrlCow::from(target), source))
Self { target, interactive: false, source }
}
}

View file

@ -1,12 +1,13 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_fs::path::expand_url;
use yazi_shared::{event::CmdCow, url::{UrlBuf, UrlCow}};
use yazi_fs::path::{clean_url, expand_url};
use yazi_shared::{event::CmdCow, url::UrlBuf};
use yazi_vfs::provider;
use crate::mgr::CdSource;
#[derive(Debug)]
pub struct RevealOpt {
pub target: UrlCow<'static>,
pub target: UrlBuf,
pub source: CdSource,
pub no_dummy: bool,
}
@ -19,30 +20,22 @@ impl From<CmdCow> for RevealOpt {
target = expand_url(target).into();
}
Self { target, source: CdSource::Reveal, no_dummy: c.bool("no-dummy") }
}
}
if let Some(u) = provider::try_absolute(&target)
&& u.is_owned()
{
target = u.into_static();
}
impl From<UrlCow<'static>> for RevealOpt {
fn from(target: UrlCow<'static>) -> Self {
Self { target, source: CdSource::Reveal, no_dummy: false }
}
}
impl From<(UrlCow<'static>, CdSource)> for RevealOpt {
fn from((target, source): (UrlCow<'static>, CdSource)) -> Self {
Self { target, source, no_dummy: false }
Self { target: clean_url(target), source: CdSource::Reveal, no_dummy: c.bool("no-dummy") }
}
}
impl From<UrlBuf> for RevealOpt {
fn from(target: UrlBuf) -> Self { Self::from(UrlCow::from(target)) }
fn from(target: UrlBuf) -> Self { Self { target, source: CdSource::Reveal, no_dummy: false } }
}
impl From<(UrlBuf, CdSource)> for RevealOpt {
fn from((target, source): (UrlBuf, CdSource)) -> Self {
Self::from((UrlCow::from(target), source))
}
fn from((target, source): (UrlBuf, CdSource)) -> Self { Self { target, source, no_dummy: false } }
}
impl FromLua for RevealOpt {

View file

@ -2,13 +2,13 @@ use anyhow::bail;
use mlua::{ExternalError, FromLua, IntoLua, Lua, LuaSerdeExt, Value};
use serde::{Deserialize, Serialize};
use yazi_binding::Url;
use yazi_shared::{event::CmdCow, url::UrlCow};
use yazi_shared::{event::CmdCow, url::UrlBuf};
use crate::mgr::{CdOpt, CdSource};
#[derive(Debug, Deserialize, Serialize)]
pub struct StashOpt {
pub target: UrlCow<'static>,
pub target: UrlBuf,
pub source: CdSource,
}

View file

@ -1,25 +1,35 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_boot::BOOT;
use yazi_fs::path::expand_url;
use yazi_fs::path::{clean_url, expand_url};
use yazi_shared::{event::CmdCow, url::UrlCow};
use yazi_vfs::provider;
#[derive(Debug)]
pub struct TabCreateOpt {
pub wd: Option<UrlCow<'static>>,
pub url: Option<UrlCow<'static>>,
}
impl From<CmdCow> for TabCreateOpt {
fn from(mut c: CmdCow) -> Self {
if c.bool("current") {
return Self { wd: None };
return Self { url: None };
}
let Ok(mut wd) = c.take_first() else {
return Self { wd: Some(UrlCow::from(&BOOT.cwds[0])) };
let Ok(mut url) = c.take_first() else {
return Self { url: Some(UrlCow::from(&BOOT.cwds[0])) };
};
if !c.bool("raw") {
wd = expand_url(wd).into();
url = expand_url(url).into();
}
Self { wd: Some(wd) }
if let Some(u) = provider::try_absolute(&url)
&& u.is_owned()
{
url = u.into_static();
}
Self { url: Some(clean_url(url).into()) }
}
}

View file

@ -60,7 +60,7 @@ pub(super) fn hide(lua: &Lua) -> mlua::Result<Value> {
pub(super) fn printable(lua: &Lua) -> mlua::Result<Value> {
let f = lua.create_function(|lua, s: mlua::String| {
Ok(match replace_to_printable(&*s.as_bytes(), false, 1, true) {
Ok(match replace_to_printable(&s.as_bytes(), false, 1, true) {
Cow::Borrowed(_) => s,
Cow::Owned(new) => lua.create_string(&new)?,
})

View file

@ -161,14 +161,23 @@ fn calc_size(lua: &Lua) -> mlua::Result<Function> {
})
}
#[allow(irrefutable_let_patterns)]
fn expand_url(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, value: Value| {
lua.create_function(|lua, value: Value| {
use yazi_fs::path::expand_url;
Ok(Url::new(match value {
Value::String(s) => expand_url(UrlCow::try_from(&*s.as_bytes())?),
Value::UserData(ud) => expand_url(&*ud.borrow::<yazi_binding::Url>()?),
match &value {
Value::String(s) => Url::new(expand_url(UrlCow::try_from(&*s.as_bytes())?)).into_lua(lua),
Value::UserData(ud) => {
if let u = expand_url(&*ud.borrow::<yazi_binding::Url>()?)
&& u.is_owned()
{
Url::new(u.into_owned()).into_lua(lua)
} else {
Ok(value)
}
}
_ => Err("must be a string or a Url".into_lua_err())?,
}))
}
})
}

View file

@ -25,10 +25,10 @@ impl AppProxy {
emit!(Call(relay!(app:update_notify, [dur.as_secs_f64()])));
}
pub fn notify_warn(title: &str, content: impl ToString) {
pub fn notify_warn(title: impl Into<String>, content: impl Into<String>) {
Self::notify(NotifyOpt {
title: title.to_owned(),
content: content.to_string(),
title: title.into(),
content: content.into(),
level: NotifyLevel::Warn,
timeout: Duration::from_secs(5),
});

View file

@ -19,7 +19,7 @@ impl TasksProxy {
block: bool,
orphan: bool,
) {
let done = CompletionToken::new();
let done = CompletionToken::default();
emit!(Call(relay!(tasks:process_open).with_any("opt", ProcessOpenOpt {
cwd,
cmd,

View file

@ -21,7 +21,7 @@ impl Process {
let (id, cmd) = (task.id, task.cmd.clone());
let result = super::shell(task.into()).await;
if let Err(e) = result {
AppProxy::notify_warn(&cmd.to_string_lossy(), format!("Failed to start process: {e}"));
AppProxy::notify_warn(cmd.to_string_lossy(), format!("Failed to start process: {e}"));
return Ok(self.ops.out(id, ProcessOutBlock::Succ));
}
@ -32,7 +32,7 @@ impl Process {
Some(code) => format!("Process exited with status code: {code}"),
None => "Process terminated by signal".to_string(),
};
AppProxy::notify_warn(&cmd.to_string_lossy(), &content);
AppProxy::notify_warn(cmd.to_string_lossy(), content);
}
Ok(self.ops.out(id, ProcessOutBlock::Succ))

View file

@ -25,7 +25,7 @@ impl Task {
name,
prog: T::default().into(),
hook: None,
done: CompletionToken::new(),
done: Default::default(),
logs: Default::default(),
logger: Default::default(),

View file

@ -2,14 +2,12 @@ use std::sync::{Arc, atomic::{AtomicU8, Ordering}};
use tokio::sync::Notify;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct CompletionToken {
inner: Arc<(AtomicU8, Notify)>,
}
impl CompletionToken {
pub fn new() -> Self { Self { inner: Arc::new((AtomicU8::new(0), Notify::new())) } }
pub fn complete(&self, success: bool) {
let new = if success { 1 } else { 2 };
self.inner.0.compare_exchange(0, new, Ordering::Relaxed, Ordering::Relaxed).ok();

View file

@ -2,7 +2,7 @@ use std::borrow::Cow;
use anyhow::Result;
use crate::path::{AsPath, PathBufDyn, PathDyn, PathKind};
use crate::path::{AsPath, PathBufDyn, PathDyn, PathDynError, PathKind};
// --- PathCow
#[derive(Debug)]
@ -19,6 +19,10 @@ impl From<PathBufDyn> for PathCow<'_> {
fn from(value: PathBufDyn) -> Self { Self::Owned(value) }
}
impl<'a> From<std::path::PathBuf> for PathCow<'a> {
fn from(value: std::path::PathBuf) -> Self { Self::Owned(value.into()) }
}
impl<'a> From<&'a PathCow<'_>> for PathCow<'a> {
fn from(value: &'a PathCow<'_>) -> Self { Self::Borrowed(value.as_path()) }
}
@ -43,11 +47,20 @@ impl PartialEq<&str> for PathCow<'_> {
impl<'a> PathCow<'a> {
pub fn into_owned(self) -> PathBufDyn {
match self {
Self::Borrowed(s) => s.to_owned(),
Self::Owned(s) => s,
Self::Borrowed(p) => p.to_owned(),
Self::Owned(p) => p,
}
}
pub fn into_os(self) -> Result<std::path::PathBuf, PathDynError> {
match self {
PathCow::Borrowed(p) => p.to_os_owned(),
PathCow::Owned(p) => p.into_os(),
}
}
pub fn is_borrowed(&self) -> bool { matches!(self, Self::Borrowed(_)) }
pub fn with<K, T>(kind: K, bytes: T) -> Result<Self>
where
K: Into<PathKind>,

View file

@ -110,3 +110,22 @@ impl From<std::path::StripPrefixError> for StripPrefixError {
impl From<typed_path::StripPrefixError> for StripPrefixError {
fn from(_: typed_path::StripPrefixError) -> Self { Self::NotPrefix }
}
// --- StripSuffixError
#[derive(Debug, Error)]
pub enum StripSuffixError {
#[error("calling strip_suffix on URLs with different schemes")]
Exotic,
#[error("the base is not a suffix of the path")]
NotSuffix,
#[error("calling strip_suffix on paths with different encodings")]
WrongEncoding,
}
impl From<StrandError> for StripSuffixError {
fn from(err: StrandError) -> Self {
match err {
StrandError::AsOs | StrandError::AsUtf8 => Self::WrongEncoding,
}
}
}

View file

@ -2,7 +2,7 @@ use std::borrow::Cow;
use anyhow::Result;
use crate::{Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError}, strand::{AsStrand, Strand}};
use crate::{Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand}};
pub trait PathLike: AsPath {
fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.as_path().as_os() }
@ -85,6 +85,13 @@ pub trait PathLike: AsPath {
{
self.as_path().try_strip_prefix(base)
}
fn try_strip_suffix<T>(&self, suffix: T) -> Result<PathDyn<'_>, StripSuffixError>
where
T: AsStrand,
{
self.as_path().try_strip_suffix(suffix)
}
}
impl<P> From<&P> for PathBufDyn

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use hashbrown::Equivalent;
use super::{RsplitOnceError, StartsWithError};
use crate::{BytesExt, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError}, strand::{AsStrand, Strand, StrandError}};
use crate::{BytesExt, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand, StrandError}};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum PathDyn<'p> {
@ -252,6 +252,26 @@ impl<'p> PathDyn<'p> {
})
}
pub fn try_strip_suffix<T>(self, suffix: T) -> Result<Self, StripSuffixError>
where
T: AsStrand,
{
let s = suffix.as_strand();
let mut me_comps = self.components();
let mut suf_comps = match self.kind() {
PathKind::Os => Components::Os(std::path::Path::new(s.as_os()?).components()),
PathKind::Unix => Components::Unix(typed_path::UnixPath::new(s.encoded_bytes()).components()),
};
while let Some(next) = suf_comps.next_back() {
if me_comps.next_back() != Some(next) {
return Err(StripSuffixError::NotSuffix);
}
}
Ok(me_comps.path())
}
pub fn with<K, S>(kind: K, strand: &'p S) -> Result<Self, StrandError>
where
K: Into<PathKind>,

View file

@ -103,6 +103,14 @@ impl<'a> TryFrom<Cow<'a, str>> for UrlCow<'a> {
}
}
impl<'a> TryFrom<(SchemeRef<'a>, PathDyn<'a>)> for UrlCow<'a> {
type Error = anyhow::Error;
fn try_from((scheme, path): (SchemeRef<'a>, PathDyn<'a>)) -> Result<Self, Self::Error> {
(SchemeCow::Borrowed(scheme), path).try_into()
}
}
impl<'a> TryFrom<(SchemeRef<'a>, PathCow<'a>)> for UrlCow<'a> {
type Error = anyhow::Error;
@ -245,6 +253,28 @@ impl<'a> UrlCow<'a> {
}
}
pub fn into_static(self) -> UrlCow<'static> {
match self {
UrlCow::Regular(loc) => UrlCow::Regular(loc),
UrlCow::Search { loc, domain } => UrlCow::Search { loc, domain: domain.into_owned().into() },
UrlCow::Archive { loc, domain } => {
UrlCow::Archive { loc, domain: domain.into_owned().into() }
}
UrlCow::Sftp { loc, domain } => UrlCow::Sftp { loc, domain: domain.into_owned().into() },
UrlCow::RegularRef(loc) => UrlCow::Regular(loc.into()),
UrlCow::SearchRef { loc, domain } => {
UrlCow::Search { loc: loc.into(), domain: domain.into_owned().into() }
}
UrlCow::ArchiveRef { loc, domain } => {
UrlCow::Archive { loc: loc.into(), domain: domain.into_owned().into() }
}
UrlCow::SftpRef { loc, domain } => {
UrlCow::Sftp { loc: loc.into(), domain: domain.into_owned().into() }
}
}
}
#[inline]
pub fn to_owned(&self) -> UrlBuf { self.as_url().into() }
}

View file

@ -54,6 +54,8 @@ where
fn trail(&self) -> Url<'_> { self.as_url().trail() }
fn triple(&self) -> (PathDyn<'_>, PathDyn<'_>, PathDyn<'_>) { self.as_url().triple() }
fn try_ends_with(&self, child: impl AsUrl) -> Result<bool, EndsWithError> {
self.as_url().try_ends_with(child)
}

View file

@ -5,7 +5,7 @@ use hashbrown::Equivalent;
use serde::Serialize;
use super::Encode as EncodeUrl;
use crate::{loc::{Loc, LocBuf}, path::{AsPath, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError}, pool::InternStr, scheme::{Encode as EncodeScheme, SchemeCow, SchemeKind, SchemeRef}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}};
use crate::{loc::{Loc, LocBuf}, path::{AsPath, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError, StripSuffixError}, pool::InternStr, scheme::{Encode as EncodeScheme, SchemeCow, SchemeKind, SchemeRef}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}};
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub enum Url<'a> {
@ -384,6 +384,52 @@ impl<'a> Url<'a> {
}
}
pub fn try_strip_suffix(self, other: impl AsUrl) -> Result<PathDyn<'a>, StripSuffixError> {
use StripSuffixError::{Exotic, NotSuffix};
use Url as U;
let other = other.as_url();
let suffix = self.loc().try_strip_suffix(other.loc())?;
match (self, other) {
// Same scheme
(U::Regular(_), U::Regular(_)) => Ok(suffix),
(U::Search { .. }, U::Search { .. }) => Ok(suffix),
(U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => {
Some(suffix).filter(|_| a == b).ok_or(Exotic)
}
(U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => {
Some(suffix).filter(|_| a == b).ok_or(Exotic)
}
// Both are local files
(U::Regular(_), U::Search { .. }) => Ok(suffix),
(U::Search { .. }, U::Regular(_)) => Ok(suffix),
// Only the entry of archives is a local file
(U::Regular(_), U::Archive { .. }) => {
Some(suffix).filter(|_| other.uri().is_empty()).ok_or(NotSuffix)
}
(U::Search { .. }, U::Archive { .. }) => {
Some(suffix).filter(|_| other.uri().is_empty()).ok_or(NotSuffix)
}
(U::Archive { .. }, U::Regular(_)) => {
Some(suffix).filter(|_| self.uri().is_empty()).ok_or(NotSuffix)
}
(U::Archive { .. }, U::Search { .. }) => {
Some(suffix).filter(|_| self.uri().is_empty()).ok_or(NotSuffix)
}
// Independent virtual file space
(U::Regular(_), U::Sftp { .. }) => Err(Exotic),
(U::Search { .. }, U::Sftp { .. }) => Err(Exotic),
(U::Archive { .. }, U::Sftp { .. }) => Err(Exotic),
(U::Sftp { .. }, U::Regular(_)) => Err(Exotic),
(U::Sftp { .. }, U::Search { .. }) => Err(Exotic),
(U::Sftp { .. }, U::Archive { .. }) => Err(Exotic),
}
}
#[inline]
pub fn uri(self) -> PathDyn<'a> {
match self {

View file

@ -54,7 +54,7 @@ pub(super) fn copy_with_progress_impl(
}
};
let chunks = (cha.len + PER_CHUNK - 1) / PER_CHUNK;
let chunks = cha.len.div_ceil(PER_CHUNK);
let it = futures::stream::iter(0..chunks)
.map(|i| {
let acc_ = acc_.clone();

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::sync::mpsc;
use yazi_fs::{cha::Cha, provider::{Attrs, Capabilities, Provider, local::Local}};
use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, UrlBuf, UrlCow}};
use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}};
use super::{Providers, ReadDir, RwFile};
@ -247,6 +247,18 @@ where
Providers::new(url.as_url()).await?.trash().await
}
pub fn try_absolute<'a, U>(url: U) -> Option<UrlCow<'a>>
where
U: Into<UrlCow<'a>>,
{
let url = url.into();
match url.as_url() {
Url::Regular(_) | Url::Search { .. } => yazi_fs::provider::local::try_absolute(url),
Url::Archive { .. } => None, // TODO
Url::Sftp { .. } => crate::provider::sftp::try_absolute(url),
}
}
pub async fn write<U, C>(url: U, contents: C) -> io::Result<()>
where
U: AsUrl,

View file

@ -0,0 +1,21 @@
use yazi_fs::CWD;
use yazi_shared::url::{UrlCow, UrlLike};
pub fn try_absolute<'a, U>(url: U) -> Option<UrlCow<'a>>
where
U: Into<UrlCow<'a>>,
{
try_absolute_impl(url.into())
}
fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option<UrlCow<'a>> {
if url.is_absolute() {
Some(url)
} else if let cwd = CWD.load()
&& cwd.scheme().covariant(url.scheme())
{
Some(cwd.try_join(url.loc()).ok()?.into())
} else {
None
}
}

View file

@ -1,4 +1,4 @@
yazi_macro::mod_flat!(conn gate metadata read_dir sftp);
yazi_macro::mod_flat!(absolute conn gate metadata read_dir sftp);
static CONN: yazi_shared::RoCell<
parking_lot::Mutex<

View file

@ -2,7 +2,7 @@ use std::{io, sync::Arc};
use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver};
use yazi_config::vfs::{ServiceSftp, Vfs};
use yazi_fs::{CWD, provider::{Capabilities, DirReader, FileHolder, Provider}};
use yazi_fs::provider::{Capabilities, DirReader, FileHolder, Provider};
use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}};
@ -26,11 +26,8 @@ impl<'a> Provider for Sftp<'a> {
type UrlCow = UrlCow<'a>;
async fn absolute(&self) -> io::Result<Self::UrlCow> {
let cwd = CWD.load();
Ok(if self.url.is_absolute() {
self.url.into()
} else if cwd.scheme().covariant(self.url.scheme()) {
cwd.try_join(self.path)?.into()
Ok(if let Some(u) = super::try_absolute(self.url) {
u
} else {
self.canonicalize().await?.into()
})