diff --git a/Cargo.lock b/Cargo.lock index ecea9b47..e9ef0bd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,9 +255,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" dependencies = [ "serde", ] @@ -343,9 +343,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -569,7 +569,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "crossterm_winapi", "mio", "parking_lot", @@ -585,7 +585,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "crossterm_winapi", "derive_more", "document-features", @@ -1148,9 +1148,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", @@ -1169,7 +1169,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "inotify-sys", "libc", ] @@ -1209,11 +1209,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "cfg-if", "libc", ] @@ -1331,7 +1331,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "libc", ] @@ -1553,7 +1553,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "fsevent-sys", "inotify", "kqueue", @@ -1676,7 +1676,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "objc2", ] @@ -1707,7 +1707,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "libc", "once_cell", "onig_sys", @@ -1824,9 +1824,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -2062,7 +2062,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "cassowary", "compact_str", "crossterm 0.28.1", @@ -2142,7 +2142,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", ] [[package]] @@ -2233,7 +2233,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2246,7 +2246,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.9.4", @@ -3427,9 +3427,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -3446,7 +3446,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", ] [[package]] @@ -3593,7 +3593,7 @@ name = "yazi-config" version = "25.6.11" dependencies = [ "anyhow", - "bitflags 2.9.2", + "bitflags 2.9.3", "crossterm 0.29.0", "dirs", "globset", @@ -3620,7 +3620,6 @@ dependencies = [ "futures", "hashbrown", "indexmap", - "libc", "notify", "parking_lot", "ratatui", @@ -3724,7 +3723,7 @@ version = "25.6.11" dependencies = [ "anyhow", "arc-swap", - "bitflags 2.9.2", + "bitflags 2.9.3", "core-foundation-sys", "dirs", "foldhash", @@ -3738,6 +3737,7 @@ dependencies = [ "serde", "tokio", "tracing", + "trash", "uzers", "windows-sys 0.60.2", "yazi-ffi", @@ -3758,7 +3758,7 @@ name = "yazi-parser" version = "25.6.11" dependencies = [ "anyhow", - "bitflags 2.9.2", + "bitflags 2.9.3", "crossterm 0.29.0", "hashbrown", "mlua", @@ -3842,7 +3842,6 @@ dependencies = [ "tokio", "tokio-util", "tracing", - "trash", "yazi-config", "yazi-dds", "yazi-fs", diff --git a/Cargo.toml b/Cargo.toml index f0e12f35..608016f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ lto = false ansi-to-tui = "7.0.0" anyhow = "1.0.99" base64 = "0.22.1" -bitflags = { version = "2.9.2", features = [ "serde" ] } +bitflags = { version = "2.9.3", features = [ "serde" ] } clap = { version = "4.5.45", features = [ "derive" ] } core-foundation-sys = "0.8.7" crossterm = { version = "0.29.0", features = [ "event-stream" ] } @@ -32,7 +32,7 @@ foldhash = "0.1.5" futures = "0.3.31" globset = "0.4.16" hashbrown = { version = "0.15.5", features = [ "serde" ] } -indexmap = { version = "2.10.0", features = [ "serde" ] } +indexmap = { version = "2.11.0", features = [ "serde" ] } libc = "0.2.175" lru = "0.16.0" mlua = { version = "0.11.2", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } diff --git a/scripts/validate-form/main.js b/scripts/validate-form/main.js index 0f616a2e..9c37cc3a 100644 --- a/scripts/validate-form/main.js +++ b/scripts/validate-form/main.js @@ -186,6 +186,25 @@ If the problem persists, please file a new issue and complete the issue template } } + async function closeUnsupportedIssue(id) { + try { + await github.rest.issues.update({ + ...context.repo, + issue_number: id, + state : "closed", + state_reason: "not_planned", + }) + await github.rest.issues.createComment({ + ...context.repo, + issue_number: id, + body : `Unsupported issue template. +Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) or [Feature Request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml) template should be used.`, + }) + } catch (e) { + core.error(`Error closing unsupported issue: ${e.message}`) + } + } + async function main() { const hash = await nightlyHash() if (!hash) return @@ -206,6 +225,8 @@ If the problem persists, please file a new issue and complete the issue template } else if (await hasLabel(id, "feature")) { const body = featureRequestBody(creator, content) await updateLabels(id, !!body, body) + } else if (context.payload.action === "opened") { + await closeUnsupportedIssue(id) } } } diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 7773317d..acc534d1 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -7,7 +7,7 @@ use scopeguard::defer; use tokio::io::AsyncWriteExt; use yazi_config::{YAZI, opener::OpenerRule}; use yazi_dds::Pubsub; -use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, paths_to_same_file, provider::{self, local::{Gate, Local}}}; +use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, provider::{self, local::{Gate, Local}}}; use yazi_macro::{err, succ}; use yazi_parser::VoidOpt; use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER}; @@ -119,7 +119,7 @@ impl BulkRename { selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(), ); - if maybe_exists(&new).await && !paths_to_same_file(&old, &new).await { + if maybe_exists(&new).await && !provider::same(&old, &new).await.unwrap_or(false) { failed.push((o, n, anyhow!("Destination already exists"))); } else if let Err(e) = provider::rename(&old, &new).await { failed.push((o, n, e.into())); diff --git a/yazi-actor/src/mgr/follow.rs b/yazi-actor/src/mgr/follow.rs index ee83ae95..e1cd3c3b 100644 --- a/yazi-actor/src/mgr/follow.rs +++ b/yazi-actor/src/mgr/follow.rs @@ -17,9 +17,7 @@ impl Actor for Follow { let Some(file) = cx.hovered() else { succ!() }; let Some(link_to) = &file.link_to else { succ!() }; - if link_to.is_absolute() { - act!(mgr:reveal, cx, link_to.clone()) - } else if let Some(p) = file.url.parent_url() { + if let Some(p) = file.url.parent_url() { act!(mgr:reveal, cx, clean_url(p.join(link_to))) } else { succ!() diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index d5b764eb..bcb4636f 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -1,7 +1,7 @@ use anyhow::Result; use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_dds::Pubsub; -use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, paths_to_same_file, provider, realname}; +use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname}; use yazi_macro::{act, err, succ}; use yazi_parser::mgr::RenameOpt; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER}; @@ -48,7 +48,8 @@ impl Actor for Rename { } let new = UrlBuf::from(old.parent().unwrap().join(name)); - if opt.force || !maybe_exists(&new).await || paths_to_same_file(&old, &new).await { + if opt.force || !maybe_exists(&new).await || provider::same(&old, &new).await.unwrap_or(false) + { Self::r#do(tab, old, new).await.ok(); } else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await { Self::r#do(tab, old, new).await.ok(); diff --git a/yazi-adapter/src/adapter.rs b/yazi-adapter/src/adapter.rs index 059e71d3..658f2def 100644 --- a/yazi-adapter/src/adapter.rs +++ b/yazi-adapter/src/adapter.rs @@ -1,9 +1,9 @@ -use std::{env, fmt::Display}; +use std::{env, fmt::Display, path::Path}; use anyhow::Result; use ratatui::layout::Rect; use tracing::warn; -use yazi_shared::{env_exists, url::UrlBuf}; +use yazi_shared::env_exists; use crate::{Emulator, SHOWN, TMUX, drivers}; @@ -35,18 +35,18 @@ impl Display for Adapter { } impl Adapter { - pub async fn image_show(self, url: &UrlBuf, max: Rect) -> Result { + pub async fn image_show(self, path: &Path, max: Rect) -> Result { if max.is_empty() { return Ok(Rect::default()); } match self { - Self::Kgp => drivers::Kgp::image_show(url, max).await, - Self::KgpOld => drivers::KgpOld::image_show(url, max).await, - Self::Iip => drivers::Iip::image_show(url, max).await, - Self::Sixel => drivers::Sixel::image_show(url, max).await, - Self::X11 | Self::Wayland => drivers::Ueberzug::image_show(url, max).await, - Self::Chafa => drivers::Chafa::image_show(url, max).await, + Self::Kgp => drivers::Kgp::image_show(path, max).await, + Self::KgpOld => drivers::KgpOld::image_show(path, max).await, + Self::Iip => drivers::Iip::image_show(path, max).await, + Self::Sixel => drivers::Sixel::image_show(path, max).await, + Self::X11 | Self::Wayland => drivers::Ueberzug::image_show(path, max).await, + Self::Chafa => drivers::Chafa::image_show(path, max).await, } } diff --git a/yazi-adapter/src/drivers/iip.rs b/yazi-adapter/src/drivers/iip.rs index 840ab273..5c76fa63 100644 --- a/yazi-adapter/src/drivers/iip.rs +++ b/yazi-adapter/src/drivers/iip.rs @@ -1,4 +1,4 @@ -use std::{fmt::Write, io::Write as ioWrite}; +use std::{fmt::Write, io::Write as ioWrite, path::Path}; use anyhow::Result; use base64::{Engine, engine::{Config, general_purpose::STANDARD}}; @@ -6,15 +6,14 @@ use crossterm::{cursor::MoveTo, queue}; use image::{DynamicImage, ExtendedColorType, ImageEncoder, codecs::{jpeg::JpegEncoder, png::PngEncoder}}; use ratatui::layout::Rect; use yazi_config::YAZI; -use yazi_shared::url::UrlBuf; use crate::{CLOSE, Emulator, Image, START, adapter::Adapter}; pub(crate) struct Iip; impl Iip { - pub(crate) async fn image_show(url: &UrlBuf, max: Rect) -> Result { - let img = Image::downscale(url, max).await?; + pub(crate) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; diff --git a/yazi-adapter/src/drivers/kgp.rs b/yazi-adapter/src/drivers/kgp.rs index bcf25cec..4d39cb4f 100644 --- a/yazi-adapter/src/drivers/kgp.rs +++ b/yazi-adapter/src/drivers/kgp.rs @@ -1,12 +1,11 @@ use core::str; -use std::io::Write; +use std::{io::Write, path::Path}; use anyhow::Result; use base64::{Engine, engine::general_purpose}; use crossterm::{cursor::MoveTo, queue}; use image::DynamicImage; use ratatui::layout::Rect; -use yazi_shared::url::UrlBuf; use crate::{CLOSE, ESCAPE, Emulator, START, adapter::Adapter, image::Image}; @@ -313,8 +312,8 @@ static DIACRITICS: [char; 297] = [ pub(crate) struct Kgp; impl Kgp { - pub(crate) async fn image_show(url: &UrlBuf, max: Rect) -> Result { - let img = Image::downscale(url, max).await?; + pub(crate) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; let area = Image::pixel_area((img.width(), img.height()), max); let b1 = Self::encode(img).await?; diff --git a/yazi-adapter/src/drivers/kgp_old.rs b/yazi-adapter/src/drivers/kgp_old.rs index 25b175bc..fe4d58f1 100644 --- a/yazi-adapter/src/drivers/kgp_old.rs +++ b/yazi-adapter/src/drivers/kgp_old.rs @@ -1,11 +1,10 @@ use core::str; -use std::io::Write; +use std::{io::Write, path::Path}; use anyhow::Result; use base64::{Engine, engine::general_purpose}; use image::DynamicImage; use ratatui::layout::Rect; -use yazi_shared::url::UrlBuf; use yazi_term::tty::TTY; use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter}; @@ -13,8 +12,8 @@ use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter}; pub(crate) struct KgpOld; impl KgpOld { - pub(crate) async fn image_show(url: &UrlBuf, max: Rect) -> Result { - let img = Image::downscale(url, max).await?; + pub(crate) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; diff --git a/yazi-adapter/src/drivers/sixel.rs b/yazi-adapter/src/drivers/sixel.rs index 9477a8f6..6d7db947 100644 --- a/yazi-adapter/src/drivers/sixel.rs +++ b/yazi-adapter/src/drivers/sixel.rs @@ -1,4 +1,4 @@ -use std::io::Write; +use std::{io::Write, path::Path}; use anyhow::{Result, bail}; use crossterm::{cursor::MoveTo, queue}; @@ -6,15 +6,14 @@ use image::{DynamicImage, GenericImageView, RgbImage}; use palette::{Srgb, cast::ComponentsAs}; use quantette::{ColorSlice, PaletteSize, QuantizeOutput, wu::UIntBinner}; use ratatui::layout::Rect; -use yazi_shared::url::UrlBuf; use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter}; pub(crate) struct Sixel; impl Sixel { - pub(crate) async fn image_show(url: &UrlBuf, max: Rect) -> Result { - let img = Image::downscale(url, max).await?; + pub(crate) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index bd599c57..f079c7c3 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -1,16 +1,17 @@ +use std::path::Path; + use anyhow::Result; -use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; +use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; use ratatui::layout::Rect; use yazi_config::YAZI; -use yazi_fs::provider; -use yazi_shared::url::UrlBuf; +use yazi_fs::provider::local::Local; use crate::Dimension; pub struct Image; impl Image { - pub async fn precache(src: &UrlBuf, cache: &UrlBuf) -> Result<()> { + pub async fn precache(src: &Path, cache: &Path) -> Result<()> { let (mut img, orientation, icc) = Self::decode_from(src).await?; let (w, h) = Self::flip_size(orientation, (YAZI.preview.max_width, YAZI.preview.max_height)); @@ -38,11 +39,11 @@ impl Image { }) .await??; - Ok(provider::write(cache, buf).await?) + Ok(Local::write(cache, buf).await?) } - pub(super) async fn downscale(url: &UrlBuf, rect: Rect) -> Result { - let (mut img, orientation, _) = Self::decode_from(url).await?; + pub(super) async fn downscale(path: &Path, rect: Rect) -> Result { + let (mut img, orientation, _) = Self::decode_from(path).await?; let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect)); // Fast path. @@ -96,7 +97,7 @@ impl Image { } } - async fn decode_from(url: &UrlBuf) -> ImageResult<(DynamicImage, Orientation, Option>)> { + async fn decode_from(path: &Path) -> ImageResult<(DynamicImage, Orientation, Option>)> { let mut limits = Limits::no_limits(); if YAZI.tasks.image_alloc > 0 { limits.max_alloc = Some(YAZI.tasks.image_alloc as u64); @@ -108,13 +109,11 @@ impl Image { limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32); } - let mut reader = ImageReader::new(provider::open(url).await?.reader_sync().await); - if let Ok(format) = ImageFormat::from_path(url) { - reader.set_format(format); - } - - reader.limits(limits); + let path = path.to_owned(); tokio::task::spawn_blocking(move || { + let mut reader = ImageReader::open(path)?; + reader.limits(limits); + let mut decoder = reader.with_guessed_format()?.into_decoder()?; let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms); let icc = decoder.icc_profile().unwrap_or_default(); diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 44604b4c..0202af53 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -1,7 +1,7 @@ use std::ops::Deref; use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; -use yazi_shared::url::{UrlBufCov, UrlCow}; +use yazi_shared::{IntoOsStr, url::{UrlBufCov, UrlCow}}; use crate::{Urn, cached_field, deprecate}; @@ -124,32 +124,38 @@ impl UserData for Url { fn add_methods>(methods: &mut M) { methods.add_method("join", |_, me, other: Value| { Ok(Self::new(match other { - Value::String(s) => me.join(s.to_str()?.as_ref()), - Value::UserData(ud) => me.join(&ud.borrow::()?.inner), - _ => Err("must be a string or a Url".into_lua_err())?, + Value::String(s) => me.join(s.as_bytes().into_os_str()?), + Value::UserData(ud) => { + let url = ud.borrow::()?; + if !me.scheme.covariant(&url.scheme) { + return Err("cannot join Urls with different schemes".into_lua_err()); + } + me.join(&url.loc) + } + _ => Err("must be a string or Url".into_lua_err())?, })) }); methods.add_method("starts_with", |_, me, base: Value| { Ok(match base { - Value::String(s) => me.starts_with(s.to_str()?.as_ref()), - Value::UserData(ud) => me.starts_with(&ud.borrow::()?.inner), - _ => Err("must be a string or a Url".into_lua_err())?, + Value::String(s) => me.loc.starts_with(s.as_bytes().into_os_str()?), + Value::UserData(ud) => me.starts_with(&*ud.borrow::()?), + _ => Err("must be a string or Url".into_lua_err())?, }) }); methods.add_method("ends_with", |_, me, child: Value| { Ok(match child { - Value::String(s) => me.ends_with(s.to_str()?.as_ref()), - Value::UserData(ud) => me.ends_with(&ud.borrow::()?.inner), - _ => Err("must be a string or a Url".into_lua_err())?, + Value::String(s) => me.loc.ends_with(s.as_bytes().into_os_str()?), + Value::UserData(ud) => me.ends_with(&*ud.borrow::()?), + _ => Err("must be a string or Url".into_lua_err())?, }) }); methods.add_method("strip_prefix", |_, me, base: Value| { - let urn = match base { - Value::String(s) => me.strip_prefix(Self::try_from(s.as_bytes().as_ref())?), - Value::UserData(ud) => me.strip_prefix(&ud.borrow::()?.inner), - _ => Err("must be a string or a Url".into_lua_err())?, + let path = match base { + Value::String(s) => me.loc.strip_prefix(s.as_bytes().into_os_str()?).ok(), + Value::UserData(ud) => me.strip_prefix(&*ud.borrow::()?).map(AsRef::as_ref), + _ => Err("must be a string or Url".into_lua_err())?, }; - Ok(urn.map(Deref::deref).map(Self::new)) // TODO: return `Urn` instead of `Url` + Ok(path.map(Self::new)) // TODO: return `Path` instead of `Url` }); methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index 8a7e137f..71feb9ab 100644 --- a/yazi-config/src/mgr/mgr.rs +++ b/yazi-config/src/mgr/mgr.rs @@ -33,7 +33,7 @@ impl Mgr { } let home = UrlBuf::from(dirs::home_dir().unwrap_or_default()); - let cwd = if let Some(u) = CWD.load().strip_prefix(home) { + let cwd = if let Some(u) = CWD.load().strip_prefix(&home) { format!("~{}{}", std::path::MAIN_SEPARATOR, u.display()) } else { format!("{}", CWD.load().display()) diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index b527453d..5c2dc03f 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -36,8 +36,5 @@ tokio-util = { workspace = true } tracing = { workspace = true } unicode-width = { workspace = true } -[target."cfg(unix)".dependencies] -libc = { workspace = true } - [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index f43538b2..4c99bbf9 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -38,3 +38,6 @@ windows-sys = { version = "0.60.2", features = [ "Win32_Storage_FileSystem" ] } [target.'cfg(target_os = "macos")'.dependencies] core-foundation-sys = { workspace = true } objc = { workspace = true } + +[target.'cfg(not(target_os = "android"))'.dependencies] +trash = "5.2.3" diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index bb339dca..e6610c02 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref}; +use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::PathBuf}; use anyhow::Result; use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn, UrnBuf}; @@ -9,7 +9,7 @@ use crate::{cha::Cha, provider}; pub struct File { pub url: UrlBuf, pub cha: Cha, - pub link_to: Option, + pub link_to: Option, } impl Deref for File { diff --git a/yazi-fs/src/fns.rs b/yazi-fs/src/fns.rs index cf55d9fa..bdafc13a 100644 --- a/yazi-fs/src/fns.rs +++ b/yazi-fs/src/fns.rs @@ -31,59 +31,6 @@ pub fn ok_or_not_found(result: io::Result) -> io::Result { } } -#[inline] -pub async fn paths_to_same_file(a: impl AsRef, b: impl AsRef) -> bool { - _paths_to_same_file(a.as_ref(), b.as_ref()).await.unwrap_or(false) -} - -#[cfg(unix)] -async fn _paths_to_same_file(a: &Path, b: &Path) -> io::Result { - use std::os::unix::fs::MetadataExt; - - let (a_, b_) = (fs::symlink_metadata(a).await?, fs::symlink_metadata(b).await?); - Ok( - a_.ino() == b_.ino() - && a_.dev() == b_.dev() - && fs::canonicalize(a).await? == fs::canonicalize(b).await?, - ) -} - -#[cfg(windows)] -async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result { - use std::os::windows::{ffi::OsStringExt, io::AsRawHandle}; - - use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}}; - - async fn final_name(p: &Path) -> std::io::Result { - let file = fs::OpenOptions::new() - .access_mode(0) - .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) - .open(p) - .await?; - - tokio::task::spawn_blocking(move || { - let mut buf = [0u16; MAX_PATH as usize]; - let len = unsafe { - GetFinalPathNameByHandleW( - file.as_raw_handle() as HANDLE, - buf.as_mut_ptr(), - buf.len() as u32, - VOLUME_NAME_DOS, - ) - }; - - if len == 0 { - Err(std::io::Error::last_os_error()) - } else { - Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize]))) - } - }) - .await? - } - - Ok(final_name(a).await? == final_name(b).await?) -} - pub async fn realname(u: &UrlBuf) -> Option { let name = u.file_name()?; if *u == provider::canonicalize(u).await.ok()? { @@ -170,7 +117,7 @@ pub fn copy_with_progress( tokio::spawn({ let (from, to) = (from.clone(), to.clone()); async move { - tick_tx.send(_copy_with_progress(from, to, cha).await).ok(); + tick_tx.send(provider::copy(&from, &to, cha).await).ok(); } }); @@ -213,54 +160,6 @@ pub fn copy_with_progress( rx } -async fn _copy_with_progress(from: UrlBuf, to: UrlBuf, cha: Cha) -> io::Result { - let mut ft = std::fs::FileTimes::new(); - cha.atime.map(|t| ft = ft.set_accessed(t)); - cha.mtime.map(|t| ft = ft.set_modified(t)); - #[cfg(target_os = "macos")] - { - use std::os::macos::fs::FileTimesExt; - cha.btime.map(|t| ft = ft.set_created(t)); - } - #[cfg(windows)] - { - use std::os::windows::fs::FileTimesExt; - cha.btime.map(|t| ft = ft.set_created(t)); - } - - #[cfg(any(target_os = "linux", target_os = "android"))] - { - use std::os::{fd::AsRawFd, unix::fs::OpenOptionsExt}; - - tokio::task::spawn_blocking(move || { - let mut reader = std::fs::File::open(from)?; - let mut writer = std::fs::OpenOptions::new() - .mode(cha.mode as u32) // Do not remove `as u32`, https://github.com/termux/termux-packages/pull/22481 - .write(true) - .create(true) - .truncate(true) - .open(to)?; - - let written = std::io::copy(&mut reader, &mut writer)?; - unsafe { libc::fchmod(writer.as_raw_fd(), cha.mode) }; - writer.set_times(ft).ok(); - - Ok(written) - }) - .await? - } - - #[cfg(not(any(target_os = "linux", target_os = "android")))] - { - tokio::task::spawn_blocking(move || { - let written = std::fs::copy(from, &to)?; - std::fs::File::options().write(true).open(to).and_then(|f| f.set_times(ft)).ok(); - Ok(written) - }) - .await? - } -} - pub async fn remove_dir_clean(dir: &UrlBuf) { let Ok(mut it) = provider::read_dir(dir).await else { return }; diff --git a/yazi-fs/src/path/mod.rs b/yazi-fs/src/path/mod.rs index 09b2a3f5..cf2daeb9 100644 --- a/yazi-fs/src/path/mod.rs +++ b/yazi-fs/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(clean expand path); +yazi_macro::mod_flat!(clean expand path relative); diff --git a/yazi-fs/src/path/path.rs b/yazi-fs/src/path/path.rs index 228590be..b4cb7162 100644 --- a/yazi-fs/src/path/path.rs +++ b/yazi-fs/src/path/path.rs @@ -1,7 +1,6 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}, future::Future, io, path::PathBuf}; +use std::{borrow::Cow, ffi::{OsStr, OsString}, future::Future, io}; -use anyhow::{Result, bail}; -use yazi_shared::{loc::LocBuf, url::{UrlBuf, UrlCow}}; +use yazi_shared::url::UrlBuf; use crate::provider; @@ -63,50 +62,6 @@ async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result { Ok(url) } -pub fn url_relative_to<'a>( - from: impl Into>, - to: impl Into>, -) -> Result> { - url_relative_to_impl(from.into(), to.into()) -} - -pub fn url_relative_to_impl<'a>(from: UrlCow<'a>, to: UrlCow<'a>) -> Result> { - use yazi_shared::url::Component::*; - - if from.is_absolute() != to.is_absolute() { - return if to.is_absolute() { - Ok(to) - } else { - bail!("Urls must be both absolute or both relative: {from:?} and {to:?}"); - }; - } - - if from.covariant(&to) { - return Ok(UrlBuf { loc: LocBuf::zeroed("."), scheme: to.scheme().clone() }.into()); - } - - let (mut f_it, mut t_it) = (from.components(), to.components()); - let (f_head, t_head) = loop { - match (f_it.next(), t_it.next()) { - (Some(Scheme(a)), Some(Scheme(b))) if a.covariant(b) => {} - (Some(RootDir), Some(RootDir)) => {} - (Some(Prefix(a)), Some(Prefix(b))) if a == b => {} - (Some(Scheme(_) | Prefix(_) | RootDir), _) | (_, Some(Scheme(_) | Prefix(_) | RootDir)) => { - return Ok(to); - } - (None, None) => break (None, None), - (a, b) if a != b => break (a, b), - _ => (), - } - }; - - let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir); - let rest = t_head.into_iter().chain(t_it); - - let buf: PathBuf = dots.chain(rest).collect(); - Ok(UrlBuf { loc: LocBuf::zeroed(buf), scheme: to.scheme().clone() }.into()) -} - #[cfg(windows)] pub fn backslash_to_slash(p: &std::path::Path) -> Cow<'_, std::path::Path> { let bytes = p.as_os_str().as_encoded_bytes(); @@ -125,63 +80,61 @@ pub fn backslash_to_slash(p: &std::path::Path) -> Cow<'_, std::path::Path> { for &b in rest { out.push(if b == b'\\' { b'/' } else { b }); } - Cow::Owned(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(out) })) + Cow::Owned(std::path::PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(out) })) } #[cfg(test)] mod tests { - use super::url_relative_to; + use yazi_shared::url::UrlCow; + + use crate::path::url_relative_to; #[test] - fn test_path_relative_to() { + fn test_url_relative_to() { yazi_shared::init_tests(); - fn assert(from: &str, to: &str, ret: &str) { - assert_eq!( - url_relative_to(&from.parse().unwrap(), &to.parse().unwrap()).unwrap(), - ret.parse().unwrap() - ); - } - #[cfg(unix)] - { + let cases = [ // Same urls - assert("", "", "."); - assert(".", ".", "."); - assert("/a", "/a", "."); - assert("regular:///", "/", "."); - assert("regular://", "regular://", "."); - assert("regular://", "search://kw/", "search://kw/."); - assert("regular:///b", "search://kw//b", "search://kw/."); - + ("", "", "."), + (".", ".", "."), + ("/a", "/a", "."), + ("regular:///", "/", "."), + ("regular://", "regular://", "."), + ("regular://", "search://kw/", "search://kw/."), + ("regular:///b", "search://kw//b", "search://kw/."), // Relative urls - assert("foo", "bar", "../bar"); - + ("foo", "bar", "../bar"), // Absolute urls - assert("/a/b/c", "/a/b", "../"); - assert("/a/b", "/a/b/c", "c"); - assert("/a/b/d", "/a/b/c", "../c"); - assert("/a/b/c", "/a", "../../"); - assert("/a/b/b", "/a/a/b", "../../a/b"); - - assert("regular:///a/b", "regular:///a/b/c", "c"); - assert("/a/b/c/", "search://kw//a/d/", "search://kw/../../d"); - assert("search://kw//a/b/c", "search://kw//a/b", "search://kw/../"); - + ("/a/b/c", "/a/b", ".."), + ("/a/b", "/a/b/c", "c"), + ("/a/b/d", "/a/b/c", "../c"), + ("/a/b/c", "/a", "../.."), + ("/a/b/b", "/a/a/b", "../../a/b"), + ("regular:///a/b", "regular:///a/b/c", "c"), + ("/a/b/c/", "search://kw//a/d/", "search://kw/../../d"), + ("search://kw//a/b/c", "search://kw//a/b", "search://kw/.."), // Different schemes - assert("", "sftp://test/", "sftp://test/"); - assert("a", "sftp://test/", "sftp://test/"); - assert("a", "sftp://test/b", "sftp://test/b"); - assert("/a", "sftp://test//b", "sftp://test//b"); - assert("sftp://test//a/b", "sftp://test//a/d", "sftp://test/../d"); - } + ("", "sftp://test/", "sftp://test/"), + ("a", "sftp://test/", "sftp://test/"), + ("a", "sftp://test/b", "sftp://test/b"), + ("/a", "sftp://test//b", "sftp://test//b"), + ("sftp://test//a/b", "sftp://test//a/d", "sftp://test:0:0/../d"), + ]; + #[cfg(windows)] - { - assert(r"C:\a\b\c", r"C:\a\b", r"..\"); - assert(r"C:\a\b", r"C:\a\b\c", "c"); - assert(r"C:\a\b\d", r"C:\a\b\c", r"..\c"); - assert(r"C:\a\b\c", r"C:\a", r"..\..\"); - assert(r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b"); + let cases = [ + (r"C:\a\b\c", r"C:\a\b", r".."), + (r"C:\a\b", r"C:\a\b\c", "c"), + (r"C:\a\b\d", r"C:\a\b\c", r"..\c"), + (r"C:\a\b\c", r"C:\a", r"..\.."), + (r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b"), + ]; + + for (from, to, expected) in cases { + let from: UrlCow = from.try_into().unwrap(); + let to: UrlCow = to.try_into().unwrap(); + assert_eq!(format!("{:?}", url_relative_to(from, to).unwrap().as_url()), expected); } } } diff --git a/yazi-fs/src/path/relative.rs b/yazi-fs/src/path/relative.rs new file mode 100644 index 00000000..b6daf65d --- /dev/null +++ b/yazi-fs/src/path/relative.rs @@ -0,0 +1,51 @@ +use std::{borrow::Cow, path::{Path, PathBuf}}; + +use anyhow::{Result, bail}; +use yazi_shared::{loc::LocBuf, url::{Url, UrlBuf, UrlCow}}; + +pub fn path_relative_to<'a>( + from: impl AsRef, + to: &'a impl AsRef, +) -> Result> { + Ok(match url_relative_to(Url::regular(&from).into(), Url::regular(to).into())? { + UrlCow::Borrowed(url) => Cow::Borrowed(url.loc.as_path()), + UrlCow::Owned(url) => Cow::Owned(url.loc.into_path()), + }) +} + +pub(super) fn url_relative_to<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> { + use yazi_shared::url::Component::*; + + if from.is_absolute() != to.is_absolute() { + return if to.is_absolute() { + Ok(to) + } else { + bail!("Urls must be both absolute or both relative: {from:?} and {to:?}"); + }; + } + + if from.covariant(&to) { + return Ok(UrlBuf { loc: LocBuf::zeroed("."), scheme: to.scheme().clone() }.into()); + } + + let (mut f_it, mut t_it) = (from.components(), to.components()); + let (f_head, t_head) = loop { + match (f_it.next(), t_it.next()) { + (Some(Scheme(a)), Some(Scheme(b))) if a.covariant(b) => {} + (Some(RootDir), Some(RootDir)) => {} + (Some(Prefix(a)), Some(Prefix(b))) if a == b => {} + (Some(Scheme(_) | Prefix(_) | RootDir), _) | (_, Some(Scheme(_) | Prefix(_) | RootDir)) => { + return Ok(to); + } + (None, None) => break (None, None), + (a, b) if a != b => break (a, b), + _ => (), + } + }; + + let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir); + let rest = t_head.into_iter().chain(t_it); + + let buf: PathBuf = dots.chain(rest).collect(); + Ok(UrlBuf { loc: LocBuf::zeroed(buf), scheme: to.scheme().clone() }.into()) +} diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index e8468ff6..5ec5e5ba 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -1,90 +1,270 @@ use std::{io, path::{Path, PathBuf}}; -use crate::provider::local::{Gate, ReadDir, ReadDirSync, RwFile}; +use crate::{cha::Cha, provider::local::{Gate, ReadDir, ReadDirSync, RwFile}}; pub struct Local; impl Local { #[inline] - pub async fn canonicalize(path: impl AsRef) -> io::Result { + pub fn cache

(_: P) -> Option + where + P: AsRef, + { + None + } + + #[inline] + pub async fn canonicalize

(path: P) -> io::Result + where + P: AsRef, + { tokio::fs::canonicalize(path).await } #[inline] - pub async fn create(path: impl AsRef) -> io::Result { + pub async fn copy(from: P, to: Q, cha: Cha) -> io::Result + where + P: AsRef, + Q: AsRef, + { + let from = from.as_ref().to_owned(); + let to = to.as_ref().to_owned(); + Self::copy_impl(from, to, cha).await + } + + async fn copy_impl(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result { + let mut ft = std::fs::FileTimes::new(); + cha.atime.map(|t| ft = ft.set_accessed(t)); + cha.mtime.map(|t| ft = ft.set_modified(t)); + #[cfg(target_os = "macos")] + { + use std::os::macos::fs::FileTimesExt; + cha.btime.map(|t| ft = ft.set_created(t)); + } + #[cfg(windows)] + { + use std::os::windows::fs::FileTimesExt; + cha.btime.map(|t| ft = ft.set_created(t)); + } + + #[cfg(any(target_os = "linux", target_os = "android"))] + { + use std::os::{fd::AsRawFd, unix::fs::OpenOptionsExt}; + + tokio::task::spawn_blocking(move || { + let mut reader = std::fs::File::open(from)?; + let mut writer = std::fs::OpenOptions::new() + .mode(cha.mode as u32) // Do not remove `as u32`, https://github.com/termux/termux-packages/pull/22481 + .write(true) + .create(true) + .truncate(true) + .open(to)?; + + let written = std::io::copy(&mut reader, &mut writer)?; + unsafe { libc::fchmod(writer.as_raw_fd(), cha.mode) }; + writer.set_times(ft).ok(); + + Ok(written) + }) + .await? + } + + #[cfg(not(any(target_os = "linux", target_os = "android")))] + { + tokio::task::spawn_blocking(move || { + let written = std::fs::copy(from, &to)?; + std::fs::File::options().write(true).open(to).and_then(|f| f.set_times(ft)).ok(); + Ok(written) + }) + .await? + } + } + + #[inline] + pub async fn create

(path: P) -> io::Result + where + P: AsRef, + { Gate::default().write(true).create(true).truncate(true).open(path).await.map(Into::into) } #[inline] - pub async fn create_dir(path: impl AsRef) -> io::Result<()> { + pub async fn create_dir

(path: P) -> io::Result<()> + where + P: AsRef, + { tokio::fs::create_dir(path).await } #[inline] - pub async fn create_dir_all(path: impl AsRef) -> io::Result<()> { + pub async fn create_dir_all

(path: P) -> io::Result<()> + where + P: AsRef, + { tokio::fs::create_dir_all(path).await } #[inline] - pub async fn hard_link(original: impl AsRef, link: impl AsRef) -> io::Result<()> { + pub async fn hard_link(original: P, link: Q) -> io::Result<()> + where + P: AsRef, + Q: AsRef, + { tokio::fs::hard_link(original, link).await } #[inline] - pub async fn metadata(url: impl AsRef) -> io::Result { + pub async fn metadata

(url: P) -> io::Result + where + P: AsRef, + { tokio::fs::metadata(url).await } #[inline] - pub async fn open(path: impl AsRef) -> io::Result { + pub async fn open

(path: P) -> io::Result + where + P: AsRef, + { Gate::default().read(true).open(path).await.map(Into::into) } #[inline] - pub async fn read(path: impl AsRef) -> io::Result> { tokio::fs::read(path).await } + pub async fn read

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

(path: P) -> io::Result + where + P: AsRef, + { tokio::fs::read_dir(path).await.map(Into::into) } #[inline] - pub fn read_dir_sync(path: impl AsRef) -> io::Result { + pub fn read_dir_sync

(path: P) -> io::Result + where + P: AsRef, + { std::fs::read_dir(path).map(Into::into) } #[inline] - pub async fn read_link(url: impl AsRef) -> io::Result { + pub async fn read_link

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

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

(path: P) -> io::Result<()> + where + P: AsRef, + { tokio::fs::remove_dir(path).await } #[inline] - pub async fn remove_dir_all(path: impl AsRef) -> io::Result<()> { + pub async fn remove_dir_all

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

(path: P) -> io::Result<()> + where + P: AsRef, + { tokio::fs::remove_file(path).await } #[inline] - pub async fn rename(from: impl AsRef, to: impl AsRef) -> io::Result<()> { + pub async fn rename(from: P, to: Q) -> io::Result<()> + where + P: AsRef, + Q: AsRef, + { tokio::fs::rename(from, to).await } #[inline] - pub async fn symlink_dir(original: impl AsRef, link: impl AsRef) -> io::Result<()> { + pub async fn same(a: P, b: Q) -> io::Result + where + P: AsRef, + Q: AsRef, + { + Self::same_impl(a.as_ref(), b.as_ref()).await + } + + #[cfg(unix)] + async fn same_impl(a: &Path, b: &Path) -> io::Result { + use std::os::unix::fs::MetadataExt; + + let (a_, b_) = (tokio::fs::symlink_metadata(a).await?, tokio::fs::symlink_metadata(b).await?); + Ok( + a_.ino() == b_.ino() + && a_.dev() == b_.dev() + && tokio::fs::canonicalize(a).await? == tokio::fs::canonicalize(b).await?, + ) + } + + #[cfg(windows)] + async fn same_impl(a: &Path, b: &Path) -> io::Result { + use std::{ffi::OsString, os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}}; + + use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}}; + + async fn final_name(path: &Path) -> io::Result { + let path = path.to_owned(); + tokio::task::spawn_blocking(move || { + let file = std::fs::OpenOptions::new() + .access_mode(0) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) + .open(path)?; + + let mut buf = [0u16; MAX_PATH as usize]; + let len = unsafe { + GetFinalPathNameByHandleW( + file.as_raw_handle() as HANDLE, + buf.as_mut_ptr(), + buf.len() as u32, + VOLUME_NAME_DOS, + ) + }; + + if len == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize]))) + } + }) + .await? + } + + Ok(final_name(a).await? == final_name(b).await?) + } + + #[inline] + pub async fn symlink_dir(original: P, link: Q) -> io::Result<()> + where + P: AsRef, + Q: AsRef, + { #[cfg(unix)] { tokio::fs::symlink(original, link).await @@ -96,7 +276,11 @@ impl Local { } #[inline] - pub async fn symlink_file(original: impl AsRef, link: impl AsRef) -> io::Result<()> { + pub async fn symlink_file(original: P, link: Q) -> io::Result<()> + where + P: AsRef, + Q: AsRef, + { #[cfg(unix)] { tokio::fs::symlink(original, link).await @@ -108,17 +292,52 @@ impl Local { } #[inline] - pub async fn symlink_metadata(path: impl AsRef) -> io::Result { + pub async fn symlink_metadata

(path: P) -> io::Result + where + P: AsRef, + { tokio::fs::symlink_metadata(path).await } #[inline] - pub fn symlink_metadata_sync(path: impl AsRef) -> io::Result { + pub fn symlink_metadata_sync

(path: P) -> io::Result + where + P: AsRef, + { std::fs::symlink_metadata(path) } + pub async fn trash

(path: P) -> io::Result<()> + where + P: AsRef, + { + let path = path.as_ref().to_owned(); + tokio::task::spawn_blocking(move || { + #[cfg(target_os = "android")] + { + Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS for trash operation")) + } + #[cfg(target_os = "macos")] + { + use trash::{TrashContext, macos::{DeleteMethod, TrashContextExtMacos}}; + let mut ctx = TrashContext::default(); + ctx.set_delete_method(DeleteMethod::NsFileManager); + ctx.delete(path).map_err(io::Error::other) + } + #[cfg(all(not(target_os = "macos"), not(target_os = "android")))] + { + trash::delete(path).map_err(io::Error::other) + } + }) + .await? + } + #[inline] - pub async fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> io::Result<()> { + pub async fn write(path: P, contents: C) -> io::Result<()> + where + P: AsRef, + C: AsRef<[u8]>, + { tokio::fs::write(path, contents).await } } diff --git a/yazi-fs/src/provider/local/rw_file.rs b/yazi-fs/src/provider/local/rw_file.rs index 43ed6858..47beac60 100644 --- a/yazi-fs/src/provider/local/rw_file.rs +++ b/yazi-fs/src/provider/local/rw_file.rs @@ -15,9 +15,4 @@ impl From for crate::provider::RwFile { impl RwFile { #[inline] pub fn reader(self) -> tokio::io::BufReader { tokio::io::BufReader::new(self.0) } - - #[inline] - pub async fn reader_sync(self) -> std::io::BufReader { - std::io::BufReader::new(self.0.into_std().await) - } } diff --git a/yazi-fs/src/provider/provider.rs b/yazi-fs/src/provider/provider.rs index 911b1dfa..2f53b188 100644 --- a/yazi-fs/src/provider/provider.rs +++ b/yazi-fs/src/provider/provider.rs @@ -1,11 +1,22 @@ -use std::io; +use std::{io, path::{Path, PathBuf}}; use yazi_shared::url::{Url, UrlBuf}; -use crate::provider::{ReadDir, ReadDirSync, RwFile, local::Local}; +use crate::{cha::Cha, provider::{ReadDir, ReadDirSync, RwFile, local::Local}}; #[inline] -pub async fn canonicalize<'a>(url: impl Into>) -> io::Result { +pub fn cache<'a, U>(url: U) -> Option +where + U: Into>, +{ + if let Some(path) = url.into().as_path() { Local::cache(path) } else { None } +} + +#[inline] +pub async fn canonicalize<'a, U>(url: U) -> io::Result +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::canonicalize(path).await.map(Into::into) } else { @@ -14,7 +25,23 @@ pub async fn canonicalize<'a>(url: impl Into>) -> io::Result { } #[inline] -pub async fn create<'a>(url: impl Into>) -> io::Result { +pub async fn copy<'a, U, V>(from: U, to: V, cha: Cha) -> io::Result +where + U: Into>, + V: Into>, +{ + if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) { + Local::copy(from, to, cha).await + } else { + Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) + } +} + +#[inline] +pub async fn create<'a, U>(url: U) -> io::Result +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::create(path).await.map(Into::into) } else { @@ -23,7 +50,10 @@ pub async fn create<'a>(url: impl Into>) -> io::Result { } #[inline] -pub async fn create_dir<'a>(url: impl Into>) -> io::Result<()> { +pub async fn create_dir<'a, U>(url: U) -> io::Result<()> +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::create_dir(path).await } else { @@ -32,7 +62,10 @@ pub async fn create_dir<'a>(url: impl Into>) -> io::Result<()> { } #[inline] -pub async fn create_dir_all<'a>(url: impl Into>) -> io::Result<()> { +pub async fn create_dir_all<'a, U>(url: U) -> io::Result<()> +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::create_dir_all(path).await } else { @@ -41,10 +74,11 @@ pub async fn create_dir_all<'a>(url: impl Into>) -> io::Result<()> { } #[inline] -pub async fn hard_link<'a>( - original: impl Into>, - link: impl Into>, -) -> io::Result<()> { +pub async fn hard_link<'a, U, V>(original: U, link: V) -> io::Result<()> +where + U: Into>, + V: Into>, +{ if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) { Local::hard_link(original, link).await } else { @@ -53,7 +87,10 @@ pub async fn hard_link<'a>( } #[inline] -pub async fn metadata<'a>(url: impl Into>) -> io::Result { +pub async fn metadata<'a, U>(url: U) -> io::Result +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::metadata(path).await } else { @@ -62,7 +99,10 @@ pub async fn metadata<'a>(url: impl Into>) -> io::Result(url: impl Into>) -> io::Result { +pub async fn open<'a, U>(url: U) -> io::Result +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::open(path).await.map(Into::into) } else { @@ -71,7 +111,10 @@ pub async fn open<'a>(url: impl Into>) -> io::Result { } #[inline] -pub async fn read_dir<'a>(url: impl Into>) -> io::Result { +pub async fn read_dir<'a, U>(url: U) -> io::Result +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::read_dir(path).await.map(Into::into) } else { @@ -80,7 +123,10 @@ pub async fn read_dir<'a>(url: impl Into>) -> io::Result { } #[inline] -pub fn read_dir_sync<'a>(url: impl Into>) -> io::Result { +pub fn read_dir_sync<'a, U>(url: U) -> io::Result +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::read_dir_sync(path).map(Into::into) } else { @@ -89,16 +135,22 @@ pub fn read_dir_sync<'a>(url: impl Into>) -> io::Result { } #[inline] -pub async fn read_link<'a>(url: impl Into>) -> io::Result { +pub async fn read_link<'a, U>(url: U) -> io::Result +where + U: Into>, +{ if let Some(path) = url.into().as_path() { - Local::read_link(path).await.map(Into::into) + Local::read_link(path).await } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) } } #[inline] -pub async fn remove_dir<'a>(url: impl Into>) -> io::Result<()> { +pub async fn remove_dir<'a, U>(url: U) -> io::Result<()> +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::remove_dir(path).await } else { @@ -107,7 +159,10 @@ pub async fn remove_dir<'a>(url: impl Into>) -> io::Result<()> { } #[inline] -pub async fn remove_dir_all<'a>(url: impl Into>) -> io::Result<()> { +pub async fn remove_dir_all<'a, U>(url: U) -> io::Result<()> +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::remove_dir_all(path).await } else { @@ -116,7 +171,10 @@ pub async fn remove_dir_all<'a>(url: impl Into>) -> io::Result<()> { } #[inline] -pub async fn remove_file<'a>(url: impl Into>) -> io::Result<()> { +pub async fn remove_file<'a, U>(url: U) -> io::Result<()> +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::remove_file(path).await } else { @@ -125,7 +183,11 @@ pub async fn remove_file<'a>(url: impl Into>) -> io::Result<()> { } #[inline] -pub async fn rename<'a>(from: impl Into>, to: impl Into>) -> io::Result<()> { +pub async fn rename<'a, U, V>(from: U, to: V) -> io::Result<()> +where + U: Into>, + V: Into>, +{ if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) { Local::rename(from, to).await } else { @@ -134,11 +196,24 @@ pub async fn rename<'a>(from: impl Into>, to: impl Into>) -> io: } #[inline] -pub async fn symlink_dir<'a>( - original: impl Into>, - link: impl Into>, -) -> io::Result<()> { - if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) { +pub async fn same<'a, U, V>(a: U, b: V) -> io::Result +where + U: Into>, + V: Into>, +{ + if let (Some(a), Some(b)) = (a.into().as_path(), b.into().as_path()) { + Local::same(a, b).await + } else { + Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) + } +} + +#[inline] +pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()> +where + U: Into>, +{ + if let Some(link) = link.into().as_path() { Local::symlink_dir(original, link).await } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) @@ -146,11 +221,11 @@ pub async fn symlink_dir<'a>( } #[inline] -pub async fn symlink_file<'a>( - original: impl Into>, - link: impl Into>, -) -> io::Result<()> { - if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) { +pub async fn symlink_file<'a, U>(original: &Path, link: U) -> io::Result<()> +where + U: Into>, +{ + if let Some(link) = link.into().as_path() { Local::symlink_file(original, link).await } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) @@ -158,7 +233,10 @@ pub async fn symlink_file<'a>( } #[inline] -pub async fn symlink_metadata<'a>(url: impl Into>) -> io::Result { +pub async fn symlink_metadata<'a, U>(url: U) -> io::Result +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::symlink_metadata(path).await } else { @@ -166,7 +244,11 @@ pub async fn symlink_metadata<'a>(url: impl Into>) -> io::Result(url: impl Into>) -> io::Result { +#[inline] +pub fn symlink_metadata_sync<'a, U>(url: U) -> io::Result +where + U: Into>, +{ if let Some(path) = url.into().as_path() { Local::symlink_metadata_sync(path) } else { @@ -175,7 +257,23 @@ pub fn symlink_metadata_sync<'a>(url: impl Into>) -> io::Result(url: impl Into>, contents: impl AsRef<[u8]>) -> io::Result<()> { +pub async fn trash<'a, U>(url: U) -> io::Result<()> +where + U: Into>, +{ + if let Some(path) = url.into().as_path() { + Local::trash(path).await + } else { + Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) + } +} + +#[inline] +pub async fn write<'a, U, C>(url: U, contents: C) -> io::Result<()> +where + U: Into>, + C: AsRef<[u8]>, +{ if let Some(path) = url.into().as_path() { Local::write(path, contents).await } else { diff --git a/yazi-fs/src/provider/rw_file.rs b/yazi-fs/src/provider/rw_file.rs index d0704f4c..6d5ab849 100644 --- a/yazi-fs/src/provider/rw_file.rs +++ b/yazi-fs/src/provider/rw_file.rs @@ -1,4 +1,4 @@ -use crate::provider::{BufRead, BufReadSync}; +use crate::provider::BufRead; pub enum RwFile { Local(super::local::RwFile), @@ -11,11 +11,4 @@ impl RwFile { RwFile::Local(local) => Box::new(local.reader()), } } - - #[inline] - pub async fn reader_sync(self) -> Box { - match self { - RwFile::Local(local) => Box::new(local.reader_sync().await), - } - } } diff --git a/yazi-scheduler/Cargo.toml b/yazi-scheduler/Cargo.toml index 757df4dd..1aceba12 100644 --- a/yazi-scheduler/Cargo.toml +++ b/yazi-scheduler/Cargo.toml @@ -32,6 +32,3 @@ tracing = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } - -[target.'cfg(not(target_os = "android"))'.dependencies] -trash = "5.2.3" diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 5b607c43..9ab415d9 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,10 +1,10 @@ -use std::collections::VecDeque; +use std::{borrow::Cow, collections::VecDeque}; -use anyhow::{Result, anyhow}; +use anyhow::{Result, anyhow, bail}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::YAZI; -use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path::{skip_url, url_relative_to}, provider::{self, DirEntry}}; +use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{self, DirEntry}}; use yazi_shared::{Id, url::{UrlBuf, UrlCow}}; use super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; @@ -61,21 +61,23 @@ impl File { FileIn::Link(task) => { let cha = task.cha.unwrap(); - let src = if task.resolve { + let src: Cow<_> = if task.resolve { match provider::read_link(&task.from).await { - Ok(u) => UrlCow::from(u), + Ok(p) => p.into(), Err(e) if e.kind() == NotFound => { warn!("Link task partially done: {task:?}"); return Ok(self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?); } Err(e) => Err(e)?, } + } else if task.from.scheme.covariant(&task.to.scheme) { + task.from.loc.as_path().into() } else { - UrlCow::from(&task.from) + bail!("Source and target must be on the same filesystem: {task:?}") }; let src = if task.relative { - url_relative_to(provider::canonicalize(&task.to.parent_url().unwrap()).await?, src)? + path_relative_to(provider::canonicalize(&task.to.parent_url().unwrap()).await?.loc, &src)? } else { src }; @@ -123,21 +125,7 @@ impl File { self.prog.send(TaskProg::Adv(task.id, 1, task.length))? } FileIn::Trash(task) => { - tokio::task::spawn_blocking(move || { - #[cfg(target_os = "macos")] - { - use trash::{TrashContext, macos::{DeleteMethod, TrashContextExtMacos}}; - let mut ctx = TrashContext::default(); - ctx.set_delete_method(DeleteMethod::NsFileManager); - ctx.delete(&task.target)?; - } - #[cfg(all(not(target_os = "macos"), not(target_os = "android")))] - { - trash::delete(&task.target)?; - } - Ok::<_, anyhow::Error>(()) - }) - .await??; + provider::trash(&task.target).await?; self.prog.send(TaskProg::Adv(task.id, 1, task.length))?; } } diff --git a/yazi-scheduler/src/process/process.rs b/yazi-scheduler/src/process/process.rs index 02bbdd2a..a41bc7c9 100644 --- a/yazi-scheduler/src/process/process.rs +++ b/yazi-scheduler/src/process/process.rs @@ -20,7 +20,7 @@ impl Process { AppProxy::stop().await; let (id, cmd) = (task.id, task.cmd.clone()); - let result = super::shell(task.into()); + 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}")); return self.succ(id); @@ -41,7 +41,7 @@ impl Process { pub async fn orphan(&self, task: ProcessInOrphan) -> Result<()> { let id = task.id; - match super::shell(task.into()) { + match super::shell(task.into()).await { Ok(_) => self.succ(id)?, Err(e) => { self.prog.send(TaskProg::New(id, 0))?; @@ -60,7 +60,8 @@ impl Process { args: task.args, piped: true, orphan: false, - })?; + }) + .await?; let mut stdout = BufReader::new(child.stdout.take().unwrap()).lines(); let mut stderr = BufReader::new(child.stderr.take().unwrap()).lines(); diff --git a/yazi-scheduler/src/process/shell.rs b/yazi-scheduler/src/process/shell.rs index 1c61c01b..e4c73364 100644 --- a/yazi-scheduler/src/process/shell.rs +++ b/yazi-scheduler/src/process/shell.rs @@ -1,7 +1,8 @@ -use std::{ffi::OsString, process::Stdio}; +use std::{borrow::Cow, ffi::OsString, process::Stdio}; -use anyhow::Result; +use anyhow::{Result, bail}; use tokio::process::{Child, Command}; +use yazi_fs::provider; use yazi_shared::url::UrlBuf; pub struct ShellOpt { @@ -25,41 +26,51 @@ impl ShellOpt { } } -pub fn shell(opt: ShellOpt) -> Result { - #[cfg(unix)] - return Ok(unsafe { - Command::new("sh") - .arg("-c") - .stdin(opt.stdio()) - .stdout(opt.stdio()) - .stderr(opt.stdio()) - .arg(opt.cmd) - .args(opt.args) - .current_dir(opt.cwd) - .kill_on_drop(!opt.orphan) - .pre_exec(move || { - if (opt.piped || opt.orphan) && libc::setsid() < 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) - }) - .spawn()? - }); +pub async fn shell(opt: ShellOpt) -> Result { + tokio::task::spawn_blocking(move || { + let cwd: Cow<_> = if let Some(path) = opt.cwd.as_path() { + path.into() + } else if let Some(cache) = provider::cache(&opt.cwd) { + std::fs::create_dir_all(&cache).ok(); + cache.into() + } else { + bail!("failed to determine a working directory"); + }; - #[cfg(windows)] - { - Ok( + #[cfg(unix)] + return Ok(unsafe { + Command::new("sh") + .arg("-c") + .stdin(opt.stdio()) + .stdout(opt.stdio()) + .stderr(opt.stdio()) + .arg(opt.cmd) + .args(opt.args) + .current_dir(cwd) + .kill_on_drop(!opt.orphan) + .pre_exec(move || { + if (opt.piped || opt.orphan) && libc::setsid() < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }) + .spawn()? + }); + + #[cfg(windows)] + return Ok( Command::new("cmd.exe") .raw_arg("/C") .raw_arg(parser::parse(&opt.cmd, &opt.args)) .stdin(opt.stdio()) .stdout(opt.stdio()) .stderr(opt.stdio()) - .current_dir(opt.cwd) + .current_dir(cwd) .kill_on_drop(!opt.orphan) .spawn()?, - ) - } + ); + }) + .await? } #[cfg(windows)] diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 7a5355d9..ee356b67 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -77,7 +77,7 @@ impl Scheduler { let mut ongoing = self.ongoing.lock(); let id = ongoing.add(TaskKind::User, format!("Cut {} to {}", from.display(), to.display())); - if to.starts_with(&from) && to != from { + if to.starts_with(&from) && !to.covariant(&from) { self.new_and_fail(id, "Cannot cut directory into itself").ok(); return; } @@ -110,7 +110,7 @@ impl Scheduler { .lock() .add(TaskKind::User, format!("Copy {} to {}", from.display(), to.display())); - if to.starts_with(&from) && to != from { + if to.starts_with(&from) && !to.covariant(&from) { self.new_and_fail(id, "Cannot copy directory into itself").ok(); return; } @@ -147,7 +147,7 @@ impl Scheduler { .lock() .add(TaskKind::User, format!("Hardlink {} to {}", from.display(), to.display())); - if to.starts_with(&from) && to != from { + if to.starts_with(&from) && !to.covariant(&from) { self.new_and_fail(id, "Cannot hardlink directory into itself").ok(); return; } diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 888dbd3c..ed24f436 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -21,7 +21,7 @@ hashbrown = { workspace = true } memchr = "2.7.5" ordered-float = { workspace = true } parking_lot = { workspace = true } -percent-encoding = "2.3.1" +percent-encoding = "2.3.2" serde = { workspace = true } tokio = { workspace = true } diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index 0e0d067f..f3095f7b 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -259,15 +259,15 @@ mod tests { crate::init_tests(); let cases = [ // Regular - ("/", "a", "regular:///a"), - ("/a/b", "c", "regular:///a/c"), + ("/", "a", "/a"), + ("/a/b", "c", "/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:///"), + ("/a", "", "/"), ("archive:////a.zip", "", "archive:////"), ("archive:////a.zip/b", "", "archive:////a.zip"), ("archive://:1:1//a.zip", "", "archive:////"), diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 3c11ade7..506d271b 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -17,6 +17,10 @@ impl Deref for Loc<'_> { fn deref(&self) -> &Self::Target { self.inner } } +impl AsRef for Loc<'_> { + fn as_ref(&self) -> &Path { self.inner } +} + impl<'a> From<&'a LocBuf> for Loc<'a> { fn from(value: &'a LocBuf) -> Self { Self { inner: &value.inner, uri: value.uri, urn: value.urn } diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 97f14d0b..4310b61f 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -64,11 +64,6 @@ impl AsRef for UrlBuf { fn as_ref(&self) -> &UrlBuf { self } } -// FIXME: remove -impl AsRef for UrlBuf { - fn as_ref(&self) -> &Path { &self.loc } -} - impl<'a> From<&'a UrlBuf> for Cow<'a, UrlBuf> { fn from(url: &'a UrlBuf) -> Self { Cow::Borrowed(url) } } @@ -104,16 +99,24 @@ impl UrlBuf { pub fn display(&self) -> Display<'_> { Display::new(self) } #[inline] - pub fn covariant(&self, other: &Self) -> bool { self.as_url().covariant(other.as_url()) } + pub fn covariant(&self, other: &Self) -> bool { self.as_url().covariant(other) } #[inline] pub fn parent_url(&self) -> Option> { self.as_url().parent_url() } - pub fn strip_prefix(&self, base: impl AsRef) -> Option<&Urn> { + #[inline] + pub fn starts_with<'a>(&self, base: impl Into>) -> bool { + self.as_url().starts_with(base) + } + + #[inline] + pub fn ends_with<'a>(&self, child: impl Into>) -> bool { self.as_url().ends_with(child) } + + pub fn strip_prefix<'a>(&self, base: impl Into>) -> Option<&Urn> { use Scheme as S; - let base = base.as_ref(); - let prefix = self.loc.strip_prefix(&base.loc).ok()?; + let base = base.into(); + let prefix = self.loc.strip_prefix(base.loc).ok()?; Some(Urn::new(match (&self.scheme, &base.scheme) { // Same scheme @@ -268,7 +271,7 @@ mod tests { crate::init_tests(); let cases = [ // Regular - ("/a", "b/c", "regular:///a/b/c"), + ("/a", "b/c", "/a/b/c"), // Search ("search://kw//a", "b/c", "search://kw:2:2//a/b/c"), ("search://kw:2:2//a/b/c", "d/e", "search://kw:4:4//a/b/c/d/e"), @@ -300,16 +303,16 @@ mod tests { crate::init_tests(); let cases = [ // Regular - ("/a", Some("regular:///")), + ("/a", Some("/")), ("/", None), // Search ("search://kw:2:2//a/b/c", Some("search://kw:1:1//a/b")), ("search://kw:1:1//a/b", Some("search://kw//a")), - ("search://kw//a", Some("regular:///")), + ("search://kw//a", Some("/")), // Archive ("archive://:2:1//a/b.zip/c/d", Some("archive://:1:1//a/b.zip/c")), ("archive://:1:1//a/b.zip/c", Some("archive:////a/b.zip")), - ("archive:////a/b.zip", Some("regular:///a")), + ("archive:////a/b.zip", Some("/a")), // SFTP ("sftp://remote:1:1//a/b", Some("sftp://remote//a")), ("sftp://remote:1:1//a", Some("sftp://remote//")), @@ -335,11 +338,11 @@ mod tests { const S: char = std::path::MAIN_SEPARATOR; let u: UrlBuf = "/root".parse()?; - assert_eq!(format!("{u:?}"), "regular:///root"); + assert_eq!(format!("{u:?}"), "/root"); let u = u.into_search("kw"); assert_eq!(format!("{u:?}"), "search://kw//root"); - assert_eq!(format!("{:?}", u.parent_url().unwrap()), "regular:///"); + assert_eq!(format!("{:?}", u.parent_url().unwrap()), "/"); let u = u.join("examples"); assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); @@ -354,7 +357,7 @@ mod tests { assert_eq!(format!("{u:?}"), "search://kw//root"); let u = u.parent_url().unwrap(); - assert_eq!(format!("{u:?}"), "regular:///"); + assert_eq!(format!("{u:?}"), "/"); Ok(()) } diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index 9d9f1a90..b37f332a 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -27,7 +27,7 @@ impl<'a> Encode<'a> { percent_encode(s.as_bytes(), SET) } - fn urn(&self) -> impl Display { + fn ports(&self) -> impl Display { struct D<'a>(&'a Encode<'a>); impl Display for D<'_> { @@ -64,9 +64,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()), - Scheme::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), self.urn()), - Scheme::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), self.urn()), + Scheme::Search(d) => write!(f, "search://{}{}/", Self::domain(d), self.ports()), + Scheme::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), self.ports()), + Scheme::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), self.ports()), } } } @@ -85,8 +85,8 @@ impl<'a> From<&'a UrlBuf> for EncodeTilded<'a> { fn from(url: &'a UrlBuf) -> Self { Self { loc: url.loc.as_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<'a> From<&EncodeTilded<'a>> for Encode<'a> { + fn from(value: &EncodeTilded<'a>) -> Self { Self::new(value.loc, value.scheme) } } impl Display for EncodeTilded<'_> { @@ -96,9 +96,11 @@ 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.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())), + Scheme::Search(d) => write!(f, "search~://{}{}/{loc}", E::domain(d), E::ports(&self.into())), + Scheme::Archive(d) => { + write!(f, "archive~://{}{}/{loc}", E::domain(d), E::ports(&self.into())) + } + Scheme::Sftp(d) => write!(f, "sftp~://{}{}/{loc}", E::domain(d), E::ports(&self.into())), } } } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index d89dd2d3..3f0ecda0 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -44,7 +44,11 @@ impl Equivalent for Url<'_> { // --- Debug impl Debug for Url<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}{}", Encode::from(self), self.loc.display()) + if self.scheme == Scheme::Regular { + write!(f, "{}", self.loc.display()) + } else { + write!(f, "{}{}", Encode::from(self), self.loc.display()) + } } } @@ -133,6 +137,18 @@ impl<'a> Url<'a> { }) } + #[inline] + pub fn starts_with<'b>(&self, base: impl Into>) -> bool { + let base: Url = base.into(); + self.scheme.covariant(&base.scheme) && self.loc.starts_with(base.loc) + } + + #[inline] + pub fn ends_with<'b>(&self, child: impl Into>) -> bool { + let child: Url = child.into(); + self.scheme.covariant(&child.scheme) && self.loc.ends_with(child.loc) + } + #[inline] pub fn components(&self) -> Components<'_> { Components::from(self) }