From d5f225bc4adc22231b6e254431183518747fadb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Tue, 5 Aug 2025 02:23:57 +0800 Subject: [PATCH] feat: virtual file system (#3034) --- Cargo.lock | 1 + yazi-actor/src/cmp/trigger.rs | 9 +- yazi-actor/src/lives/file.rs | 2 +- yazi-actor/src/mgr/bulk_rename.rs | 17 ++- yazi-actor/src/mgr/cd.rs | 6 +- yazi-actor/src/mgr/create.rs | 14 +- yazi-actor/src/mgr/quit.rs | 2 +- yazi-actor/src/mgr/rename.rs | 6 +- yazi-adapter/src/image.rs | 8 +- yazi-binding/src/url.rs | 31 ++--- yazi-boot/Cargo.toml | 7 +- yazi-boot/src/actions/debug.rs | 11 +- yazi-boot/src/args.rs | 4 +- yazi-boot/src/boot.rs | 34 ++--- yazi-cli/src/package/delete.rs | 2 +- yazi-cli/src/package/deploy.rs | 2 +- yazi-cli/src/package/hash.rs | 2 +- yazi-cli/src/package/package.rs | 2 +- yazi-cli/src/shared/shared.rs | 6 +- yazi-config/preset/theme-dark.toml | 12 +- yazi-config/preset/theme-light.toml | 12 +- yazi-config/preset/yazi-default.toml | 18 +-- yazi-config/src/open/open.rs | 17 +-- yazi-config/src/open/rule.rs | 6 +- yazi-config/src/pattern.rs | 79 ++++++++--- yazi-config/src/plugin/fetcher.rs | 10 +- yazi-config/src/plugin/plugin.rs | 19 +-- yazi-config/src/plugin/preloader.rs | 10 +- yazi-config/src/plugin/previewer.rs | 14 +- yazi-config/src/plugin/spotter.rs | 14 +- yazi-config/src/theme/filetype.rs | 4 +- yazi-config/src/theme/icon.rs | 2 +- yazi-core/src/mgr/watcher.rs | 6 +- yazi-core/src/tab/selected.rs | 2 +- yazi-dds/src/state.rs | 9 +- yazi-dds/src/stream.rs | 2 +- yazi-fm/build.rs | 2 +- yazi-fm/src/app/commands/bootstrap.rs | 8 +- yazi-fm/src/app/commands/quit.rs | 4 +- yazi-fs/src/calculator.rs | 16 +-- yazi-fs/src/cha/cha.rs | 6 +- yazi-fs/src/file.rs | 6 +- yazi-fs/src/files.rs | 12 +- yazi-fs/src/fns.rs | 40 +++--- yazi-fs/src/lib.rs | 2 +- yazi-fs/src/path.rs | 14 +- yazi-fs/src/provider/buffer.rs | 9 ++ yazi-fs/src/provider/dir_entry.rs | 68 ++++++++++ yazi-fs/src/provider/local/dir_entry.rs | 46 +++++++ yazi-fs/src/provider/local/gate.rs | 14 ++ .../src/{services => provider/local}/local.rs | 18 +-- yazi-fs/src/provider/local/mod.rs | 1 + yazi-fs/src/provider/local/read_dir.rs | 38 ++++++ yazi-fs/src/provider/local/rw_file.rs | 23 ++++ yazi-fs/src/provider/mod.rs | 3 + .../services.rs => provider/provider.rs} | 18 +-- yazi-fs/src/provider/read_dir.rs | 30 +++++ yazi-fs/src/provider/rw_file.rs | 21 +++ yazi-fs/src/services/mod.rs | 1 - yazi-parser/src/mgr/tab_create.rs | 2 +- yazi-plugin/preset/components/header.lua | 2 +- yazi-plugin/src/external/highlighter.rs | 8 +- yazi-plugin/src/fs/fs.rs | 31 ++--- yazi-plugin/src/loader/loader.rs | 2 +- yazi-scheduler/src/file/file.rs | 52 ++++---- yazi-scheduler/src/scheduler.rs | 4 +- yazi-shared/src/event/data.rs | 12 +- yazi-shared/src/url/component.rs | 16 ++- yazi-shared/src/url/display.rs | 4 +- yazi-shared/src/url/encode.rs | 76 +++++++++++ yazi-shared/src/url/loc.rs | 108 +++++++++------ yazi-shared/src/url/mod.rs | 2 +- yazi-shared/src/url/scheme.rs | 109 ++++++++------- yazi-shared/src/url/url.rs | 125 ++++++++++++------ 74 files changed, 908 insertions(+), 447 deletions(-) create mode 100644 yazi-fs/src/provider/buffer.rs create mode 100644 yazi-fs/src/provider/dir_entry.rs create mode 100644 yazi-fs/src/provider/local/dir_entry.rs create mode 100644 yazi-fs/src/provider/local/gate.rs rename yazi-fs/src/{services => provider/local}/local.rs (81%) create mode 100644 yazi-fs/src/provider/local/mod.rs create mode 100644 yazi-fs/src/provider/local/read_dir.rs create mode 100644 yazi-fs/src/provider/local/rw_file.rs create mode 100644 yazi-fs/src/provider/mod.rs rename yazi-fs/src/{services/services.rs => provider/provider.rs} (90%) create mode 100644 yazi-fs/src/provider/read_dir.rs create mode 100644 yazi-fs/src/provider/rw_file.rs delete mode 100644 yazi-fs/src/services/mod.rs create mode 100644 yazi-shared/src/url/encode.rs diff --git a/Cargo.lock b/Cargo.lock index 3187a76e..b6ddd520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3556,6 +3556,7 @@ dependencies = [ "clap_complete", "clap_complete_fig", "clap_complete_nushell", + "futures", "regex", "serde", "vergen-gitcl", diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index 532774c7..d7f838c7 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -1,7 +1,7 @@ use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR}; use anyhow::Result; -use yazi_fs::{CWD, expand_url, services::Local}; +use yazi_fs::{CWD, expand_url, provider}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_proxy::CmpProxy; @@ -36,8 +36,7 @@ impl Actor for Trigger { let ticket = cmp.ticket; tokio::spawn(async move { - // TODO: support VFS - let mut dir = Local::read_dir(&parent).await?; + let mut dir = provider::read_dir(&parent).await?; let mut cache = vec![]; // "/" is both a directory separator and the root directory per se @@ -68,7 +67,7 @@ impl Actor for Trigger { impl Trigger { fn split_url(s: &str) -> Option<(Url, UrnBuf)> { - let (scheme, path) = Url::parse(s.as_bytes()).ok()?; + let (scheme, path, _) = Url::parse(s.as_bytes()).ok()?; if !scheme.is_virtual() && path.as_os_str() == "~" { return None; // We don't autocomplete a `~`, but `~/` @@ -96,7 +95,7 @@ mod tests { fn compare(s: &str, parent: &str, child: &str) { let (p, c) = Trigger::split_url(s).unwrap(); let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(p); - assert_eq!((p, c.as_urn()), (parent.try_into().unwrap(), Urn::new(child))); + assert_eq!((p, c.as_urn()), (parent.parse().unwrap(), Urn::new(child))); } #[cfg(unix)] diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index 11dce848..e6483417 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -145,7 +145,7 @@ impl UserData for File { return None; } - let h = finder.filter.highlighted(me.name())?; + let h = finder.filter.highlighted(me.url.file_name()?)?; Some(h.into_iter().map(Range::from).collect::>()) }) }); diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index e41c2baa..76a84bc6 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -1,12 +1,12 @@ -use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, hash::Hash, io::{Read, Write}, ops::Deref}; +use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, hash::Hash, io::{Read, Write}, ops::Deref, path::Path}; use anyhow::{Result, anyhow}; use crossterm::{execute, style::Print}; use scopeguard::defer; use tokio::io::AsyncWriteExt; -use yazi_config::YAZI; +use yazi_config::{YAZI, opener::OpenerRule}; use yazi_dds::Pubsub; -use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file, services::{self, Local}, skip_url}; +use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file, provider::{self, local::{Gate, Local}}, skip_url}; use yazi_macro::{err, succ}; use yazi_parser::VoidOpt; use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER}; @@ -23,7 +23,7 @@ impl Actor for BulkRename { const NAME: &str = "bulk_rename"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { - let Some(opener) = YAZI.opener.block(YAZI.open.all("bulk-rename.txt", "text/plain")) else { + let Some(opener) = Self::opener() else { succ!(AppProxy::notify_warn("Bulk rename", "No text opener found")); }; @@ -39,8 +39,7 @@ impl Actor for BulkRename { let cwd = cx.cwd().clone(); tokio::spawn(async move { let tmp = YAZI.preview.tmpfile("bulk"); - // TODO: pull `OpenOptions` into `yazi_fs` - tokio::fs::OpenOptions::new() + Gate::default() .write(true) .create_new(true) .open(&tmp) @@ -116,7 +115,7 @@ impl BulkRename { if maybe_exists(&new).await && !paths_to_same_file(&old, &new).await { failed.push((o, n, anyhow!("Destination already exists"))); - } else if let Err(e) = services::rename(&old, &new).await { + } else if let Err(e) = provider::rename(&old, &new).await { failed.push((o, n, e.into())); } else if let Ok(f) = File::new(new).await { succeeded.insert(old, f); @@ -138,6 +137,10 @@ impl BulkRename { Ok(()) } + fn opener() -> Option<&'static OpenerRule> { + YAZI.opener.block(YAZI.open.all(Url::from(Path::new("bulk-rename.txt")), "text/plain")) + } + async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { let mut stdout = TTY.lockout(); terminal_clear(&mut *stdout)?; diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 751c819a..b1819ca3 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -5,7 +5,7 @@ use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; use yazi_dds::Pubsub; -use yazi_fs::{File, FilesOp, expand_path}; +use yazi_fs::{File, FilesOp, expand_url}; use yazi_macro::{act, err, render, succ}; use yazi_parser::mgr::CdOpt; use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; @@ -76,9 +76,9 @@ impl Cd { while let Some(result) = rx.next().await { match result { Ok(s) => { - let url = Url::from(expand_path(s)); + let Ok(url) = Url::try_from(s).map(expand_url) else { return }; - let Ok(file) = File::new(url.clone()).await else { return }; + let Ok(file) = File::new(url.as_ref().clone()).await else { return }; if file.is_dir() { return MgrProxy::cd(&url); } diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 58e0793c..791b5a9b 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -1,6 +1,6 @@ use anyhow::Result; use yazi_config::popup::{ConfirmCfg, InputCfg}; -use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, realname, services}; +use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname}; use yazi_macro::succ; use yazi_parser::mgr::CreateOpt; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER}; @@ -45,15 +45,15 @@ impl Create { let _permit = WATCHER.acquire().await.unwrap(); if dir { - services::create_dir_all(&new).await?; + provider::create_dir_all(&new).await?; } else if let Some(real) = realname(&new).await { - ok_or_not_found(services::remove_file(&new).await)?; + ok_or_not_found(provider::remove_file(&new).await)?; FilesOp::Deleting(parent.clone(), [UrnBuf::from(real)].into()).emit(); - services::create(&new).await?; + provider::create(&new).await?; } else { - services::create_dir_all(&parent).await.ok(); - ok_or_not_found(services::remove_file(&new).await)?; - services::create(&new).await?; + provider::create_dir_all(&parent).await.ok(); + ok_or_not_found(provider::remove_file(&new).await)?; + provider::create(&new).await?; } if let Ok(f) = File::new(new.clone()).await { diff --git a/yazi-actor/src/mgr/quit.rs b/yazi-actor/src/mgr/quit.rs index c05378cf..771000d8 100644 --- a/yazi-actor/src/mgr/quit.rs +++ b/yazi-actor/src/mgr/quit.rs @@ -75,7 +75,7 @@ impl Quit { } let paths = selected.fold(OsString::new(), |mut s, u| { - s.push(u.as_os_str()); + s.push(u.os_str()); s.push("\n"); s }); diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index c54ad464..dae3ca76 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, realname, services}; +use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, paths_to_same_file, provider, realname}; use yazi_macro::{act, err, succ}; use yazi_parser::mgr::RenameOpt; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER}; @@ -65,10 +65,10 @@ impl Rename { let _permit = WATCHER.acquire().await.unwrap(); let overwritten = realname(&new).await; - services::rename(&old, &new).await?; + provider::rename(&old, &new).await?; if let Some(o) = overwritten { - ok_or_not_found(services::rename(p_new.join(&o), &new).await)?; + ok_or_not_found(provider::rename(p_new.join(&o), &new).await)?; FilesOp::Deleting(p_new.clone(), [UrnBuf::from(o)].into()).emit(); } diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index 802f328f..6a779a7c 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -1,10 +1,8 @@ -use std::io::BufReader; - use anyhow::Result; use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; use ratatui::layout::Rect; use yazi_config::YAZI; -use yazi_fs::services; +use yazi_fs::provider; use yazi_shared::url::Url; use crate::Dimension; @@ -40,7 +38,7 @@ impl Image { }) .await??; - Ok(services::write(cache, buf).await?) + Ok(provider::write(cache, buf).await?) } pub(super) async fn downscale(url: &Url, rect: Rect) -> Result { @@ -110,7 +108,7 @@ impl Image { limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32); } - let mut reader = ImageReader::new(BufReader::new(services::open(&url).await?.into_std().await)); + let mut reader = ImageReader::new(provider::open(&url).await?.reader_sync().await); if let Ok(format) = ImageFormat::from_path(url) { reader.set_format(format); } diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 07d1f333..7bf73728 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -1,9 +1,9 @@ use std::{borrow::Cow, ops::Deref}; use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; -use yazi_shared::url::{CovUrl, Scheme}; +use yazi_shared::url::CovUrl; -use crate::{Urn, cached_field}; +use crate::{Urn, cached_field, deprecate}; pub type UrlRef = UserDataRef; @@ -16,7 +16,7 @@ pub struct Url { v_urn: Option, v_base: Option, v_parent: Option, - v_frag: Option, + v_domain: Option, } impl Deref for Url { @@ -64,7 +64,7 @@ impl Url { v_urn: None, v_base: None, v_parent: None, - v_frag: None, + v_domain: None, } } @@ -94,10 +94,7 @@ impl FromLua for Url { impl UserData for Url { fn add_fields>(fields: &mut F) { cached_field!(fields, name, |lua, me| { - Some(me.name()) - .filter(|&s| !s.is_empty()) - .map(|s| lua.create_string(s.as_encoded_bytes())) - .transpose() + me.file_name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() }); cached_field!(fields, stem, |lua, me| { me.file_stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() @@ -110,13 +107,13 @@ impl UserData for Url { cached_field!(fields, base, |_, me| { Ok(if me.base().as_os_str().is_empty() { None } else { Some(Self::new(me.base())) }) }); - // TODO: remove - cached_field!(fields, frag, |lua, me| { - if let Scheme::Search(kw) = &me.scheme { - Some(lua.create_string(kw)).transpose() - } else { - Ok(None) - } + cached_field!(fields, domain, |lua, me| { + me.scheme.domain().map(|s| lua.create_string(s)).transpose() + }); + + fields.add_field_method_get("frag", |lua, me| { + deprecate!(lua, "`frag` property of Url is deprecated and renamed to `domain`, please use the new name instead, in your {}"); + me.scheme.domain().map(|s| lua.create_string(s)).transpose() }); fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); @@ -157,8 +154,8 @@ impl UserData for Url { Ok(url.map(Self::new)) }); - methods.add_function_mut("into_search", |_, (ud, frag): (AnyUserData, mlua::String)| { - Ok(Self::new(ud.take::()?.inner.into_search(frag.to_str()?))) + methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { + Ok(Self::new(ud.take::()?.inner.into_search(domain.to_str()?))) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner)); diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 48c885a0..38a4d5b1 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -16,9 +16,10 @@ yazi-macro = { path = "../yazi-macro", version = "25.6.11" } yazi-shared = { path = "../yazi-shared", version = "25.6.11" } # External dependencies -clap = { workspace = true } -regex = { workspace = true } -serde = { workspace = true } +clap = { workspace = true } +futures = { workspace = true } +regex = { workspace = true } +serde = { workspace = true } [build-dependencies] yazi-shared = { path = "../yazi-shared", version = "25.6.11" } diff --git a/yazi-boot/src/actions/debug.rs b/yazi-boot/src/actions/debug.rs index 9ee15264..8d956249 100644 --- a/yazi-boot/src/actions/debug.rs +++ b/yazi-boot/src/actions/debug.rs @@ -1,8 +1,9 @@ -use std::{env, ffi::OsStr, fmt::Write}; +use std::{env, ffi::OsStr, fmt::Write, path::Path}; use regex::Regex; use yazi_adapter::Mux; -use yazi_shared::timestamp_us; +use yazi_config::YAZI; +use yazi_shared::{timestamp_us, url::Url}; use super::Actions; @@ -57,17 +58,17 @@ impl Actions { writeln!( s, " default : {:?}", - yazi_config::YAZI.opener.first(yazi_config::YAZI.open.all("f75a.txt", "text/plain")) + YAZI.opener.first(YAZI.open.all(Url::from(Path::new("f75a.txt")), "text/plain")) )?; writeln!( s, " block-create: {:?}", - yazi_config::YAZI.opener.block(yazi_config::YAZI.open.all("bulk-create.txt", "text/plain")) + YAZI.opener.block(YAZI.open.all(Url::from(Path::new("bulk-create.txt")), "text/plain")) )?; writeln!( s, " block-rename: {:?}", - yazi_config::YAZI.opener.block(yazi_config::YAZI.open.all("bulk-rename.txt", "text/plain")) + YAZI.opener.block(YAZI.open.all(Url::from(Path::new("bulk-rename.txt")), "text/plain")) )?; writeln!(s, "\nMultiplexers")?; diff --git a/yazi-boot/src/args.rs b/yazi-boot/src/args.rs index ee6960d1..dbc921df 100644 --- a/yazi-boot/src/args.rs +++ b/yazi-boot/src/args.rs @@ -1,14 +1,14 @@ use std::path::PathBuf; use clap::{Parser, command}; -use yazi_shared::Id; +use yazi_shared::{Id, url::Url}; #[derive(Debug, Default, Parser)] #[command(name = "yazi")] pub struct Args { /// Set the current working entry #[arg(index = 1, num_args = 1..=9)] - pub entries: Vec, + pub entries: Vec, /// Write the cwd on exit to this file #[arg(long)] diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index f013ad65..a41380d1 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -1,12 +1,14 @@ -use std::{collections::HashSet, ffi::OsString, path::PathBuf}; +use std::{borrow::Cow, collections::HashSet, path::PathBuf}; +use futures::executor::block_on; use serde::Serialize; -use yazi_fs::{CWD, Xdg, expand_path}; +use yazi_fs::{CWD, Xdg, expand_url, provider}; +use yazi_shared::url::{Url, UrnBuf}; #[derive(Debug, Default, Serialize)] pub struct Boot { - pub cwds: Vec, - pub files: Vec, + pub cwds: Vec, + pub files: Vec, pub local_events: HashSet, pub remote_events: HashSet, @@ -18,31 +20,31 @@ pub struct Boot { } impl Boot { - fn parse_entries(entries: &[PathBuf]) -> (Vec, Vec) { + async fn parse_entries(entries: &[Url]) -> (Vec, Vec) { if entries.is_empty() { - return (vec![CWD.load().to_path_buf()], vec![OsString::new()]); + return (vec![CWD.load().as_ref().clone()], vec![UrnBuf::default()]); } - let mut cwds = Vec::with_capacity(entries.len()); - let mut files = Vec::with_capacity(entries.len()); - for entry in entries.iter().map(expand_path) { - if let Some(p) = entry.parent().filter(|_| !entry.is_dir()) { - cwds.push(p.to_owned()); - files.push(entry.file_name().unwrap().to_owned()); + async fn go<'a>(entry: Cow<'a, Url>) -> (Url, UrnBuf) { + let Some((parent, child)) = entry.pair() else { + return (entry.into_owned(), UrnBuf::default()); + }; + + if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) { + (parent, child) } else { - cwds.push(entry); - files.push(OsString::new()); + (entry.into_owned(), UrnBuf::default()) } } - (cwds, files) + futures::future::join_all(entries.iter().map(expand_url).map(go)).await.into_iter().unzip() } } impl From<&crate::Args> for Boot { fn from(args: &crate::Args) -> Self { let config_dir = Xdg::config_dir(); - let (cwds, files) = Self::parse_entries(&args.entries); + let (cwds, files) = block_on(Self::parse_entries(&args.entries)); let local_events = args .local_events diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs index c55d1790..92299fd0 100644 --- a/yazi-cli/src/package/delete.rs +++ b/yazi-cli/src/package/delete.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use yazi_fs::{ok_or_not_found, remove_dir_clean, services::Local}; +use yazi_fs::{ok_or_not_found, provider::local::Local, remove_dir_clean}; use yazi_macro::outln; use super::Dependency; diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index 95275de4..62d5ee7b 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; -use yazi_fs::{remove_dir_clean, services::Local}; +use yazi_fs::{provider::local::Local, remove_dir_clean}; use yazi_macro::outln; use super::Dependency; diff --git a/yazi-cli/src/package/hash.rs b/yazi-cli/src/package/hash.rs index cae08fe5..10a48c53 100644 --- a/yazi-cli/src/package/hash.rs +++ b/yazi-cli/src/package/hash.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, bail}; use twox_hash::XxHash3_128; -use yazi_fs::{ok_or_not_found, services::Local}; +use yazi_fs::{ok_or_not_found, provider::local::Local}; use super::Dependency; diff --git a/yazi-cli/src/package/package.rs b/yazi-cli/src/package/package.rs index 9d70678e..eae7c13c 100644 --- a/yazi-cli/src/package/package.rs +++ b/yazi-cli/src/package/package.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, str::FromStr}; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use yazi_fs::{Xdg, services::Local}; +use yazi_fs::{Xdg, provider::local::Local}; use yazi_macro::outln; use super::Dependency; diff --git a/yazi-cli/src/shared/shared.rs b/yazi-cli/src/shared/shared.rs index 8578e544..f3e42c1c 100644 --- a/yazi-cli/src/shared/shared.rs +++ b/yazi-cli/src/shared/shared.rs @@ -1,7 +1,7 @@ use std::{io, path::Path}; use tokio::io::AsyncWriteExt; -use yazi_fs::{ok_or_not_found, services::Local}; +use yazi_fs::{ok_or_not_found, provider::local::{Gate, Local}}; #[inline] pub async fn must_exists(path: impl AsRef) -> bool { @@ -16,13 +16,11 @@ pub async fn maybe_exists(path: impl AsRef) -> bool { } } -// TODO: use `yazi_fs` instead of `tokio::fs` pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { let b = Local::read(from).await?; ok_or_not_found(remove_sealed(to).await)?; - let mut file = - tokio::fs::OpenOptions::new().create_new(true).write(true).truncate(true).open(to).await?; + let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?; file.write_all(&b).await?; let mut perm = file.metadata().await?.permissions(); diff --git a/yazi-config/preset/theme-dark.toml b/yazi-config/preset/theme-dark.toml index 940901db..b82362db 100644 --- a/yazi-config/preset/theme-dark.toml +++ b/yazi-config/preset/theme-dark.toml @@ -239,16 +239,16 @@ rules = [ # { mime = "inode/empty", fg = "red" }, # Special files - { name = "*", is = "orphan", bg = "red" }, - { name = "*", is = "exec" , fg = "green" }, + { url = "*", is = "orphan", bg = "red" }, + { url = "*", is = "exec" , fg = "green" }, # Dummy files - { name = "*", is = "dummy", bg = "red" }, - { name = "*/", is = "dummy", bg = "red" }, + { url = "*", is = "dummy", bg = "red" }, + { url = "*/", is = "dummy", bg = "red" }, # Fallback - # { name = "*", fg = "white" }, - { name = "*/", fg = "blue" } + # { url = "*", fg = "white" }, + { url = "*/", fg = "blue" } ] # : }}} diff --git a/yazi-config/preset/theme-light.toml b/yazi-config/preset/theme-light.toml index 55f99796..60342931 100644 --- a/yazi-config/preset/theme-light.toml +++ b/yazi-config/preset/theme-light.toml @@ -239,16 +239,16 @@ rules = [ # { mime = "inode/empty", fg = "red" }, # Special files - { name = "*", is = "orphan", bg = "red" }, - { name = "*", is = "exec" , fg = "green" }, + { url = "*", is = "orphan", bg = "red" }, + { url = "*", is = "exec" , fg = "green" }, # Dummy files - { name = "*", is = "dummy", bg = "red" }, - { name = "*/", is = "dummy", bg = "red" }, + { url = "*", is = "dummy", bg = "red" }, + { url = "*/", is = "dummy", bg = "red" }, # Fallback - # { name = "*", fg = "white" }, - { name = "*/", fg = "blue" } + # { url = "*", fg = "white" }, + { url = "*/", fg = "blue" } ] # : }}} diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index 4255f1a2..25add0a8 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -63,7 +63,7 @@ play = [ [open] rules = [ # Folder - { name = "*/", use = [ "edit", "open", "reveal" ] }, + { url = "*/", use = [ "edit", "open", "reveal" ] }, # Text { mime = "text/*", use = [ "edit", "reveal" ] }, # Image @@ -78,7 +78,7 @@ rules = [ # Empty file { mime = "inode/empty", use = [ "edit", "reveal" ] }, # Fallback - { name = "*", use = [ "open", "reveal" ] }, + { url = "*", use = [ "open", "reveal" ] }, ] [tasks] @@ -92,10 +92,10 @@ suppress_preload = false [plugin] fetchers = [ # Mimetype - { id = "mime", name = "*", run = "mime", prio = "high" }, + { id = "mime", url = "*", run = "mime", prio = "high" }, ] spotters = [ - { name = "*/", run = "folder" }, + { url = "*/", run = "folder" }, # Code { mime = "text/*", run = "code" }, { mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" }, @@ -106,7 +106,7 @@ spotters = [ # Video { mime = "video/*", run = "video" }, # Fallback - { name = "*", run = "file" }, + { url = "*", run = "file" }, ] preloaders = [ # Image @@ -122,7 +122,7 @@ preloaders = [ { mime = "application/ms-opentype", run = "font" }, ] previewers = [ - { name = "*/", run = "folder" }, + { url = "*/", run = "folder" }, # Code { mime = "text/*", run = "code" }, { mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" }, @@ -139,18 +139,18 @@ previewers = [ # Archive { mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", run = "archive" }, { mime = "application/{debian*-package,redhat-package-manager,rpm,android.package-archive}", run = "archive" }, - { name = "*.{AppImage,appimage}", run = "archive" }, + { url = "*.{AppImage,appimage}", run = "archive" }, # Virtual Disk / Disk Image { mime = "application/{iso9660-image,qemu-disk,ms-wim,apple-diskimage}", run = "archive" }, { mime = "application/virtualbox-{vhd,vhdx}", run = "archive" }, - { name = "*.{img,fat,ext,ext2,ext3,ext4,squashfs,ntfs,hfs,hfsx}", run = "archive" }, + { url = "*.{img,fat,ext,ext2,ext3,ext4,squashfs,ntfs,hfs,hfsx}", run = "archive" }, # Font { mime = "font/*", run = "font" }, { mime = "application/ms-opentype", run = "font" }, # Empty file { mime = "inode/empty", run = "empty" }, # Fallback - { name = "*", run = "file" }, + { url = "*", run = "file" }, ] [input] diff --git a/yazi-config/src/open/open.rs b/yazi-config/src/open/open.rs index 8e72fb26..7775ec84 100644 --- a/yazi-config/src/open/open.rs +++ b/yazi-config/src/open/open.rs @@ -1,10 +1,10 @@ -use std::{ops::Deref, path::Path}; +use std::ops::Deref; use anyhow::Result; use indexmap::IndexSet; use serde::Deserialize; use yazi_codegen::DeserializeOver2; -use yazi_shared::MIME_DIR; +use yazi_shared::{MIME_DIR, url::Url}; use crate::{Preset, open::OpenRule}; @@ -24,10 +24,10 @@ impl Deref for Open { } impl Open { - pub fn all<'a, 'b, P, M>(&'a self, path: P, mime: M) -> impl Iterator + 'b + pub fn all<'a, 'b, P, M>(&'a self, url: P, mime: M) -> impl Iterator + 'b where 'a: 'b, - P: AsRef + 'b, + P: AsRef + 'b, M: AsRef + 'b, { let is_dir = mime.as_ref() == MIME_DIR; @@ -36,19 +36,16 @@ impl Open { .iter() .filter(move |&r| { r.mime.as_ref().is_some_and(|p| p.match_mime(&mime)) - || r.name.as_ref().is_some_and(|p| p.match_path(&path, is_dir)) + || r.url.as_ref().is_some_and(|p| p.match_url(&url, is_dir)) }) .flat_map(|r| &r.r#use) .map(String::as_str) } - pub fn common<'a>( - &'a self, - targets: &[(impl AsRef, impl AsRef)], - ) -> IndexSet<&'a str> { + pub fn common<'a>(&'a self, targets: &[(impl AsRef, impl AsRef)]) -> IndexSet<&'a str> { let each: Vec> = targets .iter() - .map(|(p, m)| self.all(p, m).collect::>()) + .map(|(u, m)| self.all(u, m).collect::>()) .filter(|s| !s.is_empty()) .collect(); diff --git a/yazi-config/src/open/rule.rs b/yazi-config/src/open/rule.rs index 8fcf8cec..9732c4f2 100644 --- a/yazi-config/src/open/rule.rs +++ b/yazi-config/src/open/rule.rs @@ -6,7 +6,7 @@ use crate::pattern::Pattern; #[derive(Debug, Deserialize)] pub struct OpenRule { - pub name: Option, + pub url: Option, pub mime: Option, #[serde(deserialize_with = "OpenRule::deserialize")] pub r#use: Vec, @@ -14,10 +14,10 @@ pub struct OpenRule { impl OpenRule { #[inline] - pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } + pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) } #[inline] - pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } + pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) } } impl OpenRule { diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 2adf707a..7bdae907 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -1,12 +1,15 @@ -use std::{path::Path, str::FromStr}; +use std::str::FromStr; +use anyhow::Result; use globset::GlobBuilder; use serde::Deserialize; +use yazi_shared::url::{Scheme, Url}; #[derive(Debug, Deserialize)] #[serde(try_from = "String")] pub struct Pattern { inner: globset::GlobMatcher, + scheme: PatternScheme, is_dir: bool, is_star: bool, #[cfg(windows)] @@ -15,28 +18,35 @@ pub struct Pattern { impl Pattern { #[inline] - pub fn match_mime(&self, mime: impl AsRef) -> bool { - self.is_star || (!mime.as_ref().is_empty() && self.inner.is_match(mime.as_ref())) - } + pub fn match_url(&self, url: impl AsRef, is_dir: bool) -> bool { + let url = url.as_ref(); - #[inline] - pub fn match_path(&self, path: impl AsRef, is_dir: bool) -> bool { if is_dir != self.is_dir { return false; } else if self.is_star { return true; + } else if !self.scheme.matches(&url.scheme) { + return false; } + #[cfg(unix)] + let path = &url.loc; + #[cfg(windows)] let path = if self.sep_lit { - yazi_fs::backslash_to_slash(path.as_ref()) + yazi_fs::backslash_to_slash(&url.loc) } else { - std::borrow::Cow::Borrowed(path.as_ref()) + std::borrow::Cow::Borrowed(url.loc.as_path()) }; self.inner.is_match(path) } + #[inline] + pub fn match_mime(&self, mime: impl AsRef) -> bool { + self.is_star || (!mime.as_ref().is_empty() && self.inner.is_match(mime.as_ref())) + } + #[inline] pub fn any_file(&self) -> bool { self.is_star && !self.is_dir } @@ -45,14 +55,23 @@ impl Pattern { } impl FromStr for Pattern { - type Err = globset::Error; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let a = s.trim_start_matches("\\s"); - let b = a.trim_end_matches('/'); - let sep_lit = b.contains('/'); + // Trim leading case-sensitive indicator + let a = s.trim_start_matches(r"\s"); - let inner = GlobBuilder::new(b) + // Parse the URL scheme if present + let (scheme, skip) = PatternScheme::parse(a)?; + let b = &a[skip..]; + + // Trim the ending slash which indicates a directory + let c = b.trim_end_matches('/'); + + // Check whether it's a filename pattern or a full path pattern + let sep_lit = c.contains('/'); + + let inner = GlobBuilder::new(c) .case_insensitive(a.len() == s.len()) .literal_separator(sep_lit) .backslash_escape(false) @@ -62,8 +81,9 @@ impl FromStr for Pattern { Ok(Self { inner, - is_dir: b.len() < a.len(), - is_star: b == "*", + scheme, + is_dir: c.len() < b.len(), + is_star: c == "*", #[cfg(windows)] sep_lit, }) @@ -71,17 +91,40 @@ impl FromStr for Pattern { } impl TryFrom for Pattern { - type Error = globset::Error; + type Error = anyhow::Error; fn try_from(s: String) -> Result { Self::from_str(s.as_str()) } } +// --- Scheme +#[derive(Debug)] +struct PatternScheme(Option<&'static str>); + +impl PatternScheme { + fn parse(s: &str) -> Result<(Self, usize)> { + let mut me = Self(None); + let Some((protocol, _)) = s.split_once("://") else { + return Ok((me, 0)); + }; + + if protocol != "*" { + me.0 = Some(Scheme::parse_kind(protocol.as_bytes())?); + } + + Ok((me, protocol.len() + 3)) + } + + #[inline] + fn matches(&self, scheme: &Scheme) -> bool { self.0.is_none_or(|s| s == scheme.kind()) } +} + +// --- Tests #[cfg(test)] mod tests { use super::*; - fn matches(glob: &str, path: &str) -> bool { - Pattern::from_str(glob).unwrap().match_path(path, false) + fn matches(glob: &str, url: &str) -> bool { + Pattern::from_str(glob).unwrap().match_url(Url::from_str(url).unwrap(), false) } #[cfg(unix)] diff --git a/yazi-config/src/plugin/fetcher.rs b/yazi-config/src/plugin/fetcher.rs index 1dfeefa3..59e041d5 100644 --- a/yazi-config/src/plugin/fetcher.rs +++ b/yazi-config/src/plugin/fetcher.rs @@ -1,7 +1,5 @@ -use std::path::Path; - use serde::Deserialize; -use yazi_shared::{MIME_DIR, event::Cmd}; +use yazi_shared::{MIME_DIR, event::Cmd, url::Url}; use crate::{Pattern, Priority}; @@ -11,7 +9,7 @@ pub struct Fetcher { pub idx: u8, pub id: String, - pub name: Option, + pub url: Option, pub mime: Option, pub run: Cmd, #[serde(default)] @@ -20,8 +18,8 @@ pub struct Fetcher { impl Fetcher { #[inline] - pub fn matches(&self, path: &Path, mime: &str) -> bool { + pub fn matches(&self, url: &Url, mime: &str) -> bool { self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) - || self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)) + || self.url.as_ref().is_some_and(|p| p.match_url(url, mime == MIME_DIR)) } } diff --git a/yazi-config/src/plugin/plugin.rs b/yazi-config/src/plugin/plugin.rs index a569c13f..d13bbd2a 100644 --- a/yazi-config/src/plugin/plugin.rs +++ b/yazi-config/src/plugin/plugin.rs @@ -1,10 +1,11 @@ -use std::{collections::HashSet, path::Path}; +use std::collections::HashSet; use anyhow::Result; use serde::Deserialize; use tracing::warn; use yazi_codegen::DeserializeOver2; use yazi_fs::File; +use yazi_shared::url::Url; use super::{Fetcher, Preloader, Previewer, Spotter}; use crate::{Preset, plugin::MAX_PREWORKERS}; @@ -39,12 +40,12 @@ pub struct Plugin { impl Plugin { pub fn fetchers<'a, 'b: 'a>( &'b self, - path: &'a Path, + url: &'a Url, mime: &'a str, ) -> impl Iterator + 'a { let mut seen = HashSet::new(); self.fetchers.iter().filter(move |&f| { - if seen.contains(&f.id) || !f.matches(path, mime) { + if seen.contains(&f.id) || !f.matches(url, mime) { return false; } seen.insert(&f.id); @@ -68,18 +69,18 @@ impl Plugin { }) } - pub fn spotter(&self, path: &Path, mime: &str) -> Option<&Spotter> { - self.spotters.iter().find(|&p| p.matches(path, mime)) + pub fn spotter(&self, url: &Url, mime: &str) -> Option<&Spotter> { + self.spotters.iter().find(|&p| p.matches(url, mime)) } pub fn preloaders<'a, 'b: 'a>( &'b self, - path: &'a Path, + url: &'a Url, mime: &'a str, ) -> impl Iterator + 'a { let mut next = true; self.preloaders.iter().filter(move |&p| { - if !next || !p.matches(path, mime) { + if !next || !p.matches(url, mime) { return false; } next = p.next; @@ -87,8 +88,8 @@ impl Plugin { }) } - pub fn previewer(&self, path: &Path, mime: &str) -> Option<&Previewer> { - self.previewers.iter().find(|&p| p.matches(path, mime)) + pub fn previewer(&self, url: &Url, mime: &str) -> Option<&Previewer> { + self.previewers.iter().find(|&p| p.matches(url, mime)) } } diff --git a/yazi-config/src/plugin/preloader.rs b/yazi-config/src/plugin/preloader.rs index 4f74c9a0..443c79b5 100644 --- a/yazi-config/src/plugin/preloader.rs +++ b/yazi-config/src/plugin/preloader.rs @@ -1,7 +1,5 @@ -use std::path::Path; - use serde::Deserialize; -use yazi_shared::{MIME_DIR, event::Cmd}; +use yazi_shared::{MIME_DIR, event::Cmd, url::Url}; use crate::{Pattern, Priority}; @@ -10,7 +8,7 @@ pub struct Preloader { #[serde(skip)] pub idx: u8, - pub name: Option, + pub url: Option, pub mime: Option, pub run: Cmd, #[serde(default)] @@ -21,8 +19,8 @@ pub struct Preloader { impl Preloader { #[inline] - pub fn matches(&self, path: &Path, mime: &str) -> bool { + pub fn matches(&self, url: &Url, mime: &str) -> bool { self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) - || self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)) + || self.url.as_ref().is_some_and(|p| p.match_url(url, mime == MIME_DIR)) } } diff --git a/yazi-config/src/plugin/previewer.rs b/yazi-config/src/plugin/previewer.rs index a84b213d..885835a1 100644 --- a/yazi-config/src/plugin/previewer.rs +++ b/yazi-config/src/plugin/previewer.rs @@ -1,27 +1,25 @@ -use std::path::Path; - use serde::Deserialize; -use yazi_shared::{MIME_DIR, event::Cmd}; +use yazi_shared::{MIME_DIR, event::Cmd, url::Url}; use crate::Pattern; #[derive(Debug, Deserialize)] pub struct Previewer { - pub name: Option, + pub url: Option, pub mime: Option, pub run: Cmd, } impl Previewer { #[inline] - pub fn matches(&self, path: &Path, mime: &str) -> bool { + pub fn matches(&self, url: &Url, mime: &str) -> bool { self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) - || self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)) + || self.url.as_ref().is_some_and(|p| p.match_url(url, mime == MIME_DIR)) } #[inline] - pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } + pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) } #[inline] - pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } + pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) } } diff --git a/yazi-config/src/plugin/spotter.rs b/yazi-config/src/plugin/spotter.rs index 86258166..e00f9107 100644 --- a/yazi-config/src/plugin/spotter.rs +++ b/yazi-config/src/plugin/spotter.rs @@ -1,27 +1,25 @@ -use std::path::Path; - use serde::Deserialize; -use yazi_shared::{MIME_DIR, event::Cmd}; +use yazi_shared::{MIME_DIR, event::Cmd, url::Url}; use crate::Pattern; #[derive(Debug, Deserialize)] pub struct Spotter { - pub name: Option, + pub url: Option, pub mime: Option, pub run: Cmd, } impl Spotter { #[inline] - pub fn matches(&self, path: &Path, mime: &str) -> bool { + pub fn matches(&self, url: &Url, mime: &str) -> bool { self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) - || self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)) + || self.url.as_ref().is_some_and(|p| p.match_url(url, mime == MIME_DIR)) } #[inline] - pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } + pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) } #[inline] - pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } + pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) } } diff --git a/yazi-config/src/theme/filetype.rs b/yazi-config/src/theme/filetype.rs index 9bfc1c2a..c4a03d64 100644 --- a/yazi-config/src/theme/filetype.rs +++ b/yazi-config/src/theme/filetype.rs @@ -22,7 +22,7 @@ impl Deref for Filetype { pub struct FiletypeRule { #[serde(default)] is: Is, - name: Option, + url: Option, mime: Option, #[serde(flatten)] pub style: Style, @@ -35,6 +35,6 @@ impl FiletypeRule { } self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) - || self.name.as_ref().is_some_and(|n| n.match_path(&file.url, file.is_dir())) + || self.url.as_ref().is_some_and(|n| n.match_url(&file.url, file.is_dir())) } } diff --git a/yazi-config/src/theme/icon.rs b/yazi-config/src/theme/icon.rs index ff38e98e..1916f37c 100644 --- a/yazi-config/src/theme/icon.rs +++ b/yazi-config/src/theme/icon.rs @@ -70,7 +70,7 @@ impl Icon { #[inline] fn match_by_glob(&self, file: &File) -> Option<&I> { - self.globs.iter().find(|(p, _)| p.match_path(&file.url, file.is_dir())).map(|(_, i)| i) + self.globs.iter().find(|(p, _)| p.match_url(&file.url, file.is_dir())).map(|(_, i)| i) } #[inline] diff --git a/yazi-core/src/mgr/watcher.rs b/yazi-core/src/mgr/watcher.rs index 68c56987..fb3ae06f 100644 --- a/yazi-core/src/mgr/watcher.rs +++ b/yazi-core/src/mgr/watcher.rs @@ -6,7 +6,7 @@ use parking_lot::RwLock; use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tracing::error; -use yazi_fs::{File, Files, FilesOp, cha::Cha, realname_unchecked, services}; +use yazi_fs::{File, Files, FilesOp, cha::Cha, provider, realname_unchecked}; use yazi_proxy::WATCHER; use yazi_shared::{RoCell, url::Url}; @@ -139,7 +139,7 @@ impl Watcher { }; let u = &file.url; - let eq = (!file.is_link() && services::canonicalize(u).await.is_ok_and(|c| c == *u)) + let eq = (!file.is_link() && provider::canonicalize(u).await.is_ok_and(|c| c == *u)) || realname_unchecked(u, &mut cached).await.is_ok_and(|s| urn.as_urn() == s); if !eq { @@ -194,7 +194,7 @@ impl Watcher { async fn go(todo: HashSet) { for from in todo { - let Ok(to) = services::canonicalize(&from).await else { continue }; + let Ok(to) = provider::canonicalize(&from).await else { continue }; if to != from && WATCHED.read().contains(&from) { LINKED.write().insert(from, to); diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs index 3a0dc99f..24d1b9b4 100644 --- a/yazi-core/src/tab/selected.rs +++ b/yazi-core/src/tab/selected.rs @@ -126,7 +126,7 @@ impl Selected { mod tests { use super::*; - fn url(s: &str) -> Url { Url::try_from(s).unwrap() } + fn url(s: &str) -> Url { s.parse().unwrap() } #[test] fn test_insert_non_conflicting() { diff --git a/yazi-dds/src/state.rs b/yazi-dds/src/state.rs index d02d87ac..e9e11043 100644 --- a/yazi-dds/src/state.rs +++ b/yazi-dds/src/state.rs @@ -2,9 +2,9 @@ use std::{collections::HashMap, mem, ops::Deref, sync::atomic::{AtomicU64, Order use anyhow::Result; use parking_lot::RwLock; -use tokio::{fs::OpenOptions, io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufWriter}; use yazi_boot::BOOT; -use yazi_fs::services::Local; +use yazi_fs::provider::local::{Gate, Local}; use yazi_shared::{RoCell, timestamp_us}; use crate::CLIENTS; @@ -59,8 +59,7 @@ impl State { Local::create_dir_all(&BOOT.state_dir).await?; let mut buf = BufWriter::new( - // TODO: VFS - OpenOptions::new() + Gate::default() .write(true) .create(true) .truncate(true) @@ -79,7 +78,7 @@ impl State { } async fn load(&self) -> Result<()> { - let mut file = BufReader::new(Local::open(BOOT.state_dir.join(".dds")).await?); + let mut file = Local::open(BOOT.state_dir.join(".dds")).await?.reader(); let mut buf = String::new(); let mut inner = HashMap::new(); diff --git a/yazi-dds/src/stream.rs b/yazi-dds/src/stream.rs index 5544d321..bd4052b9 100644 --- a/yazi-dds/src/stream.rs +++ b/yazi-dds/src/stream.rs @@ -38,7 +38,7 @@ impl Stream { pub(super) async fn bind() -> std::io::Result { let p = Self::socket_file(); - yazi_fs::services::Local::remove_file(&p).await.ok(); + yazi_fs::provider::local::Local::remove_file(&p).await.ok(); tokio::net::UnixListener::bind(p) } diff --git a/yazi-fm/build.rs b/yazi-fm/build.rs index 9785ed08..2bb340da 100644 --- a/yazi-fm/build.rs +++ b/yazi-fm/build.rs @@ -9,7 +9,7 @@ fn main() -> Result<(), Box> { // C:\Users\Ika\AppData\Local\Temp\cargo-installTFU8cj\release\build\ // yazi-fm-45dffef2500eecd0\out - if dir.contains("\\release\\build\\yazi-fm-") { + if dir.contains(r"\release\build\yazi-fm-") { panic!( "Unwinding must be enabled for Windows. Please use `cargo build --profile release-windows --locked` instead to build Yazi." ); diff --git a/yazi-fm/src/app/commands/bootstrap.rs b/yazi-fm/src/app/commands/bootstrap.rs index 3ffbe4cf..eb08a1ff 100644 --- a/yazi-fm/src/app/commands/bootstrap.rs +++ b/yazi-fm/src/app/commands/bootstrap.rs @@ -3,7 +3,7 @@ use yazi_actor::Ctx; use yazi_boot::BOOT; use yazi_macro::act; use yazi_parser::{VoidOpt, mgr::CdSource}; -use yazi_shared::{event::Data, url::Url}; +use yazi_shared::event::Data; use crate::app::App; @@ -18,10 +18,10 @@ impl App { let cx = &mut Ctx::active(&mut self.core); cx.tab = i; - if file.is_empty() { - act!(mgr:cd, cx, (Url::from(&BOOT.cwds[i]), CdSource::Tab))?; + if file.as_os_str().is_empty() { + act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?; } else { - act!(mgr:reveal, cx, (Url::from(BOOT.cwds[i].join(file)), CdSource::Tab))?; + act!(mgr:reveal, cx, (BOOT.cwds[i].join(file), CdSource::Tab))?; } } diff --git a/yazi-fm/src/app/commands/quit.rs b/yazi-fm/src/app/commands/quit.rs index 3dd39157..6ff6b155 100644 --- a/yazi-fm/src/app/commands/quit.rs +++ b/yazi-fm/src/app/commands/quit.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use yazi_boot::ARGS; -use yazi_fs::services::Local; +use yazi_fs::provider::local::Local; use yazi_shared::event::EventQuit; use crate::{Term, app::App}; @@ -25,7 +25,7 @@ impl App { async fn cwd_to_file(&self, no: bool) { if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) { - let cwd = self.core.mgr.cwd().as_os_str(); + let cwd = self.core.mgr.cwd().os_str(); Local::write(p, cwd.as_encoded_bytes()).await.ok(); } } diff --git a/yazi-fs/src/calculator.rs b/yazi-fs/src/calculator.rs index d6388f10..75d33c67 100644 --- a/yazi-fs/src/calculator.rs +++ b/yazi-fs/src/calculator.rs @@ -1,11 +1,11 @@ -use std::{collections::VecDeque, fs::ReadDir, future::poll_fn, io, mem, pin::Pin, task::{Poll, ready}, time::{Duration, Instant}}; +use std::{collections::VecDeque, future::poll_fn, io, mem, pin::Pin, task::{Poll, ready}, time::{Duration, Instant}}; use tokio::task::JoinHandle; use yazi_shared::{Either, url::Url}; -use crate::services; +use crate::provider::{self, ReadDirSync}; -type Task = Either; +type Task = Either; pub enum SizeCalculator { Idle((VecDeque, Option)), @@ -16,12 +16,12 @@ impl SizeCalculator { pub async fn new(url: &Url) -> io::Result { let u = url.to_owned(); tokio::task::spawn_blocking(move || { - let meta = services::symlink_metadata_sync(&u)?; + let meta = provider::symlink_metadata_sync(&u)?; if !meta.is_dir() { return Ok(Self::Idle((VecDeque::new(), Some(meta.len())))); } - let mut buf = VecDeque::from([Either::Right(services::read_dir_sync(u)?)]); + let mut buf = VecDeque::from([Either::Right(provider::read_dir_sync(u)?)]); let size = Self::next_chunk(&mut buf); Ok(Self::Idle((buf, size))) }) @@ -63,7 +63,7 @@ impl SizeCalculator { .await } - fn next_chunk(buf: &mut VecDeque>) -> Option { + fn next_chunk(buf: &mut VecDeque>) -> Option { let (mut i, mut size, now) = (0, 0, Instant::now()); macro_rules! pop_and_continue { () => {{ @@ -80,7 +80,7 @@ impl SizeCalculator { let front = buf.front_mut()?; if let Either::Left(u) = front { - *front = match services::read_dir_sync(u) { + *front = match provider::read_dir_sync(u) { Ok(it) => Either::Right(it), Err(_) => pop_and_continue!(), }; @@ -93,7 +93,7 @@ impl SizeCalculator { let Ok(ent) = next else { continue }; let Ok(ft) = ent.file_type() else { continue }; if ft.is_dir() { - buf.push_back(Either::Left(ent.path().into())); + buf.push_back(Either::Left(ent.url())); } else if let Ok(meta) = ent.metadata() { size += meta.len(); } diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 49a9d459..7a9535e1 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -4,7 +4,7 @@ use yazi_macro::{unix_either, win_either}; use yazi_shared::url::Url; use super::ChaKind; -use crate::services; +use crate::provider; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Cha { @@ -59,14 +59,14 @@ impl Cha { #[inline] pub async fn from_url(url: &Url) -> std::io::Result { - Ok(Self::from_follow(url, services::symlink_metadata(url).await?).await) + Ok(Self::from_follow(url, provider::symlink_metadata(url).await?).await) } pub async fn from_follow(url: &Url, mut meta: Metadata) -> Self { let mut attached = ChaKind::hidden(url, &meta); if meta.is_symlink() { attached |= ChaKind::LINK; - meta = services::metadata(url).await.unwrap_or(meta); + meta = provider::metadata(url).await.unwrap_or(meta); } if meta.is_symlink() { attached |= ChaKind::ORPHAN; diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index 598877f6..958e4f4f 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -3,7 +3,7 @@ use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher use anyhow::Result; use yazi_shared::url::{Url, Urn, UrnBuf}; -use crate::{cha::Cha, services}; +use crate::{cha::Cha, provider}; #[derive(Clone, Debug, Default)] pub struct File { @@ -22,13 +22,13 @@ impl Deref for File { impl File { #[inline] pub async fn new(url: Url) -> Result { - let meta = services::symlink_metadata(&url).await?; + let meta = provider::symlink_metadata(&url).await?; Ok(Self::from_follow(url, meta).await) } #[inline] pub async fn from_follow(url: Url, meta: Metadata) -> Self { - let link_to = if meta.is_symlink() { services::read_link(&url).await.ok() } else { None }; + let link_to = if meta.is_symlink() { provider::read_link(&url).await.ok() } else { None }; let cha = Cha::from_follow(&url, meta).await; diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index 8a6aee7f..402d71ac 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -4,7 +4,7 @@ use tokio::{select, sync::mpsc::{self, UnboundedReceiver}}; use yazi_shared::{Id, url::{Url, Urn, UrnBuf}}; use super::{FilesSorter, Filter}; -use crate::{FILES_TICKET, File, FilesOp, SortBy, cha::Cha, mounts::PARTITIONS, services}; +use crate::{FILES_TICKET, File, FilesOp, SortBy, cha::Cha, mounts::PARTITIONS, provider::{self, DirEntry}}; #[derive(Default)] pub struct Files { @@ -31,7 +31,7 @@ impl Files { pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } } pub async fn from_dir(dir: &Url) -> std::io::Result> { - let mut it = services::read_dir(dir).await?; + let mut it = provider::read_dir(dir).await?; let (tx, rx) = mpsc::unbounded_channel(); tokio::spawn(async move { @@ -39,7 +39,7 @@ impl Files { select! { _ = tx.closed() => break, result = item.metadata() => { - let url = Url::from(item.path()); + let url = item.url(); _ = tx.send(match result { Ok(meta) => File::from_follow(url, meta).await, Err(_) => File::from_dummy(url, item.file_type().await.ok()) @@ -52,7 +52,7 @@ impl Files { } pub async fn from_dir_bulk(dir: &Url) -> std::io::Result> { - let mut it = services::read_dir(dir).await?; + let mut it = provider::read_dir(dir).await?; let mut entries = Vec::with_capacity(5000); while let Ok(Some(entry)) = it.next_entry().await { entries.push(entry); @@ -60,10 +60,10 @@ impl Files { let (first, rest) = entries.split_at(entries.len() / 3); let (second, third) = rest.split_at(entries.len() / 3); - async fn go(entries: &[tokio::fs::DirEntry]) -> Vec { + async fn go(entries: &[DirEntry]) -> Vec { let mut files = Vec::with_capacity(entries.len()); for entry in entries { - let url = Url::from(entry.path()); + let url = entry.url(); files.push(match entry.metadata().await { Ok(meta) => File::from_follow(url, meta).await, Err(_) => File::from_dummy(url, entry.file_type().await.ok()), diff --git a/yazi-fs/src/fns.rs b/yazi-fs/src/fns.rs index f433fe44..eaf24171 100644 --- a/yazi-fs/src/fns.rs +++ b/yazi-fs/src/fns.rs @@ -6,11 +6,11 @@ use anyhow::{Result, bail}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; use yazi_shared::url::{Component, Url}; -use crate::{cha::Cha, services}; +use crate::{cha::Cha, provider}; #[inline] pub async fn maybe_exists(u: impl AsRef) -> bool { - match services::symlink_metadata(u).await { + match provider::symlink_metadata(u).await { Ok(_) => true, Err(e) => e.kind() != io::ErrorKind::NotFound, } @@ -18,7 +18,7 @@ pub async fn maybe_exists(u: impl AsRef) -> bool { #[inline] pub async fn must_be_dir(u: impl AsRef) -> bool { - services::metadata(u).await.is_ok_and(|m| m.is_dir()) + provider::metadata(u).await.is_ok_and(|m| m.is_dir()) } #[inline] @@ -85,7 +85,7 @@ async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result { pub async fn realname(u: &Url) -> Option { let name = u.file_name()?; - if *u == services::canonicalize(u).await.ok()? { + if *u == provider::canonicalize(u).await.ok()? { return None; } @@ -98,16 +98,16 @@ pub async fn realname(u: &Url) -> Option { #[cfg(unix)] #[tokio::test] -async fn test_realname_unchecked() { - use crate::services::Local; +async fn test_realname_unchecked() -> Result<()> { + use crate::provider::local::Local; Local::remove_dir_all("/tmp/issue-1173").await.ok(); - Local::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap(); - Local::create("/tmp/issue-1173/A").await.unwrap(); - Local::create("/tmp/issue-1173/b").await.unwrap(); - Local::create("/tmp/issue-1173/real-dir/C").await.unwrap(); - Local::symlink_file("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap(); - Local::symlink_dir("real-dir", "/tmp/issue-1173/link-dir").await.unwrap(); + Local::create_dir_all("/tmp/issue-1173/real-dir").await?; + Local::create("/tmp/issue-1173/A").await?; + Local::create("/tmp/issue-1173/b").await?; + Local::create("/tmp/issue-1173/real-dir/C").await?; + Local::symlink_file("/tmp/issue-1173/b", "/tmp/issue-1173/D").await?; + Local::symlink_dir("real-dir", "/tmp/issue-1173/link-dir").await?; let c = &mut HashMap::new(); async fn check(a: &str, b: &str, c: &mut HashMap>) { @@ -125,6 +125,7 @@ async fn test_realname_unchecked() { check("/tmp/issue-1173/d", "D", c).await; check("/tmp/issue-1173/D", "D", c).await; + Ok(()) } // realpath(3) without resolving symlinks. This is useful for case-insensitive @@ -199,7 +200,7 @@ pub fn copy_with_progress( None => {} } - let len = services::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0); + let len = provider::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0); if len > last { tx.send(Ok(len - last)).await.ok(); last = len; @@ -260,17 +261,17 @@ async fn _copy_with_progress(from: Url, to: Url, cha: Cha) -> io::Result { } pub async fn remove_dir_clean(dir: &Url) { - let Ok(mut it) = services::read_dir(dir).await else { return }; + let Ok(mut it) = provider::read_dir(dir).await else { return }; while let Ok(Some(entry)) = it.next_entry().await { if entry.file_type().await.is_ok_and(|t| t.is_dir()) { - let path = entry.path().into(); - Box::pin(remove_dir_clean(&path)).await; - services::remove_dir(path).await.ok(); + let url = entry.url(); + Box::pin(remove_dir_clean(&url)).await; + provider::remove_dir(url).await.ok(); } } - services::remove_dir(dir).await.ok(); + provider::remove_dir(dir).await.ok(); } // Convert a file mode to a string representation @@ -369,7 +370,8 @@ pub fn max_common_root(urls: &[Url]) -> usize { #[test] fn test_max_common_root() { fn assert(input: &[&str], expected: &str) { - let urls: Vec<_> = input.iter().copied().map(Url::try_from).collect::>().unwrap(); + use std::str::FromStr; + let urls: Vec<_> = input.iter().copied().map(Url::from_str).collect::>().unwrap(); let mut comp = urls[0].components(); for _ in 0..comp.clone().count() - max_common_root(&urls) { diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index 03587eb3..771d61e1 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::if_same_then_else, clippy::option_map_unit_fn)] -yazi_macro::mod_pub!(cha mounts services); +yazi_macro::mod_pub!(cha mounts provider); yazi_macro::mod_flat!(calculator cwd file files filter fns op path sorter sorting stage xdg); diff --git a/yazi-fs/src/path.rs b/yazi-fs/src/path.rs index ac8290d7..5c19a29b 100644 --- a/yazi-fs/src/path.rs +++ b/yazi-fs/src/path.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{P use anyhow::{Result, bail}; use yazi_shared::url::{Loc, Url}; -use crate::{CWD, services}; +use crate::{CWD, provider}; pub fn clean_url<'a>(url: impl Into>) -> Cow<'a, Url> { let url = url.into(); @@ -12,7 +12,7 @@ pub fn clean_url<'a>(url: impl Into>) -> Cow<'a, Url> { if path.as_os_str() == url.loc.as_os_str() { url } else { - url.with(Loc::with(&clean_path(url.loc.base()), path)).into() + url.with(Loc::with_lossy(&clean_path(url.loc.base()), path)).into() } } @@ -106,7 +106,7 @@ pub async fn unique_name(u: Url, append: F) -> io::Result where F: Future, { - match services::symlink_metadata(&u).await { + match provider::symlink_metadata(&u).await { Ok(_) => _unique_name(u, append.await).await, Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(u), Err(e) => Err(e), @@ -142,7 +142,7 @@ async fn _unique_name(mut url: Url, append: bool) -> io::Result { } url.set_name(&name); - match services::symlink_metadata(&url).await { + match provider::symlink_metadata(&url).await { Ok(_) => i += 1, Err(e) if e.kind() == io::ErrorKind::NotFound => break, Err(e) => return Err(e), @@ -214,16 +214,14 @@ pub fn backslash_to_slash(p: &Path) -> Cow<'_, Path> { mod tests { use std::borrow::Cow; - use yazi_shared::url::Url; - use super::url_relative_to; #[test] fn test_path_relative_to() { fn assert(from: &str, to: &str, ret: &str) { assert_eq!( - url_relative_to(&Url::try_from(from).unwrap(), &Url::try_from(to).unwrap()).unwrap(), - Cow::Owned(Url::try_from(ret).unwrap()) + url_relative_to(&from.parse().unwrap(), &to.parse().unwrap()).unwrap(), + Cow::Owned(ret.parse().unwrap()) ); } diff --git a/yazi-fs/src/provider/buffer.rs b/yazi-fs/src/provider/buffer.rs new file mode 100644 index 00000000..4913981d --- /dev/null +++ b/yazi-fs/src/provider/buffer.rs @@ -0,0 +1,9 @@ +// --- BufRead +pub trait BufRead: tokio::io::AsyncRead + Send {} + +impl BufRead for T {} + +// --- BufReadSync +pub trait BufReadSync: std::io::BufRead + std::io::Seek + Send {} + +impl BufReadSync for T {} diff --git a/yazi-fs/src/provider/dir_entry.rs b/yazi-fs/src/provider/dir_entry.rs new file mode 100644 index 00000000..4aa54c52 --- /dev/null +++ b/yazi-fs/src/provider/dir_entry.rs @@ -0,0 +1,68 @@ +use std::{ffi::OsString, io}; + +use yazi_shared::url::Url; + +pub enum DirEntry { + Local(super::local::DirEntry), +} + +impl DirEntry { + #[must_use] + pub fn url(&self) -> Url { + match self { + DirEntry::Local(local) => local.url(), + } + } + + #[must_use] + pub fn file_name(&self) -> OsString { + match self { + DirEntry::Local(local) => local.file_name(), + } + } + + pub async fn metadata(&self) -> io::Result { + match self { + DirEntry::Local(local) => local.metadata().await, + } + } + + pub async fn file_type(&self) -> io::Result { + match self { + DirEntry::Local(local) => local.file_type().await, + } + } +} + +// --- DirEntrySync +pub enum DirEntrySync { + Local(super::local::DirEntrySync), +} + +impl DirEntrySync { + #[must_use] + pub fn url(&self) -> Url { + match self { + DirEntrySync::Local(local) => local.url(), + } + } + + #[must_use] + pub fn file_name(&self) -> OsString { + match self { + DirEntrySync::Local(local) => local.file_name(), + } + } + + pub fn metadata(&self) -> io::Result { + match self { + DirEntrySync::Local(local) => local.metadata(), + } + } + + pub fn file_type(&self) -> io::Result { + match self { + DirEntrySync::Local(local) => local.file_type(), + } + } +} diff --git a/yazi-fs/src/provider/local/dir_entry.rs b/yazi-fs/src/provider/local/dir_entry.rs new file mode 100644 index 00000000..ea938397 --- /dev/null +++ b/yazi-fs/src/provider/local/dir_entry.rs @@ -0,0 +1,46 @@ +use std::ops::Deref; + +use yazi_shared::url::Url; + +pub struct DirEntry(tokio::fs::DirEntry); + +impl Deref for DirEntry { + type Target = tokio::fs::DirEntry; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From for DirEntry { + fn from(value: tokio::fs::DirEntry) -> Self { Self(value) } +} + +impl From for crate::provider::DirEntry { + fn from(value: DirEntry) -> Self { crate::provider::DirEntry::Local(value) } +} + +impl DirEntry { + #[must_use] + pub fn url(&self) -> Url { self.0.path().into() } +} + +// --- DirEntrySync +pub struct DirEntrySync(std::fs::DirEntry); + +impl Deref for DirEntrySync { + type Target = std::fs::DirEntry; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From for DirEntrySync { + fn from(value: std::fs::DirEntry) -> Self { Self(value) } +} + +impl From for crate::provider::DirEntrySync { + fn from(value: DirEntrySync) -> Self { crate::provider::DirEntrySync::Local(value) } +} + +impl DirEntrySync { + #[must_use] + pub fn url(&self) -> Url { self.0.path().into() } +} diff --git a/yazi-fs/src/provider/local/gate.rs b/yazi-fs/src/provider/local/gate.rs new file mode 100644 index 00000000..4dcaf356 --- /dev/null +++ b/yazi-fs/src/provider/local/gate.rs @@ -0,0 +1,14 @@ +use std::ops::{Deref, DerefMut}; + +#[derive(Default)] +pub struct Gate(tokio::fs::OpenOptions); + +impl Deref for Gate { + type Target = tokio::fs::OpenOptions; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Gate { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} diff --git a/yazi-fs/src/services/local.rs b/yazi-fs/src/provider/local/local.rs similarity index 81% rename from yazi-fs/src/services/local.rs rename to yazi-fs/src/provider/local/local.rs index a832115d..e8468ff6 100644 --- a/yazi-fs/src/services/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -1,5 +1,7 @@ use std::{io, path::{Path, PathBuf}}; +use crate::provider::local::{Gate, ReadDir, ReadDirSync, RwFile}; + pub struct Local; impl Local { @@ -9,8 +11,8 @@ impl Local { } #[inline] - pub async fn create(path: impl AsRef) -> io::Result { - tokio::fs::File::create(path).await + pub async fn create(path: impl AsRef) -> io::Result { + Gate::default().write(true).create(true).truncate(true).open(path).await.map(Into::into) } #[inline] @@ -34,21 +36,21 @@ impl Local { } #[inline] - pub async fn open(path: impl AsRef) -> io::Result { - tokio::fs::File::open(path).await + pub async fn open(path: impl AsRef) -> io::Result { + 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 } #[inline] - pub async fn read_dir(path: impl AsRef) -> io::Result { - tokio::fs::read_dir(path).await + pub async fn read_dir(path: impl AsRef) -> io::Result { + tokio::fs::read_dir(path).await.map(Into::into) } #[inline] - pub fn read_dir_sync(path: impl AsRef) -> io::Result { - std::fs::read_dir(path) + pub fn read_dir_sync(path: impl AsRef) -> io::Result { + std::fs::read_dir(path).map(Into::into) } #[inline] diff --git a/yazi-fs/src/provider/local/mod.rs b/yazi-fs/src/provider/local/mod.rs new file mode 100644 index 00000000..6e6b0857 --- /dev/null +++ b/yazi-fs/src/provider/local/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(dir_entry gate local read_dir rw_file); diff --git a/yazi-fs/src/provider/local/read_dir.rs b/yazi-fs/src/provider/local/read_dir.rs new file mode 100644 index 00000000..1f241149 --- /dev/null +++ b/yazi-fs/src/provider/local/read_dir.rs @@ -0,0 +1,38 @@ +use std::io; + +use super::{DirEntry, DirEntrySync}; + +pub struct ReadDir(tokio::fs::ReadDir); + +impl From for ReadDir { + fn from(value: tokio::fs::ReadDir) -> Self { Self(value) } +} + +impl From for crate::provider::ReadDir { + fn from(value: ReadDir) -> Self { crate::provider::ReadDir::Local(value) } +} + +impl ReadDir { + pub async fn next_entry(&mut self) -> io::Result> { + self.0.next_entry().await.map(|entry| entry.map(Into::into)) + } +} + +// --- ReadDirSync +pub struct ReadDirSync(std::fs::ReadDir); + +impl From for ReadDirSync { + fn from(value: std::fs::ReadDir) -> Self { Self(value) } +} + +impl From for crate::provider::ReadDirSync { + fn from(value: ReadDirSync) -> Self { crate::provider::ReadDirSync::Local(value) } +} + +impl Iterator for ReadDirSync { + type Item = io::Result; + + fn next(&mut self) -> Option> { + self.0.next().map(|result| result.map(Into::into)) + } +} diff --git a/yazi-fs/src/provider/local/rw_file.rs b/yazi-fs/src/provider/local/rw_file.rs new file mode 100644 index 00000000..43ed6858 --- /dev/null +++ b/yazi-fs/src/provider/local/rw_file.rs @@ -0,0 +1,23 @@ +pub struct RwFile(tokio::fs::File); + +impl From for RwFile { + fn from(value: tokio::fs::File) -> Self { Self(value) } +} + +impl From for crate::provider::RwFile { + fn from(value: RwFile) -> Self { crate::provider::RwFile::Local(value) } +} + +impl From for crate::provider::RwFile { + fn from(value: tokio::fs::File) -> Self { RwFile(value).into() } +} + +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/mod.rs b/yazi-fs/src/provider/mod.rs new file mode 100644 index 00000000..c1d7908e --- /dev/null +++ b/yazi-fs/src/provider/mod.rs @@ -0,0 +1,3 @@ +yazi_macro::mod_pub!(local); + +yazi_macro::mod_flat!(buffer dir_entry provider read_dir rw_file); diff --git a/yazi-fs/src/services/services.rs b/yazi-fs/src/provider/provider.rs similarity index 90% rename from yazi-fs/src/services/services.rs rename to yazi-fs/src/provider/provider.rs index 2b8cba8d..9f5c6ffc 100644 --- a/yazi-fs/src/services/services.rs +++ b/yazi-fs/src/provider/provider.rs @@ -2,7 +2,7 @@ use std::io; use yazi_shared::url::Url; -use crate::services::Local; +use crate::provider::{ReadDir, ReadDirSync, RwFile, local::Local}; #[inline] pub async fn canonicalize(url: impl AsRef) -> io::Result { @@ -14,9 +14,9 @@ pub async fn canonicalize(url: impl AsRef) -> io::Result { } #[inline] -pub async fn create(url: impl AsRef) -> io::Result { +pub async fn create(url: impl AsRef) -> io::Result { if let Some(path) = url.as_ref().as_path() { - Local::create(path).await + Local::create(path).await.map(Into::into) } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) } @@ -59,27 +59,27 @@ pub async fn metadata(url: impl AsRef) -> io::Result { } #[inline] -pub async fn open(url: impl AsRef) -> io::Result { +pub async fn open(url: impl AsRef) -> io::Result { if let Some(path) = url.as_ref().as_path() { - Local::open(path).await + Local::open(path).await.map(Into::into) } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) } } #[inline] -pub async fn read_dir(url: impl AsRef) -> io::Result { +pub async fn read_dir(url: impl AsRef) -> io::Result { if let Some(path) = url.as_ref().as_path() { - Local::read_dir(path).await + Local::read_dir(path).await.map(Into::into) } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) } } #[inline] -pub fn read_dir_sync(url: impl AsRef) -> io::Result { +pub fn read_dir_sync(url: impl AsRef) -> io::Result { if let Some(path) = url.as_ref().as_path() { - Local::read_dir_sync(path) + Local::read_dir_sync(path).map(Into::into) } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) } diff --git a/yazi-fs/src/provider/read_dir.rs b/yazi-fs/src/provider/read_dir.rs new file mode 100644 index 00000000..8a5b2e88 --- /dev/null +++ b/yazi-fs/src/provider/read_dir.rs @@ -0,0 +1,30 @@ +use std::io; + +use super::{DirEntry, DirEntrySync}; + +pub enum ReadDir { + Local(super::local::ReadDir), +} + +impl ReadDir { + pub async fn next_entry(&mut self) -> io::Result> { + match self { + ReadDir::Local(local) => local.next_entry().await.map(|entry| entry.map(Into::into)), + } + } +} + +// --- ReadDirSync +pub enum ReadDirSync { + Local(super::local::ReadDirSync), +} + +impl Iterator for ReadDirSync { + type Item = io::Result; + + fn next(&mut self) -> Option> { + match self { + ReadDirSync::Local(local) => local.next().map(|result| result.map(Into::into)), + } + } +} diff --git a/yazi-fs/src/provider/rw_file.rs b/yazi-fs/src/provider/rw_file.rs new file mode 100644 index 00000000..d0704f4c --- /dev/null +++ b/yazi-fs/src/provider/rw_file.rs @@ -0,0 +1,21 @@ +use crate::provider::{BufRead, BufReadSync}; + +pub enum RwFile { + Local(super::local::RwFile), +} + +impl RwFile { + #[inline] + pub fn reader(self) -> Box { + match self { + 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-fs/src/services/mod.rs b/yazi-fs/src/services/mod.rs deleted file mode 100644 index 288105e4..00000000 --- a/yazi-fs/src/services/mod.rs +++ /dev/null @@ -1 +0,0 @@ -yazi_macro::mod_flat!(local services); diff --git a/yazi-parser/src/mgr/tab_create.rs b/yazi-parser/src/mgr/tab_create.rs index b8862fa0..7c974e81 100644 --- a/yazi-parser/src/mgr/tab_create.rs +++ b/yazi-parser/src/mgr/tab_create.rs @@ -16,7 +16,7 @@ impl From for TabCreateOpt { return Self { wd: None }; } let Some(mut wd) = c.take_first_url() else { - return Self { wd: Some(Url::from(&BOOT.cwds[0])) }; + return Self { wd: Some(BOOT.cwds[0].clone()) }; }; if !c.bool("raw") && let Cow::Owned(u) = expand_url(&wd) diff --git a/yazi-plugin/preset/components/header.lua b/yazi-plugin/preset/components/header.lua index e0ee2e77..fc8c9d60 100644 --- a/yazi-plugin/preset/components/header.lua +++ b/yazi-plugin/preset/components/header.lua @@ -38,7 +38,7 @@ function Header:flags() local t = {} if cwd.is_search then - t[#t + 1] = string.format("search: %s", cwd.frag) + t[#t + 1] = string.format("search: %s", cwd.domain) end if filter then t[#t + 1] = string.format("filter: %s", filter) diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 1c177cee..4386b7cf 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -3,9 +3,9 @@ use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}, sync::OnceLock}; use anyhow::{Result, anyhow}; use ratatui::{layout::Size, text::{Line, Span, Text}}; use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}}; -use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::io::AsyncBufReadExt; use yazi_config::{THEME, YAZI, preview::PreviewWrap}; -use yazi_fs::services::Local; +use yazi_fs::provider::local::Local; use yazi_shared::{Ids, errors::PeekError, replace_to_printable}; static INCR: Ids = Ids::new(); @@ -39,7 +39,7 @@ impl Highlighter { pub fn abort() { INCR.next(); } pub async fn highlight(&self, skip: usize, size: Size) -> Result, PeekError> { - let mut reader = BufReader::new(Local::open(&self.path).await?); + let mut reader = Local::open(&self.path).await?.reader(); let syntax = Self::find_syntax(&self.path).await; let mut plain = syntax.is_err(); @@ -143,7 +143,7 @@ impl Highlighter { } let mut line = String::new(); - let mut reader = BufReader::new(Local::open(&path).await?); + let mut reader = Local::open(&path).await?.reader(); reader.read_line(&mut line).await?; syntaxes.find_syntax_by_first_line(&line).ok_or_else(|| anyhow!("No syntax found")) } diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 1e062d02..900dd903 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use globset::GlobBuilder; use mlua::{ExternalError, ExternalResult, Function, IntoLua, IntoLuaMulti, Lua, Table, Value}; use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef}; -use yazi_fs::{mounts::PARTITIONS, remove_dir_clean, services}; +use yazi_fs::{mounts::PARTITIONS, provider, remove_dir_clean}; use crate::bindings::SizeCalculator; @@ -50,9 +50,9 @@ fn cwd(lua: &Lua) -> mlua::Result { fn cha(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (url, follow): (UrlRef, Option)| async move { let meta = if follow.unwrap_or(false) { - services::metadata(&*url).await + provider::metadata(&*url).await } else { - services::symlink_metadata(&*url).await + provider::symlink_metadata(&*url).await }; match meta { @@ -64,7 +64,7 @@ fn cha(lua: &Lua) -> mlua::Result { fn write(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move { - match services::write(&*url, data.as_bytes()).await { + match provider::write(&*url, data.as_bytes()).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), } @@ -74,8 +74,8 @@ fn write(lua: &Lua) -> mlua::Result { fn create(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move { let result = match r#type.as_bytes().as_ref() { - b"dir" => services::create_dir(&*url).await, - b"dir_all" => services::create_dir_all(&*url).await, + b"dir" => provider::create_dir(&*url).await, + b"dir_all" => provider::create_dir_all(&*url).await, _ => Err("Creation type must be 'dir' or 'dir_all'".into_lua_err())?, }; @@ -89,9 +89,9 @@ fn create(lua: &Lua) -> mlua::Result { fn remove(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move { let result = match r#type.as_bytes().as_ref() { - b"file" => services::remove_file(&*url).await, - b"dir" => services::remove_dir(&*url).await, - b"dir_all" => services::remove_dir_all(&*url).await, + b"file" => provider::remove_file(&*url).await, + b"dir" => provider::remove_dir(&*url).await, + b"dir_all" => provider::remove_dir_all(&*url).await, b"dir_clean" => Ok(remove_dir_clean(&url).await), _ => Err("Removal type must be 'file', 'dir', 'dir_all', or 'dir_clean'".into_lua_err())?, }; @@ -105,7 +105,9 @@ fn remove(lua: &Lua) -> mlua::Result { fn read_dir(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (dir, options): (UrlRef, Table)| async move { + // FIXME: VFS let glob = if let Ok(s) = options.raw_get::("glob") { + return Err("`glob` is disabled temporarily".into_lua_err()); Some( GlobBuilder::new(&s.to_str()?) .case_insensitive(true) @@ -123,7 +125,7 @@ fn read_dir(lua: &Lua) -> mlua::Result { let limit = options.raw_get("limit").unwrap_or(usize::MAX); let resolve = options.raw_get("resolve").unwrap_or(false); - let mut it = match services::read_dir(&*dir).await { + let mut it = match provider::read_dir(&*dir).await { Ok(it) => it, Err(e) => return (Value::Nil, Error::Io(e)).into_lua_multi(&lua), }; @@ -134,12 +136,11 @@ fn read_dir(lua: &Lua) -> mlua::Result { break; } - let path = next.path(); - if glob.as_ref().is_some_and(|g| !g.is_match(&path)) { - continue; - } + let url = next.url(); + // if glob.as_ref().is_some_and(|g| !g.is_match(&path)) { + // continue; + // } - let url = yazi_shared::url::Url::from(path); let file = if !resolve { yazi_fs::File::from_dummy(url, next.file_type().await.ok()) } else if let Ok(meta) = next.metadata().await { diff --git a/yazi-plugin/src/loader/loader.rs b/yazi-plugin/src/loader/loader.rs index 609fa8e0..935e214b 100644 --- a/yazi-plugin/src/loader/loader.rs +++ b/yazi-plugin/src/loader/loader.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result, bail}; use mlua::{ChunkMode, ExternalError, Lua, Table}; use parking_lot::RwLock; use yazi_boot::BOOT; -use yazi_fs::services::Local; +use yazi_fs::provider::local::Local; use yazi_macro::plugin_preset as preset; use yazi_shared::{LOG_LEVEL, RoCell}; diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 21be8206..68b41235 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,10 +1,10 @@ use std::{borrow::Cow, collections::VecDeque}; use anyhow::{Result, anyhow}; -use tokio::{fs::DirEntry, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; +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, services, skip_url, url_relative_to}; +use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, provider::{self, DirEntry}, skip_url, url_relative_to}; use yazi_shared::{Id, url::Url}; use super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; @@ -26,14 +26,14 @@ impl File { pub async fn work(&self, r#in: FileIn) -> Result<()> { match r#in { FileIn::Paste(mut task) => { - ok_or_not_found(services::remove_file(&task.to).await)?; + ok_or_not_found(provider::remove_file(&task.to).await)?; let mut it = copy_with_progress(&task.from, &task.to, task.cha.unwrap()); while let Some(res) = it.recv().await { match res { Ok(0) => { if task.cut { - services::remove_file(&task.from).await.ok(); + provider::remove_file(&task.from).await.ok(); } break; } @@ -62,7 +62,7 @@ impl File { let cha = task.cha.unwrap(); let src = if task.resolve { - match services::read_link(&task.from).await { + match provider::read_link(&task.from).await { Ok(u) => Cow::Owned(u), Err(e) if e.kind() == NotFound => { warn!("Link task partially done: {task:?}"); @@ -75,20 +75,20 @@ impl File { }; let src = if task.relative { - url_relative_to(&services::canonicalize(task.to.parent_url().unwrap()).await?, &src)? + url_relative_to(&provider::canonicalize(task.to.parent_url().unwrap()).await?, &src)? } else { src }; - ok_or_not_found(services::remove_file(&task.to).await)?; + ok_or_not_found(provider::remove_file(&task.to).await)?; if cha.is_dir() { - services::symlink_dir(src, &task.to).await?; + provider::symlink_dir(src, &task.to).await?; } else { - services::symlink_file(src, &task.to).await?; + provider::symlink_file(src, &task.to).await?; } if task.delete { - services::remove_file(&task.from).await.ok(); + provider::remove_file(&task.from).await.ok(); } self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?; } @@ -96,14 +96,14 @@ impl File { let cha = task.cha.unwrap(); let src = if !task.follow { Cow::Borrowed(&task.from) - } else if let Ok(p) = services::canonicalize(&task.from).await { + } else if let Ok(p) = provider::canonicalize(&task.from).await { Cow::Owned(p) } else { Cow::Borrowed(&task.from) }; - ok_or_not_found(services::remove_file(&task.to).await)?; - match services::hard_link(src, &task.to).await { + ok_or_not_found(provider::remove_file(&task.to).await)?; + match provider::hard_link(src, &task.to).await { Err(e) if e.kind() == NotFound => { warn!("Hardlink task partially done: {task:?}"); } @@ -113,7 +113,7 @@ impl File { self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?; } FileIn::Delete(task) => { - if let Err(e) = services::remove_file(&task.target).await + if let Err(e) = provider::remove_file(&task.target).await && e.kind() != NotFound && maybe_exists(&task.target).await { @@ -145,7 +145,7 @@ impl File { } pub async fn paste(&self, mut task: FileInPaste) -> Result<()> { - if task.cut && ok_or_not_found(services::rename(&task.from, &task.to).await).is_ok() { + if task.cut && ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok() { return self.succ(task.id); } @@ -185,14 +185,14 @@ impl File { while let Some(src) = dirs.pop_front() { let dest = root.join(skip_url(&src, skip)); - continue_unless_ok!(match services::create_dir(&dest).await { + continue_unless_ok!(match provider::create_dir(&dest).await { Err(e) if e.kind() != AlreadyExists => Err(e), _ => Ok(()), }); - let mut it = continue_unless_ok!(services::read_dir(&src).await); + let mut it = continue_unless_ok!(provider::read_dir(&src).await); while let Ok(Some(entry)) = it.next_entry().await { - let from = Url::from(entry.path()); + let from = entry.url(); let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await); if cha.is_dir() { @@ -256,14 +256,14 @@ impl File { while let Some(src) = dirs.pop_front() { let dest = root.join(skip_url(&src, skip)); - continue_unless_ok!(match services::create_dir(&dest).await { + continue_unless_ok!(match provider::create_dir(&dest).await { Err(e) if e.kind() != AlreadyExists => Err(e), _ => Ok(()), }); - let mut it = continue_unless_ok!(services::read_dir(&src).await); + let mut it = continue_unless_ok!(provider::read_dir(&src).await); while let Ok(Some(entry)) = it.next_entry().await { - let from = Url::from(entry.path()); + let from = entry.url(); let cha = continue_unless_ok!(Self::cha_from(entry, &from, task.follow).await); if cha.is_dir() { @@ -280,7 +280,7 @@ impl File { } pub async fn delete(&self, mut task: FileInDelete) -> Result<()> { - let meta = services::symlink_metadata(&task.target).await?; + let meta = provider::symlink_metadata(&task.target).await?; if !meta.is_dir() { let id = task.id; task.length = meta.len(); @@ -291,17 +291,17 @@ impl File { let mut dirs = VecDeque::from([task.target]); while let Some(target) = dirs.pop_front() { - let Ok(mut it) = services::read_dir(target).await else { continue }; + let Ok(mut it) = provider::read_dir(target).await else { continue }; while let Ok(Some(entry)) = it.next_entry().await { let Ok(meta) = entry.metadata().await else { continue }; if meta.is_dir() { - dirs.push_front(Url::from(entry.path())); + dirs.push_front(entry.url()); continue; } - task.target = Url::from(entry.path()); + task.target = entry.url(); task.length = meta.len(); self.prog.send(TaskProg::New(task.id, meta.len()))?; self.queue(FileIn::Delete(task.clone()), NORMAL).await?; @@ -321,7 +321,7 @@ impl File { #[inline] async fn cha(url: &Url, follow: bool) -> io::Result { - let meta = services::symlink_metadata(url).await?; + let meta = provider::symlink_metadata(url).await?; Ok(if follow { Cha::from_follow(url, meta).await } else { Cha::new(url, meta) }) } diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index a0fbed72..18589bb1 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -6,7 +6,7 @@ use parking_lot::Mutex; use tokio::{select, sync::mpsc::{self, UnboundedReceiver}, task::JoinHandle}; use yazi_config::{YAZI, plugin::{Fetcher, Preloader}}; use yazi_dds::Pump; -use yazi_fs::{must_be_dir, remove_dir_clean, services, unique_name}; +use yazi_fs::{must_be_dir, provider, remove_dir_clean, unique_name}; use yazi_parser::{app::PluginOpt, tasks::ProcessExecOpt}; use yazi_proxy::MgrProxy; use yazi_shared::{Id, Throttle, url::Url}; @@ -175,7 +175,7 @@ impl Scheduler { move |canceled: bool| { async move { if !canceled { - services::remove_dir_all(&target).await.ok(); + provider::remove_dir_all(&target).await.ok(); MgrProxy::update_tasks(&target); Pump::push_delete(target); } diff --git a/yazi-shared/src/event/data.rs b/yazi-shared/src/event/data.rs index 7aefce8d..89e9b8f4 100644 --- a/yazi-shared/src/event/data.rs +++ b/yazi-shared/src/event/data.rs @@ -66,9 +66,9 @@ impl Data { #[inline] pub fn into_url(self) -> Option { match self { - Self::String(s) => Url::try_from(s.as_ref()).ok(), + Self::String(s) => s.parse().ok(), Self::Url(u) => Some(u), - Self::Bytes(b) => Url::try_from(b.as_slice()).ok(), + Self::Bytes(b) => b.as_slice().try_into().ok(), _ => None, } } @@ -96,9 +96,9 @@ impl Data { #[inline] pub fn to_url(&self) -> Option { match self { - Self::String(s) => Url::try_from(s.as_ref()).ok(), + Self::String(s) => s.parse().ok(), Self::Url(u) => Some(u.clone()), - Self::Bytes(b) => Url::try_from(b.as_slice()).ok(), + Self::Bytes(b) => b.as_slice().try_into().ok(), _ => None, } } @@ -186,9 +186,9 @@ impl DataKey { #[inline] pub fn into_url(self) -> Option { match self { - Self::String(s) => Url::try_from(s.as_ref()).ok(), + Self::String(s) => s.parse().ok(), Self::Url(u) => Some(u), - Self::Bytes(b) => Url::try_from(b.as_slice()).ok(), + Self::Bytes(b) => b.as_slice().try_into().ok(), _ => None, } } diff --git a/yazi-shared/src/url/component.rs b/yazi-shared/src/url/component.rs index dc5d10f5..26f6e0c2 100644 --- a/yazi-shared/src/url/component.rs +++ b/yazi-shared/src/url/component.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, path::{self, PathBuf, PrefixComponent}}; -use crate::url::{Scheme, Url}; +use crate::url::{Encode, Loc, Scheme, Url}; #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum Component<'a> { @@ -60,6 +60,7 @@ impl<'a> FromIterator> for PathBuf { #[derive(Clone)] pub struct Components<'a> { inner: path::Components<'a>, + loc: &'a Loc, scheme: &'a Scheme, scheme_yielded: bool, } @@ -68,6 +69,7 @@ impl<'a> Components<'a> { pub fn new(url: &'a Url) -> Self { Self { inner: url.loc.components(), + loc: &url.loc, scheme: &url.scheme, scheme_yielded: false, } @@ -79,7 +81,7 @@ impl<'a> Components<'a> { return path.as_os_str().into(); } - let mut s = OsString::from(format!("{}", self.scheme)); + let mut s = OsString::from(Encode::new(self.loc, self.scheme).to_string()); s.reserve_exact(path.as_os_str().len()); s.push(path); s.into() @@ -147,14 +149,16 @@ mod tests { #[test] fn test_collect() { - let search = Url::try_from("search://keyword//root/projects/yazi").unwrap(); + let search: Url = "search://keyword//root/projects/yazi".parse().unwrap(); + assert_eq!(search.loc.urn().as_os_str(), OsStr::new("")); assert_eq!(search.scheme, Scheme::Search("keyword".to_owned())); let item = search.join("main.rs"); - assert_eq!(item.scheme, Scheme::SearchItem); + assert_eq!(item.loc.urn().as_os_str(), OsStr::new("main.rs")); + assert_eq!(item.scheme, Scheme::Search("keyword".to_owned())); let u: Url = item.components().take(4).collect(); - assert_eq!(u.scheme, Scheme::SearchItem); + assert_eq!(u.scheme, Scheme::Search("keyword".to_owned())); assert_eq!(u.loc.as_path(), Path::new("/root/projects")); let u: Url = item @@ -162,7 +166,7 @@ mod tests { .take(5) .chain([Component::Normal(OsStr::new("target/release/yazi"))]) .collect(); - assert_eq!(u.scheme, Scheme::SearchItem); + assert_eq!(u.scheme, Scheme::Search("keyword".to_owned())); assert_eq!(u.loc.as_path(), Path::new("/root/projects/yazi/target/release/yazi")); } } diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs index 76ce1916..92ef33ac 100644 --- a/yazi-shared/src/url/display.rs +++ b/yazi-shared/src/url/display.rs @@ -1,4 +1,4 @@ -use crate::url::Url; +use crate::url::{Encode, Url}; pub struct Display<'a> { inner: &'a Url, @@ -13,7 +13,7 @@ impl<'a> std::fmt::Display for Display<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Url { loc, scheme } = self.inner; if scheme.is_virtual() { - scheme.fmt(f)?; + Encode::from(self.inner).fmt(f)?; } loc.display().fmt(f) } diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs new file mode 100644 index 00000000..e1ffcbba --- /dev/null +++ b/yazi-shared/src/url/encode.rs @@ -0,0 +1,76 @@ +use std::fmt::{self, Display}; + +use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; + +use crate::url::{Loc, Scheme, Url}; + +pub struct Encode<'a> { + loc: &'a Loc, + scheme: &'a Scheme, +} + +impl<'a> From<&'a Url> for Encode<'a> { + fn from(url: &'a Url) -> Self { Self::new(&url.loc, &url.scheme) } +} + +impl<'a> Encode<'a> { + #[inline] + pub(super) fn new(loc: &'a Loc, scheme: &'a Scheme) -> Self { Self { loc, scheme } } + + #[inline] + fn domain<'s>(s: &'s str) -> PercentEncode<'s> { + const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); + percent_encode(s.as_bytes(), SET) + } + + #[inline] + fn urn(loc: &'a Loc) -> impl Display { + struct D(usize); + + impl Display for D { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0 != 0 { + write!(f, ":{}", self.0)?; + } + Ok(()) + } + } + + D(loc.urn().components().count()) + } +} + +impl Display for Encode<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.scheme { + Scheme::Regular => write!(f, "regular://"), + Scheme::Search(d) => write!(f, "search://{}{}/", Self::domain(d), Self::urn(self.loc)), + Scheme::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), Self::urn(self.loc)), + Scheme::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), Self::urn(self.loc)), + } + } +} + +// --- Tilded +pub struct EncodeTilded<'a> { + loc: &'a Loc, + scheme: &'a Scheme, +} + +impl<'a> From<&'a Url> for EncodeTilded<'a> { + fn from(url: &'a Url) -> Self { Self { loc: &url.loc, scheme: &url.scheme } } +} + +impl Display for EncodeTilded<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Encode as E; + + let loc = percent_encode(self.loc.as_os_str().as_encoded_bytes(), CONTROLS); + match self.scheme { + Scheme::Regular => write!(f, "regular~://{loc}"), + Scheme::Search(d) => write!(f, "search~://{}{}/{loc}", E::domain(d), E::urn(self.loc)), + Scheme::Archive(d) => write!(f, "archive~://{}{}/{loc}", E::domain(d), E::urn(self.loc)), + Scheme::Sftp(d) => write!(f, "sftp~://{}{}/{loc}", E::domain(d), E::urn(self.loc)), + } + } +} diff --git a/yazi-shared/src/url/loc.rs b/yazi-shared/src/url/loc.rs index 00d4c467..8b8e1d97 100644 --- a/yazi-shared/src/url/loc.rs +++ b/yazi-shared/src/url/loc.rs @@ -1,12 +1,13 @@ use std::{borrow::Cow, cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; +use anyhow::{Result, bail}; + use crate::url::{Urn, UrnBuf}; #[derive(Clone, Default)] pub struct Loc { inner: PathBuf, urn: usize, - name: usize, } impl Deref for Loc { @@ -39,11 +40,7 @@ impl Hash for Loc { impl Debug for Loc { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Loc") - .field("path", &self.inner) - .field("urn", &self.urn()) - .field("name", &self.name()) - .finish() + f.debug_struct("Loc").field("path", &self.inner).field("urn", &self.urn()).finish() } } @@ -59,20 +56,22 @@ impl From for Loc { fn from(path: PathBuf) -> Self { let Some(name) = path.file_name() else { let urn = path.as_os_str().len(); - return Self { inner: path, urn, name: 0 }; + return Self { inner: path, urn }; }; let name_len = name.len(); let prefix_len = unsafe { - name.as_encoded_bytes().as_ptr().offset_from(path.as_os_str().as_encoded_bytes().as_ptr()) + name + .as_encoded_bytes() + .as_ptr() + .offset_from_unsigned(path.as_os_str().as_encoded_bytes().as_ptr()) }; let mut bytes = path.into_os_string().into_encoded_bytes(); - bytes.truncate(name_len + prefix_len as usize); + bytes.truncate(prefix_len + name_len); Self { inner: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), urn: name_len, - name: name_len, } } } @@ -86,7 +85,26 @@ impl> From<&T> for Loc { } impl Loc { - pub fn with(base: &Path, path: PathBuf) -> Self { + pub fn zeroed(path: PathBuf) -> Self { + let mut loc = Self::from(path); + loc.urn = 0; + loc + } + + pub fn with(urn: usize, path: PathBuf) -> Result { + let mut loc = Self::from(path); + let mut it = loc.inner.components(); + for _ in 0..urn { + if it.next_back().is_none() { + bail!("URN exceeds its entire URL"); + } + } + + loc.urn = loc.strip_prefix(it).unwrap().as_os_str().len(); + Ok(loc) + } + + pub fn with_lossy(base: &Path, path: PathBuf) -> Self { let mut loc = Self::from(path); loc.urn = loc.inner.strip_prefix(base).unwrap_or(&loc.inner).as_os_str().len(); loc @@ -105,28 +123,20 @@ impl Loc { pub fn urn_owned(&self) -> UrnBuf { self.urn().to_owned() } #[inline] - pub fn name(&self) -> &OsStr { - unsafe { - OsStr::from_encoded_bytes_unchecked( - self.bytes().get_unchecked(self.bytes().len() - self.name..), - ) - } - } + pub fn name(&self) -> &OsStr { self.inner.file_name().unwrap_or(OsStr::new("")) } pub fn set_name(&mut self, name: impl AsRef) { - let name = name.as_ref(); - if name == self.name() { + let (old, new) = (self.name(), name.as_ref()); + if old == new { return; } - if self.name > name.len() { - self.urn -= self.name - name.len(); + if old.len() > new.len() { + self.urn -= old.len() - new.len(); } else { - self.urn += name.len() - self.name; + self.urn += new.len() - old.len(); } - - self.name = name.len(); - self.inner.set_file_name(name); + self.inner.set_file_name(new); } #[inline] @@ -143,13 +153,16 @@ impl Loc { #[inline] pub fn rebase(&self, parent: &Path) -> Self { - debug_assert!(self.urn == self.name); + debug_assert!(self.urn == self.name().len()); let path = parent.join(self.name()); - debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name)); - Self { inner: path, urn: self.name, name: self.name } + debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name().len())); + Self { inner: path, urn: self.urn } } + #[inline] + pub fn to_path(&self) -> PathBuf { self.inner.clone() } + #[inline] pub fn into_path(self) -> PathBuf { self.inner } @@ -159,8 +172,6 @@ impl Loc { #[cfg(test)] mod tests { - use std::path::MAIN_SEPARATOR; - use super::*; #[test] @@ -182,18 +193,37 @@ mod tests { } #[test] - fn test_from() { - let loc = Loc::with(Path::new("/"), "/".into()); + fn test_with() -> Result<()> { + let loc = Loc::with(0, "/".into())?; assert_eq!(loc.urn().as_os_str(), OsStr::new("")); assert_eq!(loc.name(), OsStr::new("")); assert_eq!(loc.base().as_os_str(), OsStr::new("/")); - let loc = Loc::with(Path::new("/root/"), "/root/code/".into()); + let loc = Loc::with(1, "/root/code/".into())?; assert_eq!(loc.urn().as_os_str(), OsStr::new("code")); assert_eq!(loc.name(), OsStr::new("code")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); - let loc = Loc::with(Path::new("/root//"), "/root/code/foo//".into()); + let loc = Loc::with(2, "/root/code/foo//".into())?; + assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); + assert_eq!(loc.name(), OsStr::new("foo")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); + Ok(()) + } + + #[test] + fn test_with_lossy() { + let loc = Loc::with_lossy(Path::new("/"), "/".into()); + assert_eq!(loc.urn().as_os_str(), OsStr::new("")); + assert_eq!(loc.name(), OsStr::new("")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/")); + + let loc = Loc::with_lossy(Path::new("/root/"), "/root/code/".into()); + assert_eq!(loc.urn().as_os_str(), OsStr::new("code")); + assert_eq!(loc.name(), OsStr::new("code")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); + + let loc = Loc::with_lossy(Path::new("/root//"), "/root/code/foo//".into()); assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.name(), OsStr::new("foo")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); @@ -201,18 +231,20 @@ mod tests { #[test] fn test_set_name() { - let mut loc = Loc::with(Path::new("/root"), "/root/code/foo/".into()); + const S: char = std::path::MAIN_SEPARATOR; + + let mut loc = Loc::with_lossy(Path::new("/root"), "/root/code/foo/".into()); assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.name(), OsStr::new("foo")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); loc.set_name("bar.txt"); - assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{MAIN_SEPARATOR}bar.txt"))); + assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{S}bar.txt"))); assert_eq!(loc.name(), OsStr::new("bar.txt")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); loc.set_name("baz"); - assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{MAIN_SEPARATOR}baz"))); + assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{S}baz"))); assert_eq!(loc.name(), OsStr::new("baz")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); } diff --git a/yazi-shared/src/url/mod.rs b/yazi-shared/src/url/mod.rs index 08f48576..62cf9b0f 100644 --- a/yazi-shared/src/url/mod.rs +++ b/yazi-shared/src/url/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(component cov display loc scheme url urn); +yazi_macro::mod_flat!(component cov display encode loc scheme url urn); diff --git a/yazi-shared/src/url/scheme.rs b/yazi-shared/src/url/scheme.rs index ec62e184..fdd021ce 100644 --- a/yazi-shared/src/url/scheme.rs +++ b/yazi-shared/src/url/scheme.rs @@ -1,9 +1,9 @@ -use std::{borrow::Cow, fmt::Display}; +use std::borrow::Cow; use anyhow::{Result, bail}; -use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_decode, percent_encode}; +use percent_encoding::percent_decode; -use crate::{BytesExt, url::Loc}; +use crate::BytesExt; #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum Scheme { @@ -11,7 +11,6 @@ pub enum Scheme { Regular, Search(String), - SearchItem, Archive(String), @@ -19,32 +18,67 @@ pub enum Scheme { } impl Scheme { - pub(super) fn parse(bytes: &[u8]) -> Result<(Self, usize, bool)> { + #[inline] + pub const fn kind(&self) -> &'static str { + match self { + Self::Regular => "regular", + Self::Search(_) => "search", + Self::Archive(_) => "archive", + Self::Sftp(_) => "sftp", + } + } + + #[inline] + pub fn domain(&self) -> Option<&str> { + match self { + Self::Regular => None, + Self::Search(s) | Self::Archive(s) | Self::Sftp(s) => Some(s), + } + } + + pub(super) fn parse(bytes: &[u8], skip: &mut usize) -> Result<(Self, bool, Option)> { let Some((mut protocol, rest)) = bytes.split_by_seq(b"://") else { - return Ok((Self::Regular, 0, false)); + return Ok((Self::Regular, false, None)); }; + // Advance to the beginning of the path + *skip += 3 + protocol.len(); + + // Tilded schemes let tilde = protocol.ends_with(b"~"); if tilde { protocol = &protocol[..protocol.len() - 1]; } - Ok(match protocol { - b"regular" => (Self::Regular, 10, tilde), + let (scheme, port) = match protocol { + b"regular" => (Self::Regular, None), b"search" => { - let (name, skip) = Self::decode_param(rest)?; - (Self::Search(name), 9 + skip, tilde) + let (domain, port) = Self::decode_param(rest, skip)?; + (Self::Search(domain), Some(port)) } b"archive" => { - let (name, skip) = Self::decode_param(rest)?; - (Self::Archive(name), 10 + skip, tilde) + let (domain, port) = Self::decode_param(rest, skip)?; + (Self::Archive(domain), Some(port)) } b"sftp" => { - let (name, skip) = Self::decode_param(rest)?; - (Self::Sftp(name), 7 + skip, tilde) + let (domain, port) = Self::decode_param(rest, skip)?; + (Self::Sftp(domain), Some(port)) } _ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)), - }) + }; + + Ok((scheme, tilde, port)) + } + + #[inline] + pub fn parse_kind(bytes: &[u8]) -> Result<&'static str> { + match bytes { + b"regular" => Ok("regular"), + b"search" => Ok("search"), + b"archive" => Ok("archive"), + b"sftp" => Ok("sftp"), + _ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)), + } } #[inline] @@ -55,49 +89,30 @@ impl Scheme { #[inline] pub fn is_virtual(&self) -> bool { match self { - Self::Regular | Self::Search(_) | Self::SearchItem => false, + Self::Regular | Self::Search(_) => false, Self::Archive(_) | Self::Sftp(_) => true, } } - fn decode_param(bytes: &[u8]) -> Result<(String, usize)> { - let len = bytes.iter().copied().take_while(|&b| b != b'/').count(); + fn decode_param(bytes: &[u8], skip: &mut usize) -> Result<(String, usize)> { + let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count(); + let slash = bytes.get(len).is_some_and(|&b| b == b'/'); + *skip += len + slash as usize; - let s = match Cow::from(percent_decode(&bytes[..len])) { + let port = Self::decode_port(&bytes[..len], &mut len)?; + let domain = match Cow::from(percent_decode(&bytes[..len])) { Cow::Borrowed(b) => str::from_utf8(b)?.to_owned(), Cow::Owned(b) => String::from_utf8(b)?, }; - let slash = bytes.get(len).is_some_and(|&b| b == b'/') as usize; - Ok((s, len + slash)) + Ok((domain, port)) } - #[inline] - fn encode_param<'a>(s: &'a str) -> PercentEncode<'a> { - const SET: AsciiSet = CONTROLS.add(b'/'); - percent_encode(s.as_bytes(), &SET) - } + fn decode_port(bytes: &[u8], skip: &mut usize) -> Result { + let Some(idx) = bytes.iter().rposition(|&b| b == b':') else { return Ok(0) }; + let len = bytes.len() - idx; - pub fn encode_tilded(&self, loc: &Loc) -> String { - let loc = percent_encode(loc.as_os_str().as_encoded_bytes(), CONTROLS); - match self { - Self::Regular => format!("regular~://{loc}"), - Self::Search(kw) => format!("search~://{}/{loc}", Self::encode_param(kw)), - Self::SearchItem => format!("search-item~://{loc}"), - Self::Archive(id) => format!("archive~://{}/{loc}", Self::encode_param(id)), - Self::Sftp(id) => format!("sftp~://{}/{loc}", Self::encode_param(id)), - } - } -} - -impl Display for Scheme { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Regular => write!(f, "regular://"), - Self::Search(kw) => write!(f, "search://{}/", Self::encode_param(kw)), - Self::SearchItem => write!(f, "search-item://"), - Self::Archive(id) => write!(f, "archive://{}/", Self::encode_param(id)), - Self::Sftp(id) => write!(f, "sftp://{}/", Self::encode_param(id)), - } + *skip -= len; + Ok(if len == 1 { 0 } else { str::from_utf8(&bytes[idx + 1..])?.parse()? }) } } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 0ddc9557..6abb58eb 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -1,11 +1,11 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}}; +use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}, str::FromStr}; use anyhow::Result; use percent_encoding::percent_decode; use serde::{Deserialize, Serialize}; use super::UrnBuf; -use crate::{IntoOsStr, url::{Components, Display, Loc, Scheme}}; +use crate::{IntoOsStr, url::{Components, Display, Encode, EncodeTilded, Loc, Scheme, Urn}}; #[derive(Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)] pub struct Url { @@ -27,6 +27,10 @@ impl From for Url { fn from(path: PathBuf) -> Self { Loc::from(path).into() } } +impl From<&Url> for Url { + fn from(url: &Url) -> Self { url.clone() } +} + impl From<&PathBuf> for Url { fn from(path: &PathBuf) -> Self { path.to_owned().into() } } @@ -39,15 +43,18 @@ impl TryFrom<&[u8]> for Url { type Error = anyhow::Error; fn try_from(bytes: &[u8]) -> Result { - let (scheme, path) = Self::parse(bytes)?; - Ok(Self { loc: path.into(), scheme }) + let (scheme, path, port) = Self::parse(bytes)?; + + let loc = if let Some(urn) = port { Loc::with(urn, path)? } else { Loc::from(path) }; + + Ok(Self { loc, scheme }) } } -impl TryFrom<&str> for Url { - type Error = anyhow::Error; +impl FromStr for Url { + type Err = anyhow::Error; - fn try_from(value: &str) -> Result { value.as_bytes().try_into() } + fn from_str(s: &str) -> Result { s.as_bytes().try_into() } } impl TryFrom for Url { @@ -82,7 +89,7 @@ impl Url { pub fn with(&self, loc: impl Into) -> Self { let loc: Loc = loc.into(); // FIXME: simplify this - Self { loc: Loc::with(self.loc.base(), loc.into_path()), scheme: self.scheme.clone() } + Self { loc: Loc::with_lossy(self.loc.base(), loc.into_path()), scheme: self.scheme.clone() } } #[inline] @@ -91,7 +98,6 @@ impl Url { match &self.scheme { Scheme::Regular => Self { loc, scheme: Scheme::Regular }, Scheme::Search(_) => self.with(loc), - Scheme::SearchItem => Self { loc, scheme: Scheme::Search(String::new()) }, Scheme::Archive(_) => self.with(loc), Scheme::Sftp(_) => self.with(loc), } @@ -100,12 +106,7 @@ impl Url { pub fn join(&self, path: impl AsRef) -> Self { match self.scheme { Scheme::Regular => Self { loc: self.loc.join(path).into(), scheme: Scheme::Regular }, - Scheme::Search(_) => { - Self { loc: Loc::with(&self.loc, self.loc.join(path)), scheme: Scheme::SearchItem } - } - Scheme::SearchItem => { - Self { loc: Loc::with(self.loc.base(), self.loc.join(path)), scheme: Scheme::SearchItem } - } + Scheme::Search(_) => self.with(self.loc.join(path)), Scheme::Archive(_) => self.with(self.loc.join(path)), Scheme::Sftp(_) => self.with(self.loc.join(path)), } @@ -127,17 +128,14 @@ impl Url { pub fn parent_url(&self) -> Option { let parent = self.loc.parent()?; - let base = self.loc.base(); + let urn = self.loc.urn(); Some(match &self.scheme { Scheme::Regular => Self { loc: parent.into(), scheme: Scheme::Regular }, - Scheme::Search(_) => Self { loc: parent.into(), scheme: Scheme::Regular }, - Scheme::SearchItem if parent == base => { - Self { loc: parent.into(), scheme: Scheme::Search(String::new()) } - } - Scheme::SearchItem => { - Self { loc: Loc::with(base, parent.to_owned()), scheme: Scheme::SearchItem } + Scheme::Search(_) if urn == Urn::new("") => { + Self { loc: parent.into(), scheme: Scheme::Regular } } + Scheme::Search(_) => self.with(parent), Scheme::Archive(_) => self.with(parent), Scheme::Sftp(_) => self.with(parent), }) @@ -172,17 +170,22 @@ impl Url { self.loc.rebase(parent).into() } - pub fn parse(bytes: &[u8]) -> Result<(Scheme, Cow<'_, Path>)> { - let (scheme, skip, tilde) = Scheme::parse(bytes)?; + pub fn parse(bytes: &[u8]) -> Result<(Scheme, PathBuf, Option)> { + let mut skip = 0; + let (scheme, tilde, port) = Scheme::parse(bytes, &mut skip)?; - let rest = &bytes[skip + tilde as usize..]; - let rest = - if tilde { Cow::from(percent_decode(rest)).into_os_str()? } else { rest.into_os_str()? }; + let rest = if tilde { + Cow::from(percent_decode(&bytes[skip..])).into_os_str()? + } else { + bytes[skip..].into_os_str()? + }; - Ok((scheme, match rest { - Cow::Borrowed(s) => Path::new(s).into(), - Cow::Owned(s) => PathBuf::from(s).into(), - })) + let path = match rest { + Cow::Borrowed(s) => Path::new(s).to_owned(), + Cow::Owned(s) => PathBuf::from(s), + }; + + Ok((scheme, path, port)) } } @@ -192,10 +195,13 @@ impl Url { pub fn is_regular(&self) -> bool { self.scheme == Scheme::Regular } #[inline] - pub fn to_regular(&self) -> Self { Self { loc: self.loc.clone(), scheme: Scheme::Regular } } + pub fn to_regular(&self) -> Self { + Self { loc: self.loc.to_path().into(), scheme: Scheme::Regular } + } #[inline] pub fn into_regular(mut self) -> Self { + self.loc = self.loc.into_path().into(); self.scheme = Scheme::Regular; self } @@ -205,13 +211,17 @@ impl Url { pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) } #[inline] - pub fn to_search(&self, frag: impl AsRef) -> Self { - Self { loc: self.loc.clone(), scheme: Scheme::Search(frag.as_ref().to_owned()) } + pub fn to_search(&self, domain: impl AsRef) -> Self { + Self { + loc: Loc::zeroed(self.loc.to_path()), + scheme: Scheme::Search(domain.as_ref().to_owned()), + } } #[inline] - pub fn into_search(mut self, frag: impl AsRef) -> Self { - self.scheme = Scheme::Search(frag.as_ref().to_owned()); + pub fn into_search(mut self, domain: impl AsRef) -> Self { + self.loc = Loc::zeroed(self.loc.into_path()); + self.scheme = Scheme::Search(domain.as_ref().to_owned()); self } @@ -226,7 +236,7 @@ impl Url { impl Debug for Url { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}{}", self.scheme, self.loc.display()) + write!(f, "{}{}", Encode::from(self), self.loc.display()) } } @@ -235,8 +245,8 @@ impl Serialize for Url { let Url { scheme, loc } = self; match (scheme.is_virtual(), loc.to_str()) { (false, Some(s)) => serializer.serialize_str(s), - (true, Some(s)) => serializer.serialize_str(&format!("{scheme}{s}")), - (_, None) => serializer.serialize_str(&scheme.encode_tilded(loc)), + (true, Some(s)) => serializer.serialize_str(&format!("{}{s}", Encode::from(self))), + (_, None) => serializer.collect_str(&EncodeTilded::from(self)), } } } @@ -250,3 +260,40 @@ impl<'de> Deserialize<'de> for Url { Self::try_from(s).map_err(serde::de::Error::custom) } } + +// --- Tests +#[cfg(test)] +mod tests { + use anyhow::Result; + + use super::*; + + #[test] + fn test_search() -> Result<()> { + const S: char = std::path::MAIN_SEPARATOR; + + let u: Url = "/root/project".parse()?; + assert_eq!(format!("{u:?}"), "regular:///root/project"); + + let u = u.into_search("readme"); + assert_eq!(format!("{u:?}"), "search://readme//root/project"); + assert_eq!(format!("{:?}", u.parent_url().unwrap()), "regular:///root"); + + let u = u.join("examples"); + assert_eq!(format!("{u:?}"), format!("search://readme:1//root/project{S}examples")); + + let u = u.join("README.md"); + assert_eq!(format!("{u:?}"), format!("search://readme:2//root/project{S}examples{S}README.md")); + + let u = u.parent_url().unwrap(); + assert_eq!(format!("{u:?}"), format!("search://readme:1//root/project{S}examples")); + + let u = u.parent_url().unwrap(); + assert_eq!(format!("{u:?}"), "search://readme//root/project"); + + let u = u.parent_url().unwrap(); + assert_eq!(format!("{u:?}"), "regular:///root"); + + Ok(()) + } +}